From 002dbc78c135ac7b67ea63810c404b74dbfcf9be Mon Sep 17 00:00:00 2001 From: seaznCode Date: Fri, 20 Feb 2026 21:45:02 +0100 Subject: [PATCH] feat: add updateContent method for abonements and corresponding route --- .../abonemments/AbonemmentController.js | 22 +++++++ .../abonemments/AbonemmentRepository.js | 38 ++++++++++++ routes/getRoutes.js | 1 + routes/patchRoutes.js | 2 + services/abonemments/AbonemmentService.js | 61 +++++++++++++++++++ 5 files changed, 124 insertions(+) diff --git a/controller/abonemments/AbonemmentController.js b/controller/abonemments/AbonemmentController.js index 5341c54..3a83123 100644 --- a/controller/abonemments/AbonemmentController.js +++ b/controller/abonemments/AbonemmentController.js @@ -74,6 +74,28 @@ module.exports = { } }, + async updateContent(req, res) { + try { + const rawUser = req.user || {}; + const actorUser = { ...rawUser, id: rawUser.id ?? rawUser.userId ?? null }; + const data = await service.updateContent({ + abonementId: req.params.id, + actorUser, + items: req.body.items, + }); + return res.json({ success: true, data }); + } catch (err) { + console.error('[ABONEMENT UPDATE CONTENT]', err); + if (err?.message === 'Not found') { + return res.status(404).json({ success: false, message: 'Abonement not found' }); + } + if (err?.message === 'Forbidden') { + return res.status(403).json({ success: false, message: 'Forbidden' }); + } + return res.status(400).json({ success: false, message: err.message }); + } + }, + async renew(req, res) { try { const rawUser = req.user || {}; diff --git a/repositories/abonemments/AbonemmentRepository.js b/repositories/abonemments/AbonemmentRepository.js index a770996..e643a1d 100644 --- a/repositories/abonemments/AbonemmentRepository.js +++ b/repositories/abonemments/AbonemmentRepository.js @@ -256,6 +256,44 @@ class AbonemmentRepository { return this.getAbonementById(id); } + async transitionContent(id, contentPayload = {}) { + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + await conn.query( + `UPDATE coffee_abonements + SET pack_breakdown = ?, price = ?, currency = ?, updated_at = ? + WHERE id = ?`, + [ + JSON.stringify(contentPayload.pack_breakdown || []), + contentPayload.price ?? null, + contentPayload.currency ?? 'EUR', + contentPayload.updated_at || new Date(), + id, + ], + ); + await conn.query( + `INSERT INTO coffee_abonement_history + (abonement_id, event_type, event_at, actor_user_id, details, created_at) + VALUES (?, ?, ?, ?, ?, NOW())`, + [ + id, + contentPayload.event_type || 'content_updated', + contentPayload.event_at || new Date(), + contentPayload.actor_user_id || null, + JSON.stringify(contentPayload.details || {}), + ], + ); + await conn.commit(); + } catch (err) { + await conn.rollback(); + throw err; + } finally { + conn.release(); + } + return this.getAbonementById(id); + } + async listDueForBilling(now) { const [rows] = await pool.query( `SELECT * FROM coffee_abonements diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 503da52..c13fb45 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -116,6 +116,7 @@ router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmi // Admin: coffee products router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list); router.get('/admin/coffee/active', authMiddleware, adminOnly, CoffeeController.listActive); +router.get('/coffee/active', authMiddleware, CoffeeController.listActive); // Matrix GETs diff --git a/routes/patchRoutes.js b/routes/patchRoutes.js index 13ddb18..b325b8a 100644 --- a/routes/patchRoutes.js +++ b/routes/patchRoutes.js @@ -12,6 +12,7 @@ const PoolController = require('../controller/pool/PoolController'); // <-- new const MatrixController = require('../controller/matrix/MatrixController'); // <-- new const AffiliateController = require('../controller/affiliate/AffiliateController'); // <-- new const NewsController = require('../controller/news/NewsController'); +const AbonemmentController = require('../controller/abonemments/AbonemmentController'); const multer = require('multer'); const upload = multer({ storage: multer.memoryStorage() }); @@ -58,6 +59,7 @@ router.patch('/admin/news/:id/status', authMiddleware, adminOnly, NewsController // Personal profile (self-service) - no admin guard router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic); router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank); +router.patch('/abonements/:id/content', authMiddleware, AbonemmentController.updateContent); // Add other PATCH routes here as needed diff --git a/services/abonemments/AbonemmentService.js b/services/abonemments/AbonemmentService.js index cade16c..2a4c479 100644 --- a/services/abonemments/AbonemmentService.js +++ b/services/abonemments/AbonemmentService.js @@ -449,6 +449,67 @@ class AbonemmentService { }); } + async updateContent({ abonementId, actorUser, items }) { + const abon = await this.repo.getAbonementById(abonementId); + if (!abon) throw new Error('Not found'); + if (!this.canManageAbonement(abon, actorUser)) throw new Error('Forbidden'); + if (!['active', 'paused'].includes(abon.status)) { + throw new Error('Only active or paused abonements can be updated'); + } + + if (!Array.isArray(items) || items.length === 0) { + throw new Error('items must be a non-empty array'); + } + + let totalPacks = 0; + let totalPrice = 0; + const breakdown = []; + + for (const item of items) { + const coffeeId = item?.coffeeId; + const packs = Number(item?.quantity ?? 0); + if (!coffeeId) throw new Error('coffeeId is required for each item'); + if (!Number.isFinite(packs) || packs <= 0) { + throw new Error('quantity must be a positive integer per item'); + } + + const product = await this.getCoffeeProduct(coffeeId); + if (!product || !product.is_active) throw new Error(`Product ${coffeeId} not available`); + + totalPacks += packs; + totalPrice += packs * Number(product.price); + breakdown.push({ + coffee_table_id: coffeeId, + coffee_title: product.title || null, + packs, + price_per_pack: Number(product.price), + currency: product.currency, + }); + } + + if (totalPacks !== 6 && totalPacks !== 12) { + throw new Error('Order must contain exactly 6 packs (60 capsules) or 12 packs (120 capsules).'); + } + + const previousPacks = Array.isArray(abon.pack_breakdown) + ? abon.pack_breakdown.reduce((sum, item) => sum + Number(item?.packs || item?.quantity || 0), 0) + : null; + + return this.repo.transitionContent(abonementId, { + pack_breakdown: breakdown, + price: Number(totalPrice.toFixed(2)), + currency: breakdown[0]?.currency || abon.currency || 'EUR', + actor_user_id: actorUser?.id || null, + event_type: 'content_updated', + details: { + pack_group: abon.pack_group, + previous_total_packs: previousPacks, + total_packs: totalPacks, + effective_from: 'next_billing_cycle', + }, + }); + } + async renew({ abonementId, actorUser, invoiceId }) { const abon = await this.repo.getAbonementById(abonementId); if (!abon) throw new Error('Not found');