test commit
This commit is contained in:
+152
-115
@@ -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" };
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user