Files
shop-server/docs/superpowers/plans/2026-05-15-order-status-simplification.md
T
Kirill f855568687 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 (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
2026-05-15 21:55:14 +05:00

951 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```prisma
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**
```bash
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`**
```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`**
```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**
```bash
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`:
```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**
```bash
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`:
```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**
```bash
cd server && npm test -- --run src/lib/__tests__/order-status.test.js
```
Expected: All tests pass.
- [ ] **Step 5: Commit**
```bash
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`**
```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`**
```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**
```bash
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`:
```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**
```bash
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:**
```js
let initialStatus = 'PENDING_PAYMENT'
if (paymentMethod === 'on_pickup') initialStatus = 'IN_PROGRESS'
else if (deliveryType === 'delivery') initialStatus = 'DELIVERY_FEE_ADJUSTMENT'
```
**After:**
```js
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 `deliveryFeeLocked` to 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):**
```js
status: initialStatus,
```
**After:**
```js
status: initialStatus,
deliveryFeeLocked,
```
- [ ] **Step 5: Run server tests**
```bash
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:
```js
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_ADJUSTMENT` check (line 21-28)
- Removed `DRAFT → PENDING_PAYMENT` transition (line 31-34)
- Removed `PAYMENT_VERIFICATION` check (line 37-39)
- Removed `PAYMENT_VERIFICATION` status update (line 112)
- Simplified to: only `PENDING_PAYMENT` allowed, stays in `PENDING_PAYMENT` after payment submission
- Changed final error message from "Сейчас нельзя выполнить оплату" to be the only guard
- [ ] **Step 2: Run server tests**
```bash
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:**
```js
status: { in: ['DELIVERY_FEE_ADJUSTMENT', 'PENDING_PAYMENT', 'PAYMENT_VERIFICATION'] },
```
**After:**
```js
status: 'PENDING_PAYMENT',
```
- [ ] **Step 2: Update delivery-fee endpoint**
Replace the entire `PATCH /api/admin/orders/:id/delivery-fee` handler (lines 115-144):
**Before:**
```js
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:**
```js
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**
```bash
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_ADJUSTMENT` UI blocks**
Find and replace lines 321-334:
**Before:**
```tsx
{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:**
```tsx
{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 `deliveryFeeLocked` to 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:**
```ts
deliveryFeeCents: number
```
**After:**
```ts
deliveryFeeCents: number
deliveryFeeLocked: boolean
```
- [ ] **Step 3: Run client typecheck**
```bash
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:
```tsx
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 `deliveryFeeLocked` to Props type
- Replaced `DELIVERY_FEE_ADJUSTMENT` check with `PENDING_PAYMENT && deliveryFeeLocked === false`
- Replaced `PENDING_PAYMENT` check with `PENDING_PAYMENT && deliveryFeeLocked === true`
- Removed `PAYMENT_VERIFICATION` block
- 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:
```tsx
deliveryFeeLocked={order.deliveryFeeLocked}
```
Also update the `OrderDetailResponse` type in `client/src/entities/order/api/order-api.ts` to include `deliveryFeeLocked`:
Add after line 26:
```ts
deliveryFeeLocked: boolean
```
- [ ] **Step 3: Run client typecheck and lint**
```bash
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 `deliveryFeeLocked` to type**
Add to `AdminOrderDetailResponse.item` (after `deliveryFeeCents`):
```ts
deliveryFeeCents: number
deliveryFeeLocked: boolean
```
- [ ] **Step 2: Run client typecheck**
```bash
cd client && npx tsc -b
```
Expected: No errors.
---
### Task 11: Run full verification
**Files:** All
- [ ] **Step 1: Run server tests**
```bash
cd server && npm test
```
Expected: All tests pass.
- [ ] **Step 2: Run client lint**
```bash
cd client && npm run lint
```
Expected: No errors.
- [ ] **Step 3: Run client format check**
```bash
cd client && npm run format:check
```
Expected: No errors.
- [ ] **Step 4: Run client typecheck**
```bash
cd client && npx tsc -b
```
Expected: No errors.
- [ ] **Step 5: Run client build**
```bash
cd client && npm run build
```
Expected: Build succeeds.
- [ ] **Step 6: Commit all changes**
```bash
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 `deliveryFeeLocked` field — 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.