const db = require('../db/db'); /* ═══════════════════════════════════════════════════════════════════════ Shop — Items, Purchases, Coins ═══════════════════════════════════════════════════════════════════════ */ /* GET /api/shop/items — list all active shop items + owned status */ function getItems(req, res) { const userId = req.user.id; const items = db.prepare(` SELECT si.*, (SELECT 1 FROM user_purchases up WHERE up.item_id = si.id AND up.user_id = ?) AS owned FROM shop_items si WHERE si.is_active = 1 ORDER BY si.price `).all(userId); const user = db.prepare('SELECT coins FROM users WHERE id = ?').get(userId); res.json({ items, coins: (user && user.coins) || 0 }); } /* POST /api/shop/items/:id/purchase — buy an item (atomic transaction) */ function purchaseItem(req, res) { const userId = req.user.id; const itemId = Number(req.params.id); const item = db.prepare('SELECT * FROM shop_items WHERE id = ? AND is_active = 1').get(itemId); if (!item) return res.status(404).json({ error: 'Предмет не найден' }); const alreadyOwned = db.prepare('SELECT 1 FROM user_purchases WHERE user_id = ? AND item_id = ?').get(userId, itemId); if (alreadyOwned) return res.status(400).json({ error: 'Вы уже купили этот предмет' }); // Atomic: conditional UPDATE (race-safe even under concurrent purchases) const doPurchase = db.transaction(() => { const upd = db.prepare( 'UPDATE users SET coins = coins - ? WHERE id = ? AND coins >= ?' ).run(item.price, userId, item.price); if (upd.changes === 0) return { err: 'Недостаточно монет' }; db.prepare('INSERT INTO user_purchases (user_id, item_id) VALUES (?, ?)').run(userId, itemId); const updated = db.prepare('SELECT coins FROM users WHERE id = ?').get(userId); return { coins: (updated && updated.coins) || 0 }; }); const result = doPurchase(); if (result.err) return res.status(400).json({ error: result.err }); res.json({ ok: true, coins: result.coins, item }); } /* GET /api/shop/purchases — list user's purchases with item details */ function getPurchases(req, res) { const rows = db.prepare(` SELECT up.id AS purchase_id, up.purchased_at, si.* FROM user_purchases up JOIN shop_items si ON si.id = up.item_id WHERE up.user_id = ? ORDER BY up.purchased_at DESC `).all(req.user.id); res.json(rows); } /* GET /api/shop/coins — return user's coin balance */ function getCoins(req, res) { const user = db.prepare('SELECT coins FROM users WHERE id = ?').get(req.user.id); res.json({ coins: (user && user.coins) || 0 }); } /* GET /api/shop/my-active — return user's active cosmetics */ function getMyActive(req, res) { const u = db.prepare('SELECT avatar_frame, active_title, active_effect FROM users WHERE id = ?').get(req.user.id); if (!u) return res.json({}); // Resolve full data for each active item const result = { frame: null, title: null, effect: null }; // Frame from avatar_frame (gamification frames) — handled separately // Shop frame override if (u.avatar_frame && u.avatar_frame !== 'default') { result.frame = { id: u.avatar_frame }; } if (u.active_title) { const item = db.prepare('SELECT data FROM shop_items WHERE id = ?').get(u.active_title); if (item) try { result.title = JSON.parse(item.data); } catch {} } if (u.active_effect) { const item = db.prepare('SELECT data FROM shop_items WHERE id = ?').get(u.active_effect); if (item) try { result.effect = JSON.parse(item.data); } catch {} } res.json(result); } /* POST /api/shop/activate — activate a purchased item (or deactivate with itemId=null) */ function activateItem(req, res) { const userId = req.user.id; const { itemId } = req.body; // Deactivate: pass itemId = null and type if (!itemId) { const { type } = req.body; if (type === 'title') db.prepare('UPDATE users SET active_title = NULL WHERE id = ?').run(userId); if (type === 'effect') db.prepare('UPDATE users SET active_effect = NULL WHERE id = ?').run(userId); if (type === 'frame') db.prepare("UPDATE users SET avatar_frame = 'default' WHERE id = ?").run(userId); return res.json({ ok: true }); } const item = db.prepare('SELECT * FROM shop_items WHERE id = ?').get(itemId); if (!item) return res.status(404).json({ error: 'Предмет не найден' }); const owned = db.prepare('SELECT 1 FROM user_purchases WHERE user_id = ? AND item_id = ?').get(userId, itemId); if (!owned) return res.status(403).json({ error: 'Предмет не куплен' }); let data; try { data = JSON.parse(item.data); } catch { data = {}; } if (item.type === 'frame') db.prepare('UPDATE users SET avatar_frame = ? WHERE id = ?').run('shop_' + itemId, userId); if (item.type === 'title') db.prepare('UPDATE users SET active_title = ? WHERE id = ?').run(itemId, userId); if (item.type === 'effect') db.prepare('UPDATE users SET active_effect = ? WHERE id = ?').run(itemId, userId); res.json({ ok: true, type: item.type, data }); } /* ═══════════════════════════════════════════════════════════════════════ Admin — CRUD shop items, award coins, stats ═══════════════════════════════════════════════════════════════════════ */ /* GET /api/shop/admin/items — all items (including inactive) */ function adminGetItems(_req, res) { const items = db.prepare(` SELECT si.*, (SELECT COUNT(*) FROM user_purchases up WHERE up.item_id = si.id) AS sold_count FROM shop_items si ORDER BY si.id `).all(); res.json(items); } /* POST /api/shop/admin/items — create item */ function adminCreateItem(req, res) { const { name, description, type, category, price, data, icon, is_active } = req.body; if (!name || !type || price == null) return res.status(400).json({ error: 'name, type, price required' }); const r = db.prepare( 'INSERT INTO shop_items (name, description, type, category, price, data, icon, is_active) VALUES (?,?,?,?,?,?,?,?)' ).run(name, description || '', type, category || 'cosmetic', price, data || '{}', icon || 'star', is_active ?? 1); res.json({ ok: true, id: r.lastInsertRowid }); } /* PUT /api/shop/admin/items/:id — update item */ function adminUpdateItem(req, res) { const id = Number(req.params.id); const item = db.prepare('SELECT * FROM shop_items WHERE id = ?').get(id); if (!item) return res.status(404).json({ error: 'Item not found' }); const { name, description, type, category, price, data, icon, is_active } = req.body; db.prepare(`UPDATE shop_items SET name=COALESCE(?,name), description=COALESCE(?,description), type=COALESCE(?,type), category=COALESCE(?,category), price=COALESCE(?,price), data=COALESCE(?,data), icon=COALESCE(?,icon), is_active=COALESCE(?,is_active) WHERE id=?` ).run(name, description, type, category, price, data, icon, is_active, id); res.json({ ok: true }); } /* DELETE /api/shop/admin/items/:id — delete item */ function adminDeleteItem(req, res) { const id = Number(req.params.id); db.prepare('DELETE FROM user_purchases WHERE item_id = ?').run(id); db.prepare('DELETE FROM shop_items WHERE id = ?').run(id); res.json({ ok: true }); } /* POST /api/shop/admin/award-coins — award coins to user */ const SHOP_AWARD_MAX = 1_000_000; function adminAwardCoins(req, res) { const { userId, amount, reason } = req.body; const amt = Number(amount); if (!userId || !Number.isFinite(amt) || amt <= 0 || amt > SHOP_AWARD_MAX) return res.status(400).json({ error: `userId and amount (1..${SHOP_AWARD_MAX}) required` }); const result = db.prepare('UPDATE users SET coins = coins + ? WHERE id = ?').run(amt, userId); if (result.changes === 0) return res.status(404).json({ error: 'User not found' }); const user = db.prepare('SELECT coins FROM users WHERE id = ?').get(userId); res.json({ ok: true, coins: user?.coins || 0 }); } /* GET /api/shop/admin/stats — shop stats */ function adminShopStats(_req, res) { const totalItems = db.prepare('SELECT COUNT(*) as c FROM shop_items').get().c; const activeItems = db.prepare('SELECT COUNT(*) as c FROM shop_items WHERE is_active=1').get().c; const totalPurchases = db.prepare('SELECT COUNT(*) as c FROM user_purchases').get().c; const totalCoinsInCirculation = db.prepare('SELECT COALESCE(SUM(coins),0) as c FROM users').get().c; const topItems = db.prepare(` SELECT si.name, si.price, COUNT(up.id) as sold FROM shop_items si LEFT JOIN user_purchases up ON up.item_id = si.id GROUP BY si.id ORDER BY sold DESC LIMIT 5 `).all(); res.json({ totalItems, activeItems, totalPurchases, totalCoinsInCirculation, topItems }); } module.exports = { getItems, purchaseItem, getPurchases, getCoins, getMyActive, activateItem, adminGetItems, adminCreateItem, adminUpdateItem, adminDeleteItem, adminAwardCoins, adminShopStats };