const pool = require('../../database/database'); const AbonemmentRepository = require('../../repositories/abonemments/AbonemmentRepository'); class AbonemmentService { constructor() { this.repo = new AbonemmentRepository(); } isAdmin(user) { return user && ['admin', 'super_admin'].includes(user.role); } addInterval(date, interval, count) { const d = new Date(date); if (interval === 'day') d.setDate(d.getDate() + count); if (interval === 'week') d.setDate(d.getDate() + 7 * count); if (interval === 'month') d.setMonth(d.getMonth() + count); if (interval === 'year') d.setFullYear(d.getFullYear() + count); return d; } async getCoffeeProduct(id) { const [rows] = await pool.query( `SELECT id, price, currency, billing_interval, interval_count, state AS is_active, pack_group FROM coffee_table WHERE id = ?`, [id], ); return rows[0] || null; } // Helper: normalize email normalizeEmail(email) { return typeof email === 'string' ? email.trim().toLowerCase() : null; } // NEW: single bundle subscribe using items array (12 packs, 120 capsules) async subscribeOrder({ items, billingInterval, intervalCount, isAutoRenew, firstName, lastName, email, street, postalCode, city, country, frequency, startDate, actorUser, referredBy, // NEW: referred_by field }) { console.log('[SUBSCRIBE ORDER] Start processing subscription order'); console.log('[SUBSCRIBE ORDER] Payload:', { firstName, lastName, email, street, postalCode, city, country, frequency, startDate, }); const normalizedEmail = this.normalizeEmail(email); 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, packs, price_per_pack: Number(product.price), currency: product.currency, }); } const now = new Date(); const startDateObj = startDate ? new Date(startDate) : now; const nextBilling = this.addInterval(startDateObj, billingInterval || 'month', intervalCount || 1); const snapshot = { status: 'active', started_at: startDateObj, next_billing_at: nextBilling, billing_interval: billingInterval || 'month', interval_count: intervalCount || 1, price: Number(totalPrice.toFixed(2)), currency: breakdown[0]?.currency || 'EUR', is_auto_renew: isAutoRenew !== false, actor_user_id: actorUser?.id, details: { origin: 'subscribe_order', total_packs: totalPacks }, pack_breakdown: breakdown, first_name: firstName, last_name: lastName, email: normalizedEmail, street, postal_code: postalCode, city, country, frequency, referred_by: referredBy || null, // Pass referred_by to snapshot }; return this.repo.createAbonement(referredBy, snapshot); // Pass referredBy to repository } async subscribe({ userId, coffeeId, billingInterval, intervalCount, isAutoRenew, targetUserId, recipientName, recipientEmail, recipientNotes, actorUser, referredBy, // NEW: referred_by field }) { console.log('[SUBSCRIBE] Start processing single subscription'); // NEW console.log('[SUBSCRIBE] Recipient email:', recipientEmail); // NEW const normalizedRecipientEmail = this.normalizeEmail(recipientEmail); console.log('[SUBSCRIBE] Normalized recipient email:', normalizedRecipientEmail); // NEW if (coffeeId === undefined || coffeeId === null) throw new Error('coffeeId is required'); const hasRecipientFields = recipientName || normalizedRecipientEmail || recipientNotes; if (targetUserId && hasRecipientFields) throw new Error('Provide either target_user_id or recipient fields, not both'); if (hasRecipientFields && !normalizedRecipientEmail) throw new Error('recipient_email is required when subscribing for another person'); const safeUserId = userId ?? null; const isForMe = !targetUserId && !hasRecipientFields; const ownerUserId = targetUserId ?? (hasRecipientFields ? null : safeUserId); const purchaserUserId = isForMe ? null : actorUser?.id || null; const recipientMeta = targetUserId ? { target_user_id: targetUserId } : hasRecipientFields ? { recipient_name: recipientName, recipient_email: normalizedRecipientEmail, recipient_notes: recipientNotes } : null; const product = await this.getCoffeeProduct(coffeeId); if (!product || !product.is_active) throw new Error('Product not available'); const canonicalPackGroup = product.pack_group || `product:${coffeeId}`; const now = new Date(); const nextBilling = this.addInterval( now, billingInterval || product.billing_interval, intervalCount || product.interval_count || 1, ); const details = { origin: 'subscribe', pack_group: canonicalPackGroup }; if (recipientMeta) details.recipient = recipientMeta; const snapshot = { status: 'active', pack_group: canonicalPackGroup, started_at: now, next_billing_at: nextBilling, billing_interval: billingInterval || product.billing_interval, interval_count: intervalCount || product.interval_count || 1, price: product.price, currency: product.currency, is_auto_renew: isAutoRenew !== false, actor_user_id: actorUser?.id, notes: recipientMeta && recipientMeta.recipient_name ? recipientMeta.recipient_name : undefined, recipient_email: normalizedRecipientEmail || null, // CHANGED details, recipient: recipientMeta || undefined, purchaser_user_id: purchaserUserId, // NEW referred_by: referredBy || null, // Pass referred_by to snapshot }; const existing = await this.repo.findActiveOrPausedByUserAndProduct(ownerUserId ?? null, canonicalPackGroup); const abonement = existing ? await this.repo.updateExistingAbonementForSubscribe(existing.id, snapshot) : await this.repo.createAbonement(ownerUserId ?? null, snapshot, referredBy); // Pass referredBy to repository console.log('[SUBSCRIBE] Single subscription completed successfully'); // NEW return abonement; } // NEW: authorization helper canManageAbonement(abon, actorUser) { if (this.isAdmin(actorUser)) return true; const actorId = actorUser?.id; if (!actorId) return false; if (abon.user_id && abon.user_id === actorId) return true; if (!abon.user_id && abon.purchaser_user_id && abon.purchaser_user_id === actorId) return true; return false; } async pause({ abonementId, actorUser }) { const abon = await this.repo.getAbonementById(abonementId); if (!abon) throw new Error('Not found'); if (!this.canManageAbonement(abon, actorUser)) throw new Error('Forbidden'); // NEW if (!abon.isActive) throw new Error('Only active abonements can be paused'); return this.repo.transitionStatus(abonementId, 'paused', { event_type: 'paused', actor_user_id: actorUser?.id, details: { pack_group: abon.pack_group }, }); } async resume({ abonementId, actorUser }) { const abon = await this.repo.getAbonementById(abonementId); if (!abon) throw new Error('Not found'); if (!this.canManageAbonement(abon, actorUser)) throw new Error('Forbidden'); // NEW if (!abon.isPaused) throw new Error('Only paused abonements can be resumed'); return this.repo.transitionStatus(abonementId, 'active', { event_type: 'resumed', actor_user_id: actorUser?.id, details: { pack_group: abon.pack_group }, }); } async cancel({ abonementId, actorUser }) { const abon = await this.repo.getAbonementById(abonementId); if (!abon) throw new Error('Not found'); if (!this.canManageAbonement(abon, actorUser)) throw new Error('Forbidden'); // NEW if (abon.isCanceled) return abon; return this.repo.transitionStatus(abonementId, 'canceled', { event_type: 'canceled', actor_user_id: actorUser?.id, ended_at: new Date(), details: { pack_group: abon.pack_group }, }); } async renew({ abonementId, actorUser, invoiceId }) { const abon = await this.repo.getAbonementById(abonementId); if (!abon) throw new Error('Not found'); if (!abon.isActive) throw new Error('Only active abonements can be renewed'); const next = this.addInterval(new Date(abon.next_billing_at || new Date()), abon.billing_interval, abon.interval_count); return this.repo.transitionBilling(abonementId, next, { event_type: 'renewed', actor_user_id: actorUser?.id || null, details: { pack_group: abon.pack_group, ...(invoiceId ? { invoiceId } : {}) }, }); } async getMyAbonements({ userId }) { return this.repo.listByUser(userId); } async getHistory({ abonementId }) { return this.repo.listHistory(abonementId); } async linkGiftFlagsToUser(email, userId) { const normalizedEmail = this.normalizeEmail(email); // NEW const pending = await this.repo.findPendingNoUserAboMailsByEmail(normalizedEmail); for (const row of pending) { await this.repo.linkAbonementToUser(row.abonement_id, userId); await this.repo.appendHistory( row.abonement_id, 'gift_linked', userId, { email, pack_group: (await this.repo.getAbonementById(row.abonement_id))?.pack_group || null } ); await this.repo.markNoUserAboMailLinked(row.id); // CHANGED } return pending.length; } async adminList({ status }) { const [rows] = await pool.query( `SELECT * FROM coffee_abonements ${status ? 'WHERE status = ?' : ''} ORDER BY created_at DESC LIMIT 200`, status ? [status] : [], ); return rows; } async adminRenew({ abonementId, actorUser }) { if (!this.isAdmin(actorUser)) throw new Error('Forbidden'); return this.renew({ abonementId, actorUser }); } async getReferredSubscriptions({ userId, email }) { if (!userId || !email) throw new Error('User ID and email are required'); const rows = await this.repo.findByReferredByAndEmail(userId, email); // Collect distinct coffee_table_ids from pack_breakdown const idsSet = new Set(); for (const r of rows) { const breakdown = Array.isArray(r.pack_breakdown) ? r.pack_breakdown : []; for (const item of breakdown) { const id = item?.coffee_table_id; if (id !== undefined && id !== null) idsSet.add(Number(id)); } } const ids = Array.from(idsSet); let nameMap = {}; if (ids.length) { const [nameRows] = await pool.query( `SELECT id, title FROM coffee_table WHERE id IN (${ids.map(() => '?').join(',')})`, ids ); nameMap = (nameRows || []).reduce((acc, row) => { acc[Number(row.id)] = row.title; // CHANGED: use title return acc; }, {}); } // Attach coffee_name to each pack_breakdown item const enriched = rows.map(r => { const breakdown = Array.isArray(r.pack_breakdown) ? r.pack_breakdown : []; const withNames = breakdown.map(item => ({ ...item, coffee_name: nameMap[Number(item.coffee_table_id)] || null, })); // Return a plain object with enriched breakdown return { ...r, pack_breakdown: withNames, }; }); return enriched; } } module.exports = AbonemmentService;