diff --git a/src/routes/admin.js b/src/routes/admin.js index 1ebd7b0..47c66f8 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -173,3 +173,63 @@ router.patch('/users/:id', async (req, res) => { res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); + +// ── PROMO CODES ────────────────────────────────────────────── + +// GET /api/admin/promos — список промокодов +router.get('/promos', async (req, res) => { + if (!await requireAdmin(req, res)) return; + try { + const { rows } = 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 + GROUP BY p.id ORDER BY p.created_at DESC + `); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST /api/admin/promos — создать промокод +router.post('/promos', async (req, res) => { + if (!await requireAdmin(req, res)) return; + const { code, type = 'credits', value, max_uses = 1, expires_at, description } = req.body; + if (!code || !value) return res.status(400).json({ error: 'code и value обязательны' }); + if (!['credits','discount_pct'].includes(type)) return res.status(400).json({ error: 'type: credits | discount_pct' }); + try { + const { rows: [promo] } = await query(` + INSERT INTO promo_codes (code, type, value, max_uses, expires_at, description) + VALUES ($1,$2,$3,$4,$5,$6) RETURNING * + `, [code.toUpperCase(), type, value, max_uses, expires_at || null, description || null]); + res.json(promo); + } catch (err) { + if (err.code === '23505') return res.status(409).json({ error: 'Такой промокод уже существует' }); + res.status(500).json({ error: err.message }); + } +}); + +// PATCH /api/admin/promos/:id — обновить (деактивировать и т.п.) +router.patch('/promos/:id', async (req, res) => { + if (!await requireAdmin(req, res)) return; + const { is_active, description, max_uses, expires_at } = req.body; + try { + const sets = []; const vals = []; + if (is_active !== undefined) sets.push(`is_active=$${vals.push(is_active)}`); + if (description !== undefined) sets.push(`description=$${vals.push(description)}`); + if (max_uses !== undefined) sets.push(`max_uses=$${vals.push(max_uses)}`); + if (expires_at !== undefined) sets.push(`expires_at=$${vals.push(expires_at)}`); + if (!sets.length) return res.status(400).json({ error: 'nothing to update' }); + vals.push(req.params.id); + const { rows: [p] } = await query(`UPDATE promo_codes SET ${sets.join(',')} WHERE id=$${vals.length} RETURNING *`, vals); + res.json(p); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// DELETE /api/admin/promos/:id +router.delete('/promos/:id', async (req, res) => { + if (!await requireAdmin(req, res)) return; + try { + await query('DELETE FROM promo_codes WHERE id=$1', [req.params.id]); + res.json({ ok: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); diff --git a/src/routes/billing.js b/src/routes/billing.js index a418afe..c6762a3 100644 --- a/src/routes/billing.js +++ b/src/routes/billing.js @@ -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 }); } +});