# Аудит заказов и оплаты 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:** Найти, зафиксировать тестами и исправить наиболее рискованные проблемы в потоке заказов, оплаты, статусов, доставки и админки заказов. **Architecture:** Идем от воспроизводимых дефектов к минимальным исправлениям. Серверные инварианты закрепляем route/unit тестами рядом с существующими тестами Fastify/Vitest; клиентские рассинхроны закрепляем component/unit тестами в существующих `__tests__`. Рефакторинг делаем только там, где он убирает подтвержденную ошибку или опасное дублирование. **Tech Stack:** Fastify, Prisma, SQLite, Vitest, React, TypeScript, React Query, MUI, FSD. --- ## Files - Create: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` — итоговый audit report с находками, приоритетами и решениями. - Create: `server/src/routes/api/__tests__/admin-orders.test.js` — route-тесты summary, delivery fee и admin status guard. - Modify: `server/src/routes/api/admin-orders.js` — исправления summary и защиты смены статуса при неподтвержденной цене доставки. - Modify: `client/src/app/providers/__tests__/SseProvider.test.tsx` — тесты на реальные query keys админской деталки и summary. - Modify: `client/src/app/providers/SseProvider.tsx` — согласовать SSE-инвалидации с фактическими query keys. - Modify: `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx` — тест на корректное направление сообщений в админской деталке. - Modify: `client/src/features/order-detail/ui/OrderDetailContent.tsx` — исправить `authorType` для `ChatMessageBubble`. - Modify: `client/src/shared/constants/order.ts` — убрать опасный delivery-only fallback из `canTransitionOrderStatus`. - Create: `client/src/shared/constants/__tests__/order.test.ts` — тесты переходов для delivery и pickup в клиентском адаптере. --- ### Task 1: Создать audit report shell **Files:** - Create: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` - [ ] **Step 1: Создать папку для audit reports, если ее нет** Run from repo root: ```powershell if (!(Test-Path "docs/superpowers/audits")) { New-Item -ItemType Directory "docs/superpowers/audits" } ``` Expected: папка `docs/superpowers/audits` существует. - [ ] **Step 2: Создать стартовый audit report** Write `docs/superpowers/audits/2026-05-28-orders-payments-audit.md`: ```markdown # Аудит заказов, оплаты и админки заказов **Дата:** 2026-05-28 **Scope:** `orders/payments/admin-orders` ## Summary - P0: не найдено на момент старта реализации. - P1: проверяются status guards, delivery fee, SSE/queryKey и админский чат. - P2: проверяется покрытие тестами ключевых контрактов. - P3: рефакторинги фиксируются отдельно и не смешиваются с исправлениями. ## Findings ### P1-001: SSE инвалидирует несуществующий queryKey админской деталки **Статус:** pending **Код:** `client/src/app/providers/SseProvider.tsx` **Симптом:** события `message:new`, `order:statusChanged`, `order:updated` инвалидируют `['admin', 'orders', orderId]`, но деталка использует `['admin', 'orders', 'detail', selectedId]`. **Решение:** добавить инвалидацию `['admin', 'orders', 'detail', orderId]`, списка и summary для order events. ### P1-002: Админский чат передает инвертированный authorType в ChatMessageBubble **Статус:** pending **Код:** `client/src/features/order-detail/ui/OrderDetailContent.tsx` **Симптом:** для `m.authorType === 'admin'` компонент получает `authorType="user"`. **Решение:** передавать фактический `authorType`: admin-message как `admin`, user-message как `user`. ### P1-003: Admin summary считает все PENDING_PAYMENT как attention **Статус:** pending **Код:** `server/src/routes/api/admin-orders.js` **Симптом:** `attentionCount` считает все `PENDING_PAYMENT`, включая заказы, где цена уже подтверждена. **Решение:** считать только `PENDING_PAYMENT + delivery + deliveryFeeLocked=false`. ### P1-004: Admin может перевести delivery-заказ в PAID до подтверждения цены **Статус:** pending **Код:** `server/src/routes/api/admin-orders.js` **Симптом:** статусный guard разрешает `PENDING_PAYMENT -> PAID` без проверки `deliveryFeeLocked`. **Решение:** для `next === 'PAID'` отклонять delivery-заказы с `deliveryFeeLocked === false` кодом 409. ## Refactor Backlog - Проверить, нужен ли `client/src/shared/constants/order.ts::canTransitionOrderStatus`; если нужен, сделать deliveryType обязательным аргументом. - После стабилизации route-тестов рассмотреть вынос части логики `admin-orders.js` в маленькие pure helpers. ``` - [ ] **Step 3: Проверить отсутствие незавершенных маркеров** Run: ```powershell $pattern = ('TO' + 'DO|T' + 'BD|PLACE' + 'HOLDER|\?\?\?'); rg -n $pattern "docs/superpowers/audits/2026-05-28-orders-payments-audit.md" ``` Expected: no matches. --- ### Task 2: Зафиксировать server defects тестами **Files:** - Create: `server/src/routes/api/__tests__/admin-orders.test.js` - [ ] **Step 1: Написать failing route tests для summary и PAID guard** Create `server/src/routes/api/__tests__/admin-orders.test.js`: ```js import jwt from '@fastify/jwt' import Fastify from 'fastify' import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { prisma } from '../../../lib/prisma.js' import { registerAdminOrderRoutes } from '../admin-orders.js' const JWT_SECRET = 'test-secret' const ADMIN_EMAIL = `admin-orders-${Date.now()}@example.com` const USER_EMAIL = `admin-orders-user-${Date.now()}@example.com` let app let adminUser let buyer async function signToken(user) { return app.jwt.sign({ sub: user.id, email: user.email }) } async function buildApp() { const fastify = Fastify({ logger: false }) await fastify.register(jwt, { secret: JWT_SECRET }) fastify.decorate('eventBus', { emit: vi.fn() }) fastify.decorate('verifyAdmin', async (request, reply) => { try { await request.jwtVerify() } catch { return reply.code(401).send({ error: 'Unauthorized' }) } if (request.user.email !== ADMIN_EMAIL) { return reply.code(401).send({ error: 'Admin only' }) } }) await registerAdminOrderRoutes(fastify) await fastify.ready() return fastify } async function createOrder(data = {}) { return prisma.order.create({ data: { userId: buyer.id, status: 'PENDING_PAYMENT', deliveryType: 'delivery', deliveryFeeLocked: false, paymentMethod: 'online', itemsSubtotalCents: 10000, deliveryFeeCents: 50000, totalCents: 60000, currency: 'RUB', ...data, }, }) } describe('admin order routes', () => { beforeAll(async () => { await prisma.payment.deleteMany() await prisma.order.deleteMany({ where: { user: { email: { in: [ADMIN_EMAIL, USER_EMAIL] } } } }) await prisma.user.deleteMany({ where: { email: { in: [ADMIN_EMAIL, USER_EMAIL] } } }) adminUser = await prisma.user.create({ data: { email: ADMIN_EMAIL } }) buyer = await prisma.user.create({ data: { email: USER_EMAIL } }) }) beforeEach(async () => { await prisma.order.deleteMany({ where: { userId: buyer.id } }) app = await buildApp() }) afterEach(async () => { await app.close() vi.clearAllMocks() }) afterAll(async () => { await prisma.order.deleteMany({ where: { userId: buyer.id } }) await prisma.user.deleteMany({ where: { id: { in: [adminUser.id, buyer.id] } } }) }) it('summary counts only delivery orders waiting for price approval', async () => { await createOrder({ deliveryFeeLocked: false, deliveryType: 'delivery' }) await createOrder({ deliveryFeeLocked: true, deliveryType: 'delivery' }) await createOrder({ deliveryFeeLocked: false, deliveryType: 'pickup' }) await createOrder({ deliveryFeeLocked: false, deliveryType: 'delivery', status: 'PAID' }) const token = await signToken(adminUser) const res = await app.inject({ method: 'GET', url: '/api/admin/orders/summary', headers: { authorization: `Bearer ${token}` }, }) expect(res.statusCode).toBe(200) expect(res.json()).toEqual({ attentionCount: 1 }) }) it('rejects PAID transition while delivery fee is not locked', async () => { const order = await createOrder({ deliveryFeeLocked: false, deliveryType: 'delivery' }) const token = await signToken(adminUser) const res = await app.inject({ method: 'PATCH', url: `/api/admin/orders/${order.id}/status`, headers: { authorization: `Bearer ${token}` }, payload: { status: 'PAID' }, }) expect(res.statusCode).toBe(409) expect(res.json().error).toContain('стоимость доставки') }) }) ``` - [ ] **Step 2: Запустить тест и убедиться, что он падает** Run from `server`: ```powershell npm test -- src/routes/api/__tests__/admin-orders.test.js ``` Expected before implementation: first test returns `{ attentionCount: 2 }` or more; second test returns 200 instead of 409. --- ### Task 3: Исправить admin summary и PAID guard **Files:** - Modify: `server/src/routes/api/admin-orders.js` - Test: `server/src/routes/api/__tests__/admin-orders.test.js` - Update: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` - [ ] **Step 1: Исправить summary filter** In `server/src/routes/api/admin-orders.js`, replace the summary `where`: ```js const attentionCount = await prisma.order.count({ where: { status: 'PENDING_PAYMENT', deliveryType: 'delivery', deliveryFeeLocked: false, }, }) ``` - [ ] **Step 2: Добавить guard перед update статуса** In `server/src/routes/api/admin-orders.js`, after `canTransitionAdminOrderStatus` check and before `prisma.order.update`, add: ```js if (next === 'PAID' && existing.deliveryType === 'delivery' && existing.deliveryFeeLocked === false) { return reply.code(409).send({ error: 'Сначала подтвердите итоговую стоимость доставки', }) } ``` - [ ] **Step 3: Запустить focused server test** Run from `server`: ```powershell npm test -- src/routes/api/__tests__/admin-orders.test.js ``` Expected: PASS. - [ ] **Step 4: Обновить audit report statuses** In `docs/superpowers/audits/2026-05-28-orders-payments-audit.md`, change: ```markdown **Статус:** pending ``` to: ```markdown **Статус:** fixed ``` for `P1-003` and `P1-004`, and add: ```markdown **Тест:** `server/src/routes/api/__tests__/admin-orders.test.js` ``` to both findings. --- ### Task 4: Зафиксировать SSE queryKey mismatch тестами **Files:** - Modify: `client/src/app/providers/__tests__/SseProvider.test.tsx` - [ ] **Step 1: Обновить ожидания для order events** In `client/src/app/providers/__tests__/SseProvider.test.tsx`, change the `message:new`, `order:statusChanged`, and `order:updated` expectations so each order event expects: ```ts expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['me', 'orders', orderId] }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'detail', orderId] }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders'] }) expect(mockInvalidateQueries).toHaveBeenCalledWith({ queryKey: ['admin', 'orders', 'summary'] }) ``` Use concrete `orderId` values already present in each test: `o1`, `o2`, `o3`. - [ ] **Step 2: Запустить focused client test и убедиться, что он падает** Run from `client`: ```powershell npm test -- src/app/providers/__tests__/SseProvider.test.tsx ``` Expected before implementation: FAIL because `['admin', 'orders', 'detail', orderId]`, list and summary are not invalidated for existing order events. --- ### Task 5: Исправить SSE invalidation keys **Files:** - Modify: `client/src/app/providers/SseProvider.tsx` - Test: `client/src/app/providers/__tests__/SseProvider.test.tsx` - Update: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` - [ ] **Step 1: Добавить локальный helper внутри useEffect** In `client/src/app/providers/SseProvider.tsx`, inside `useEffect` before `handleEvent`, add: ```ts function invalidateOrderQueries(orderId: unknown) { if (!orderId) return queryClient.invalidateQueries({ queryKey: ['me', 'orders', orderId] }) queryClient.invalidateQueries({ queryKey: ['admin', 'orders', 'detail', orderId] }) queryClient.invalidateQueries({ queryKey: ['admin', 'orders'] }) queryClient.invalidateQueries({ queryKey: ['admin', 'orders', 'summary'] }) } ``` - [ ] **Step 2: Использовать helper в three order event cases** Replace duplicated `if (orderId) { ... }` blocks in `message:new`, `order:statusChanged`, and `order:updated` with: ```ts invalidateOrderQueries(orderId) ``` Keep `message:new` invalidations for unread count and conversations before this call. - [ ] **Step 3: Запустить focused client test** Run from `client`: ```powershell npm test -- src/app/providers/__tests__/SseProvider.test.tsx ``` Expected: PASS. - [ ] **Step 4: Обновить audit report** Mark `P1-001` as fixed and add: ```markdown **Тест:** `client/src/app/providers/__tests__/SseProvider.test.tsx` ``` --- ### Task 6: Зафиксировать и исправить authorType в админском чате **Files:** - Modify: `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx` - Modify: `client/src/features/order-detail/ui/OrderDetailContent.tsx` - Update: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` - [ ] **Step 1: Замокать ChatMessageBubble в тесте** In `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx`, add this mock after existing UI mocks: ```ts vi.mock('@/shared/ui/ChatMessageBubble', () => ({ ChatMessageBubble: ({ authorType, children }: { authorType: 'admin' | 'user'; children: React.ReactNode }) => (
{children}
), })) ``` - [ ] **Step 2: Добавить failing test** Add to `describe('OrderDetailContent quick status transitions', () => { ... })`: ```ts it('передает фактический authorType в пузырь сообщения', () => { renderComponent( createDetail({ messages: [ { id: 'message-admin', authorType: 'admin', text: 'Ответ администратора', attachmentUrl: null, createdAt: '2026-05-28T10:00:00.000Z', }, { id: 'message-user', authorType: 'user', text: 'Сообщение покупателя', attachmentUrl: null, createdAt: '2026-05-28T10:01:00.000Z', }, ], }), ) expect(screen.getByTestId('chat-message-admin')).toHaveTextContent('Ответ администратора') expect(screen.getByTestId('chat-message-user')).toHaveTextContent('Сообщение покупателя') }) ``` - [ ] **Step 3: Запустить focused test и убедиться, что он падает** Run from `client`: ```powershell npm test -- src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx ``` Expected before implementation: FAIL because admin message is rendered under `chat-message-user`. - [ ] **Step 4: Исправить implementation** In `client/src/features/order-detail/ui/OrderDetailContent.tsx`, replace: ```tsx ``` with: ```tsx ``` - [ ] **Step 5: Запустить focused test** Run from `client`: ```powershell npm test -- src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx ``` Expected: PASS. - [ ] **Step 6: Обновить audit report** Mark `P1-002` as fixed and add: ```markdown **Тест:** `client/src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx` ``` --- ### Task 7: Исправить клиентский status helper без delivery-only fallback **Files:** - Modify: `client/src/shared/constants/order.ts` - Create: `client/src/shared/constants/__tests__/order.test.ts` - [ ] **Step 1: Создать тесты для delivery и pickup переходов** Create `client/src/shared/constants/__tests__/order.test.ts`: ```ts import { describe, expect, it } from 'vitest' import { canTransitionOrderStatus, getAdminNextOrderStatuses } from '../order' describe('client order status helpers', () => { it('returns delivery-specific next statuses', () => { expect(getAdminNextOrderStatuses('IN_PROGRESS', 'delivery')).toEqual(['SHIPPED', 'CANCELLED']) }) it('returns pickup-specific next statuses', () => { expect(getAdminNextOrderStatuses('IN_PROGRESS', 'pickup')).toEqual(['READY_FOR_PICKUP', 'CANCELLED']) }) it('checks pickup transition without falling back to delivery rules', () => { expect(canTransitionOrderStatus('IN_PROGRESS', 'READY_FOR_PICKUP', 'pickup')).toBe(true) expect(canTransitionOrderStatus('IN_PROGRESS', 'SHIPPED', 'pickup')).toBe(false) }) }) ``` - [ ] **Step 2: Запустить тест и убедиться, что он падает на сигнатуре** Run from `client`: ```powershell npm test -- src/shared/constants/__tests__/order.test.ts ``` Expected before implementation: TypeScript/test failure because `canTransitionOrderStatus` accepts only `(from, to)` and hardcodes delivery. - [ ] **Step 3: Изменить helper signature** In `client/src/shared/constants/order.ts`, replace `canTransitionOrderStatus` with: ```ts export function canTransitionOrderStatus( from: string, to: string, deliveryType: 'delivery' | 'pickup', ): boolean { if (from === to) return true return getAdminNextOrderStatuses(from, deliveryType).includes(to as OrderStatus) } ``` - [ ] **Step 4: Проверить callers** Run from repo root: ```powershell rg -n "canTransitionOrderStatus\\(" "client/src" ``` Expected: only the new test imports/calls it, or all callers pass `deliveryType`. - [ ] **Step 5: Запустить focused test** Run from `client`: ```powershell npm test -- src/shared/constants/__tests__/order.test.ts ``` Expected: PASS. --- ### Task 8: Полные проверки и финализация отчета **Files:** - Update: `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` - Verify: changed server/client files - [ ] **Step 1: Запустить server checks** Run from `server`: ```powershell npm test npm run lint npm run format:check ``` Expected: all commands pass. - [ ] **Step 2: Запустить client checks** Run from `client`: ```powershell npm run lint npm run format:check npm test -- src/app/providers/__tests__/SseProvider.test.tsx src/features/order-detail/ui/__tests__/OrderDetailContent.test.tsx src/shared/constants/__tests__/order.test.ts ``` Expected: all commands pass. - [ ] **Step 3: Запустить build при изменении TypeScript сигнатур** Run from `client`: ```powershell npm run build ``` Expected: typecheck and Vite build pass. - [ ] **Step 4: Финализировать audit report summary** Update `docs/superpowers/audits/2026-05-28-orders-payments-audit.md` summary to: ```markdown ## Summary - P0: не найдено в первой итерации. - P1: исправлены SSE/queryKey для админской деталки, authorType админского чата, summary внимания и guard оплаты до подтверждения доставки. - P2: добавлены focused tests для найденных дефектов. - P3: рефакторинги оставлены в backlog. ``` - [ ] **Step 5: Зафиксировать проверки в audit report** Add: ```markdown ## Verification - `server npm test` — pass - `server npm run lint` — pass - `server npm run format:check` — pass - `client npm run lint` — pass - `client npm run format:check` — pass - `client focused vitest` — pass - `client npm run build` — pass ``` If a command fails for an unrelated existing issue, record the exact failing command and reason instead of marking it pass. - [ ] **Step 6: Проверить рабочее дерево** Run from repo root: ```powershell git status --short ``` Expected: only files from this plan and any intentional generated files are changed. Do not commit unless the user explicitly asks for a commit.