feat: yukassa reads keys from app_settings + monthly reset endpoint

- 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
This commit is contained in:
Ник (Claude)
2026-06-11 19:40:10 +03:00
parent 9baa0f0959
commit 4580264de9
2 changed files with 29 additions and 7 deletions
+12
View File
@@ -108,3 +108,15 @@ router.post('/webhook', express.json({ type: '*/*' }), async (req, res) => {
res.status(500).json({ error: err.message }); 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 });
}
});
+17 -7
View File
@@ -15,10 +15,20 @@ const { YooCheckout } = require('@a2seven/yoo-checkout');
const { query } = require('../config/db'); const { query } = require('../config/db');
const billing = require('./billing'); const billing = require('./billing');
function getClient() { async function getConfig() {
const shopId = process.env.YUKASSA_SHOP_ID; const { rows } = await query(
const secret = process.env.YUKASSA_SECRET; "SELECT key, value FROM app_settings WHERE key IN ('YUKASSA_SHOP_ID','YUKASSA_SECRET','YUKASSA_RETURN_URL')"
if (!shopId || !secret) throw new Error('YUKASSA_SHOP_ID и YUKASSA_SECRET не настроены'); );
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 }); return new YooCheckout({ shopId, secretKey: secret });
} }
@@ -31,14 +41,14 @@ async function createPayment({ userId, planCode, userEmail }) {
if (!plan) throw new Error(`План "${planCode}" не найден`); if (!plan) throw new Error(`План "${planCode}" не найден`);
if (plan.price_rub === 0) throw new Error('Free план не требует оплаты'); if (plan.price_rub === 0) throw new Error('Free план не требует оплаты');
const returnUrl = process.env.YUKASSA_RETURN_URL || 'https://app.zeropost.ru/billing?paid=1'; const cfg = await getConfig();
const client = getClient(); const client = getClient(cfg.shopId, cfg.secret);
const idempotenceKey = `${userId}-${planCode}-${Date.now()}`; const idempotenceKey = `${userId}-${planCode}-${Date.now()}`;
const payment = await client.createPayment({ const payment = await client.createPayment({
amount: { value: String(plan.price_rub) + '.00', currency: 'RUB' }, amount: { value: String(plan.price_rub) + '.00', currency: 'RUB' },
payment_method_data: { type: 'bank_card' }, payment_method_data: { type: 'bank_card' },
confirmation: { type: 'redirect', return_url: returnUrl }, confirmation: { type: 'redirect', return_url: cfg.returnUrl },
capture: true, capture: true,
description: `${plan.name} — ZeroPost`, description: `${plan.name} — ZeroPost`,
metadata: { user_id: String(userId), plan_code: planCode }, metadata: { user_id: String(userId), plan_code: planCode },