test commit

This commit is contained in:
Kirill
2026-05-18 13:54:05 +05:00
parent 7421384161
commit 2f67c37502
10 changed files with 875 additions and 627 deletions
+152 -115
View File
@@ -1,77 +1,98 @@
import { isDeliveryCarrier } from '../lib/delivery-carrier.js'
import { prisma } from '../lib/prisma.js'
import { NOTIFICATION_EVENTS } from '../../shared/constants/notification-events.js'
import { isDeliveryCarrier } from "../lib/delivery-carrier.js";
import { prisma } from "../lib/prisma.js";
import { NOTIFICATION_EVENTS } from "../../../shared/constants/notification-events.js";
export async function registerUserOrderRoutes(fastify) {
// ---- Создание заказа (checkout) ----
fastify.post(
'/api/me/orders',
"/api/me/orders",
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const userId = request.user.sub
const deliveryTypeRaw = request.body?.deliveryType
const userId = request.user.sub;
const deliveryTypeRaw = request.body?.deliveryType;
const deliveryType =
deliveryTypeRaw === undefined || deliveryTypeRaw === null || deliveryTypeRaw === ''
? 'delivery'
: String(deliveryTypeRaw).trim()
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 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 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' })
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' })
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 carrierRaw = request.body?.deliveryCarrier;
let deliveryCarrier = null;
if (deliveryType === "delivery") {
const carrierStr =
carrierRaw === undefined || carrierRaw === null || carrierRaw === ''
? ''
: String(carrierRaw).trim()
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',
})
return reply.code(400).send({
error:
"deliveryCarrier обязателен для доставки: RUSSIAN_POST | OZON_PVZ | YANDEX_PVZ | FIVE_POST",
});
}
deliveryCarrier = carrierStr
deliveryCarrier = carrierStr;
}
if (paymentMethod === 'on_pickup' && deliveryType !== 'pickup') {
return reply.code(400).send({ error: 'Оплата при получении доступна только для самовывоза' })
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: 'Адрес не найден' })
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: 'Корзина пуста' })
});
if (cartItems.length === 0)
return reply.code(400).send({ error: "Корзина пуста" });
for (const ci of cartItems) {
const available = ci.product.inStock ? ci.product.quantity : 1
const available = ci.product.inStock ? ci.product.quantity : 1;
if (ci.qty > available) {
return reply.code(409).send({ error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.` })
return reply
.code(409)
.send({
error: `Недостаточно товара: "${ci.product.title}". Доступно: ${available} шт.`,
});
}
}
@@ -80,17 +101,20 @@ export async function registerUserOrderRoutes(fastify) {
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 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' })
deliveryType === "pickup"
? JSON.stringify({ deliveryType: "pickup" })
: JSON.stringify({
deliveryType: 'delivery',
deliveryType: "delivery",
id: address.id,
label: address.label,
recipientName: address.recipientName,
@@ -99,31 +123,30 @@ export async function registerUserOrderRoutes(fastify) {
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 initialStatus = "PENDING_PAYMENT";
let deliveryFeeLocked = true;
if (paymentMethod === "on_pickup") {
initialStatus = "IN_PROGRESS";
} else if (deliveryType === "delivery") {
initialStatus = "PENDING_PAYMENT";
deliveryFeeLocked = false;
}
let created
let created;
try {
created = await prisma.$transaction(async (tx) => {
for (const ci of cartItems) {
if (!ci.product.inStock) continue
if (!ci.product.inStock) continue;
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}"`)
throw new Error(`Недостаточно товара: "${ci.product.title}"`);
}
}
const order = await tx.order.create({
@@ -137,7 +160,7 @@ export async function registerUserOrderRoutes(fastify) {
itemsSubtotalCents,
deliveryFeeCents,
totalCents,
currency: 'RUB',
currency: "RUB",
addressSnapshotJson,
comment: comment && comment.length ? comment : null,
items: {
@@ -149,12 +172,16 @@ export async function registerUserOrderRoutes(fastify) {
})),
},
},
})
await tx.cartItem.deleteMany({ where: { userId } })
return order
})
});
await tx.cartItem.deleteMany({ where: { userId } });
return order;
});
} catch (e) {
return reply.code(409).send({ error: (e instanceof Error && e.message) || 'Недостаточно товара' })
return reply
.code(409)
.send({
error: (e instanceof Error && e.message) || "Недостаточно товара",
});
}
// Emit notification events
@@ -163,31 +190,31 @@ export async function registerUserOrderRoutes(fastify) {
userId,
totalCents: created.totalCents,
itemsCount: cartItems.length,
})
});
// Also emit admin notification
request.server.eventBus.emit('order:created:admin', {
request.server.eventBus.emit("order:created:admin", {
orderId: created.id,
userId,
userEmail: request.user.email || '',
userEmail: request.user.email || "",
totalCents: created.totalCents,
itemsCount: cartItems.length,
})
});
return reply.code(201).send({ orderId: created.id })
return reply.code(201).send({ orderId: created.id });
},
)
);
fastify.get(
'/api/me/orders',
"/api/me/orders",
{ preHandler: [fastify.authenticate] },
async (request) => {
const userId = request.user.sub
const userId = request.user.sub;
const orders = await prisma.order.findMany({
where: { userId },
include: { items: true },
orderBy: { createdAt: 'desc' },
})
orderBy: { createdAt: "desc" },
});
return {
items: orders.map((o) => ({
id: o.id,
@@ -198,76 +225,86 @@ export async function registerUserOrderRoutes(fastify) {
updatedAt: o.updatedAt,
itemsCount: o.items.reduce((s, i) => s + i.qty, 0),
})),
}
};
},
)
);
fastify.get(
'/api/me/orders/:id',
"/api/me/orders/:id",
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const userId = request.user.sub
const { id } = request.params
const userId = request.user.sub;
const { id } = request.params;
const order = await prisma.order.findFirst({
where: { id, userId },
include: { items: true, messages: { orderBy: { createdAt: 'asc' } } },
})
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
return { item: order }
include: { items: true, messages: { orderBy: { createdAt: "asc" } } },
});
if (!order) return reply.code(404).send({ error: "Заказ не найден" });
return { item: order };
},
)
);
fastify.get(
'/api/me/orders/:id/review-eligibility',
"/api/me/orders/:id/review-eligibility",
{ preHandler: [fastify.authenticate] },
async (request, reply) => {
const userId = request.user.sub
const { id } = request.params
const order = await prisma.order.findFirst({ where: { id, userId }, include: { items: true } })
if (!order) return reply.code(404).send({ error: 'Заказ не найден' })
if (order.status !== 'DONE') {
return { canReview: false, items: [] }
const userId = request.user.sub;
const { id } = request.params;
const order = await prisma.order.findFirst({
where: { id, userId },
include: { items: true },
});
if (!order) return reply.code(404).send({ error: "Заказ не найден" });
if (order.status !== "DONE") {
return { canReview: false, items: [] };
}
const uniq = new Map()
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 })
uniq.set(it.productId, {
productId: it.productId,
title: it.titleSnapshot,
});
}
}
const productIds = [...uniq.keys()]
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))
});
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',
"/api/me/orders/:id/confirm-received",
{ 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 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 okDelivery = order.deliveryType === 'delivery' && order.status === 'SHIPPED'
const okPickup = order.deliveryType === 'pickup' && order.status === 'READY_FOR_PICKUP'
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: 'Сейчас нельзя подтвердить получение заказа' })
return reply
.code(409)
.send({ error: "Сейчас нельзя подтвердить получение заказа" });
}
await prisma.order.update({ where: { id }, data: { status: 'DONE' } })
return { ok: true, status: 'DONE' }
await prisma.order.update({ where: { id }, data: { status: "DONE" } });
return { ok: true, status: "DONE" };
},
)
);
}