185 lines
6.0 KiB
JavaScript
185 lines
6.0 KiB
JavaScript
const pool = require('../../database/database');
|
|
const Invoice = require('../../models/Invoice');
|
|
|
|
function genInvoiceNumber() {
|
|
const now = new Date();
|
|
const y = now.getFullYear();
|
|
const m = String(now.getMonth() + 1).padStart(2, '0');
|
|
const d = String(now.getDate()).padStart(2, '0');
|
|
const rand = Math.floor(Math.random() * 1e6).toString().padStart(6, '0');
|
|
return `INV-${y}${m}${d}-${rand}`;
|
|
}
|
|
|
|
class InvoiceRepository {
|
|
async createInvoiceWithItems({
|
|
source_type,
|
|
source_id,
|
|
user_id,
|
|
buyer_name,
|
|
buyer_email,
|
|
buyer_street,
|
|
buyer_postal_code,
|
|
buyer_city,
|
|
buyer_country,
|
|
currency,
|
|
items = [],
|
|
status = 'draft',
|
|
issued_at = null,
|
|
due_at = null,
|
|
context = null,
|
|
vat_rate = null, // NEW: default VAT for invoice and lines
|
|
}) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const invoice_number = genInvoiceNumber();
|
|
|
|
// compute totals
|
|
let total_net = 0;
|
|
let total_tax = 0;
|
|
let total_gross = 0;
|
|
for (const it of items) {
|
|
const qty = Number(it.quantity || 1);
|
|
const unit = Number(it.unit_price || 0);
|
|
const rate = it.tax_rate != null ? Number(it.tax_rate) : (vat_rate != null ? Number(vat_rate) : null); // CHANGED
|
|
const line_net = qty * unit;
|
|
const line_tax = rate != null ? +(line_net * (rate / 100)).toFixed(2) : 0;
|
|
const line_gross = +(line_net + line_tax).toFixed(2);
|
|
total_net += line_net;
|
|
total_tax += line_tax;
|
|
total_gross += line_gross;
|
|
}
|
|
total_net = +total_net.toFixed(2);
|
|
total_tax = +total_tax.toFixed(2);
|
|
total_gross = +total_gross.toFixed(2);
|
|
|
|
const [res] = await conn.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,
|
|
buyer_name || null,
|
|
buyer_email || null,
|
|
buyer_street || null,
|
|
buyer_postal_code || null,
|
|
buyer_city || null,
|
|
buyer_country || null,
|
|
currency,
|
|
total_net,
|
|
total_tax,
|
|
total_gross,
|
|
vat_rate != null ? Number(vat_rate) : null, // CHANGED
|
|
status,
|
|
issued_at || null,
|
|
due_at || null,
|
|
context ? JSON.stringify(context) : null,
|
|
],
|
|
);
|
|
|
|
const invoiceId = res.insertId;
|
|
|
|
for (const it of items) {
|
|
const qty = Number(it.quantity || 1);
|
|
const unit = Number(it.unit_price || 0);
|
|
const rate = it.tax_rate != null ? Number(it.tax_rate) : (vat_rate != null ? Number(vat_rate) : null); // CHANGED
|
|
const line_net = +(qty * unit).toFixed(2);
|
|
const line_tax = rate != null ? +(line_net * (rate / 100)).toFixed(2) : 0;
|
|
const line_gross = +(line_net + line_tax).toFixed(2);
|
|
|
|
await conn.query(
|
|
`INSERT INTO invoice_items
|
|
(invoice_id, product_id, sku, description, quantity, unit_price, tax_rate, line_net, line_tax, line_gross, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())`,
|
|
[
|
|
invoiceId,
|
|
it.product_id || null,
|
|
it.sku || null,
|
|
it.description || 'Subscription',
|
|
qty,
|
|
unit,
|
|
rate, // CHANGED
|
|
line_net,
|
|
line_tax,
|
|
line_gross,
|
|
],
|
|
);
|
|
}
|
|
|
|
await conn.commit();
|
|
return this.getById(invoiceId);
|
|
} catch (e) {
|
|
await conn.rollback();
|
|
throw e;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}
|
|
|
|
async markPaid(invoiceId, { payment_method, transaction_id, amount, paid_at, details }) {
|
|
const conn = await pool.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
await conn.query(`UPDATE invoices SET status='paid', updated_at=NOW() WHERE id = ?`, [invoiceId]);
|
|
await conn.query(
|
|
`INSERT INTO invoice_payments
|
|
(invoice_id, payment_method, transaction_id, amount, paid_at, status, details, created_at)
|
|
VALUES (?, ?, ?, ?, ?, 'succeeded', ?, NOW())`,
|
|
[invoiceId, payment_method || 'manual', transaction_id || null, amount || null, paid_at || new Date(), details ? JSON.stringify(details) : null],
|
|
);
|
|
await conn.commit();
|
|
return this.getById(invoiceId);
|
|
} catch (e) {
|
|
await conn.rollback();
|
|
throw e;
|
|
} finally {
|
|
conn.release();
|
|
}
|
|
}
|
|
|
|
async getById(id) {
|
|
const [rows] = await pool.query(`SELECT * FROM invoices WHERE id = ?`, [id]);
|
|
return rows[0] ? new Invoice(rows[0]) : null;
|
|
}
|
|
|
|
async listByUser(userId, { status, limit = 50, offset = 0 } = {}) {
|
|
const params = [userId];
|
|
let sql = `SELECT * FROM invoices WHERE user_id = ?`;
|
|
if (status) {
|
|
sql += ` AND status = ?`;
|
|
params.push(status);
|
|
}
|
|
sql += ` ORDER BY issued_at DESC, created_at DESC LIMIT ? OFFSET ?`;
|
|
params.push(Number(limit), Number(offset));
|
|
const [rows] = await pool.query(sql, params);
|
|
return rows.map((r) => new Invoice(r));
|
|
}
|
|
|
|
async listAll({ status, limit = 200, offset = 0 } = {}) {
|
|
const params = [];
|
|
let sql = `SELECT * FROM invoices`;
|
|
if (status) {
|
|
sql += ` WHERE status = ?`;
|
|
params.push(status);
|
|
}
|
|
sql += ` ORDER BY issued_at DESC, created_at DESC LIMIT ? OFFSET ?`;
|
|
params.push(Number(limit), Number(offset));
|
|
const [rows] = await pool.query(sql, params);
|
|
return rows.map((r) => new Invoice(r));
|
|
}
|
|
|
|
async findByAbonement(abonementId) {
|
|
const [rows] = await pool.query(
|
|
`SELECT * FROM invoices WHERE source_type='subscription' AND source_id = ? ORDER BY issued_at DESC, id DESC`,
|
|
[abonementId],
|
|
);
|
|
return rows.map((r) => new Invoice(r));
|
|
}
|
|
}
|
|
|
|
module.exports = InvoiceRepository;
|