From 4580264de9ec53767caa46ccb28bf5df529ab1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=20=28Claude=29?= Date: Thu, 11 Jun 2026 19:40:10 +0300 Subject: [PATCH] feat: yukassa reads keys from app_settings + monthly reset endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - yukassa.js: getConfig() читает YUKASSA_SHOP_ID/SECRET/RETURN_URL из app_settings (fallback env) - routes/billing.js: POST /monthly-reset — запускает processMonthlyResets() - app_settings: категория 'payments' с ключами ЮKassa - cron: 0 6 * * * zeropost-billing-reset.sh --- src/routes/billing.js | 12 ++++++++++++ src/services/yukassa.js | 24 +++++++++++++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/routes/billing.js b/src/routes/billing.js index 27ed70a..6263490 100644 --- a/src/routes/billing.js +++ b/src/routes/billing.js @@ -108,3 +108,15 @@ router.post('/webhook', express.json({ type: '*/*' }), async (req, res) => { res.status(500).json({ error: err.message }); } }); + +// POST /api/billing/monthly-reset — ежемесячный сброс кредитов (вызывается cron) +router.post('/monthly-reset', async (req, res) => { + try { + const processed = await billing.processMonthlyResets(); + console.log(`[Billing] monthly reset: ${processed} users processed`); + res.json({ ok: true, processed }); + } catch (err) { + console.error('[Billing] monthly-reset error:', err.message); + res.status(500).json({ error: err.message }); + } +}); diff --git a/src/services/yukassa.js b/src/services/yukassa.js index 714a5e3..979bab3 100644 --- a/src/services/yukassa.js +++ b/src/services/yukassa.js @@ -15,10 +15,20 @@ const { YooCheckout } = require('@a2seven/yoo-checkout'); const { query } = require('../config/db'); const billing = require('./billing'); -function getClient() { - const shopId = process.env.YUKASSA_SHOP_ID; - const secret = process.env.YUKASSA_SECRET; - if (!shopId || !secret) throw new Error('YUKASSA_SHOP_ID и YUKASSA_SECRET не настроены'); +async function getConfig() { + const { rows } = await query( + "SELECT key, value FROM app_settings WHERE key IN ('YUKASSA_SHOP_ID','YUKASSA_SECRET','YUKASSA_RETURN_URL')" + ); + const s = Object.fromEntries(rows.map(r => [r.key, r.value?.trim()])); + return { + shopId: s.YUKASSA_SHOP_ID || process.env.YUKASSA_SHOP_ID || '', + secret: s.YUKASSA_SECRET || process.env.YUKASSA_SECRET || '', + returnUrl: s.YUKASSA_RETURN_URL || process.env.YUKASSA_RETURN_URL || 'https://app.zeropost.ru/billing?paid=1', + }; +} + +function getClient(shopId, secret) { + if (!shopId || !secret) throw new Error('YUKASSA_SHOP_ID и YUKASSA_SECRET не настроены в app_settings'); return new YooCheckout({ shopId, secretKey: secret }); } @@ -31,14 +41,14 @@ async function createPayment({ userId, planCode, userEmail }) { if (!plan) throw new Error(`План "${planCode}" не найден`); if (plan.price_rub === 0) throw new Error('Free план не требует оплаты'); - const returnUrl = process.env.YUKASSA_RETURN_URL || 'https://app.zeropost.ru/billing?paid=1'; - const client = getClient(); + const cfg = await getConfig(); + const client = getClient(cfg.shopId, cfg.secret); const idempotenceKey = `${userId}-${planCode}-${Date.now()}`; const payment = await client.createPayment({ amount: { value: String(plan.price_rub) + '.00', currency: 'RUB' }, payment_method_data: { type: 'bank_card' }, - confirmation: { type: 'redirect', return_url: returnUrl }, + confirmation: { type: 'redirect', return_url: cfg.returnUrl }, capture: true, description: `${plan.name} — ZeroPost`, metadata: { user_id: String(userId), plan_code: planCode },