Files
shop-server/server/src/routes/api/admin-orders.js
T
2026-05-22 12:51:41 +05:00

175 lines
6.5 KiB
JavaScript

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',
},
})
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,
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}`,
})
}
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 })
})
}