bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
80 lines
3.5 KiB
JavaScript
80 lines
3.5 KiB
JavaScript
/**
|
|
* routes/invoices.mjs — Invoice endpoints (revenue entries as invoices)
|
|
*/
|
|
|
|
function extractCustomer(description) {
|
|
if (!description) return 'Okänd kund';
|
|
const parts = description.split(/[|]/);
|
|
const raw = parts[0].trim();
|
|
return raw.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
|
|
}
|
|
|
|
export default function registerInvoices(app, pool) {
|
|
// GET /api/ledger/invoices — hämta verifikat med intäktsposter som "fakturor"
|
|
app.get('/api/ledger/invoices', async (req, res) => {
|
|
const tenant_id = req.auth?.tenant_id || req.headers['x-tenant-id'] || 'wavult-group';
|
|
const fiscal_year = req.query.fiscal_year ? parseInt(req.query.fiscal_year) : null;
|
|
try {
|
|
const conditions = ['e.tenant_id=$1', "e.status='posted'"];
|
|
const params = [tenant_id];
|
|
if (fiscal_year) { params.push(fiscal_year); conditions.push(`e.fiscal_year=$${params.length}`); }
|
|
const { rows: entries } = await pool.query(`
|
|
SELECT DISTINCT ON (e.id)
|
|
e.id, e.entry_number, e.fiscal_year, e.period, e.entry_date, e.description,
|
|
e.reference, e.status, e.metadata
|
|
FROM ledger_journal_entries e
|
|
JOIN ledger_journal_lines l ON l.entry_id = e.id AND l.tenant_id = e.tenant_id
|
|
WHERE ${conditions.join(' AND ')}
|
|
AND l.account_number LIKE '3%'
|
|
AND l.credit > 0
|
|
ORDER BY e.id, e.entry_date DESC
|
|
`, params);
|
|
|
|
const entryIds = entries.map(e => e.id);
|
|
let lines = [];
|
|
if (entryIds.length > 0) {
|
|
const placeholders = entryIds.map((_,i) => `$${i+2}`).join(',');
|
|
const { rows } = await pool.query(
|
|
`SELECT * FROM ledger_journal_lines WHERE tenant_id=$1 AND entry_id IN (${placeholders}) ORDER BY line_number`,
|
|
[tenant_id, ...entryIds]
|
|
);
|
|
lines = rows;
|
|
}
|
|
|
|
const invoices = entries.map(e => {
|
|
const entryLines = lines.filter(l => l.entry_id === e.id);
|
|
const revenueLines = entryLines.filter(l => (l.account_number||'').startsWith('3') && parseFloat(l.credit||0) > 0);
|
|
const vatLines = entryLines.filter(l => ['2611','2614','2615','2616'].includes(l.account_number) && parseFloat(l.credit||0) > 0);
|
|
const receivableLines = entryLines.filter(l => l.account_number === '1510' && parseFloat(l.debit||0) > 0);
|
|
const cashLines = entryLines.filter(l => /^19[0-9]{2}$/.test(l.account_number||'') && parseFloat(l.debit||0) > 0);
|
|
const amount = revenueLines.reduce((s,l) => s + parseFloat(l.credit||0), 0);
|
|
const vatAmount = vatLines.reduce((s,l) => s + parseFloat(l.credit||0), 0);
|
|
const total = amount + vatAmount;
|
|
const isPaid = cashLines.length > 0;
|
|
const isSent = receivableLines.length > 0;
|
|
const date = e.entry_date ? new Date(e.entry_date).toISOString().split('T')[0] : '';
|
|
return {
|
|
id: e.id,
|
|
entry_number: e.entry_number,
|
|
period: e.period,
|
|
date,
|
|
description: e.description,
|
|
reference: e.reference,
|
|
customer: extractCustomer(e.description),
|
|
amount,
|
|
vatAmount,
|
|
total,
|
|
status: isPaid ? 'paid' : isSent ? 'sent' : 'posted',
|
|
lines: entryLines,
|
|
revenueAccounts: revenueLines.map(l => l.account_number).join(', '),
|
|
};
|
|
});
|
|
|
|
invoices.sort((a,b) => new Date(b.date) - new Date(a.date));
|
|
res.json({ ok: true, invoices, count: invoices.length });
|
|
} catch (e) {
|
|
res.status(500).json({ ok: false, error: e.message });
|
|
}
|
|
});
|
|
}
|