344 lines
12 KiB
JavaScript
344 lines
12 KiB
JavaScript
|
|
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;
|
||
|
|
|