diff --git a/controller/invoice/InvoiceController.js b/controller/invoice/InvoiceController.js index b74c99b..1b928fb 100644 --- a/controller/invoice/InvoiceController.js +++ b/controller/invoice/InvoiceController.js @@ -96,4 +96,34 @@ module.exports = { return res.status(400).json({ success: false, message: e.message }); } }, + + async adminCreate(req, res) { + try { + const fields = { + buyer_name: req.body.buyer_name, + buyer_email: req.body.buyer_email, + buyer_street: req.body.buyer_street, + buyer_postal_code: req.body.buyer_postal_code, + buyer_city: req.body.buyer_city, + buyer_country: req.body.buyer_country, + currency: req.body.currency || 'EUR', + total_net: req.body.total_net, + total_tax: req.body.total_tax, + total_gross: req.body.total_gross, + vat_rate: req.body.vat_rate, + status: req.body.status || 'issued', + issued_at: req.body.issued_at, + due_at: req.body.due_at, + }; + if (!fields.total_gross || isNaN(Number(fields.total_gross))) { + return res.status(400).json({ success: false, message: 'total_gross is required and must be a number' }); + } + const pdfBuffer = req.file ? req.file.buffer : null; + const data = await service.adminCreateManual(fields, pdfBuffer); + return res.status(201).json({ success: true, data }); + } catch (e) { + console.error('[INVOICE ADMIN CREATE]', e); + return res.status(400).json({ success: false, message: e.message }); + } + }, }; diff --git a/repositories/invoice/InvoiceRepository.js b/repositories/invoice/InvoiceRepository.js index 292a7aa..8dcff86 100644 --- a/repositories/invoice/InvoiceRepository.js +++ b/repositories/invoice/InvoiceRepository.js @@ -225,6 +225,57 @@ class InvoiceRepository { ); return rows || []; } + + async createManualInvoice({ + source_type = 'manual', + source_id = null, + user_id = null, + buyer_name = null, + buyer_email = null, + buyer_street = null, + buyer_postal_code = null, + buyer_city = null, + buyer_country = null, + currency = 'EUR', + total_net = 0, + total_tax = 0, + total_gross = 0, + vat_rate = null, + status = 'issued', + issued_at = null, + due_at = null, + context = null, + }) { + const invoice_number = await genInvoiceNumber(); + const [res] = await pool.query( + `INSERT INTO invoices + (invoice_number, user_id, source_type, source_id, buyer_name, buyer_email, buyer_street, buyer_postal_code, buyer_city, buyer_country, + currency, total_net, total_tax, total_gross, vat_rate, status, issued_at, due_at, pdf_storage_key, context, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, ?, NOW(), NOW())`, + [ + invoice_number, + user_id || null, + source_type, + source_id || 0, + buyer_name || null, + buyer_email || null, + buyer_street || null, + buyer_postal_code || null, + buyer_city || null, + buyer_country || null, + currency, + +Number(total_net).toFixed(2), + +Number(total_tax).toFixed(2), + +Number(total_gross).toFixed(2), + vat_rate != null ? Number(vat_rate) : null, + status, + issued_at || null, + due_at || null, + context ? JSON.stringify(context) : null, + ], + ); + return this.getById(res.insertId); + } } module.exports = InvoiceRepository; diff --git a/routes/postRoutes.js b/routes/postRoutes.js index dc4b1fd..bba38c8 100644 --- a/routes/postRoutes.js +++ b/routes/postRoutes.js @@ -188,6 +188,7 @@ 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); +router.post('/admin/invoices', authMiddleware, adminOnly, upload.single('pdf'), InvoiceController.adminCreate); // Existing registration handlers (keep) router.post('/register/personal', (req, res) => { diff --git a/services/invoice/InvoiceService.js b/services/invoice/InvoiceService.js index 78d2609..6a3bbf7 100644 --- a/services/invoice/InvoiceService.js +++ b/services/invoice/InvoiceService.js @@ -949,6 +949,42 @@ class InvoiceService { return { sentCount: paidInvoices.length }; } + + async adminCreateManual(fields, pdfBuffer = null) { + const { uploadBuffer } = require('../../utils/exoscaleUploader'); + + const invoice = await this.repo.createManualInvoice({ + source_type: 'manual', + buyer_name: fields.buyer_name || null, + buyer_email: fields.buyer_email || null, + buyer_street: fields.buyer_street || null, + buyer_postal_code: fields.buyer_postal_code || null, + buyer_city: fields.buyer_city || null, + buyer_country: fields.buyer_country || null, + currency: fields.currency || 'EUR', + total_net: fields.total_net != null ? Number(fields.total_net) : 0, + total_tax: fields.total_tax != null ? Number(fields.total_tax) : 0, + total_gross: Number(fields.total_gross || 0), + vat_rate: fields.vat_rate != null ? Number(fields.vat_rate) : null, + status: fields.status || 'issued', + issued_at: fields.issued_at ? new Date(fields.issued_at) : new Date(), + due_at: fields.due_at ? new Date(fields.due_at) : null, + context: { source: 'admin_manual_upload' }, + }); + + if (pdfBuffer && pdfBuffer.length > 0) { + const { objectKey } = await uploadBuffer( + pdfBuffer, + `invoice-${invoice.invoice_number}.pdf`, + 'application/pdf', + `invoices/admin/${invoice.id}`, + ); + await this.repo.updateStorageKey(invoice.id, objectKey); + return this.repo.getById(invoice.id); + } + + return invoice; + } } module.exports = InvoiceService;