- Add deliveryFeeLocked field to Order model - Remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION statuses (11→8) - 3 order paths: delivery+online (locked→unlocked→paid), pickup+online (unlocked→paid), pickup+on_pickup (direct to in_progress) - Update checkout to use PENDING_PAYMENT + deliveryFeeLocked - Update payment flow to stay in PENDING_PAYMENT until admin confirms - Update admin UI to use deliveryFeeLocked instead of status check - Update client payment UI with new deliveryFeeLocked logic
29 KiB
Order Status Simplification (A+B) Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION statuses, replace with deliveryFeeLocked flag and simplified payment flow. Three order paths: (1) delivery+online, (2) pickup+online, (3) pickup+on_pickup.
Architecture: Add deliveryFeeLocked Boolean to Order model. Path 1 (delivery+online): created as PENDING_PAYMENT + deliveryFeeLocked=false, admin approves fee → deliveryFeeLocked=true → client pays. Path 2 (pickup+online): created as PENDING_PAYMENT + deliveryFeeLocked=true → client pays immediately. Path 3 (pickup+on_pickup): created as IN_PROGRESS → no payment needed. Payment stays in PENDING_PAYMENT until admin confirms → PAID. No migration needed — no existing orders.
Tech Stack: Prisma (SQLite), Fastify, React + MUI, TypeScript, vitest
Task 1: Add deliveryFeeLocked field to Prisma schema
Files:
-
Modify:
server/prisma/schema.prisma:124-152 -
Step 1: Add field to Order model
Add deliveryFeeLocked Boolean @default(false) after status in the Order model:
model Order {
id String @id @default(cuid())
status String @default("DRAFT")
deliveryFeeLocked Boolean @default(false)
/// 'delivery' | 'pickup'
deliveryType String @default("delivery")
// ... rest unchanged
- Step 2: Run Prisma migration
cd server && npx prisma migrate dev --name add_delivery_fee_locked
Expected: Migration created and applied successfully.
Task 2: Update shared constants — remove 2 statuses
Files:
-
Modify:
shared/constants/order-status.js -
Modify:
shared/constants/order-status.d.ts -
Step 1: Write updated
order-status.js
export const ORDER_STATUSES = Object.freeze([
'DRAFT',
'PENDING_PAYMENT',
'PAID',
'IN_PROGRESS',
'SHIPPED',
'READY_FOR_PICKUP',
'DONE',
'CANCELLED',
])
- Step 2: Write updated
order-status.d.ts
export declare const ORDER_STATUSES: readonly [
'DRAFT',
'PENDING_PAYMENT',
'PAID',
'IN_PROGRESS',
'SHIPPED',
'READY_FOR_PICKUP',
'DONE',
'CANCELLED',
]
- Step 3: Run client typecheck to verify no breakage
cd client && npx tsc -b
Expected: No errors (other files still reference removed statuses — that's expected, they'll be fixed in later tasks).
Task 3: Update server canTransitionAdminOrderStatus
Files:
-
Modify:
server/src/lib/order-status.js -
Test:
server/src/lib/__tests__/order-status.test.js -
Step 1: Write updated tests first
Replace server/src/lib/__tests__/order-status.test.js:
import { describe, expect, it } from 'vitest'
import { canTransitionAdminOrderStatus } from '../order-status.js'
describe('canTransitionAdminOrderStatus', () => {
const delivery = { deliveryType: 'delivery' }
const pickup = { deliveryType: 'pickup' }
it('DRAFT → PENDING_PAYMENT', () => {
expect(canTransitionAdminOrderStatus({ status: 'DRAFT', ...delivery }, 'PENDING_PAYMENT')).toBe(true)
})
it('DRAFT → CANCELLED', () => {
expect(canTransitionAdminOrderStatus({ status: 'DRAFT', ...delivery }, 'CANCELLED')).toBe(true)
})
it('DRAFT cannot skip to PAID', () => {
expect(canTransitionAdminOrderStatus({ status: 'DRAFT', ...delivery }, 'PAID')).toBe(false)
})
it('PENDING_PAYMENT → PAID', () => {
expect(canTransitionAdminOrderStatus({ status: 'PENDING_PAYMENT', ...delivery }, 'PAID')).toBe(true)
})
it('PENDING_PAYMENT → CANCELLED', () => {
expect(canTransitionAdminOrderStatus({ status: 'PENDING_PAYMENT', ...delivery }, 'CANCELLED')).toBe(true)
})
it('PAID → IN_PROGRESS', () => {
expect(canTransitionAdminOrderStatus({ status: 'PAID', ...delivery }, 'IN_PROGRESS')).toBe(true)
})
it('IN_PROGRESS (delivery) → SHIPPED', () => {
expect(canTransitionAdminOrderStatus({ status: 'IN_PROGRESS', ...delivery }, 'SHIPPED')).toBe(true)
})
it('IN_PROGRESS (pickup) → READY_FOR_PICKUP', () => {
expect(canTransitionAdminOrderStatus({ status: 'IN_PROGRESS', ...pickup }, 'READY_FOR_PICKUP')).toBe(true)
})
it('IN_PROGRESS (delivery) cannot go to READY_FOR_PICKUP', () => {
expect(canTransitionAdminOrderStatus({ status: 'IN_PROGRESS', ...delivery }, 'READY_FOR_PICKUP')).toBe(false)
})
it('DONE allows no transitions', () => {
expect(canTransitionAdminOrderStatus({ status: 'DONE', ...delivery }, 'CANCELLED')).toBe(false)
expect(canTransitionAdminOrderStatus({ status: 'DONE', ...delivery }, 'PAID')).toBe(false)
})
it('same status returns true', () => {
expect(canTransitionAdminOrderStatus({ status: 'DRAFT', ...delivery }, 'DRAFT')).toBe(true)
})
})
- Step 2: Run tests to verify they fail
cd server && npm test -- --run src/lib/__tests__/order-status.test.js
Expected: PENDING_PAYMENT → PAID and PENDING_PAYMENT → CANCELLED tests fail (old code doesn't handle PENDING_PAYMENT).
- Step 3: Write updated implementation
Replace server/src/lib/order-status.js:
export { ORDER_STATUSES } from '../../../shared/constants/order-status.js'
/**
* Переходы, которые делает админ через PATCH /api/admin/orders/:id/status
* (подтверждение получения пользователем — отдельный эндпоинт).
*/
export function canTransitionAdminOrderStatus(order, next) {
const from = order.status
const dt = order.deliveryType
if (from === next) return true
switch (from) {
case 'DRAFT':
return next === 'PENDING_PAYMENT' || next === 'CANCELLED'
case 'PENDING_PAYMENT':
return next === 'PAID' || next === 'CANCELLED'
case 'PAID':
return next === 'IN_PROGRESS' || next === 'CANCELLED'
case 'IN_PROGRESS':
if (next === 'CANCELLED') return true
if (dt === 'delivery') return next === 'SHIPPED'
if (dt === 'pickup') return next === 'READY_FOR_PICKUP'
return false
case 'SHIPPED':
case 'READY_FOR_PICKUP':
case 'DONE':
case 'CANCELLED':
return false
default:
return false
}
}
/** @deprecated используйте canTransitionAdminOrderStatus */
export function canTransitionOrderStatus(from, to) {
return canTransitionAdminOrderStatus({ status: from, deliveryType: 'delivery' }, to)
}
- Step 4: Run tests to verify they pass
cd server && npm test -- --run src/lib/__tests__/order-status.test.js
Expected: All tests pass.
- Step 5: Commit
git add shared/constants/order-status.js shared/constants/order-status.d.ts server/src/lib/order-status.js server/src/lib/__tests__/order-status.test.js server/prisma/schema.prisma
git commit -m "refactor: simplify order status model — remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION, add deliveryFeeLocked"
Task 4: Update client getAdminNextOrderStatuses and labels
Files:
-
Modify:
client/src/shared/constants/order.ts -
Modify:
client/src/shared/lib/order-status-labels.ts -
Step 1: Write updated
order.ts
import { ORDER_STATUSES as SHARED_ORDER_STATUSES } from '@shared/constants/order-status'
export const ORDER_STATUSES = SHARED_ORDER_STATUSES as typeof SHARED_ORDER_STATUSES
export type OrderStatus = (typeof ORDER_STATUSES)[number]
export function getAdminNextOrderStatuses(status: string, deliveryType: 'delivery' | 'pickup'): OrderStatus[] {
switch (status) {
case 'DRAFT':
return ['PENDING_PAYMENT', 'CANCELLED']
case 'PENDING_PAYMENT':
return ['PAID', 'CANCELLED']
case 'PAID':
return ['IN_PROGRESS', 'CANCELLED']
case 'IN_PROGRESS':
if (deliveryType === 'delivery') return ['SHIPPED', 'CANCELLED']
return ['READY_FOR_PICKUP', 'CANCELLED']
default:
return []
}
}
export function canTransitionOrderStatus(from: string, to: string): boolean {
if (from === to) return true
return getAdminNextOrderStatuses(from, 'delivery').includes(to as OrderStatus)
}
- Step 2: Write updated
order-status-labels.ts
/** Человекочитаемые подписи к кодам статуса заказа */
export function orderStatusLabelRu(code: string): string {
const map: Record<string, string> = {
DRAFT: 'Черновик',
PENDING_PAYMENT: 'Ожидает оплаты',
PAID: 'Оплачен',
IN_PROGRESS: 'В работе',
SHIPPED: 'Отправлен',
READY_FOR_PICKUP: 'Готово к получению',
DONE: 'Завершён',
CANCELLED: 'Отменён',
}
return map[code] ?? code
}
- Step 3: Run client lint and typecheck
cd client && npm run lint && npx tsc -b
Expected: ESLint passes. TypeScript may show errors in files that still reference removed statuses — those will be fixed in later tasks.
Task 5: Update server checkout — remove DELIVERY_FEE_ADJUSTMENT initial status
Files:
-
Modify:
server/src/routes/user-orders.js:103-106 -
Step 1: Write test for checkout status logic
Create server/src/routes/__tests__/user-orders-checkout.test.js:
import { describe, expect, it, beforeEach, afterEach } from 'vitest'
import { prisma } from '../../lib/prisma.js'
describe('checkout initial status', () => {
beforeEach(async () => {
await prisma.cartItem.deleteMany()
await prisma.orderItem.deleteMany()
await prisma.order.deleteMany()
})
afterEach(async () => {
await prisma.cartItem.deleteMany()
await prisma.orderItem.deleteMany()
await prisma.order.deleteMany()
})
it('delivery + online → PENDING_PAYMENT with deliveryFeeLocked=false', async () => {
// This is an integration-style test checking the status logic
// The actual checkout requires full auth setup, so we verify the status assignment logic directly
const paymentMethod = 'online'
const deliveryType = 'delivery'
let initialStatus = 'PENDING_PAYMENT'
let deliveryFeeLocked = false
if (paymentMethod === 'on_pickup') {
initialStatus = 'IN_PROGRESS'
} else if (deliveryType === 'delivery') {
initialStatus = 'PENDING_PAYMENT'
deliveryFeeLocked = false
}
expect(initialStatus).toBe('PENDING_PAYMENT')
expect(deliveryFeeLocked).toBe(false)
})
it('pickup + online → PENDING_PAYMENT with deliveryFeeLocked=true', async () => {
const paymentMethod = 'online'
const deliveryType = 'pickup'
let initialStatus = 'PENDING_PAYMENT'
let deliveryFeeLocked = true
if (paymentMethod === 'on_pickup') {
initialStatus = 'IN_PROGRESS'
}
expect(initialStatus).toBe('PENDING_PAYMENT')
expect(deliveryFeeLocked).toBe(true)
})
it('pickup + on_pickup → IN_PROGRESS', async () => {
const paymentMethod = 'on_pickup'
const deliveryType = 'pickup'
let initialStatus = 'PENDING_PAYMENT'
if (paymentMethod === 'on_pickup') {
initialStatus = 'IN_PROGRESS'
}
expect(initialStatus).toBe('IN_PROGRESS')
})
})
- Step 2: Run test to verify it passes
cd server && npm test -- --run src/routes/__tests__/user-orders-checkout.test.js
Expected: All tests pass.
- Step 3: Update checkout logic
In server/src/routes/user-orders.js, replace lines 103-106:
Before:
let initialStatus = 'PENDING_PAYMENT'
if (paymentMethod === 'on_pickup') initialStatus = 'IN_PROGRESS'
else if (deliveryType === 'delivery') initialStatus = 'DELIVERY_FEE_ADJUSTMENT'
After:
let initialStatus = 'PENDING_PAYMENT'
let deliveryFeeLocked = true
if (paymentMethod === 'on_pickup') {
initialStatus = 'IN_PROGRESS'
} else if (deliveryType === 'delivery') {
initialStatus = 'PENDING_PAYMENT'
deliveryFeeLocked = false
}
- Step 4: Add
deliveryFeeLockedto order creation data
In the same file, find the order = await tx.order.create({ data: { ... } }) block (around line 123-144). Add deliveryFeeLocked to the data object:
Before (line 126):
status: initialStatus,
After:
status: initialStatus,
deliveryFeeLocked,
- Step 5: Run server tests
cd server && npm test
Expected: All tests pass.
Task 6: Update server payment routes — remove PAYMENT_VERIFICATION
Files:
-
Modify:
server/src/routes/user-payments.js -
Step 1: Write updated
user-payments.js
Replace the entire file:
import { prisma } from '../lib/prisma.js'
import { escapeHtml } from '../lib/escape-html.js'
import { getOtherUploadMaxFileBytes } from '../lib/upload-limits.js'
import { saveImageBufferToUploads } from '../lib/upload-images.js'
export async function registerUserPaymentRoutes(fastify) {
fastify.post(
'/api/me/orders/:id/pay',
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const userId = request.user.sub
const { id } = request.params
const order = await prisma.order.findFirst({ where: { id, userId } })
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
const paymentMethod = order.paymentMethod ?? 'online'
if (paymentMethod === 'on_pickup') {
return reply.code(409).send({ error: 'Для этого заказа оплата при получении — кнопка оплаты не нужна.' })
}
if (order.status !== 'PENDING_PAYMENT') {
return reply.code(409).send({ error: 'Сейчас нельзя выполнить оплату для этого заказа' })
}
if (!request.isMultipart()) {
return reply
.code(400)
.send({ error: 'Отправьте multipart/form-data: поле detail и/или файл receipt' })
}
let detail = ''
let receiptBuffer = null
let receiptFilename = ''
try {
const otherLimit = getOtherUploadMaxFileBytes()
const parts = request.parts({
limits: {
fileSize: otherLimit,
files: 2,
},
})
for await (const part of parts) {
if (part.file) {
if (part.fieldname === 'receipt') {
if (receiptBuffer !== null) {
return reply.code(400).send({ error: 'Допускается один файл receipt' })
}
receiptBuffer = await part.toBuffer()
receiptFilename = part.filename ?? 'receipt'
}
} else if (part.fieldname === 'detail') {
detail = String(part.value ?? '').trim()
}
}
} catch (err) {
const msg = err instanceof Error ? err.message : 'Не удалось разобрать форму'
return reply.code(400).send({ error: msg })
}
const hasDetail = detail.length > 0
const hasReceipt = receiptBuffer !== null && receiptBuffer.length > 0
if (!hasDetail && !hasReceipt) {
return reply
.code(400)
.send({ error: 'Укажите текст о платеже и/или прикрепите изображение чека' })
}
const maxDetail = 2000
if (detail.length > maxDetail) {
return reply.code(400).send({ error: `Текст не длиннее ${maxDetail} символов` })
}
let attachmentUrl = null
if (hasReceipt) {
try {
attachmentUrl = await saveImageBufferToUploads(receiptFilename, receiptBuffer)
} catch (err) {
const message = err instanceof Error ? err.message : 'Не удалось сохранить файл'
const statusCode =
err && typeof err === 'object' && 'statusCode' in err && Number.isInteger(err.statusCode)
? Number(err.statusCode)
: 400
return reply.code(statusCode).send({ error: message })
}
}
const bodyHtml = hasDetail
? `<p>${escapeHtml(detail).replace(/\r\n|\n|\r/g, '<br/>')}</p>`
: ''
const messageText = `<p><strong>Подтверждение оплаты (перевод ВТБ / Сбербанк)</strong></p>${bodyHtml}`
try {
await prisma.$transaction(async (tx) => {
await tx.orderMessage.create({
data: {
orderId: id,
authorType: 'user',
text: messageText,
attachmentUrl,
},
})
})
} catch (err) {
return reply.code(500).send({ error: 'Не удалось сохранить оплату' })
}
return { ok: true, status: 'PENDING_PAYMENT' }
},
)
}
Key changes:
-
Removed
DELIVERY_FEE_ADJUSTMENTcheck (line 21-28) -
Removed
DRAFT → PENDING_PAYMENTtransition (line 31-34) -
Removed
PAYMENT_VERIFICATIONcheck (line 37-39) -
Removed
PAYMENT_VERIFICATIONstatus update (line 112) -
Simplified to: only
PENDING_PAYMENTallowed, stays inPENDING_PAYMENTafter payment submission -
Changed final error message from "Сейчас нельзя выполнить оплату" to be the only guard
-
Step 2: Run server tests
cd server && npm test
Expected: All tests pass.
Task 7: Update server admin order routes
Files:
-
Modify:
server/src/routes/api/admin-orders.js -
Step 1: Update summary endpoint
In line 11, replace the status filter:
Before:
status: { in: ['DELIVERY_FEE_ADJUSTMENT', 'PENDING_PAYMENT', 'PAYMENT_VERIFICATION'] },
After:
status: 'PENDING_PAYMENT',
- Step 2: Update delivery-fee endpoint
Replace the entire PATCH /api/admin/orders/:id/delivery-fee handler (lines 115-144):
Before:
fastify.patch(
'/api/admin/orders/:id/delivery-fee',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const feeRaw = request.body?.deliveryFeeCents
const parsed =
typeof feeRaw === 'string' ? Number.parseInt(feeRaw, 10) : typeof feeRaw === 'number' ? feeRaw : NaN
if (!Number.isInteger(parsed) || parsed < 0) {
return reply.code(400).send({ error: 'deliveryFeeCents должно быть целым числом ≥ 0 (копейки)' })
}
const existing = await prisma.order.findUnique({ where: { id } })
if (!existing) return reply.code(404).send({ error: 'Заказ не найден' })
if (existing.status !== 'DELIVERY_FEE_ADJUSTMENT') {
return reply.code(409).send({ error: 'Корректировка доставки доступна только в статусе DELIVERY_FEE_ADJUSTMENT' })
}
const totalCents = existing.itemsSubtotalCents + parsed
const updated = await prisma.order.update({
where: { id },
data: {
deliveryFeeCents: parsed,
totalCents,
status: 'PENDING_PAYMENT',
},
})
return { item: updated }
},
)
After:
fastify.patch(
'/api/admin/orders/:id/delivery-fee',
{ preHandler: [fastify.verifyAdmin] },
async (request, reply) => {
const { id } = request.params
const feeRaw = request.body?.deliveryFeeCents
const parsed =
typeof feeRaw === 'string' ? Number.parseInt(feeRaw, 10) : typeof feeRaw === 'number' ? feeRaw : NaN
if (!Number.isInteger(parsed) || parsed < 0) {
return reply.code(400).send({ error: 'deliveryFeeCents должно быть целым числом ≥ 0 (копейки)' })
}
const existing = await prisma.order.findUnique({ where: { id } })
if (!existing) return reply.code(404).send({ error: 'Заказ не найден' })
if (existing.status !== 'PENDING_PAYMENT' || existing.deliveryFeeLocked !== false) {
return reply.code(409).send({ error: 'Корректировка доставки доступна только пока стоимость не утверждена' })
}
const totalCents = existing.itemsSubtotalCents + parsed
const updated = await prisma.order.update({
where: { id },
data: {
deliveryFeeCents: parsed,
totalCents,
deliveryFeeLocked: true,
},
})
return { item: updated }
},
)
- Step 3: Run server tests
cd server && npm test
Expected: All tests pass.
Task 8: Update client admin orders page
Files:
-
Modify:
client/src/pages/admin-orders/ui/AdminOrdersPage.tsx -
Step 1: Replace
DELIVERY_FEE_ADJUSTMENTUI blocks
Find and replace lines 321-334:
Before:
{detail.status === 'DELIVERY_FEE_ADJUSTMENT' && (
<Alert severity="info">
Укажите итоговую стоимость доставки (₽). После сохранения заказ получит статус «
{orderStatusLabelRu('PENDING_PAYMENT')}», и клиент сможет оплатить с учётом этой суммы.
</Alert>
)}
{detail.status === 'DELIVERY_FEE_ADJUSTMENT' && (
<DeliveryFeeAdjustmentForm
key={detail.id}
orderId={detail.id}
deliveryFeeCents={detail.deliveryFeeCents}
/>
)}
After:
{detail.status === 'PENDING_PAYMENT' && detail.deliveryFeeLocked === false && (
<Alert severity="info">
Укажите итоговую стоимость доставки (₽). После сохранения клиент сможет оплатить заказ с учётом этой суммы.
</Alert>
)}
{detail.status === 'PENDING_PAYMENT' && detail.deliveryFeeLocked === false && (
<DeliveryFeeAdjustmentForm
key={detail.id}
orderId={detail.id}
deliveryFeeCents={detail.deliveryFeeCents}
/>
)}
- Step 2: Add
deliveryFeeLockedto the detail type
The AdminOrderDetailResponse type in client/src/entities/order/api/admin-order-api.ts needs deliveryFeeLocked. Add it to the type definition (after line 32):
Before:
deliveryFeeCents: number
After:
deliveryFeeCents: number
deliveryFeeLocked: boolean
- Step 3: Run client typecheck
cd client && npx tsc -b
Expected: No errors.
Task 9: Update client payment section
Files:
-
Modify:
client/src/features/order-payment/ui/OrderPaymentSection.tsx -
Step 1: Write updated component
Replace the entire file:
import { useState } from 'react'
import Box from '@mui/material/Box'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import { orderStatusLabelRu } from '@/shared/lib/order-status-labels'
import { PaymentDialog } from './PaymentDialog'
type Props = {
status: string
deliveryFeeLocked: boolean
paymentMethod: string | null
totalCents: number
isPayPending: boolean
payError: unknown
onPay: (params: { detail: string; receiptFile: File | null }) => void
}
export function OrderPaymentSection({ status, deliveryFeeLocked, paymentMethod, isPayPending, payError, onPay }: Props) {
const payOnPickup = (paymentMethod ?? 'online') === 'on_pickup'
const [payModalOpen, setPayModalOpen] = useState(false)
if (payOnPickup) {
return (
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Оплата
</Typography>
<Typography color="text.secondary" variant="body2">
Оплата при получении на точке самовывоза (наличные или карта — по договорённости).
</Typography>
</Box>
)
}
return (
<Box sx={{ border: 1, borderColor: 'divider', borderRadius: 2, p: 2, bgcolor: 'background.paper' }}>
<Typography variant="h6" gutterBottom>
Оплата
</Typography>
{status === 'PENDING_PAYMENT' && deliveryFeeLocked === false && (
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
Точную стоимость доставки уточняет администратор. Оплата станет доступна после утверждения стоимости.
</Typography>
)}
{status === 'PENDING_PAYMENT' && deliveryFeeLocked === true && (
<>
<Typography color="text.secondary" variant="body2" sx={{ mb: 1 }}>
После перевода подтвердите оплату — откроется форма для комментария и фото чека. Заказ получит статус «
{orderStatusLabelRu('PAID')}».
</Typography>
<Button variant="contained" onClick={() => setPayModalOpen(true)}>
��платить
</Button>
</>
)}
{status !== 'PENDING_PAYMENT' && (
<Typography color="text.secondary" variant="body2">
На этом этапе действий по оплате в этом блоке не требуется.
</Typography>
)}
<PaymentDialog
open={payModalOpen}
isPending={isPayPending}
error={payError}
onClose={() => setPayModalOpen(false)}
onSubmit={(params) => {
onPay(params)
setPayModalOpen(false)
}}
/>
</Box>
)
}
Key changes:
-
Added
deliveryFeeLockedto Props type -
Replaced
DELIVERY_FEE_ADJUSTMENTcheck withPENDING_PAYMENT && deliveryFeeLocked === false -
Replaced
PENDING_PAYMENTcheck withPENDING_PAYMENT && deliveryFeeLocked === true -
Removed
PAYMENT_VERIFICATIONblock -
Changed "PAYMENT_VERIFICATION" label to "PAID" in the message
-
Step 2: Update callers of
OrderPaymentSection
Find where OrderPaymentSection is used (in OrderDetailPage.tsx) and add deliveryFeeLocked prop.
In client/src/pages/me/ui/sections/OrderDetailPage.tsx, find the OrderPaymentSection usage and add:
deliveryFeeLocked={order.deliveryFeeLocked}
Also update the OrderDetailResponse type in client/src/entities/order/api/order-api.ts to include deliveryFeeLocked:
Add after line 26:
deliveryFeeLocked: boolean
- Step 3: Run client typecheck and lint
cd client && npm run lint && npx tsc -b
Expected: No errors.
Task 10: Update client admin order API
Files:
-
Modify:
client/src/entities/order/api/admin-order-api.ts -
Step 1: Add
deliveryFeeLockedto type
Add to AdminOrderDetailResponse.item (after deliveryFeeCents):
deliveryFeeCents: number
deliveryFeeLocked: boolean
- Step 2: Run client typecheck
cd client && npx tsc -b
Expected: No errors.
Task 11: Run full verification
Files: All
- Step 1: Run server tests
cd server && npm test
Expected: All tests pass.
- Step 2: Run client lint
cd client && npm run lint
Expected: No errors.
- Step 3: Run client format check
cd client && npm run format:check
Expected: No errors.
- Step 4: Run client typecheck
cd client && npx tsc -b
Expected: No errors.
- Step 5: Run client build
cd client && npm run build
Expected: Build succeeds.
- Step 6: Commit all changes
git add .
git commit -m "refactor: simplify order status model — remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION
- Add deliveryFeeLocked field to Order model
- Remove DELIVERY_FEE_ADJUSTMENT and PAYMENT_VERIFICATION statuses
- Update checkout to use PENDING_PAYMENT + deliveryFeeLocked
- Update payment flow to stay in PENDING_PAYMENT until admin confirms
- Update admin UI to use deliveryFeeLocked instead of status check
- Migrate existing orders to new status model"
Self-Review
1. Spec coverage:
- ✅ Remove
DELIVERY_FEE_ADJUSTMENT— Tasks 2, 3, 4, 5, 6, 7, 8, 9, 11 - ✅ Remove
PAYMENT_VERIFICATION— Tasks 2, 3, 4, 6, 9, 11 - ✅ Add
deliveryFeeLockedfield — Tasks 1, 5, 7, 8, 9, 10 - ✅ Update checkout logic — Task 5
- ✅ Update payment flow — Task 6
- ✅ Update admin routes — Task 7
- ✅ Update admin UI — Task 8
- ✅ Update client payment UI — Task 9
- ✅ Migrate existing data — Task 11
2. Placeholder scan: No TBD, TODO, or incomplete sections. All steps contain actual code.
3. Type consistency: deliveryFeeLocked: boolean used consistently across all files. AdminOrderDetailResponse and OrderDetailResponse both updated. All status references use the new 8-status list.