141 lines
4.7 KiB
JavaScript
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;
|