"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { authenticateUser, createSession, destroySession } from "@/lib/auth"; import { getDb } from "@/lib/db"; type ParsedLine = { sku: string; quantity: number; amount: number; }; type ParsedFulfillmentLine = { sku: string; quantity: number; }; function db() { return getDb(); } function getText(formData: FormData, key: string) { return String(formData.get(key) ?? "").trim(); } function getNumber(formData: FormData, key: string) { const value = Number(getText(formData, key)); return Number.isFinite(value) ? value : 0; } function parseLines(raw: string): ParsedLine[] { return raw .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .map((line) => { const [sku, quantity, amount] = line.split(",").map((piece) => piece.trim()); if (!sku || !quantity || !amount) { throw new Error(`Invalid line format: "${line}". Use SKU,quantity,amount.`); } const parsedQuantity = Number(quantity); const parsedAmount = Number(amount); if (!Number.isFinite(parsedQuantity) || parsedQuantity <= 0 || !Number.isFinite(parsedAmount) || parsedAmount < 0) { throw new Error(`Invalid line values: "${line}".`); } return { sku, quantity: parsedQuantity, amount: parsedAmount }; }); } function parseFulfillmentLines(raw: string): ParsedFulfillmentLine[] { return raw .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .map((line) => { const [sku, quantity] = line.split(",").map((piece) => piece.trim()); if (!sku || !quantity) { throw new Error(`Invalid fulfillment line format: "${line}". Use SKU,quantity.`); } const parsedQuantity = Number(quantity); if (!Number.isFinite(parsedQuantity) || parsedQuantity <= 0) { throw new Error(`Invalid fulfillment quantity: "${line}".`); } return { sku, quantity: parsedQuantity }; }); } function getPartIdBySku(sku: string) { const row = db().prepare(`SELECT id FROM parts WHERE sku = ?`).get(sku) as { id: number } | undefined; if (!row) { throw new Error(`Part with SKU "${sku}" does not exist.`); } return row.id; } function getOrderNumber(prefix: string, table: "sales_orders" | "purchase_orders") { const row = db().prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }; return `${prefix}-${String((row.count ?? 0) + 1).padStart(5, "0")}`; } function getDocumentNumber(prefix: string, table: "customer_invoices" | "vendor_bills") { const row = db().prepare(`SELECT COUNT(*) AS count FROM ${table}`).get() as { count: number }; return `${prefix}-${String((row.count ?? 0) + 1).padStart(5, "0")}`; } function createJournalEntry( entryType: string, referenceType: string, referenceId: number | null, description: string, lines: Array<{ accountCode: string; accountName: string; debit: number; credit: number }> ) { const tx = db().transaction(() => { const result = db() .prepare( ` INSERT INTO journal_entries (entry_type, reference_type, reference_id, description) VALUES (?, ?, ?, ?) ` ) .run(entryType, referenceType, referenceId, description); const journalEntryId = Number(result.lastInsertRowid); const insertLine = db().prepare( ` INSERT INTO journal_lines (journal_entry_id, account_code, account_name, debit, credit) VALUES (?, ?, ?, ?, ?) ` ); for (const line of lines) { insertLine.run(journalEntryId, line.accountCode, line.accountName, line.debit, line.credit); } }); tx(); } function resolveAccount(code: string) { const row = db().prepare(`SELECT code, name FROM accounts WHERE code = ?`).get(code) as { code: string; name: string } | undefined; if (!row) { throw new Error(`Account "${code}" does not exist.`); } return row; } function parseJournalLines(raw: string) { return raw .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .map((line) => { const [accountCode, debit, credit] = line.split(",").map((piece) => piece.trim()); if (!accountCode || debit === undefined || credit === undefined) { throw new Error(`Invalid journal line format: "${line}". Use account code,debit,credit.`); } const parsedDebit = Number(debit); const parsedCredit = Number(credit); if (!Number.isFinite(parsedDebit) || !Number.isFinite(parsedCredit) || parsedDebit < 0 || parsedCredit < 0) { throw new Error(`Invalid journal amounts: "${line}".`); } if ((parsedDebit === 0 && parsedCredit === 0) || (parsedDebit > 0 && parsedCredit > 0)) { throw new Error(`Journal lines must have either a debit or a credit: "${line}".`); } const account = resolveAccount(accountCode); return { accountCode: account.code, accountName: account.name, debit: parsedDebit, credit: parsedCredit }; }); } export async function createPart(formData: FormData) { db() .prepare( ` INSERT INTO parts (sku, name, description, kind, unit_cost, sale_price, reorder_point, unit_of_measure) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ` ) .run( getText(formData, "sku"), getText(formData, "name"), getText(formData, "description"), getText(formData, "kind"), getNumber(formData, "unitCost"), getNumber(formData, "salePrice"), getNumber(formData, "reorderPoint"), getText(formData, "unitOfMeasure") || "ea" ); revalidatePath("/"); revalidatePath("/parts"); revalidatePath("/assemblies"); } export async function addKitComponent(formData: FormData) { const assemblyId = getPartIdBySku(getText(formData, "assemblySku")); const componentId = getPartIdBySku(getText(formData, "componentSku")); db() .prepare( ` INSERT INTO kit_components (assembly_part_id, component_part_id, quantity) VALUES (?, ?, ?) ON CONFLICT (assembly_part_id, component_part_id) DO UPDATE SET quantity = excluded.quantity ` ) .run(assemblyId, componentId, getNumber(formData, "quantity")); revalidatePath("/assemblies"); } export async function buildAssembly(formData: FormData) { const assemblySku = getText(formData, "assemblySku"); const buildQuantity = getNumber(formData, "quantity"); if (buildQuantity <= 0) { throw new Error("Build quantity must be greater than zero."); } const assemblyId = getPartIdBySku(assemblySku); const components = db() .prepare( ` SELECT kc.component_part_id AS componentId, kc.quantity AS componentQuantity, p.sku, p.unit_cost AS unitCost, COALESCE(ib.quantity_on_hand, 0) AS quantityOnHand FROM kit_components kc INNER JOIN parts p ON p.id = kc.component_part_id LEFT JOIN inventory_balances ib ON ib.part_id = p.id WHERE kc.assembly_part_id = ? ` ) .all(assemblyId) as Array<{ componentId: number; componentQuantity: number; sku: string; unitCost: number; quantityOnHand: number; }>; if (components.length === 0) { throw new Error("Assembly has no bill of materials defined."); } for (const component of components) { const needed = component.componentQuantity * buildQuantity; if (component.quantityOnHand < needed) { throw new Error(`Not enough stock for component ${component.sku}. Need ${needed}, have ${component.quantityOnHand}.`); } } const buildCost = components.reduce((sum, component) => sum + component.unitCost * component.componentQuantity, 0); const tx = db().transaction(() => { const insertInventory = db().prepare( ` INSERT INTO inventory_transactions (part_id, quantity_delta, unit_cost, transaction_type, reference_type, reference_id, notes) VALUES (?, ?, ?, ?, ?, ?, ?) ` ); const updateAssemblyCost = db().prepare(`UPDATE parts SET unit_cost = ? WHERE id = ?`); for (const component of components) { insertInventory.run( component.componentId, component.componentQuantity * buildQuantity * -1, component.unitCost, "assembly_consume", "assembly_build", assemblyId, `Consumed for ${assemblySku}` ); } insertInventory.run( assemblyId, buildQuantity, buildCost, "assembly_build", "assembly_build", assemblyId, `Built ${buildQuantity} of ${assemblySku}` ); updateAssemblyCost.run(buildCost, assemblyId); }); tx(); revalidatePath("/"); revalidatePath("/parts"); revalidatePath("/assemblies"); } export async function recordAdjustment(formData: FormData) { const partId = getPartIdBySku(getText(formData, "sku")); const quantityDelta = getNumber(formData, "quantityDelta"); const unitCost = getNumber(formData, "unitCost"); const notes = getText(formData, "notes"); const inventoryImpact = Math.abs(quantityDelta * unitCost); db() .prepare( ` INSERT INTO inventory_transactions (part_id, quantity_delta, unit_cost, transaction_type, reference_type, notes) VALUES (?, ?, ?, 'adjustment', 'manual_adjustment', ?) ` ) .run(partId, quantityDelta, unitCost, notes); if (inventoryImpact > 0) { createJournalEntry("adjustment", "manual_adjustment", partId, notes || "Inventory adjustment posted", [ { accountCode: quantityDelta >= 0 ? "1200" : "6100", accountName: quantityDelta >= 0 ? "Inventory" : "Inventory Adjustments", debit: quantityDelta >= 0 ? inventoryImpact : 0, credit: quantityDelta >= 0 ? 0 : inventoryImpact }, { accountCode: quantityDelta >= 0 ? "3000" : "1200", accountName: quantityDelta >= 0 ? "Owner Equity" : "Inventory", debit: quantityDelta >= 0 ? 0 : inventoryImpact, credit: quantityDelta >= 0 ? inventoryImpact : 0 } ]); } revalidatePath("/"); revalidatePath("/parts"); revalidatePath("/accounting"); } export async function createCustomer(formData: FormData) { db() .prepare( ` INSERT INTO customers (code, name, email, phone, billing_address, shipping_address) VALUES (?, ?, ?, ?, ?, ?) ` ) .run( getText(formData, "code"), getText(formData, "name"), getText(formData, "email"), getText(formData, "phone"), getText(formData, "billingAddress"), getText(formData, "shippingAddress") ); revalidatePath("/customers"); revalidatePath("/"); } export async function createAccount(formData: FormData) { const category = getText(formData, "category"); if (!["asset", "liability", "equity", "revenue", "expense"].includes(category)) { throw new Error("Invalid account category."); } db() .prepare( ` INSERT INTO accounts (code, name, category, is_system) VALUES (?, ?, ?, 0) ` ) .run(getText(formData, "code"), getText(formData, "name"), category); revalidatePath("/accounting"); } export async function createVendor(formData: FormData) { db() .prepare( ` INSERT INTO vendors (code, name, email, phone, address) VALUES (?, ?, ?, ?, ?) ` ) .run( getText(formData, "code"), getText(formData, "name"), getText(formData, "email"), getText(formData, "phone"), getText(formData, "address") ); revalidatePath("/vendors"); revalidatePath("/"); } export async function createSalesOrder(formData: FormData) { const customerCode = getText(formData, "customerCode"); const lines = parseLines(getText(formData, "lines")); const customerRow = db().prepare(`SELECT id FROM customers WHERE code = ?`).get(customerCode) as { id: number } | undefined; if (!customerRow) { throw new Error(`Customer "${customerCode}" does not exist.`); } const tx = db().transaction(() => { const result = db() .prepare( ` INSERT INTO sales_orders (order_number, customer_id, status, notes) VALUES (?, ?, 'open', ?) ` ) .run(getOrderNumber("SO", "sales_orders"), customerRow.id, getText(formData, "notes")); const orderId = Number(result.lastInsertRowid); const insertLine = db().prepare( ` INSERT INTO sales_order_lines (sales_order_id, part_id, quantity, unit_price) VALUES (?, ?, ?, ?) ` ); for (const line of lines) { insertLine.run(orderId, getPartIdBySku(line.sku), line.quantity, line.amount); } }); tx(); revalidatePath("/"); revalidatePath("/sales-orders"); } export async function shipSalesOrder(formData: FormData) { const orderId = Number(getText(formData, "orderId")); const order = db() .prepare(`SELECT customer_id AS customerId, status FROM sales_orders WHERE id = ?`) .get(orderId) as { customerId: number; status: string } | undefined; if (!order) { throw new Error("Sales order not found."); } if (order.status === "shipped") { throw new Error("Sales order has already been shipped."); } const orderLines = db() .prepare( ` SELECT sol.id AS lineId, so.order_number AS orderNumber, p.id AS partId, p.sku, p.unit_cost AS unitCost, sol.quantity, sol.shipped_quantity AS shippedQuantity, sol.unit_price AS unitPrice, COALESCE(ib.quantity_on_hand, 0) AS quantityOnHand FROM sales_order_lines sol INNER JOIN sales_orders so ON so.id = sol.sales_order_id INNER JOIN parts p ON p.id = sol.part_id LEFT JOIN inventory_balances ib ON ib.part_id = p.id WHERE sol.sales_order_id = ? ` ) .all(orderId) as Array<{ lineId: number; orderNumber: string; partId: number; sku: string; unitCost: number; quantity: number; shippedQuantity: number; unitPrice: number; quantityOnHand: number; }>; if (orderLines.length === 0) { throw new Error("Sales order has no lines."); } const requestedLines = parseFulfillmentLines(getText(formData, "lines")); const fulfilledLines = requestedLines.length ? requestedLines.map((request) => { const matchingLine = orderLines.find((line) => line.sku === request.sku); if (!matchingLine) { throw new Error(`SKU ${request.sku} is not on this sales order.`); } const remaining = matchingLine.quantity - matchingLine.shippedQuantity; if (request.quantity > remaining) { throw new Error(`Cannot ship ${request.quantity} of ${request.sku}; only ${remaining} remain.`); } if (matchingLine.quantityOnHand < request.quantity) { throw new Error(`Insufficient stock for ${matchingLine.sku}. Need ${request.quantity}, have ${matchingLine.quantityOnHand}.`); } return { ...matchingLine, shipQuantity: request.quantity }; }) : orderLines .map((line) => { const remaining = line.quantity - line.shippedQuantity; return remaining > 0 ? { ...line, shipQuantity: remaining } : null; }) .filter((line): line is NonNullable => line !== null); if (fulfilledLines.length === 0) { throw new Error("No shippable quantities were provided."); } const revenue = fulfilledLines.reduce((sum, line) => sum + line.shipQuantity * line.unitPrice, 0); const cogs = fulfilledLines.reduce((sum, line) => sum + line.shipQuantity * line.unitCost, 0); const orderNumber = orderLines[0].orderNumber; const tx = db().transaction(() => { const insertInventory = db().prepare( ` INSERT INTO inventory_transactions (part_id, quantity_delta, unit_cost, transaction_type, reference_type, reference_id, notes) VALUES (?, ?, ?, 'sales_shipment', 'sales_order', ?, ?) ` ); const updateLine = db().prepare(`UPDATE sales_order_lines SET shipped_quantity = shipped_quantity + ? WHERE id = ?`); for (const line of fulfilledLines) { insertInventory.run(line.partId, line.shipQuantity * -1, line.unitCost, orderId, `Shipment for ${orderNumber}`); updateLine.run(line.shipQuantity, line.lineId); } const remainingCount = db() .prepare(`SELECT COUNT(*) AS count FROM sales_order_lines WHERE sales_order_id = ? AND shipped_quantity < quantity`) .get(orderId) as { count: number }; const nextStatus = remainingCount.count > 0 ? "partial" : "shipped"; db().prepare(`UPDATE sales_orders SET status = ?, shipped_at = CURRENT_TIMESTAMP WHERE id = ?`).run(nextStatus, orderId); db() .prepare( ` INSERT INTO customer_invoices (invoice_number, sales_order_id, customer_id, status, due_date, total_amount, paid_amount) VALUES (?, ?, ?, 'open', DATE('now', '+30 day'), ?, 0) ` ) .run(getDocumentNumber("INV", "customer_invoices"), orderId, order.customerId, revenue); }); tx(); createJournalEntry("shipment", "sales_order", orderId, `Shipment posted for ${orderNumber}`, [ { accountCode: "1100", accountName: "Accounts Receivable", debit: revenue, credit: 0 }, { accountCode: "4000", accountName: "Sales Revenue", debit: 0, credit: revenue }, { accountCode: "5000", accountName: "Cost of Goods Sold", debit: cogs, credit: 0 }, { accountCode: "1200", accountName: "Inventory", debit: 0, credit: cogs } ]); revalidatePath("/"); revalidatePath("/parts"); revalidatePath("/sales-orders"); revalidatePath("/accounting"); revalidatePath("/invoices"); } export async function createPurchaseOrder(formData: FormData) { const vendorCode = getText(formData, "vendorCode"); const lines = parseLines(getText(formData, "lines")); const vendorRow = db().prepare(`SELECT id FROM vendors WHERE code = ?`).get(vendorCode) as { id: number } | undefined; if (!vendorRow) { throw new Error(`Vendor "${vendorCode}" does not exist.`); } const tx = db().transaction(() => { const result = db() .prepare( ` INSERT INTO purchase_orders (order_number, vendor_id, status, notes) VALUES (?, ?, 'ordered', ?) ` ) .run(getOrderNumber("PO", "purchase_orders"), vendorRow.id, getText(formData, "notes")); const orderId = Number(result.lastInsertRowid); const insertLine = db().prepare( ` INSERT INTO purchase_order_lines (purchase_order_id, part_id, quantity, unit_cost) VALUES (?, ?, ?, ?) ` ); for (const line of lines) { insertLine.run(orderId, getPartIdBySku(line.sku), line.quantity, line.amount); } }); tx(); revalidatePath("/"); revalidatePath("/purchase-orders"); } export async function receivePurchaseOrder(formData: FormData) { const orderId = Number(getText(formData, "orderId")); const order = db() .prepare(`SELECT vendor_id AS vendorId, status FROM purchase_orders WHERE id = ?`) .get(orderId) as { vendorId: number; status: string } | undefined; if (!order) { throw new Error("Purchase order not found."); } if (order.status === "received") { throw new Error("Purchase order has already been received."); } const lines = db() .prepare( ` SELECT pol.id AS lineId, po.order_number AS orderNumber, pol.part_id AS partId, pol.quantity, pol.received_quantity AS receivedQuantity, p.sku, pol.unit_cost AS unitCost FROM purchase_order_lines pol INNER JOIN purchase_orders po ON po.id = pol.purchase_order_id INNER JOIN parts p ON p.id = pol.part_id WHERE pol.purchase_order_id = ? ` ) .all(orderId) as Array<{ lineId: number; orderNumber: string; partId: number; quantity: number; receivedQuantity: number; sku: string; unitCost: number; }>; if (lines.length === 0) { throw new Error("Purchase order has no lines."); } const requestedLines = parseFulfillmentLines(getText(formData, "lines")); const fulfilledLines = requestedLines.length ? requestedLines.map((request) => { const matchingLine = lines.find((line) => line.sku === request.sku); if (!matchingLine) { throw new Error(`SKU ${request.sku} is not on this purchase order.`); } const remaining = matchingLine.quantity - matchingLine.receivedQuantity; if (request.quantity > remaining) { throw new Error(`Cannot receive ${request.quantity} of ${request.sku}; only ${remaining} remain.`); } return { ...matchingLine, receiveQuantity: request.quantity }; }) : lines .map((line) => { const remaining = line.quantity - line.receivedQuantity; return remaining > 0 ? { ...line, receiveQuantity: remaining } : null; }) .filter((line): line is NonNullable => line !== null); if (fulfilledLines.length === 0) { throw new Error("No receivable quantities were provided."); } const receiptValue = fulfilledLines.reduce((sum, line) => sum + line.receiveQuantity * line.unitCost, 0); const orderNumber = lines[0].orderNumber; const tx = db().transaction(() => { const insertInventory = db().prepare( ` INSERT INTO inventory_transactions (part_id, quantity_delta, unit_cost, transaction_type, reference_type, reference_id, notes) VALUES (?, ?, ?, 'purchase_receipt', 'purchase_order', ?, ?) ` ); const updatePartCost = db().prepare(`UPDATE parts SET unit_cost = ? WHERE id = ?`); const updateLine = db().prepare(`UPDATE purchase_order_lines SET received_quantity = received_quantity + ? WHERE id = ?`); for (const line of fulfilledLines) { insertInventory.run(line.partId, line.receiveQuantity, line.unitCost, orderId, `Receipt for ${orderNumber}`); updatePartCost.run(line.unitCost, line.partId); updateLine.run(line.receiveQuantity, line.lineId); } const remainingCount = db() .prepare(`SELECT COUNT(*) AS count FROM purchase_order_lines WHERE purchase_order_id = ? AND received_quantity < quantity`) .get(orderId) as { count: number }; const nextStatus = remainingCount.count > 0 ? "partial" : "received"; db().prepare(`UPDATE purchase_orders SET status = ?, received_at = CURRENT_TIMESTAMP WHERE id = ?`).run(nextStatus, orderId); db() .prepare( ` INSERT INTO vendor_bills (bill_number, purchase_order_id, vendor_id, status, due_date, total_amount, paid_amount) VALUES (?, ?, ?, 'open', DATE('now', '+30 day'), ?, 0) ` ) .run(getDocumentNumber("BILL", "vendor_bills"), orderId, order.vendorId, receiptValue); }); tx(); createJournalEntry("receipt", "purchase_order", orderId, `Receipt posted for ${orderNumber}`, [ { accountCode: "1200", accountName: "Inventory", debit: receiptValue, credit: 0 }, { accountCode: "2000", accountName: "Accounts Payable", debit: 0, credit: receiptValue } ]); revalidatePath("/"); revalidatePath("/parts"); revalidatePath("/purchase-orders"); revalidatePath("/accounting"); revalidatePath("/vendor-bills"); } export async function createManualJournalEntry(formData: FormData) { const description = getText(formData, "description"); const lines = parseJournalLines(getText(formData, "lines")); const debitTotal = lines.reduce((sum, line) => sum + line.debit, 0); const creditTotal = lines.reduce((sum, line) => sum + line.credit, 0); if (Math.abs(debitTotal - creditTotal) > 0.005) { throw new Error("Manual journal entry is not balanced."); } createJournalEntry("manual", "manual_journal", null, description || "Manual journal entry", lines); revalidatePath("/accounting"); } export async function loginAction(formData: FormData) { const email = getText(formData, "email"); const password = getText(formData, "password"); const user = authenticateUser(db(), email, password); if (!user) { throw new Error("Invalid email or password."); } await createSession(user); redirect("/"); } export async function logoutAction() { await destroySession(); redirect("/login"); } export async function receiveCustomerPayment(formData: FormData) { const invoiceId = Number(getText(formData, "invoiceId")); const amount = getNumber(formData, "amount"); const notes = getText(formData, "notes"); if (amount <= 0) { throw new Error("Payment amount must be greater than zero."); } const invoice = db() .prepare( ` SELECT id, invoice_number AS invoiceNumber, total_amount AS totalAmount, paid_amount AS paidAmount FROM customer_invoices WHERE id = ? ` ) .get(invoiceId) as { id: number; invoiceNumber: string; totalAmount: number; paidAmount: number } | undefined; if (!invoice) { throw new Error("Invoice not found."); } const balanceDue = invoice.totalAmount - invoice.paidAmount; if (amount > balanceDue) { throw new Error("Payment cannot exceed invoice balance."); } const newPaidAmount = invoice.paidAmount + amount; const newStatus = Math.abs(newPaidAmount - invoice.totalAmount) <= 0.005 ? "paid" : "partial"; const tx = db().transaction(() => { db() .prepare(`INSERT INTO customer_payments (invoice_id, amount, notes) VALUES (?, ?, ?)`) .run(invoiceId, amount, notes); db() .prepare(`UPDATE customer_invoices SET paid_amount = ?, status = ? WHERE id = ?`) .run(newPaidAmount, newStatus, invoiceId); }); tx(); createJournalEntry("customer_payment", "customer_invoice", invoiceId, `Payment received for ${invoice.invoiceNumber}`, [ { accountCode: "1000", accountName: "Cash", debit: amount, credit: 0 }, { accountCode: "1100", accountName: "Accounts Receivable", debit: 0, credit: amount } ]); revalidatePath("/"); revalidatePath("/accounting"); revalidatePath("/invoices"); } export async function payVendorBill(formData: FormData) { const vendorBillId = Number(getText(formData, "vendorBillId")); const amount = getNumber(formData, "amount"); const notes = getText(formData, "notes"); if (amount <= 0) { throw new Error("Payment amount must be greater than zero."); } const bill = db() .prepare( ` SELECT id, bill_number AS billNumber, total_amount AS totalAmount, paid_amount AS paidAmount FROM vendor_bills WHERE id = ? ` ) .get(vendorBillId) as { id: number; billNumber: string; totalAmount: number; paidAmount: number } | undefined; if (!bill) { throw new Error("Vendor bill not found."); } const balanceDue = bill.totalAmount - bill.paidAmount; if (amount > balanceDue) { throw new Error("Payment cannot exceed vendor bill balance."); } const newPaidAmount = bill.paidAmount + amount; const newStatus = Math.abs(newPaidAmount - bill.totalAmount) <= 0.005 ? "paid" : "partial"; const tx = db().transaction(() => { db() .prepare(`INSERT INTO vendor_payments (vendor_bill_id, amount, notes) VALUES (?, ?, ?)`) .run(vendorBillId, amount, notes); db() .prepare(`UPDATE vendor_bills SET paid_amount = ?, status = ? WHERE id = ?`) .run(newPaidAmount, newStatus, vendorBillId); }); tx(); createJournalEntry("vendor_payment", "vendor_bill", vendorBillId, `Vendor payment posted for ${bill.billNumber}`, [ { accountCode: "2000", accountName: "Accounts Payable", debit: amount, credit: 0 }, { accountCode: "1000", accountName: "Cash", debit: 0, credit: amount } ]); revalidatePath("/"); revalidatePath("/accounting"); revalidatePath("/vendor-bills"); }