import { getDb } from './db.js'; import { recordMovement } from './movements.js'; export function getPendingInvoice() { const db = getDb(); const invoice = db.prepare(` SELECT * FROM invoices WHERE status = 'pending' LIMIT 1 `).get(); if (!invoice) return null; return { invoice, lines: linesFor(invoice.id) }; } export function getOrCreatePendingInvoice() { const db = getDb(); const existing = db.prepare(`SELECT * FROM invoices WHERE status = 'pending' LIMIT 1`).get(); if (existing) return { invoice: existing, lines: linesFor(existing.id) }; const id = db.prepare(`INSERT INTO invoices (status) VALUES ('pending')`).run().lastInsertRowid; const invoice = db.prepare(`SELECT * FROM invoices WHERE id = ?`).get(id); return { invoice, lines: [] }; } export function getInvoice(id) { const db = getDb(); const invoice = db.prepare(`SELECT * FROM invoices WHERE id = ?`).get(Number(id)); if (!invoice) return null; return { invoice, lines: linesFor(invoice.id) }; } function linesFor(invoiceId) { return getDb().prepare(` SELECT l.*, p.sku AS part_sku, p.name_en AS part_name_en, p.name_tg AS part_name_tg, p.quantity_on_hand AS part_on_hand FROM invoice_lines l LEFT JOIN parts p ON p.id = l.part_id WHERE l.invoice_id = ? ORDER BY l.sort_order, l.id `).all(invoiceId); } function nextSortOrder(invoiceId) { const row = getDb().prepare(` SELECT COALESCE(MAX(sort_order), 0) + 1 AS n FROM invoice_lines WHERE invoice_id = ? `).get(invoiceId); return row.n; } /** * Add a parts-based line to an invoice. If a line for the same part_id * already exists on this invoice, increments that line's quantity rather * than inserting a new row (per-edit merge). The line's unit price is left * alone in the merge case so a user-edited price isn't clobbered. */ export function addLine({ invoice_id, part_id, quantity, unit_price_dirams }) { const db = getDb(); const invoiceId = Number(invoice_id); const partId = Number(part_id); const qty = Math.floor(Number(quantity)); if (!invoiceId) throw new Error('invoice_id required'); if (!partId) throw new Error('part_id required'); if (!Number.isInteger(qty) || qty <= 0) throw new Error('quantity must be a positive integer'); return db.transaction(() => { const part = db.prepare(`SELECT id, sale_price FROM parts WHERE id = ?`).get(partId); if (!part) throw new Error(`part ${partId} not found`); const price = (unit_price_dirams === '' || unit_price_dirams == null) ? Number(part.sale_price || 0) : Math.round(Number(unit_price_dirams)); const existing = db.prepare(` SELECT id, quantity FROM invoice_lines WHERE invoice_id = ? AND part_id = ? AND affects_inventory = 1 LIMIT 1 `).get(invoiceId, partId); if (existing) { db.prepare(`UPDATE invoice_lines SET quantity = ? WHERE id = ?`) .run(existing.quantity + qty, existing.id); return existing.id; } return db.prepare(` INSERT INTO invoice_lines (invoice_id, part_id, label, quantity, unit_price_dirams, affects_inventory, sort_order) VALUES (?, ?, NULL, ?, ?, 1, ?) `).run(invoiceId, partId, qty, price, nextSortOrder(invoiceId)).lastInsertRowid; })(); } export function addCustomLine({ invoice_id, label, quantity, unit_price_dirams }) { const db = getDb(); const invoiceId = Number(invoice_id); const cleanLabel = (label || '').trim(); const qty = Math.floor(Number(quantity)); const price = Math.round(Number(unit_price_dirams || 0)); if (!invoiceId) throw new Error('invoice_id required'); if (!cleanLabel) throw new Error('label required'); if (!Number.isInteger(qty) || qty <= 0) throw new Error('quantity must be a positive integer'); return db.prepare(` INSERT INTO invoice_lines (invoice_id, part_id, label, quantity, unit_price_dirams, affects_inventory, sort_order) VALUES (?, NULL, ?, ?, ?, 0, ?) `).run(invoiceId, cleanLabel, qty, price, nextSortOrder(invoiceId)).lastInsertRowid; } export function updateLine(line_id, { quantity, unit_price_dirams, label }) { const db = getDb(); const id = Number(line_id); if (!id) throw new Error('line_id required'); const current = db.prepare(`SELECT * FROM invoice_lines WHERE id = ?`).get(id); if (!current) throw new Error(`line ${id} not found`); const qty = quantity == null || quantity === '' ? current.quantity : Math.floor(Number(quantity)); if (!Number.isInteger(qty) || qty <= 0) throw new Error('quantity must be a positive integer'); const price = unit_price_dirams == null || unit_price_dirams === '' ? current.unit_price_dirams : Math.round(Number(unit_price_dirams)); const newLabel = current.affects_inventory === 0 ? (label == null ? current.label : String(label).trim() || current.label) : current.label; db.prepare(` UPDATE invoice_lines SET quantity = ?, unit_price_dirams = ?, label = ? WHERE id = ? `).run(qty, price, newLabel, id); } export function removeLine(line_id) { getDb().prepare(`DELETE FROM invoice_lines WHERE id = ?`).run(Number(line_id)); } /** * Commit the invoice: for each inventoried line, record an 'out' stock * movement (which decrements parts.quantity_on_hand atomically and rejects * if there's not enough stock). All movements + the status flip happen in * one transaction — if any line fails, nothing is decremented and the * draft stays pending. */ export function saveInvoice(invoice_id) { const db = getDb(); const id = Number(invoice_id); return db.transaction(() => { const invoice = db.prepare(`SELECT * FROM invoices WHERE id = ?`).get(id); if (!invoice) throw new Error(`invoice ${id} not found`); if (invoice.status !== 'pending') throw new Error('invoice already saved'); const lines = db.prepare(` SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY sort_order, id `).all(id); if (lines.length === 0) throw new Error('cannot save empty invoice'); const reference = `INV-${id}`; for (const line of lines) { if (line.affects_inventory !== 1) continue; recordMovement({ part_id: line.part_id, movement_type: 'out', quantity: line.quantity, unit_price: line.unit_price_dirams / 100, reference }); } const total = lines.reduce( (sum, l) => sum + (l.quantity * l.unit_price_dirams), 0 ); db.prepare(` UPDATE invoices SET status = 'saved', saved_at = datetime('now'), total_dirams = ? WHERE id = ? `).run(total, id); return id; })(); } export function cancelInvoice(invoice_id) { getDb().prepare(` DELETE FROM invoices WHERE id = ? AND status = 'pending' `).run(Number(invoice_id)); }