CentralBackend/services/invoice/InvoiceService.js
2025-12-15 16:58:55 +01:00

141 lines
4.7 KiB
JavaScript

const InvoiceRepository = require('../../repositories/invoice/InvoiceRepository');
const UnitOfWork = require('../../database/UnitOfWork'); // NEW
const TaxRepository = require('../../repositories/tax/taxRepository'); // NEW
class InvoiceService {
constructor() {
this.repo = new InvoiceRepository();
}
// NEW: resolve current standard VAT rate for a buyer country code
async resolveVatRateForCountry(countryCode) {
if (!countryCode) return null;
const uow = new UnitOfWork();
await uow.start();
const taxRepo = new TaxRepository(uow);
try {
const country = await taxRepo.getCountryByCode(String(countryCode).toUpperCase());
if (!country) {
await uow.commit();
return null;
}
// get current vat row for this country
const [rows] = await taxRepo.conn.query(
`SELECT standard_rate FROM vat_rates WHERE country_id = ? AND effective_to IS NULL LIMIT 1`,
[country.id]
);
await uow.commit();
const rate = rows?.[0]?.standard_rate;
return rate == null ? null : Number(rate);
} catch (e) {
await uow.rollback();
throw e;
}
}
// Issue invoice for a subscription period, with items from pack_breakdown
async issueForAbonement(abonement, periodStart, periodEnd, { actorUserId } = {}) {
console.log('[INVOICE ISSUE] Inputs:', {
abonement_id: abonement?.id,
abonement_user_id: abonement?.user_id,
abonement_purchaser_user_id: abonement?.purchaser_user_id,
actorUserId,
periodStart,
periodEnd,
});
const buyerName = [abonement.first_name, abonement.last_name].filter(Boolean).join(' ') || null;
const buyerEmail = abonement.email || null;
const addr = {
street: abonement.street || null,
postal_code: abonement.postal_code || null,
city: abonement.city || null,
country: abonement.country || null,
};
const currency = abonement.currency || 'EUR';
// 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: `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',
pack_group: abonement.pack_group || null,
period_start: periodStart,
period_end: periodEnd,
referred_by: abonement.referred_by || null,
};
// CHANGED: prioritize token user id for invoice ownership
const userIdForInvoice =
actorUserId ?? abonement.user_id ?? abonement.purchaser_user_id ?? null;
console.log('[INVOICE ISSUE] Resolved user_id for invoice:', userIdForInvoice);
const invoice = await this.repo.createInvoiceWithItems({
source_type: 'subscription',
source_id: abonement.id,
user_id: userIdForInvoice,
buyer_name: buyerName,
buyer_email: buyerEmail,
buyer_street: addr.street,
buyer_postal_code: addr.postal_code,
buyer_city: addr.city,
buyer_country: addr.country,
currency,
items,
status: 'issued',
issued_at: new Date(),
due_at: periodEnd,
context,
vat_rate, // NEW: persist on invoice
});
console.log('[INVOICE ISSUE] Created invoice:', {
id: invoice?.id,
user_id: invoice?.user_id,
source_type: invoice?.source_type,
source_id: invoice?.source_id,
total_net: invoice?.total_net,
total_tax: invoice?.total_tax,
total_gross: invoice?.total_gross,
});
return invoice;
}
async markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at = new Date(), details } = {}) {
return this.repo.markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at, details });
}
async listMine(userId, { status, limit = 50, offset = 0 } = {}) {
return this.repo.listByUser(userId, { status, limit, offset });
}
async adminList({ status, limit = 200, offset = 0 } = {}) {
return this.repo.listAll({ status, limit, offset });
}
}
module.exports = InvoiceService;