import { prisma } from "../lib/prisma.js"; import { escapeHtml } from "../lib/escape-html.js"; import { getOtherUploadMaxFileBytes } from "../lib/upload-limits.js"; import { saveImageBufferToUploads } from "../lib/upload-images.js"; import { NOTIFICATION_EVENTS } from "../../../shared/constants/notification-events.js"; export async function registerUserPaymentRoutes(fastify) { fastify.post( "/api/me/orders/:id/pay", { 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 paymentMethod = order.paymentMethod ?? "online"; if (paymentMethod === "on_pickup") { return reply .code(409) .send({ error: "Для этого заказа оплата при получении — кнопка оплаты не нужна.", }); } if (order.status !== "PENDING_PAYMENT") { return reply .code(409) .send({ error: "Сейчас нельзя выполнить оплату для этого заказа" }); } if (!request.isMultipart()) { return reply .code(400) .send({ error: "Отправьте multipart/form-data: поле detail и/или файл receipt", }); } let detail = ""; let receiptBuffer = null; let receiptFilename = ""; try { const otherLimit = getOtherUploadMaxFileBytes(); const parts = request.parts({ limits: { fileSize: otherLimit, files: 2, }, }); for await (const part of parts) { if (part.file) { if (part.fieldname === "receipt") { if (receiptBuffer !== null) { return reply .code(400) .send({ error: "Допускается один файл receipt" }); } receiptBuffer = await part.toBuffer(); receiptFilename = part.filename ?? "receipt"; } } else if (part.fieldname === "detail") { detail = String(part.value ?? "").trim(); } } } catch (err) { const msg = err instanceof Error ? err.message : "Не удалось разобрать форму"; return reply.code(400).send({ error: msg }); } const hasDetail = detail.length > 0; const hasReceipt = receiptBuffer !== null && receiptBuffer.length > 0; if (!hasDetail && !hasReceipt) { return reply .code(400) .send({ error: "Укажите текст о платеже и/или прикрепите изображение чека", }); } const maxDetail = 2000; if (detail.length > maxDetail) { return reply .code(400) .send({ error: `Текст не длиннее ${maxDetail} символов` }); } let attachmentUrl = null; if (hasReceipt) { try { attachmentUrl = await saveImageBufferToUploads( receiptFilename, receiptBuffer, ); } catch (err) { const message = err instanceof Error ? err.message : "Не удалось сохранить файл"; const statusCode = err && typeof err === "object" && "statusCode" in err && Number.isInteger(err.statusCode) ? Number(err.statusCode) : 400; return reply.code(statusCode).send({ error: message }); } } const bodyHtml = hasDetail ? `
${escapeHtml(detail).replace(/\r\n|\n|\r/g, "
")}
Подтверждение оплаты (перевод ВТБ / Сбербанк)
${bodyHtml}`; try { await prisma.$transaction(async (tx) => { await tx.orderMessage.create({ data: { orderId: id, authorType: "user", text: messageText, attachmentUrl, }, }); }); } catch (err) { return reply.code(500).send({ error: "Не удалось сохранить оплату" }); } request.server.eventBus.emit(NOTIFICATION_EVENTS.PAYMENT_STATUS_CHANGED, { orderId: id, userId, paymentStatus: "pending", }); return { ok: true, status: "PENDING_PAYMENT" }; }, ); }