diff --git a/services/abonemments/AbonemmentService.js b/services/abonemments/AbonemmentService.js index 79e1b82..dd1c46d 100644 --- a/services/abonemments/AbonemmentService.js +++ b/services/abonemments/AbonemmentService.js @@ -92,6 +92,14 @@ class AbonemmentService { const normalizedRecipientEmail = this.normalizeEmail(recipientEmail); const forSelf = isForSelf !== false && !normalizedRecipientEmail; + if (typeof signingCity !== 'string' || signingCity.trim() === '') { + throw new Error('signingCity is required'); + } + + if (typeof signatureDataUrl !== 'string' || signatureDataUrl.trim() === '') { + throw new Error('signatureDataUrl is required'); + } + if (!forSelf && !normalizedRecipientEmail) { throw new Error('recipient_email is required when subscription is for another person'); } @@ -349,6 +357,14 @@ class AbonemmentService { const normalizedRecipientEmail = this.normalizeEmail(recipientEmail); console.log('[SUBSCRIBE] Normalized recipient email:', normalizedRecipientEmail); // NEW + if (typeof signingCity !== 'string' || signingCity.trim() === '') { + throw new Error('signingCity is required'); + } + + if (typeof signatureDataUrl !== 'string' || signatureDataUrl.trim() === '') { + throw new Error('signatureDataUrl is required'); + } + if (coffeeId === undefined || coffeeId === null) throw new Error('coffeeId is required'); const hasRecipientFields = recipientName || normalizedRecipientEmail || recipientNotes; diff --git a/services/invoice/InvoiceService.js b/services/invoice/InvoiceService.js index cfbd226..3592b61 100644 --- a/services/invoice/InvoiceService.js +++ b/services/invoice/InvoiceService.js @@ -12,6 +12,7 @@ const fs = require('fs/promises'); const path = require('path'); const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository'); +const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService'); class InvoiceService { constructor() { @@ -58,6 +59,54 @@ class InvoiceService { return null; } + async _resolveShippingFeeItem({ abonement, vatRate, lang }) { + const pieceCount = this._resolvePieceCountForQr(abonement); + if (!pieceCount) return null; + + const shippingFee = await CoffeeShippingFeeService.get(pieceCount); + const unitPrice = Number(shippingFee?.price || 0); + if (!(unitPrice > 0)) return null; + + return { + product_id: null, + sku: `SHIPPING-${pieceCount}`, + description: lang === 'de' ? `Versandkosten (${pieceCount} Stk.)` : `Shipping fee (${pieceCount} pcs)` , + quantity: 1, + unit_price: unitPrice, + tax_rate: vatRate, + }; + } + + async _buildInvoiceItems({ abonement, vatRate, lang }) { + const breakdown = Array.isArray(abonement?.pack_breakdown) ? abonement.pack_breakdown : []; + const items = breakdown.length + ? breakdown.map((b) => ({ + product_id: Number(b.coffee_table_id) || null, + sku: `COFFEE-${b.coffee_table_id || 'N/A'}`, + description: b.coffee_title || `Coffee subscription: ${b.coffee_table_id}`, + quantity: Number(b.packs || 1), + unit_price: Number(b.price_per_pack || 0), + tax_rate: b.tax_rate != null ? Number(b.tax_rate) : vatRate, + })) + : [ + { + product_id: null, + sku: 'SUBSCRIPTION', + description: `Subscription ${abonement?.pack_group || ''}`, + quantity: 1, + unit_price: Number(abonement?.price || 0), + tax_rate: vatRate, + }, + ]; + + const shippingItem = await this._resolveShippingFeeItem({ abonement, vatRate, lang }); + if (shippingItem) { + items.push(shippingItem); + } + + return items; + } + async _getCompanySettingsQrDataUri(pieceCount) { const safePieceCount = pieceCount === 120 ? 120 : 60; try { @@ -598,26 +647,7 @@ class InvoiceService { // NEW: resolve invoice vat_rate (standard) from buyer country const vat_rate = await this.resolveVatRateForCountry(addr.country); - const breakdown = Array.isArray(abonement.pack_breakdown) ? abonement.pack_breakdown : []; - const items = breakdown.length - ? breakdown.map((b) => ({ - product_id: Number(b.coffee_table_id) || null, - sku: `COFFEE-${b.coffee_table_id || 'N/A'}`, - description: b.coffee_title || `Coffee subscription: ${b.coffee_table_id}`, - quantity: Number(b.packs || 1), - unit_price: Number(b.price_per_pack || 0), - tax_rate: b.tax_rate != null ? Number(b.tax_rate) : vat_rate, // CHANGED: default to invoice vat_rate - })) - : [ - { - product_id: null, - sku: 'SUBSCRIPTION', - description: `Subscription ${abonement.pack_group || ''}`, - quantity: 1, - unit_price: Number(abonement.price || 0), - tax_rate: vat_rate, // CHANGED - }, - ]; + const items = await this._buildInvoiceItems({ abonement, vatRate: vat_rate, lang }); const context = { source: 'abonement', @@ -677,6 +707,9 @@ class InvoiceService { logger.error('InvoiceService.issueForAbonement:invoice_email_error', { invoiceId: invoice?.id, message: mailError?.message, + stack: mailError?.stack, + brevoStatus: mailError?.statusCode ?? mailError?.response?.status ?? null, + brevoData: mailError?.body ?? mailError?.response?.data ?? mailError?.response?.text ?? null, }); }