feat: add updateContent method for abonements and corresponding route

This commit is contained in:
seaznCode 2026-02-20 21:45:02 +01:00
parent b7d9ef8371
commit 002dbc78c1
5 changed files with 124 additions and 0 deletions

View File

@ -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) { async renew(req, res) {
try { try {
const rawUser = req.user || {}; const rawUser = req.user || {};

View File

@ -256,6 +256,44 @@ class AbonemmentRepository {
return this.getAbonementById(id); 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) { async listDueForBilling(now) {
const [rows] = await pool.query( const [rows] = await pool.query(
`SELECT * FROM coffee_abonements `SELECT * FROM coffee_abonements

View File

@ -116,6 +116,7 @@ router.get('/company-stamps/all', authMiddleware, adminOnly, forceCompanyForAdmi
// Admin: coffee products // Admin: coffee products
router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list); router.get('/admin/coffee', authMiddleware, adminOnly, CoffeeController.list);
router.get('/admin/coffee/active', authMiddleware, adminOnly, CoffeeController.listActive); router.get('/admin/coffee/active', authMiddleware, adminOnly, CoffeeController.listActive);
router.get('/coffee/active', authMiddleware, CoffeeController.listActive);
// Matrix GETs // Matrix GETs

View File

@ -12,6 +12,7 @@ const PoolController = require('../controller/pool/PoolController'); // <-- new
const MatrixController = require('../controller/matrix/MatrixController'); // <-- new const MatrixController = require('../controller/matrix/MatrixController'); // <-- new
const AffiliateController = require('../controller/affiliate/AffiliateController'); // <-- new const AffiliateController = require('../controller/affiliate/AffiliateController'); // <-- new
const NewsController = require('../controller/news/NewsController'); const NewsController = require('../controller/news/NewsController');
const AbonemmentController = require('../controller/abonemments/AbonemmentController');
const multer = require('multer'); const multer = require('multer');
const upload = multer({ storage: multer.memoryStorage() }); 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 // Personal profile (self-service) - no admin guard
router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic); router.patch('/profile/personal/basic', authMiddleware, PersonalProfileController.updateBasic);
router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank); router.patch('/profile/personal/bank', authMiddleware, PersonalProfileController.updateBank);
router.patch('/abonements/:id/content', authMiddleware, AbonemmentController.updateContent);
// Add other PATCH routes here as needed // Add other PATCH routes here as needed

View File

@ -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 }) { async renew({ abonementId, actorUser, invoiceId }) {
const abon = await this.repo.getAbonementById(abonementId); const abon = await this.repo.getAbonementById(abonementId);
if (!abon) throw new Error('Not found'); if (!abon) throw new Error('Not found');