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

29 KiB
Raw Blame History

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 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):

              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_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

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_ADJUSTMENT UI 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 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:

    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 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:

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 deliveryFeeLocked to 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 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.