import { Router } from "express"; import { randomUUID } from "crypto"; import { authenticateToken, requirePermission, type AuthenticatedRequest } from "../middleware/auth.middleware"; import { getPaymentProvider, getPaymentProviderName } from "../billing/payment-provider"; import { createBillingPayment, listBillingPayments, getBillingPaymentById, getBillingPaymentByExternalId, markBillingPaymentCanceled, applySucceededPayment, } from "../billing/payments.service"; // Самообслуживание оплаты: создание платежей админом организации, // тестовое подтверждение (mock-провайдер) и публичные webhook'и провайдера. const router = Router(); const MAX_PAYMENT_AMOUNT = 10_000_000; // POST /api/billing/payments — создать платёж на пополнение баланса. // Идемпотентность: заголовок Idempotency-Key (опционально), иначе генерируется ключ. router.post('/api/billing/payments', authenticateToken, requirePermission('billing.manage'), async (req: AuthenticatedRequest, res) => { try { const amountNum = parseFloat(String(req.body?.amount)); if (!amountNum || isNaN(amountNum) || amountNum <= 0) { return res.status(400).json({ success: false, error: 'Сумма должна быть положительным числом' }); } if (amountNum > MAX_PAYMENT_AMOUNT) { return res.status(400).json({ success: false, error: 'Сумма превышает максимально допустимую' }); } const orgId = req.organizationId!; const provider = getPaymentProvider(); const headerKey = req.headers['idempotency-key']; const idempotencyKey = typeof headerKey === 'string' && headerKey.trim() ? `org:${orgId}:${headerKey.trim()}`.slice(0, 255) : `org:${orgId}:payment:${randomUUID()}`; const created = await provider.createPayment({ organizationId: orgId, amount: amountNum, currency: 'RUB', idempotencyKey, description: 'Пополнение баланса', }); const payment = await createBillingPayment({ organizationId: orgId, provider: provider.name, externalId: created.externalId, amount: amountNum, currency: 'RUB', idempotencyKey, }); return res.status(201).json({ success: true, payment, confirmationUrl: created.confirmationUrl ?? null, }); } catch (error) { console.error('Billing payment create error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // GET /api/billing/payments — список платежей организации (новые первыми). router.get('/api/billing/payments', authenticateToken, requirePermission('billing.manage'), async (req: AuthenticatedRequest, res) => { try { const payments = await listBillingPayments(req.organizationId!, 50); return res.json({ success: true, payments }); } catch (error) { console.error('Billing payments list error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/billing/payments/:id/confirm-test — тестовое подтверждение платежа. // Доступно только при PAYMENT_PROVIDER=mock; в остальных режимах маршрут скрыт (404). router.post('/api/billing/payments/:id/confirm-test', authenticateToken, requirePermission('billing.manage'), async (req: AuthenticatedRequest, res) => { try { if (getPaymentProviderName() !== 'mock') { return res.status(404).json({ success: false, error: 'Маршрут недоступен' }); } const id = parseInt(req.params.id); if (isNaN(id)) { return res.status(400).json({ success: false, error: 'Некорректный ID' }); } const payment = await getBillingPaymentById(id, req.organizationId!); if (!payment) { return res.status(404).json({ success: false, error: 'Платёж не найден' }); } const result = await applySucceededPayment(payment.id); return res.json({ success: true, applied: result.applied, payment: result.payment }); } catch (error) { console.error('Billing payment confirm-test error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); // POST /api/billing/webhooks/:provider — публичный webhook платёжного провайдера (без auth). // Подпись проверяется провайдером; повторный webhook по уже обработанному // платежу не зачисляет средства дважды (guard в applySucceededPayment). router.post('/api/billing/webhooks/:provider', async (req, res) => { let provider; try { provider = getPaymentProvider(String(req.params.provider).toLowerCase()); } catch { return res.status(404).json({ success: false, error: 'Неизвестный провайдер' }); } try { const verification = provider.verifyWebhook(req.headers as Record, req.body); if (!verification.valid || !verification.externalId) { return res.status(401).json({ success: false, error: verification.error || 'Неверная подпись webhook' }); } const payment = await getBillingPaymentByExternalId(verification.externalId); if (!payment) { return res.status(404).json({ success: false, error: 'Платёж не найден' }); } if (verification.status === 'succeeded') { await applySucceededPayment(payment.id); } else if (verification.status === 'canceled') { await markBillingPaymentCanceled(payment.id); } return res.json({ success: true }); } catch (error) { console.error('Billing webhook error:', error); return res.status(500).json({ success: false, error: 'Внутренняя ошибка сервера' }); } }); export default router;