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'); const apiClient = require('../api-client'); if (!token || apiClient.isTokenExpired(expiry) || value !== token) { return reply.code(401).send({ error: true, message: 'Not authenticated' }); } } 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 }; // Get today's date for stats const today = new Date(); filters.date = today; const orders = database.getOrders(filters); const stats = database.getOrderStats(); 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;