2025-10-23 19:02:56 -04:00
const config = require ( '../config' ) ;
const database = require ( '../database' ) ;
const apiClient = require ( '../api-client' ) ;
const printer = require ( '../printer' ) ;
// Middleware to check authentication via signed cookie (JSON response)
async function requireAuth ( req , reply ) {
const raw = req . cookies && req . cookies . kitchen _session ;
if ( ! raw ) { return reply . code ( 401 ) . send ( { error : true , message : 'Not authenticated' } ) ; }
const { valid , value } = req . unsignCookie ( raw || '' ) ;
if ( ! valid ) { return reply . code ( 401 ) . send ( { error : true , message : 'Not authenticated' } ) ; }
const token = config . get ( 'authToken' ) ;
const expiry = config . get ( 'tokenExpiry' ) ;
2026-03-01 17:10:03 -05:00
if ( ! token || apiClient . isTokenExpired ( expiry ) ) {
2025-10-23 19:02:56 -04:00
return reply . code ( 401 ) . send ( { error : true , message : 'Not authenticated' } ) ;
}
2026-03-01 17:10:03 -05:00
if ( value === token ) return ;
const previousToken = config . get ( 'previousAuthToken' ) ;
if ( previousToken && value === previousToken ) {
const isHttps = ( req . protocol === 'https' ) || ( ( req . headers [ 'x-forwarded-proto' ] || '' ) . toString ( ) . toLowerCase ( ) === 'https' ) ;
reply . setCookie ( 'kitchen_session' , token , {
signed : true , httpOnly : true , secure : isHttps ,
sameSite : 'strict' , maxAge : 30 * 24 * 60 * 60 , path : '/'
} ) ;
return ;
}
return reply . code ( 401 ) . send ( { error : true , message : 'Not authenticated' } ) ;
2025-10-23 19:02:56 -04:00
}
async function ordersRoutes ( fastify , options ) {
// Get orders with filters
fastify . get ( '/api/orders' , { preHandler : requireAuth } , async ( req , reply ) => {
const filters = {
status : req . query . status ,
limit : parseInt ( req . query . limit , 10 ) || 50
} ;
2026-03-10 20:36:25 -04:00
const normalizeUnixSeconds = ( v ) => {
const n = Number ( v ) ;
if ( ! Number . isFinite ( n ) || n <= 0 ) return 0 ;
// Accept both seconds and milliseconds.
return n > 1e12 ? Math . floor ( n / 1000 ) : Math . floor ( n ) ;
} ;
let startDate = normalizeUnixSeconds ( req . query . startDate ) ;
let endDate = normalizeUnixSeconds ( req . query . endDate ) ;
// Be resilient to inverted ranges
if ( startDate > 0 && endDate > 0 && startDate > endDate ) {
const tmp = startDate ;
startDate = endDate ;
endDate = tmp ;
}
if ( startDate > 0 && endDate > 0 ) {
filters . startDate = startDate ;
filters . endDate = endDate ;
} else if ( startDate > 0 ) {
filters . startDate = startDate ;
} else if ( endDate > 0 ) {
filters . endDate = endDate ;
} else {
filters . date = new Date ( ) ;
}
2025-10-23 19:02:56 -04:00
const orders = database . getOrders ( filters ) ;
2026-03-10 20:36:25 -04:00
const stats = database . getOrderStats (
filters . startDate || null ,
filters . endDate || null
) ;
2025-10-23 19:02:56 -04:00
return { error : false , orders , stats } ;
} ) ;
// Update order status
fastify . post ( '/api/orders/:id/status' , { preHandler : requireAuth } , async ( req , reply ) => {
const orderId = parseInt ( req . params . id , 10 ) ;
const { status } = req . body ;
if ( ! status ) {
return { error : true , message : 'Status is required' } ;
}
// Valid local statuses
const validStatuses = [ 'new' , 'preparing' , 'ready' , 'completed' ] ;
if ( ! validStatuses . includes ( status ) ) {
return { error : true , message : 'Invalid status' } ;
}
try {
// Update local database
database . updateOrderStatus ( orderId , status ) ;
// Sync to backend if status is completed (maps to finished)
if ( status === 'completed' ) {
const appConfig = config . getAll ( ) ;
const order = database . getOrderById ( orderId ) ;
if ( order && appConfig . authToken && appConfig . selectedBotId ) {
// Determine the action based on order type
let action = 'finished' ;
if ( order . order . type === 'delivery' ) {
action = 'delivered' ;
} else if ( order . order . type === 'pickup' ) {
action = 'picked_up' ;
}
const result = await apiClient . modifyOrder (
appConfig . authToken ,
appConfig . selectedBotId ,
orderId ,
action
) ;
if ( result . error ) {
console . error ( 'Failed to sync order status to backend:' , result . message ) ;
}
}
}
return { error : false } ;
} catch ( error ) {
console . error ( 'Failed to update order status:' , error . message ) ;
return { error : true , message : 'Failed to update order status' } ;
}
} ) ;
// Cancel order
fastify . post ( '/api/orders/:id/cancel' , { preHandler : requireAuth } , async ( req , reply ) => {
const orderId = parseInt ( req . params . id , 10 ) ;
const { reason } = req . body ;
try {
// Check if cancellation already printed to prevent duplicates
if ( database . hasPrintedCancellation ( orderId ) ) {
console . log ( ` [API] Cancellation already printed for order # ${ orderId } , skipping duplicate ` ) ;
database . updateOrderStatus ( orderId , 'canceled' ) ;
return { error : false , message : 'Order already canceled' } ;
}
// Update local database
database . updateOrderStatus ( orderId , 'canceled' ) ;
// Sync to backend
const appConfig = config . getAll ( ) ;
if ( appConfig . authToken && appConfig . selectedBotId ) {
const result = await apiClient . modifyOrder (
appConfig . authToken ,
appConfig . selectedBotId ,
orderId ,
'cancel' ,
reason || 'Canceled by kitchen'
) ;
if ( result . error ) {
console . error ( 'Failed to sync cancellation to backend:' , result . message ) ;
}
}
// Print cancellation receipt
const order = database . getOrderById ( orderId ) ;
if ( order ) {
try {
// Add to print queue with deduplication check
const jobId = database . addToPrintQueue ( orderId , 'canceled' ) ;
if ( ! jobId ) {
console . log ( ` [API] Cancellation print job not created (duplicate prevention) for order # ${ orderId } ` ) ;
return { error : false , message : 'Cancellation recorded' } ;
}
database . markPrintJobProcessing ( jobId ) ;
// Get enabled printers from database
const printerConfigs = database . getEnabledPrinters ( ) ;
let result ;
if ( printerConfigs && printerConfigs . length > 0 ) {
// Use new per-printer config system
result = await printer . printOrderReceiptWithPrinterConfigs (
order ,
printerConfigs ,
'canceled' ,
{ reason : reason || 'Canceled by kitchen' }
) ;
} else {
// Fallback to legacy system if no printer configs
if ( ! printer . printer ) {
printer . initializePrinter ( appConfig ) ;
}
result = await printer . printOrderReceipt ( order , 'canceled' , { reason : reason || 'Canceled by kitchen' } ) ;
}
if ( result && result . success ) {
database . markOrderPrinted ( orderId ) ;
database . markPrintJobCompleted ( jobId ) ;
// Cleanup any other pending jobs for this order+type
database . cleanupDuplicateJobs ( jobId , orderId , 'canceled' ) ;
} else {
// Mark as pending for worker retry
database . markPrintJobPending ( jobId ) ;
}
} catch ( error ) {
console . error ( 'Failed to print cancellation receipt:' , error . message ) ;
// Let the worker retry - find the job and mark it pending
try {
const lastJobIdRow = database . db . prepare ( "SELECT id FROM print_queue WHERE order_id = ? AND print_type = 'canceled' ORDER BY id DESC LIMIT 1" ) . get ( orderId ) ;
if ( lastJobIdRow && lastJobIdRow . id ) { database . markPrintJobPending ( lastJobIdRow . id ) ; }
} catch ( _ ) { }
}
}
return { error : false } ;
} catch ( error ) {
console . error ( 'Failed to cancel order:' , error . message ) ;
return { error : true , message : 'Failed to cancel order' } ;
}
} ) ;
// Reprint order
fastify . post ( '/api/orders/:id/reprint' , { preHandler : requireAuth } , async ( req , reply ) => {
const orderId = parseInt ( req . params . id , 10 ) ;
try {
const order = database . getOrderById ( orderId ) ;
if ( ! order ) {
return { error : true , message : 'Order not found' } ;
}
const printType = order . localStatus === 'canceled' ? 'canceled' : 'new' ;
// Check for recent ACTIVE job to prevent double-enqueue while in-flight
const activeCheck = database . hasActiveOrRecentJob ( orderId , 'reprint' , 10 ) ;
if ( activeCheck . hasActive && ( activeCheck . status === 'pending' || activeCheck . status === 'processing' ) ) {
console . log ( ` [API] Reprint request for order # ${ orderId } blocked - active job ${ activeCheck . jobId } ( ${ activeCheck . status } ) exists ` ) ;
return { error : false , message : 'Print already in progress' } ;
}
// Add to print queue with deduplication
const jobId = database . addToPrintQueue ( orderId , 'reprint' ) ;
if ( ! jobId ) {
console . log ( ` [API] Reprint job not created (duplicate prevention) for order # ${ orderId } ` ) ;
return { error : false , message : 'Print recently completed, skipping duplicate' } ;
}
database . markPrintJobProcessing ( jobId ) ;
// Print receipt using per-printer configs
const printerConfigs = database . getEnabledPrinters ( ) ;
let result ;
if ( printerConfigs && printerConfigs . length > 0 ) {
// Use new per-printer config system
result = await printer . printOrderReceiptWithPrinterConfigs ( order , printerConfigs , printType , { cooldownMs : 2000 } ) ;
} else {
// Fallback to legacy system
const appConfig = config . getAll ( ) ;
printer . initializePrinter ( appConfig ) ;
result = await printer . printOrderReceipt ( order , printType , { cooldownMs : 2000 } ) ;
}
if ( result && result . success ) {
database . markOrderPrinted ( orderId ) ;
database . markPrintJobCompleted ( jobId ) ;
// Cleanup any other pending jobs for this order+type
database . cleanupDuplicateJobs ( jobId , orderId , 'reprint' ) ;
return { error : false , message : ( result && result . message ) ? result . message : 'Receipt sent to printer' } ;
} else {
// Mark as pending so the worker can retry when printer is online
database . markPrintJobPending ( jobId ) ;
return { error : true , message : result . error || 'Print failed' } ;
}
} catch ( error ) {
console . error ( 'Failed to reprint order:' , error . message ) ;
try { database . resetStuckProcessingJobs ( 60 ) ; } catch ( _ ) { }
return { error : true , message : 'Failed to reprint order' } ;
}
} ) ;
// Get single order details
fastify . get ( '/api/orders/:id' , { preHandler : requireAuth } , async ( req , reply ) => {
const orderId = parseInt ( req . params . id , 10 ) ;
const order = database . getOrderById ( orderId ) ;
if ( ! order ) {
return { error : true , message : 'Order not found' } ;
}
return { error : false , order } ;
} ) ;
// Manual sync trigger (for testing/debugging)
fastify . post ( '/api/sync-now' , { preHandler : requireAuth } , async ( req , reply ) => {
try {
// Trigger the poller manually
if ( fastify . orderPoller ) {
fastify . orderPoller . poll ( ) ;
return { error : false , message : 'Manual sync triggered' } ;
} else {
return { error : true , message : 'Order poller not available' } ;
}
} catch ( error ) {
console . error ( 'Manual sync error:' , error . message ) ;
return { error : true , message : error . message } ;
}
} ) ;
// Health check for external API server
fastify . get ( '/api/health/external' , { preHandler : requireAuth } , async ( req , reply ) => {
try {
const appConfig = config . getAll ( ) ;
const token = appConfig . authToken ;
const botId = appConfig . selectedBotId ;
if ( ! token || ! botId ) {
return {
error : false ,
status : 'unconfigured' ,
message : 'API not configured' ,
timestamp : new Date ( ) . toISOString ( )
} ;
}
// Check if token is expired
if ( apiClient . isTokenExpired ( appConfig . tokenExpiry ) ) {
return {
error : false ,
status : 'offline' ,
message : 'Token expired' ,
timestamp : new Date ( ) . toISOString ( )
} ;
}
// Try to fetch bots list as a lightweight health check
const startTime = Date . now ( ) ;
const result = await apiClient . getBots ( token ) ;
const responseTime = Date . now ( ) - startTime ;
if ( result . error ) {
return {
error : false ,
status : 'offline' ,
message : result . message || 'API server unreachable' ,
responseTime : responseTime ,
timestamp : new Date ( ) . toISOString ( )
} ;
}
return {
error : false ,
status : 'online' ,
message : 'API server connected' ,
responseTime : responseTime ,
timestamp : new Date ( ) . toISOString ( )
} ;
} catch ( error ) {
console . error ( 'External API health check error:' , error . message ) ;
return {
error : false ,
status : 'offline' ,
message : error . message || 'Health check failed' ,
timestamp : new Date ( ) . toISOString ( )
} ;
}
} ) ;
// Health check for local dashboard server
fastify . get ( '/api/health/local' , { preHandler : requireAuth } , async ( req , reply ) => {
return {
error : false ,
status : 'online' ,
message : 'Local server connected' ,
timestamp : new Date ( ) . toISOString ( )
} ;
} ) ;
}
module . exports = ordersRoutes ;