import { NOTIFICATION_EVENTS } from '../../../shared/constants/notification-events.js' import { prisma } from '../lib/prisma.js' import { createPayment, buildReceipt, getPayment } from '../lib/yookassa.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 userEmail = request.user.email if (!userEmail) { return reply.code(422).send({ error: 'Для онлайн-оплаты необходим email в профиле' }) } 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.paymentMethod === 'on_pickup') { return reply.code(409).send({ error: 'Для этого заказа оплата при получении — онлайн-оплата недоступна', }) } if (order.status !== 'PENDING_PAYMENT') { return reply.code(409).send({ error: 'Сейчас нельзя выполнить оплату для этого заказа' }) } if (!order.deliveryFeeLocked) { return reply.code(409).send({ error: 'Стоимость доставки ещё утверждается — оплата станет доступна позже', }) } const existingPayment = await prisma.payment.findFirst({ where: { orderId: id, status: { in: ['pending', 'waiting_for_capture'] }, OR: [{ expiresAt: null }, { expiresAt: { gt: new Date() } }], }, orderBy: { createdAt: 'desc' }, }) if (existingPayment && existingPayment.confirmationUrl) { return { confirmationUrl: existingPayment.confirmationUrl } } const idempotencyKey = `${id}-${Date.now()}` const returnUrl = `${process.env.CLIENT_PUBLIC_URL || 'http://127.0.0.1:5173'}/me/orders/${id}?paid=1` const clientIp = request.ip const amount = { value: (order.totalCents / 100).toFixed(2), currency: order.currency, } const receipt = buildReceipt({ orderItems: order.items, deliveryFeeCents: order.deliveryFeeCents, userEmail: userEmail, }) let result try { result = await createPayment({ amount, description: `Оплата заказа №${order.id.slice(-6)}`, receipt, confirmation: { type: 'redirect', return_url: returnUrl }, metadata: { orderId: order.id }, idempotencyKey, clientIp, }) } catch (err) { request.log.error({ err, orderId: id }, 'YooKassa createPayment failed') return reply.code(502).send({ error: 'Не удалось создать платёж. Платёжный сервис временно недоступен.', }) } await prisma.payment.create({ data: { orderId: order.id, yookassaPaymentId: result.paymentId, status: result.status, amountCents: order.totalCents, currency: order.currency, confirmationUrl: result.confirmationUrl, expiresAt: result.expiresAt ? new Date(result.expiresAt) : null, }, }) return { confirmationUrl: result.confirmationUrl } }) fastify.get('/api/me/orders/:orderId/payment', { preHandler: [fastify.authenticate] }, async (request, reply) => { const userId = request.user.sub const { orderId } = request.params const order = await prisma.order.findFirst({ where: { id: orderId, userId } }) if (!order) return reply.code(404).send({ error: 'Заказ не найден' }) const payment = await prisma.payment.findFirst({ where: { orderId }, orderBy: { createdAt: 'desc' }, }) if (!payment) { return { status: null, paid: false } } if (payment.status === 'succeeded' || payment.status === 'canceled') { return { status: payment.status, paid: payment.status === 'succeeded' } } try { const ykPayment = await getPayment(payment.yookassaPaymentId) if (ykPayment.status !== payment.status) { await prisma.payment.update({ where: { id: payment.id }, data: { status: ykPayment.status }, }) if (ykPayment.status === 'succeeded' && order.status === 'PENDING_PAYMENT') { const updated = await prisma.order.updateMany({ where: { id: orderId, status: 'PENDING_PAYMENT' }, data: { status: 'PAID' }, }) if (updated.count > 0) { request.server.eventBus.emit(NOTIFICATION_EVENTS.PAYMENT_STATUS_CHANGED, { orderId, userId: order.userId, paymentStatus: 'paid', }) } } } return { status: ykPayment.status, paid: ykPayment.paid } } catch { return { status: payment.status, paid: payment.status === 'succeeded' } } }) }