feat: promo codes system

DB: promo_codes, promo_usages tables
routes/admin.js: CRUD /api/admin/promos (GET/POST/PATCH/DELETE)
routes/billing.js: POST /api/billing/apply-promo
  Валидация: exists, active, not expired, not exhausted, not used by this user
  type=credits → начисляет через billing.credit()
This commit is contained in:
Ник (Claude)
2026-06-13 09:36:32 +03:00
parent 2360e1f7ae
commit ce74ac9909
2 changed files with 110 additions and 0 deletions
+50
View File
@@ -204,3 +204,53 @@ router.get('/admin/dashboard', async (req, res) => {
});
} catch (err) { res.status(500).json({ error: err.message }); }
});
// POST /api/billing/apply-promo — применить промокод
router.post('/apply-promo', async (req, res) => {
const userId = uid(req);
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
const { code } = req.body;
if (!code?.trim()) return res.status(400).json({ error: 'code обязателен' });
try {
// Ищем промокод
const { rows: [promo] } = await query(`
SELECT p.*, count(pu.id)::int as uses_real
FROM promo_codes p
LEFT JOIN promo_usages pu ON pu.code_id = p.id
WHERE p.code = $1 AND p.is_active = true
AND (p.expires_at IS NULL OR p.expires_at > NOW())
GROUP BY p.id
`, [code.toUpperCase()]);
if (!promo) return res.status(404).json({ error: 'Промокод не найден или истёк' });
if (promo.max_uses !== -1 && promo.uses_real >= promo.max_uses)
return res.status(410).json({ error: 'Промокод исчерпан' });
// Проверяем что пользователь ещё не использовал
const { rows: [used] } = await query(
'SELECT id FROM promo_usages WHERE code_id=$1 AND user_id=$2',
[promo.id, userId]
);
if (used) return res.status(409).json({ error: 'Вы уже использовали этот промокод' });
// Применяем
const billing = require('../services/billing');
if (promo.type === 'credits') {
await billing.credit(userId, promo.value, 'bonus', `Промокод ${promo.code}`, { promo_id: promo.id });
}
// Записываем использование
await query('INSERT INTO promo_usages (code_id, user_id) VALUES ($1,$2)', [promo.id, userId]);
await query('UPDATE promo_codes SET used_count = used_count + 1 WHERE id=$1', [promo.id]);
res.json({
ok: true,
type: promo.type,
value: promo.value,
message: promo.type === 'credits'
? `+${promo.value} кредитов начислено на ваш баланс`
: `Скидка ${promo.value}% применена`,
});
} catch (err) { res.status(500).json({ error: err.message }); }
});