import { NOTIFICATION_EVENTS } from '../../../../shared/constants/notification-events.js' import { canTransitionAdminOrderStatus } from '../../lib/order-status.js' import { prisma } from '../../lib/prisma.js' export async function registerAdminOrderRoutes(fastify) { fastify.get('/api/admin/orders/summary', { preHandler: [fastify.verifyAdmin] }, async () => { const attentionCount = await prisma.order.count({ where: { status: 'PENDING_PAYMENT', deliveryType: 'delivery', deliveryFeeLocked: false, }, }) return { attentionCount } }) fastify.get('/api/admin/orders', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const status = typeof request.query?.status === 'string' ? request.query.status.trim() : '' const q = typeof request.query?.q === 'string' ? request.query.q.trim() : '' const deliveryTypeRaw = request.query?.deliveryType const deliveryType = typeof deliveryTypeRaw === 'string' ? deliveryTypeRaw.trim() : '' const pageRaw = request.query?.page const pageParsed = typeof pageRaw === 'string' ? Number(pageRaw) : Number(pageRaw) const page = Number.isFinite(pageParsed) && pageParsed > 0 ? Math.floor(pageParsed) : 1 const pageSizeRaw = request.query?.pageSize const pageSizeParsed = typeof pageSizeRaw === 'string' ? Number(pageSizeRaw) : Number(pageSizeRaw) const pageSize = Number.isFinite(pageSizeParsed) && pageSizeParsed > 0 ? Math.floor(pageSizeParsed) : 20 if (pageSize > 100) return reply.code(400).send({ error: 'pageSize должен быть ≤ 100' }) const where = {} if (status) where.status = status if (deliveryType) { if (deliveryType !== 'delivery' && deliveryType !== 'pickup') { return reply.code(400).send({ error: 'deliveryType должен быть delivery | pickup' }) } where.deliveryType = deliveryType } if (q) { where.OR = [{ id: { contains: q } }, { user: { email: { contains: q } } }] } const total = await prisma.order.count({ where }) const items = await prisma.order.findMany({ where, include: { user: { select: { id: true, email: true } }, items: true }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * pageSize, take: pageSize, }) return { items: items.map((o) => ({ id: o.id, status: o.status, deliveryType: o.deliveryType, deliveryFeeLocked: o.deliveryFeeLocked, deliveryCarrier: o.deliveryCarrier, paymentMethod: o.paymentMethod, totalCents: o.totalCents, currency: o.currency, createdAt: o.createdAt, updatedAt: o.updatedAt, user: o.user, itemsCount: o.items.reduce((s, i) => s + i.qty, 0), })), total, page, pageSize, } }) fastify.get('/api/admin/orders/:id', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { id } = request.params const order = await prisma.order.findUnique({ where: { id }, include: { user: { select: { id: true, email: true, displayName: true, avatar: true, avatarStyle: true }, }, items: true, messages: { orderBy: { createdAt: 'asc' } }, }, }) if (!order) return reply.code(404).send({ error: 'Заказ не найден' }) return { item: order } }) fastify.patch('/api/admin/orders/:id/status', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { id } = request.params const next = String(request.body?.status || '').trim() if (!next) return reply.code(400).send({ error: 'status обязателен' }) const existing = await prisma.order.findUnique({ where: { id } }) if (!existing) return reply.code(404).send({ error: 'Заказ не найден' }) if (!canTransitionAdminOrderStatus(existing, next)) { return reply.code(409).send({ error: `Нельзя сменить статус ${existing.status} → ${next}`, }) } if (next === 'PAID' && existing.deliveryType === 'delivery' && existing.deliveryFeeLocked === false) { return reply.code(409).send({ error: 'Сначала подтвердите итоговую стоимость доставки', }) } const updated = await prisma.order.update({ where: { id }, data: { status: next }, }) request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_STATUS_CHANGED, { orderId: updated.id, userId: existing.userId, oldStatus: existing.status, newStatus: next, }) return { item: updated } }) 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, }, }) request.server.eventBus.emit(NOTIFICATION_EVENTS.DELIVERY_FEE_ADJUSTED, { orderId: updated.id, userId: existing.userId, totalCents: updated.totalCents, }) return { item: updated } }) fastify.post('/api/admin/orders/:id/messages', { preHandler: [fastify.verifyAdmin] }, async (request, reply) => { const { id } = request.params const text = String(request.body?.text || '').trim() if (!text) return reply.code(400).send({ error: 'Сообщение пустое' }) if (text.length > 2000) return reply.code(400).send({ error: 'Сообщение слишком длинное' }) const order = await prisma.order.findUnique({ where: { id } }) if (!order) return reply.code(404).send({ error: 'Заказ не найден' }) const msg = await prisma.orderMessage.create({ data: { orderId: id, authorType: 'admin', text }, }) request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_MESSAGE_ADMIN_REPLY, { orderId: id, userId: order.userId, messageId: msg.id, preview: text, }) return reply.code(201).send({ item: msg }) }) }