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 });
}
});
// 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 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 },