2026-03-23 16:16:45 -05:00
"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 ;
} ;
2026-03-23 16:56:23 -05:00
type RelationalOrderLine = {
partId : number ;
quantity : number ;
amount : number ;
} ;
2026-03-23 16:16:45 -05:00
type ParsedFulfillmentLine = {
sku : string ;
quantity : number ;
} ;
2026-03-23 17:12:35 -05:00
type RelationalFulfillmentLine = {
lineId : number ;
quantity : number ;
} ;
2026-03-23 16:16:45 -05:00
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
} ;
} ) ;
}
2026-03-23 16:56:23 -05:00
function parseRelationalOrderLines ( raw : string ) : RelationalOrderLine [ ] {
try {
const parsed = JSON . parse ( raw ) as Array < Record < string , unknown > > ;
if ( ! Array . isArray ( parsed ) || parsed . length === 0 ) {
throw new Error ( "Order must contain at least one line." ) ;
}
const lines = parsed . map ( ( line ) = > {
const partId = Number ( line . partId ) ;
const quantity = Number ( line . quantity ) ;
const amount = Number ( line . amount ) ;
if ( ! Number . isInteger ( partId ) || partId <= 0 ) {
throw new Error ( "Invalid item selection." ) ;
}
if ( ! Number . isFinite ( quantity ) || quantity <= 0 ) {
throw new Error ( "Invalid line quantity." ) ;
}
if ( ! Number . isFinite ( amount ) || amount < 0 ) {
throw new Error ( "Invalid line amount." ) ;
}
return { partId , quantity , amount } ;
} ) ;
const uniquePartIds = new Set ( lines . map ( ( line ) = > line . partId ) ) ;
if ( uniquePartIds . size !== lines . length ) {
throw new Error ( "Each inventory item can only appear once per order." ) ;
}
return lines ;
} catch {
throw new Error ( "Invalid relational order payload." ) ;
}
}
2026-03-23 16:16:45 -05:00
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 } ;
} ) ;
}
2026-03-23 17:12:35 -05:00
function parseRelationalFulfillmentLines ( raw : string ) : RelationalFulfillmentLine [ ] {
try {
const parsed = JSON . parse ( raw ) as Array < Record < string , unknown > > ;
if ( ! Array . isArray ( parsed ) || parsed . length === 0 ) {
throw new Error ( "No fulfillment lines selected." ) ;
}
return parsed . map ( ( line ) = > {
const lineId = Number ( line . lineId ) ;
const quantity = Number ( line . quantity ) ;
if ( ! Number . isInteger ( lineId ) || lineId <= 0 ) {
throw new Error ( "Invalid line selection." ) ;
}
if ( ! Number . isFinite ( quantity ) || quantity <= 0 ) {
throw new Error ( "Invalid fulfillment quantity." ) ;
}
return { lineId , quantity } ;
} ) ;
} catch {
throw new Error ( "Invalid relational fulfillment payload." ) ;
}
}
2026-03-23 16:16:45 -05:00
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 ;
}
2026-03-23 16:56:23 -05:00
function getExistingPart ( partId : number ) {
const row = db ( )
. prepare ( ` SELECT id FROM parts WHERE id = ? ` )
. get ( partId ) as { id : number } | undefined ;
if ( ! row ) {
throw new Error ( ` Selected inventory item ${ partId } does not exist. ` ) ;
}
return row . id ;
}
2026-03-23 16:16:45 -05:00
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" ) ;
2026-03-23 17:12:35 -05:00
if ( ! assemblySku ) {
redirect ( "/assemblies?error=Select an assembly before building." ) ;
}
2026-03-23 16:16:45 -05:00
if ( buildQuantity <= 0 ) {
2026-03-23 17:12:35 -05:00
redirect ( "/assemblies?error=Build quantity must be greater than zero." ) ;
2026-03-23 16:16:45 -05:00
}
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 ) {
2026-03-23 17:12:35 -05:00
redirect ( "/assemblies?error=Assembly has no bill of materials defined." ) ;
2026-03-23 16:16:45 -05:00
}
for ( const component of components ) {
const needed = component . componentQuantity * buildQuantity ;
if ( component . quantityOnHand < needed ) {
2026-03-23 17:12:35 -05:00
redirect ( ` /assemblies?error= ${ encodeURIComponent ( ` Not enough stock for component ${ component . sku } . Need ${ needed } , have ${ component . quantityOnHand } . ` ) } ` ) ;
2026-03-23 16:16:45 -05:00
}
}
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" ) ;
2026-03-23 17:12:35 -05:00
redirect ( ` /assemblies?success= ${ encodeURIComponent ( ` Built ${ buildQuantity } of ${ assemblySku } . ` ) } ` ) ;
2026-03-23 16:16:45 -05:00
}
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 ) {
2026-03-23 16:56:23 -05:00
const customerId = Number ( getText ( formData , "customerId" ) ) ;
const relationalLinesPayload = getText ( formData , "lineItems" ) ;
const lines = relationalLinesPayload
? parseRelationalOrderLines ( relationalLinesPayload )
: parseLines ( getText ( formData , "lines" ) ) . map ( ( line ) = > ( {
partId : getPartIdBySku ( line . sku ) ,
quantity : line.quantity ,
amount : line.amount
} ) ) ;
const customerRow = db ( ) . prepare ( ` SELECT id FROM customers WHERE id = ? ` ) . get ( customerId ) as { id : number } | undefined ;
2026-03-23 16:16:45 -05:00
if ( ! customerRow ) {
2026-03-23 16:56:23 -05:00
throw new Error ( "Selected customer does not exist." ) ;
2026-03-23 16:16:45 -05:00
}
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 ) {
2026-03-23 16:56:23 -05:00
insertLine . run ( orderId , getExistingPart ( line . partId ) , line . quantity , line . amount ) ;
2026-03-23 16:16:45 -05:00
}
} ) ;
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." ) ;
}
2026-03-23 17:12:35 -05:00
const relationalPayload = getText ( formData , "fulfillmentLines" ) ;
const fulfilledLines = relationalPayload
? parseRelationalFulfillmentLines ( relationalPayload ) . map ( ( request ) = > {
const matchingLine = orderLines . find ( ( line ) = > line . lineId === request . lineId ) ;
2026-03-23 16:16:45 -05:00
if ( ! matchingLine ) {
2026-03-23 17:12:35 -05:00
throw new Error ( "Selected sales order line is invalid." ) ;
2026-03-23 16:16:45 -05:00
}
const remaining = matchingLine . quantity - matchingLine . shippedQuantity ;
if ( request . quantity > remaining ) {
2026-03-23 17:12:35 -05:00
throw new Error ( ` Cannot ship ${ request . quantity } of ${ matchingLine . sku } ; only ${ remaining } remain. ` ) ;
2026-03-23 16:16:45 -05:00
}
if ( matchingLine . quantityOnHand < request . quantity ) {
throw new Error ( ` Insufficient stock for ${ matchingLine . sku } . Need ${ request . quantity } , have ${ matchingLine . quantityOnHand } . ` ) ;
}
return { . . . matchingLine , shipQuantity : request.quantity } ;
} )
2026-03-23 17:12:35 -05:00
: parseFulfillmentLines ( getText ( formData , "lines" ) ) . length
? parseFulfillmentLines ( getText ( formData , "lines" ) ) . 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
2026-03-23 16:16:45 -05:00
. map ( ( line ) = > {
const remaining = line . quantity - line . shippedQuantity ;
return remaining > 0 ? { . . . line , shipQuantity : remaining } : null ;
} )
. filter ( ( line ) : line is NonNullable < typeof line > = > 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 ) {
2026-03-23 16:56:23 -05:00
const vendorId = Number ( getText ( formData , "vendorId" ) ) ;
const relationalLinesPayload = getText ( formData , "lineItems" ) ;
const lines = relationalLinesPayload
? parseRelationalOrderLines ( relationalLinesPayload )
: parseLines ( getText ( formData , "lines" ) ) . map ( ( line ) = > ( {
partId : getPartIdBySku ( line . sku ) ,
quantity : line.quantity ,
amount : line.amount
} ) ) ;
const vendorRow = db ( ) . prepare ( ` SELECT id FROM vendors WHERE id = ? ` ) . get ( vendorId ) as { id : number } | undefined ;
2026-03-23 16:16:45 -05:00
if ( ! vendorRow ) {
2026-03-23 16:56:23 -05:00
throw new Error ( "Selected vendor does not exist." ) ;
2026-03-23 16:16:45 -05:00
}
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 ) {
2026-03-23 16:56:23 -05:00
insertLine . run ( orderId , getExistingPart ( line . partId ) , line . quantity , line . amount ) ;
2026-03-23 16:16:45 -05:00
}
} ) ;
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." ) ;
}
2026-03-23 17:12:35 -05:00
const relationalPayload = getText ( formData , "fulfillmentLines" ) ;
const fulfilledLines = relationalPayload
? parseRelationalFulfillmentLines ( relationalPayload ) . map ( ( request ) = > {
const matchingLine = lines . find ( ( line ) = > line . lineId === request . lineId ) ;
2026-03-23 16:16:45 -05:00
if ( ! matchingLine ) {
2026-03-23 17:12:35 -05:00
throw new Error ( "Selected purchase order line is invalid." ) ;
2026-03-23 16:16:45 -05:00
}
const remaining = matchingLine . quantity - matchingLine . receivedQuantity ;
if ( request . quantity > remaining ) {
2026-03-23 17:12:35 -05:00
throw new Error ( ` Cannot receive ${ request . quantity } of ${ matchingLine . sku } ; only ${ remaining } remain. ` ) ;
2026-03-23 16:16:45 -05:00
}
return { . . . matchingLine , receiveQuantity : request.quantity } ;
} )
2026-03-23 17:12:35 -05:00
: parseFulfillmentLines ( getText ( formData , "lines" ) ) . length
? parseFulfillmentLines ( getText ( formData , "lines" ) ) . 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
2026-03-23 16:16:45 -05:00
. map ( ( line ) = > {
const remaining = line . quantity - line . receivedQuantity ;
return remaining > 0 ? { . . . line , receiveQuantity : remaining } : null ;
} )
. filter ( ( line ) : line is NonNullable < typeof line > = > 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" ) ;
}