diff --git a/controller/invoice/InvoiceController.js b/controller/invoice/InvoiceController.js index b74c99b..d4a7628 100644 --- a/controller/invoice/InvoiceController.js +++ b/controller/invoice/InvoiceController.js @@ -69,31 +69,4 @@ module.exports = { return res.status(400).json({ success: false, message: e.message }); } }, - - async downloadPdf(req, res) { - try { - const stream = await service.getInvoicePdfStream(req.params.id, req.user); - res.setHeader('Content-Type', 'application/pdf'); - res.setHeader('Content-Disposition', `inline; filename="invoice-${req.params.id}.pdf"`); - stream.pipe(res); - } catch (e) { - console.error('[INVOICE DOWNLOAD PDF]', e); - if (e.message?.includes('not found') || e.message?.includes('No PDF')) { - return res.status(404).json({ success: false, message: e.message }); - } - return res.status(400).json({ success: false, message: e.message }); - } - }, - - async sendEmailReport(req, res) { - try { - const { email, from, to } = req.body; - if (!email) return res.status(400).json({ success: false, message: 'email is required' }); - const data = await service.sendEmailReport({ email, from, to }); - return res.json({ success: true, data }); - } catch (e) { - console.error('[INVOICE EMAIL REPORT]', e); - return res.status(400).json({ success: false, message: e.message }); - } - }, }; diff --git a/routes/getRoutes.js b/routes/getRoutes.js index 9f32e0a..436795e 100644 --- a/routes/getRoutes.js +++ b/routes/getRoutes.js @@ -188,7 +188,6 @@ router.get('/news/:slug', NewsController.getPublic); // NEW: Invoice GETs router.get('/invoices/mine', authMiddleware, InvoiceController.listMine); -router.get('/invoices/:id/pdf', authMiddleware, InvoiceController.downloadPdf); router.get('/admin/invoices', authMiddleware, adminOnly, InvoiceController.adminList); router.get('/admin/invoices/:id/detail', authMiddleware, adminOnly, InvoiceController.getDetail); diff --git a/routes/postRoutes.js b/routes/postRoutes.js index dc4b1fd..d04e3ef 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -187,7 +187,6 @@ router.post('/abonements/referred', authMiddleware, ensureUserFromBody, Abonemme // NEW: Invoice POSTs router.post('/invoices/:id/pay', authMiddleware, adminOnly, InvoiceController.pay); -router.post('/admin/invoices/email-report', authMiddleware, adminOnly, InvoiceController.sendEmailReport); // Existing registration handlers (keep) router.post('/register/personal', (req, res) => { diff --git a/services/abonemments/AbonemmentService.js b/services/abonemments/AbonemmentService.js index dd1c46d..79e1b82 100644 --- a/services/abonemments/AbonemmentService.js +++ b/services/abonemments/AbonemmentService.js @@ -92,14 +92,6 @@ 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'); } @@ -357,14 +349,6 @@ 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 78d2609..cfbd226 100644 --- a/services/invoice/InvoiceService.js +++ b/services/invoice/InvoiceService.js @@ -12,7 +12,6 @@ const fs = require('fs/promises'); const path = require('path'); const CompanySettingsRepository = require('../../repositories/settings/CompanySettingsRepository'); -const CoffeeShippingFeeService = require('../subscriptions/CoffeeShippingFeeService'); class InvoiceService { constructor() { @@ -59,54 +58,6 @@ 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 { @@ -647,7 +598,26 @@ class InvoiceService { // NEW: resolve invoice vat_rate (standard) from buyer country const vat_rate = await this.resolveVatRateForCountry(addr.country); - const items = await this._buildInvoiceItems({ abonement, vatRate: vat_rate, 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) : 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 context = { source: 'abonement', @@ -707,9 +677,6 @@ 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, }); } @@ -778,177 +745,6 @@ class InvoiceService { const payments = await this.repo.getPaymentsByInvoiceId(invoiceId); return { invoice, items, payments }; } - - async getInvoicePdfStream(invoiceId, user) { - const invoice = await this.repo.getById(invoiceId); - if (!invoice) throw new Error(`Invoice ${invoiceId} not found.`); - - // Non-admin users can only access their own invoices - const isAdmin = user?.role === 'admin' || user?.role === 'super_admin'; - if (!isAdmin && String(invoice.user_id) !== String(user?.id)) { - throw new Error('Invoice not found.'); - } - - if (!invoice.pdf_storage_key) { - throw new Error('No PDF available for this invoice.'); - } - - const command = new GetObjectCommand({ - Bucket: process.env.EXOSCALE_BUCKET, - Key: invoice.pdf_storage_key, - }); - const obj = await sharedExoscaleClient.send(command); - return obj.Body; - } - - /** - * Send an email report of all paid invoices to a given email. - * Optionally filter by date range. - * @param {{ email: string, from?: string, to?: string }} opts - * @returns {{ sentCount: number }} - */ - async sendEmailReport({ email, from, to }) { - if (!email) throw new Error('email is required'); - - const allInvoices = await this.repo.listAll({ status: 'paid', limit: 10000, offset: 0 }); - - // Optionally filter by date range - let paidInvoices = allInvoices; - if (from) { - const fromDate = new Date(from); - paidInvoices = paidInvoices.filter((inv) => { - const d = new Date(inv.issued_at || inv.created_at); - return d >= fromDate; - }); - } - if (to) { - const toDate = new Date(to); - toDate.setHours(23, 59, 59, 999); - paidInvoices = paidInvoices.filter((inv) => { - const d = new Date(inv.issued_at || inv.created_at); - return d <= toDate; - }); - } - - if (!paidInvoices.length) { - throw new Error('No paid invoices found matching the criteria.'); - } - - // Collect PDF attachments for each paid invoice - const attachments = []; - for (const inv of paidInvoices) { - if (inv.pdf_storage_key) { - try { - const command = new GetObjectCommand({ - Bucket: process.env.EXOSCALE_BUCKET, - Key: inv.pdf_storage_key, - }); - const obj = await sharedExoscaleClient.send(command); - if (typeof obj.Body.transformToByteArray === 'function') { - const bytes = await obj.Body.transformToByteArray(); - attachments.push({ - name: `${inv.invoice_number || `invoice-${inv.id}`}.pdf`, - content: Buffer.from(bytes).toString('base64'), - }); - } else { - const chunks = []; - for await (const chunk of obj.Body) { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - } - attachments.push({ - name: `${inv.invoice_number || `invoice-${inv.id}`}.pdf`, - content: Buffer.concat(chunks).toString('base64'), - }); - } - } catch (e) { - logger.warn('InvoiceService.sendEmailReport:pdf_download_error', { - invoiceId: inv.id, - storageKey: inv.pdf_storage_key, - message: e?.message, - }); - } - } - } - - // Build email body with a summary table - const totalGross = paidInvoices.reduce((sum, inv) => sum + Number(inv.total_gross || 0), 0); - const currency = paidInvoices[0]?.currency || 'EUR'; - const dateRange = [from, to].filter(Boolean).join(' – ') || 'All time'; - - const invoiceRows = paidInvoices - .map( - (inv) => - `
| - - |