274 lines
9.3 KiB
JavaScript
274 lines
9.3 KiB
JavaScript
import { NOTIFICATION_EVENTS } from '../../../shared/constants/notification-events.js'
|
|
import { asyncHandler } from '../lib/async-handler.js'
|
|
import { isDeliveryCarrier } from '../lib/delivery-carrier.js'
|
|
import { findUserOrder } from '../lib/find-user-order.js'
|
|
import { prisma } from '../lib/prisma.js'
|
|
|
|
export async function registerUserOrderRoutes(fastify) {
|
|
// ---- Создание заказа (checkout) ----
|
|
|
|
fastify.post('/api/me/orders', { preHandler: [fastify.authenticate] }, async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const deliveryTypeRaw = request.body?.deliveryType
|
|
const deliveryType =
|
|
deliveryTypeRaw === undefined || deliveryTypeRaw === null || deliveryTypeRaw === ''
|
|
? 'delivery'
|
|
: String(deliveryTypeRaw).trim()
|
|
|
|
const addressId = String(request.body?.addressId || '').trim()
|
|
const commentRaw = request.body?.comment
|
|
const comment = commentRaw === null || commentRaw === undefined ? null : String(commentRaw).trim()
|
|
|
|
const paymentMethodRaw = request.body?.paymentMethod
|
|
const paymentMethod =
|
|
paymentMethodRaw === undefined || paymentMethodRaw === null || paymentMethodRaw === ''
|
|
? 'online'
|
|
: String(paymentMethodRaw).trim()
|
|
if (paymentMethod !== 'online' && paymentMethod !== 'on_pickup') {
|
|
return reply.code(400).send({ error: 'paymentMethod должен быть online | on_pickup' })
|
|
}
|
|
|
|
if (deliveryType !== 'delivery' && deliveryType !== 'pickup') {
|
|
return reply.code(400).send({ error: 'deliveryType должен быть delivery | pickup' })
|
|
}
|
|
|
|
const carrierRaw = request.body?.deliveryCarrier
|
|
let deliveryCarrier = null
|
|
if (deliveryType === 'delivery') {
|
|
const carrierStr =
|
|
carrierRaw === undefined || carrierRaw === null || carrierRaw === '' ? '' : String(carrierRaw).trim()
|
|
if (!isDeliveryCarrier(carrierStr)) {
|
|
return reply.code(400).send({
|
|
error: 'deliveryCarrier обязателен для доставки: RUSSIAN_POST | OZON_PVZ | YANDEX_PVZ | FIVE_POST | WB_PVZ',
|
|
})
|
|
}
|
|
deliveryCarrier = carrierStr
|
|
}
|
|
|
|
if (paymentMethod === 'on_pickup' && deliveryType !== 'pickup') {
|
|
return reply.code(400).send({
|
|
error: 'Оплата при получении доступна только для самовывоза',
|
|
})
|
|
}
|
|
|
|
let address = null
|
|
if (deliveryType === 'delivery') {
|
|
if (!addressId) return reply.code(400).send({ error: 'Выберите адрес доставки' })
|
|
address = await prisma.shippingAddress.findFirst({
|
|
where: { id: addressId, userId },
|
|
})
|
|
if (!address) return reply.code(404).send({ error: 'Адрес не найден' })
|
|
}
|
|
|
|
const cartItems = await prisma.cartItem.findMany({
|
|
where: { userId },
|
|
include: { product: true },
|
|
})
|
|
if (cartItems.length === 0) return reply.code(400).send({ error: 'Корзина пуста' })
|
|
|
|
for (const ci of cartItems) {
|
|
const available = ci.product.quantity
|
|
if (ci.qty > available) {
|
|
return reply.code(409).send({
|
|
error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.`,
|
|
})
|
|
}
|
|
}
|
|
|
|
const itemsPayload = cartItems.map((ci) => ({
|
|
productId: ci.productId,
|
|
qty: ci.qty,
|
|
titleSnapshot: ci.product.title,
|
|
priceCentsSnapshot: ci.product.priceCents,
|
|
}))
|
|
|
|
const itemsSubtotalCents = itemsPayload.reduce((sum, i) => sum + i.priceCentsSnapshot * i.qty, 0)
|
|
const deliveryFeeCents = deliveryType === 'delivery' ? 50000 : 0
|
|
const totalCents = itemsSubtotalCents + deliveryFeeCents
|
|
|
|
const addressSnapshotJson =
|
|
deliveryType === 'pickup'
|
|
? JSON.stringify({ deliveryType: 'pickup' })
|
|
: JSON.stringify({
|
|
deliveryType: 'delivery',
|
|
id: address.id,
|
|
label: address.label,
|
|
recipientName: address.recipientName,
|
|
recipientPhone: address.recipientPhone,
|
|
addressLine: address.addressLine,
|
|
comment: address.comment,
|
|
lat: address.lat,
|
|
lng: address.lng,
|
|
})
|
|
|
|
let initialStatus = 'PENDING_PAYMENT'
|
|
let deliveryFeeLocked = true
|
|
if (paymentMethod === 'on_pickup') {
|
|
initialStatus = 'IN_PROGRESS'
|
|
} else if (deliveryType === 'delivery') {
|
|
initialStatus = 'PENDING_PAYMENT'
|
|
deliveryFeeLocked = false
|
|
}
|
|
|
|
let created
|
|
try {
|
|
created = await prisma.$transaction(async (tx) => {
|
|
for (const ci of cartItems) {
|
|
const res = await tx.product.updateMany({
|
|
where: { id: ci.productId, quantity: { gte: ci.qty } },
|
|
data: { quantity: { decrement: ci.qty } },
|
|
})
|
|
if (res.count !== 1) {
|
|
throw new Error(`Недостаточно товара: "${ci.product.title}"`)
|
|
}
|
|
}
|
|
|
|
const order = await tx.order.create({
|
|
data: {
|
|
userId,
|
|
status: initialStatus,
|
|
deliveryFeeLocked,
|
|
deliveryType,
|
|
deliveryCarrier,
|
|
paymentMethod,
|
|
itemsSubtotalCents,
|
|
deliveryFeeCents,
|
|
totalCents,
|
|
currency: 'RUB',
|
|
addressSnapshotJson,
|
|
comment: comment && comment.length ? comment : null,
|
|
items: {
|
|
create: itemsPayload.map((i) => ({
|
|
productId: i.productId,
|
|
qty: i.qty,
|
|
titleSnapshot: i.titleSnapshot,
|
|
priceCentsSnapshot: i.priceCentsSnapshot,
|
|
})),
|
|
},
|
|
},
|
|
})
|
|
await tx.cartItem.deleteMany({ where: { userId } })
|
|
return order
|
|
})
|
|
} catch (e) {
|
|
return reply.code(409).send({
|
|
error: (e instanceof Error && e.message) || 'Недостаточно товара',
|
|
})
|
|
}
|
|
|
|
// Emit notification events
|
|
request.server.eventBus.emit(NOTIFICATION_EVENTS.ORDER_CREATED, {
|
|
orderId: created.id,
|
|
userId,
|
|
totalCents: created.totalCents,
|
|
itemsCount: cartItems.length,
|
|
deliveryType: created.deliveryType,
|
|
})
|
|
|
|
// Also emit admin notification
|
|
request.server.eventBus.emit('order:created:admin', {
|
|
orderId: created.id,
|
|
userId,
|
|
userEmail: request.user.email || '',
|
|
totalCents: created.totalCents,
|
|
itemsCount: cartItems.length,
|
|
deliveryType: created.deliveryType,
|
|
})
|
|
|
|
return reply.code(201).send({ orderId: created.id })
|
|
})
|
|
|
|
fastify.get(
|
|
'/api/me/orders',
|
|
{ preHandler: [fastify.authenticate] },
|
|
asyncHandler(async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const orders = await prisma.order.findMany({
|
|
where: { userId },
|
|
include: { items: { select: { qty: true } } },
|
|
orderBy: { createdAt: 'desc' },
|
|
})
|
|
return {
|
|
items: orders.map((o) => ({
|
|
id: o.id,
|
|
status: o.status,
|
|
totalCents: o.totalCents,
|
|
currency: o.currency,
|
|
createdAt: o.createdAt,
|
|
updatedAt: o.updatedAt,
|
|
itemsCount: o.items.reduce((s, i) => s + i.qty, 0),
|
|
})),
|
|
}
|
|
}),
|
|
)
|
|
|
|
fastify.get(
|
|
'/api/me/orders/:id',
|
|
{ preHandler: [fastify.authenticate] },
|
|
asyncHandler(async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const { id } = request.params
|
|
const order = await findUserOrder(prisma, id, userId, {
|
|
items: true,
|
|
messages: { orderBy: { createdAt: 'asc' } },
|
|
})
|
|
return { item: order }
|
|
}),
|
|
)
|
|
|
|
fastify.get(
|
|
'/api/me/orders/:id/review-eligibility',
|
|
{ preHandler: [fastify.authenticate] },
|
|
asyncHandler(async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const { id } = request.params
|
|
const order = await findUserOrder(prisma, id, userId, { items: true })
|
|
if (order.status !== 'DONE') {
|
|
return { canReview: false, items: [] }
|
|
}
|
|
|
|
const uniq = new Map()
|
|
for (const it of order.items) {
|
|
if (!uniq.has(it.productId)) {
|
|
uniq.set(it.productId, {
|
|
productId: it.productId,
|
|
title: it.titleSnapshot,
|
|
})
|
|
}
|
|
}
|
|
const productIds = [...uniq.keys()]
|
|
const existing = await prisma.review.findMany({
|
|
where: { userId, productId: { in: productIds } },
|
|
select: { productId: true },
|
|
})
|
|
const reviewed = new Set(existing.map((r) => r.productId))
|
|
return {
|
|
canReview: true,
|
|
items: [...uniq.values()].map((x) => ({
|
|
...x,
|
|
hasReview: reviewed.has(x.productId),
|
|
})),
|
|
}
|
|
}),
|
|
)
|
|
|
|
fastify.post(
|
|
'/api/me/orders/:id/confirm-received',
|
|
{ preHandler: [fastify.authenticate] },
|
|
asyncHandler(async (request, reply) => {
|
|
const userId = request.user.sub
|
|
const { id } = request.params
|
|
const order = await findUserOrder(prisma, id, userId)
|
|
|
|
const okDelivery = order.deliveryType === 'delivery' && order.status === 'SHIPPED'
|
|
const okPickup = order.deliveryType === 'pickup' && order.status === 'READY_FOR_PICKUP'
|
|
if (!okDelivery && !okPickup) {
|
|
return reply.code(409).send({ error: 'Сейчас нельзя подтвердить получение заказа' })
|
|
}
|
|
|
|
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
|
|
return { ok: true, status: 'DONE' }
|
|
}),
|
|
)
|
|
}
|