diff --git a/package.json b/package.json index 29d987cd6..c85f7e4fa 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "pretest": "npm run test:script-coverage && node scripts/run-test.cjs npm run test:payment-methods-split && node scripts/run-test.cjs npm run test:release-regressions && node scripts/run-test.cjs npm run test:product-variants-db && node scripts/run-test.cjs npm run test:product-variants-api", "start": "electron .", "rebuild": "HOME=~/.electron-gyp node-gyp rebuild --target=$(node -p \"require('./node_modules/electron/package.json').version\") --arch=$(node -p \"process.arch\") --dist-url=https://electronjs.org/headers --runtime=electron --directory node_modules/better-sqlite3", - "test": "node scripts/run-test.cjs npm run test:smoke && node scripts/run-test.cjs npm run test:server-port-collision && node scripts/run-test.cjs npm run test:kds-integration && node scripts/run-test.cjs npm run test:kds-broadcast-grouping && node scripts/run-test.cjs npm run test:kds-contract && node scripts/run-test.cjs npm run test:kds-frontend-conflict && node scripts/run-test.cjs npm run test:kds-window-hardening && node scripts/run-test.cjs npm run test:electron-api-contract && node scripts/run-test.cjs npm run test:titlebar-window-options && node scripts/run-test.cjs npm run test:update-ipc-sender-validation && node scripts/run-test.cjs npm run test:menu-surface && node scripts/run-test.cjs npm run test:pos-keyboard-shortcuts && node scripts/run-test.cjs npm run test:window-readiness && node scripts/run-test.cjs npm run test:window-load-retry && node scripts/run-test.cjs npm run test:cors && node scripts/run-test.cjs npm run test:csp-lan && node scripts/run-test.cjs npm run test:release-config && node scripts/run-test.cjs npm run test:update-state && node scripts/run-test.cjs npm run test:update-ui && node scripts/run-test.cjs npm run test:update-channel && node scripts/run-test.cjs npm run test:telemetry && node scripts/run-test.cjs npm run test:telemetry-install-provenance && node scripts/run-test.cjs npm run test:country-provenance && node scripts/run-test.cjs npm run test:first-run && node scripts/run-test.cjs npm run test:phase7-setup-i18n && node scripts/run-test.cjs npm run test:security && node scripts/run-test.cjs npm run test:jwt-secret-cache-identity && node scripts/run-test.cjs npm run test:jwt-secret-persist-before-cache && node scripts/run-test.cjs npm run test:jwt-secret-safe-storage && node scripts/run-test.cjs npm run test:staff-authz && node scripts/run-test.cjs npm run test:orders-authz && node scripts/run-test.cjs npm run test:order-item-cancel-restore && node scripts/run-test.cjs npm run test:order-item-edit && node scripts/run-test.cjs npm run test:item-price-override && node scripts/run-test.cjs npm run test:offline-mode && node scripts/run-test.cjs npm run test:product-variants-orders && node scripts/run-test.cjs npm run test:addon-inventory-pos-surface && node scripts/run-test.cjs npm run test:addon-inventory-orders && node scripts/run-test.cjs npm run test:addon-inventory-lifecycle && node scripts/run-test.cjs npm run test:order-totals-recompute && node scripts/run-test.cjs npm run test:issue-718-whatsapp-order-status && node scripts/run-test.cjs npm run test:order-audit-log && node scripts/run-test.cjs npm run test:authz-phase3 && node scripts/run-test.cjs npm run test:authorization-permissions && node scripts/run-test.cjs npm run test:auth-ui-deterministic && node scripts/run-test.cjs npm run test:desktop-trailing-slash-routing && node scripts/run-test.cjs npm run test:customer-auth && node scripts/run-test.cjs npm run test:customer-soft-reactivate-bindings && node scripts/run-test.cjs npm run test:customer-pagination && node scripts/run-test.cjs npm run test:customer-top-items && node scripts/run-test.cjs npm run test:server-app-server-role && node scripts/run-test.cjs npm run test:backup && node scripts/run-test.cjs npm run test:issue-278-fail-closed-db && node scripts/run-test.cjs npm run test:recovery-cloud && node scripts/run-test.cjs npm run test:cloud-account-status && node scripts/run-test.cjs npm run test:cloud-live-orders-batching && node scripts/run-test.cjs npm run test:printer && node scripts/run-test.cjs npm run test:printing-settings && node scripts/run-test.cjs npm run test:printer-width-refresh && node scripts/run-test.cjs npm run test:printer-migrations && node scripts/run-test.cjs npm run test:printer-text-size && node scripts/run-test.cjs npm run test:print-parity && node scripts/run-test.cjs npm run test:delivery-slip && node scripts/run-test.cjs npm run test:delivery-address && node scripts/run-test.cjs npm run test:thermal-capabilities && node scripts/run-test.cjs npm run test:raster && node scripts/run-test.cjs npm run test:merchant-print-templates && node scripts/run-test.cjs npm run test:merchant-template-transfer && node scripts/run-test.cjs npm run test:print-document && node scripts/run-test.cjs npm run test:menu-printing && node scripts/run-test.cjs npm run test:print-kernel && node scripts/run-test.cjs npm run test:translations && node scripts/run-test.cjs npm run test:print-labels && node scripts/run-test.cjs npm run test:locale-chunks && node scripts/run-test.cjs npm run test:rtl-foundation && node scripts/run-test.cjs npm run test:rtl-setup-auth-settings && node scripts/run-test.cjs npm run test:rtl-dashboard-pos-common && node scripts/run-test.cjs npm run test:rtl-kds-server-whatsapp && node scripts/run-test.cjs npm run test:phone && node scripts/run-test.cjs npm run test:country-localization && node scripts/run-test.cjs npm run test:e2e-argentina-flow && node scripts/run-test.cjs npm run test:charges && node scripts/run-test.cjs npm run test:charges-engine && node scripts/run-test.cjs npm run test:currency && node scripts/run-test.cjs npm run test:tax-engine && node scripts/run-test.cjs npm run test:tax-components && node scripts/run-test.cjs npm run test:tax-pack-catalog && node scripts/run-test.cjs npm run test:tax-pack-management && node scripts/run-test.cjs npm run test:manual-tax-config && node scripts/run-test.cjs npm run test:legacy-tax-pack-digest && node scripts/run-test.cjs npm run test:community-tax-packs && node scripts/run-test.cjs npm run test:support-ticket && node scripts/run-test.cjs npm run test:customer-phone-search && node scripts/run-test.cjs npm run test:phone-search-integration && node scripts/run-test.cjs npm run test:orders-search && node scripts/run-test.cjs npm run test:orders-layout-settings && node scripts/run-test.cjs npm run test:pos-catalog-sort && node scripts/run-test.cjs npm run test:pos-sort-settings && node scripts/run-test.cjs npm run test:receipt-column-width && node scripts/run-test.cjs npm run test:receipt-column-oracle && node scripts/run-test.cjs npm run test:notes-validation && node scripts/run-test.cjs npm run test:receipt-printing && node scripts/run-test.cjs npm run test:cancel-override && node scripts/run-test.cjs npm run test:refunds && node scripts/run-test.cjs npm run test:async-refund-pin-event-loop && node scripts/run-test.cjs npm run test:refund-completed-orders && node scripts/run-test.cjs npm run test:cash-closures && node scripts/run-test.cjs npm run test:cash-sessions && node scripts/run-test.cjs npm run test:kitchen-addons && node scripts/run-test.cjs npm run test:kot-order-notes && node scripts/run-test.cjs npm run test:pos-info-network-ranking && node scripts/run-test.cjs npm run test:order-item-addons && node scripts/run-test.cjs npm run test:issue-125-addon-reads && node scripts/run-test.cjs npm run test:windows-country-code-crash && node scripts/run-test.cjs npm run test:reports-insights && node scripts/run-test.cjs npm run test:reports-daily-stats-table-turn && node scripts/run-test.cjs npm run test:timezone-report-boundaries && node scripts/run-test.cjs npm run test:business-date && node scripts/run-test.cjs npm run test:dashboard-business-date && node scripts/run-test.cjs npm run test:sequence && node scripts/run-test.cjs npm run test:order-numbering-settings && node scripts/run-test.cjs npm run test:integration-happy && node scripts/run-test.cjs npm run test:integration-order-hold-payment && node scripts/run-test.cjs npm run test:integration-tax && node scripts/run-test.cjs npm run test:integration-payments && node scripts/run-test.cjs npm run test:kitchen-delivery-enforcement && node scripts/run-test.cjs npm run test:issue-214 && node scripts/run-test.cjs npm run test:issue-214-auth && node scripts/run-test.cjs npm run test:issue-214-migration && node scripts/run-test.cjs npm run test:issue-255 && node scripts/run-test.cjs npm run test:cart-variant-identity && node scripts/run-test.cjs npm run test:cart-price-override && node scripts/run-test.cjs npm run test:product-variants-screen && node scripts/run-test.cjs npm run test:variant-pos-surface && node scripts/run-test.cjs npm run test:issue-788-order-attempt-storage && node scripts/run-test.cjs npm run test:integration-lifecycle && node scripts/run-test.cjs npm run test:integration-reconciliation && node scripts/run-test.cjs npm run test:integration-loyalty && node scripts/run-test.cjs npm run test:integration-discount && node scripts/run-test.cjs npm run test:loyalty-toggle && node scripts/run-test.cjs npm run test:discount-system && node scripts/run-test.cjs npm run test:integration-discount-settings && node scripts/run-test.cjs npm run test:integration-loyalty-global && node scripts/run-test.cjs npm run test:issue-248-csv && node scripts/run-test.cjs npm run test:integration-loyalty-redemption && node scripts/run-test.cjs npm run test:bills-print-api && node scripts/run-test.cjs npm run test:issue-24 && node scripts/run-test.cjs npm run test:issue-134-routing && node scripts/run-test.cjs npm run test:issue-134-mgmt && node scripts/run-test.cjs npm run test:issue-137-barcode && node scripts/run-test.cjs npm run test:issue-244-product-addon-links && node scripts/run-test.cjs npm run test:issue-245-246-addon-groups && node scripts/run-test.cjs npm run test:issue-247-catalog-invariants && node scripts/run-test.cjs npm run test:issue-250-catalog-perf && node scripts/run-test.cjs npm run test:issue-252 && node scripts/run-test.cjs npm run test:inventory-ledger && node scripts/run-test.cjs npm run test:issue-printing-and-kds-resilience && node scripts/run-test.cjs npm run test:issue-258-bill-pagination && node scripts/run-test.cjs npm run test:issue-265-morocco-profile && node scripts/run-test.cjs npm run test:issue-266-currency-symbol-print && node scripts/run-test.cjs npm run test:tables-string-ids && node scripts/run-test.cjs npm run test:issue-646-floor-management && node scripts/run-test.cjs npm run test:held-orders && node scripts/run-test.cjs npm run test:product-images && node scripts/run-test.cjs npm run test:schema-health && node scripts/run-test.cjs npm run test:migration-registry && node scripts/run-test.cjs npm run test:upgrade-path && node scripts/run-test.cjs npm run test:upgrade-matrix-harness && node scripts/run-test.cjs npm run test:migration-v56-v57 && node scripts/run-test.cjs npm run test:migration-v71-repair && node scripts/run-test.cjs npm run test:migration-v80-cash-drawer-pulse && node scripts/run-test.cjs npm run test:migration-v81-cash-closures && node scripts/run-test.cjs npm run test:master-pin && node scripts/run-test.cjs npm run test:google-drive && node scripts/run-test.cjs npm run test:google-drive-qr-menu && node scripts/run-test.cjs npm run test:database-tools-api && node scripts/run-test.cjs npm run test:phone-validation && node scripts/run-test.cjs npm run test:phone-migration && node scripts/run-test.cjs npm run test:issue-133-kds-kot-toggles && node scripts/run-test.cjs npm run test:whatsapp-schema && node scripts/run-test.cjs npm run test:whatsapp-service && node scripts/run-test.cjs npm run test:whatsapp-middleware && node scripts/run-test.cjs npm run test:issue-127-password-recovery && node scripts/run-test.cjs npm run test:dev-tooling && node scripts/run-test.cjs npm run test:windows-uninstaller && node scripts/run-test.cjs npm run test:shutdown-lifecycle && node scripts/run-test.cjs npm run test:redos-hardening && node scripts/run-test.cjs npm run test:startup-cache && node scripts/run-test.cjs npm run test:service-worker && node scripts/run-test.cjs npm run test:issue-389-timezone-override && node scripts/run-test.cjs npm run test:issue-390-locale-preference-invariants && node scripts/run-test.cjs npm run test:issue-475-picker-highlight && node scripts/run-test.cjs npm run test:theme-mode-settings && node scripts/run-test.cjs npm run test:theme-fouc-script && node scripts/run-test.cjs npm run test:ui-regressions-621-623-626 && node scripts/run-test.cjs npm run test:issue-zero-balance-settlement && node scripts/run-test.cjs npm run test:daily-sales-export && node scripts/run-test.cjs npm run test:customer-sales-export && node scripts/run-test.cjs npm run test:card-transaction-id && node scripts/run-test.cjs npm run test:issue-terminal-state-guards && node scripts/run-test.cjs npm run test:issue-exact-balance-settlement && node scripts/run-test.cjs npm run test:financial-safety-remediation && node scripts/run-test.cjs npm run test:issue-payload-and-discount-bounds && node scripts/run-test.cjs npm run test:customer-display-config && node scripts/run-test.cjs npm run test:customer-display-settings-route && node scripts/run-test.cjs npm run test:customer-display-pairing && node scripts/run-test.cjs npm run test:customer-display-feed && node scripts/run-test.cjs npm run test:units && node scripts/run-test.cjs npm run test:supplies-service && node scripts/run-test.cjs npm run test:recipe-order-lifecycle && node scripts/run-test.cjs npm run test:ingredient-cost && node scripts/run-test.cjs npm run test:lint-engine && node scripts/run-test.cjs npm run test:oxc-contributor-checks && node scripts/run-test.cjs npm run test:expenses && node scripts/run-test.cjs npm run test:diagnostics && node scripts/run-test.cjs npm run test:reports-export && node scripts/run-test.cjs npm run test:category-addon-groups && node scripts/run-test.cjs npm run test:addon-inventory-db && node scripts/run-test.cjs npm run test:addon-inventory-api && node scripts/run-test.cjs npm run test:discount-kitchen-approver-identity && node scripts/run-test.cjs npm run test:staff-approvers-directory && node scripts/run-test.cjs npm run test:local-backup && npm run test:password-policy && npm run test:cloud-password-recovery", + "test": "node scripts/run-test.cjs npm run test:smoke && node scripts/run-test.cjs npm run test:server-port-collision && node scripts/run-test.cjs npm run test:kds-integration && node scripts/run-test.cjs npm run test:kds-broadcast-grouping && node scripts/run-test.cjs npm run test:kds-contract && node scripts/run-test.cjs npm run test:kds-frontend-conflict && node scripts/run-test.cjs npm run test:kds-window-hardening && node scripts/run-test.cjs npm run test:electron-api-contract && node scripts/run-test.cjs npm run test:titlebar-window-options && node scripts/run-test.cjs npm run test:update-ipc-sender-validation && node scripts/run-test.cjs npm run test:menu-surface && node scripts/run-test.cjs npm run test:pos-keyboard-shortcuts && node scripts/run-test.cjs npm run test:window-readiness && node scripts/run-test.cjs npm run test:window-load-retry && node scripts/run-test.cjs npm run test:cors && node scripts/run-test.cjs npm run test:csp-lan && node scripts/run-test.cjs npm run test:release-config && node scripts/run-test.cjs npm run test:update-state && node scripts/run-test.cjs npm run test:update-ui && node scripts/run-test.cjs npm run test:update-channel && node scripts/run-test.cjs npm run test:telemetry && node scripts/run-test.cjs npm run test:telemetry-install-provenance && node scripts/run-test.cjs npm run test:country-provenance && node scripts/run-test.cjs npm run test:first-run && node scripts/run-test.cjs npm run test:phase7-setup-i18n && node scripts/run-test.cjs npm run test:security && node scripts/run-test.cjs npm run test:jwt-secret-cache-identity && node scripts/run-test.cjs npm run test:jwt-secret-persist-before-cache && node scripts/run-test.cjs npm run test:jwt-secret-safe-storage && node scripts/run-test.cjs npm run test:staff-authz && node scripts/run-test.cjs npm run test:orders-authz && node scripts/run-test.cjs npm run test:order-item-cancel-restore && node scripts/run-test.cjs npm run test:order-item-edit && node scripts/run-test.cjs npm run test:item-price-override && node scripts/run-test.cjs npm run test:offline-mode && node scripts/run-test.cjs npm run test:product-variants-orders && node scripts/run-test.cjs npm run test:addon-inventory-pos-surface && node scripts/run-test.cjs npm run test:addon-inventory-orders && node scripts/run-test.cjs npm run test:addon-inventory-lifecycle && node scripts/run-test.cjs npm run test:order-totals-recompute && node scripts/run-test.cjs npm run test:issue-718-whatsapp-order-status && node scripts/run-test.cjs npm run test:order-audit-log && node scripts/run-test.cjs npm run test:authz-phase3 && node scripts/run-test.cjs npm run test:authorization-permissions && node scripts/run-test.cjs npm run test:auth-ui-deterministic && node scripts/run-test.cjs npm run test:desktop-trailing-slash-routing && node scripts/run-test.cjs npm run test:customer-auth && node scripts/run-test.cjs npm run test:customer-soft-reactivate-bindings && node scripts/run-test.cjs npm run test:customer-pagination && node scripts/run-test.cjs npm run test:customer-top-items && node scripts/run-test.cjs npm run test:server-app-server-role && node scripts/run-test.cjs npm run test:backup && node scripts/run-test.cjs npm run test:issue-278-fail-closed-db && node scripts/run-test.cjs npm run test:recovery-cloud && node scripts/run-test.cjs npm run test:cloud-account-status && node scripts/run-test.cjs npm run test:cloud-live-orders-batching && node scripts/run-test.cjs npm run test:printer && node scripts/run-test.cjs npm run test:printing-settings && node scripts/run-test.cjs npm run test:printer-width-refresh && node scripts/run-test.cjs npm run test:printer-migrations && node scripts/run-test.cjs npm run test:printer-text-size && node scripts/run-test.cjs npm run test:print-parity && node scripts/run-test.cjs npm run test:delivery-slip && node scripts/run-test.cjs npm run test:delivery-address && node scripts/run-test.cjs npm run test:thermal-capabilities && node scripts/run-test.cjs npm run test:raster && node scripts/run-test.cjs npm run test:merchant-print-templates && node scripts/run-test.cjs npm run test:merchant-template-transfer && node scripts/run-test.cjs npm run test:print-document && node scripts/run-test.cjs npm run test:menu-printing && node scripts/run-test.cjs npm run test:print-kernel && node scripts/run-test.cjs npm run test:translations && node scripts/run-test.cjs npm run test:print-labels && node scripts/run-test.cjs npm run test:locale-chunks && node scripts/run-test.cjs npm run test:rtl-foundation && node scripts/run-test.cjs npm run test:rtl-setup-auth-settings && node scripts/run-test.cjs npm run test:rtl-dashboard-pos-common && node scripts/run-test.cjs npm run test:rtl-kds-server-whatsapp && node scripts/run-test.cjs npm run test:phone && node scripts/run-test.cjs npm run test:country-localization && node scripts/run-test.cjs npm run test:e2e-argentina-flow && node scripts/run-test.cjs npm run test:charges && node scripts/run-test.cjs npm run test:charges-engine && node scripts/run-test.cjs npm run test:currency && node scripts/run-test.cjs npm run test:tax-engine && node scripts/run-test.cjs npm run test:tax-components && node scripts/run-test.cjs npm run test:tax-pack-catalog && node scripts/run-test.cjs npm run test:tax-pack-management && node scripts/run-test.cjs npm run test:manual-tax-config && node scripts/run-test.cjs npm run test:legacy-tax-pack-digest && node scripts/run-test.cjs npm run test:community-tax-packs && node scripts/run-test.cjs npm run test:support-ticket && node scripts/run-test.cjs npm run test:customer-phone-search && node scripts/run-test.cjs npm run test:phone-search-integration && node scripts/run-test.cjs npm run test:orders-search && node scripts/run-test.cjs npm run test:orders-layout-settings && node scripts/run-test.cjs npm run test:pos-catalog-sort && node scripts/run-test.cjs npm run test:pos-sort-settings && node scripts/run-test.cjs npm run test:receipt-column-width && node scripts/run-test.cjs npm run test:receipt-column-oracle && node scripts/run-test.cjs npm run test:notes-validation && node scripts/run-test.cjs npm run test:receipt-printing && node scripts/run-test.cjs npm run test:cancel-override && node scripts/run-test.cjs npm run test:refunds && node scripts/run-test.cjs npm run test:async-refund-pin-event-loop && node scripts/run-test.cjs npm run test:refund-completed-orders && node scripts/run-test.cjs npm run test:cash-closures && node scripts/run-test.cjs npm run test:cash-sessions && node scripts/run-test.cjs npm run test:kitchen-addons && node scripts/run-test.cjs npm run test:kot-order-notes && node scripts/run-test.cjs npm run test:pos-info-network-ranking && node scripts/run-test.cjs npm run test:order-item-addons && node scripts/run-test.cjs npm run test:issue-125-addon-reads && node scripts/run-test.cjs npm run test:windows-country-code-crash && node scripts/run-test.cjs npm run test:reports-insights && node scripts/run-test.cjs npm run test:reports-daily-stats-table-turn && node scripts/run-test.cjs npm run test:timezone-report-boundaries && node scripts/run-test.cjs npm run test:business-date && node scripts/run-test.cjs npm run test:dashboard-business-date && node scripts/run-test.cjs npm run test:sequence && node scripts/run-test.cjs npm run test:order-numbering-settings && node scripts/run-test.cjs npm run test:integration-happy && node scripts/run-test.cjs npm run test:integration-order-hold-payment && node scripts/run-test.cjs npm run test:integration-tax && node scripts/run-test.cjs npm run test:integration-payments && node scripts/run-test.cjs npm run test:kitchen-delivery-enforcement && node scripts/run-test.cjs npm run test:issue-214 && node scripts/run-test.cjs npm run test:issue-214-auth && node scripts/run-test.cjs npm run test:issue-214-migration && node scripts/run-test.cjs npm run test:issue-255 && node scripts/run-test.cjs npm run test:cart-variant-identity && node scripts/run-test.cjs npm run test:cart-price-override && node scripts/run-test.cjs npm run test:product-variants-screen && node scripts/run-test.cjs npm run test:variant-pos-surface && node scripts/run-test.cjs npm run test:issue-788-order-attempt-storage && node scripts/run-test.cjs npm run test:integration-lifecycle && node scripts/run-test.cjs npm run test:integration-reconciliation && node scripts/run-test.cjs npm run test:integration-loyalty && node scripts/run-test.cjs npm run test:integration-discount && node scripts/run-test.cjs npm run test:loyalty-toggle && node scripts/run-test.cjs npm run test:discount-system && node scripts/run-test.cjs npm run test:integration-discount-settings && node scripts/run-test.cjs npm run test:integration-loyalty-global && node scripts/run-test.cjs npm run test:issue-248-csv && node scripts/run-test.cjs npm run test:integration-loyalty-redemption && node scripts/run-test.cjs npm run test:bills-print-api && node scripts/run-test.cjs npm run test:issue-24 && node scripts/run-test.cjs npm run test:issue-134-routing && node scripts/run-test.cjs npm run test:server-app-station-routing && node scripts/run-test.cjs npm run test:issue-134-mgmt && node scripts/run-test.cjs npm run test:issue-137-barcode && node scripts/run-test.cjs npm run test:issue-244-product-addon-links && node scripts/run-test.cjs npm run test:issue-245-246-addon-groups && node scripts/run-test.cjs npm run test:issue-247-catalog-invariants && node scripts/run-test.cjs npm run test:issue-250-catalog-perf && node scripts/run-test.cjs npm run test:issue-252 && node scripts/run-test.cjs npm run test:inventory-ledger && node scripts/run-test.cjs npm run test:issue-printing-and-kds-resilience && node scripts/run-test.cjs npm run test:issue-258-bill-pagination && node scripts/run-test.cjs npm run test:issue-265-morocco-profile && node scripts/run-test.cjs npm run test:issue-266-currency-symbol-print && node scripts/run-test.cjs npm run test:tables-string-ids && node scripts/run-test.cjs npm run test:issue-646-floor-management && node scripts/run-test.cjs npm run test:held-orders && node scripts/run-test.cjs npm run test:product-images && node scripts/run-test.cjs npm run test:schema-health && node scripts/run-test.cjs npm run test:migration-registry && node scripts/run-test.cjs npm run test:upgrade-path && node scripts/run-test.cjs npm run test:upgrade-matrix-harness && node scripts/run-test.cjs npm run test:migration-v56-v57 && node scripts/run-test.cjs npm run test:migration-v71-repair && node scripts/run-test.cjs npm run test:migration-v80-cash-drawer-pulse && node scripts/run-test.cjs npm run test:migration-v81-cash-closures && node scripts/run-test.cjs npm run test:master-pin && node scripts/run-test.cjs npm run test:google-drive && node scripts/run-test.cjs npm run test:google-drive-qr-menu && node scripts/run-test.cjs npm run test:database-tools-api && node scripts/run-test.cjs npm run test:phone-validation && node scripts/run-test.cjs npm run test:phone-migration && node scripts/run-test.cjs npm run test:issue-133-kds-kot-toggles && node scripts/run-test.cjs npm run test:whatsapp-schema && node scripts/run-test.cjs npm run test:whatsapp-service && node scripts/run-test.cjs npm run test:whatsapp-middleware && node scripts/run-test.cjs npm run test:issue-127-password-recovery && node scripts/run-test.cjs npm run test:dev-tooling && node scripts/run-test.cjs npm run test:windows-uninstaller && node scripts/run-test.cjs npm run test:shutdown-lifecycle && node scripts/run-test.cjs npm run test:redos-hardening && node scripts/run-test.cjs npm run test:startup-cache && node scripts/run-test.cjs npm run test:service-worker && node scripts/run-test.cjs npm run test:issue-389-timezone-override && node scripts/run-test.cjs npm run test:issue-390-locale-preference-invariants && node scripts/run-test.cjs npm run test:issue-475-picker-highlight && node scripts/run-test.cjs npm run test:theme-mode-settings && node scripts/run-test.cjs npm run test:theme-fouc-script && node scripts/run-test.cjs npm run test:ui-regressions-621-623-626 && node scripts/run-test.cjs npm run test:issue-zero-balance-settlement && node scripts/run-test.cjs npm run test:daily-sales-export && node scripts/run-test.cjs npm run test:customer-sales-export && node scripts/run-test.cjs npm run test:card-transaction-id && node scripts/run-test.cjs npm run test:issue-terminal-state-guards && node scripts/run-test.cjs npm run test:issue-exact-balance-settlement && node scripts/run-test.cjs npm run test:financial-safety-remediation && node scripts/run-test.cjs npm run test:issue-payload-and-discount-bounds && node scripts/run-test.cjs npm run test:customer-display-config && node scripts/run-test.cjs npm run test:customer-display-settings-route && node scripts/run-test.cjs npm run test:customer-display-pairing && node scripts/run-test.cjs npm run test:customer-display-feed && node scripts/run-test.cjs npm run test:units && node scripts/run-test.cjs npm run test:supplies-service && node scripts/run-test.cjs npm run test:recipe-order-lifecycle && node scripts/run-test.cjs npm run test:ingredient-cost && node scripts/run-test.cjs npm run test:lint-engine && node scripts/run-test.cjs npm run test:oxc-contributor-checks && node scripts/run-test.cjs npm run test:expenses && node scripts/run-test.cjs npm run test:diagnostics && node scripts/run-test.cjs npm run test:reports-export && node scripts/run-test.cjs npm run test:category-addon-groups && node scripts/run-test.cjs npm run test:addon-inventory-db && node scripts/run-test.cjs npm run test:addon-inventory-api && node scripts/run-test.cjs npm run test:discount-kitchen-approver-identity && node scripts/run-test.cjs npm run test:staff-approvers-directory && node scripts/run-test.cjs npm run test:local-backup && npm run test:password-policy && npm run test:cloud-password-recovery", "test:dev-tooling": "ts-node --transpile-only -P tests/tsconfig.json tests/dev-tooling-scripts.test.ts && npm run test:phase2 && npm run test:runtime-recovery && npm run test:backend-health && npm run test:url-allowlist && npm run test:static-routes", "test:script-coverage": "node scripts/ci/validate-test-script-coverage.cjs && node tests/test-script-coverage.test.cjs && ts-node --transpile-only -P tests/tsconfig.json tests/test-harness-assertions.test.ts", "test:lint-engine": "ts-node --transpile-only -P tests/tsconfig.json tests/lint-engine-parity.test.ts", @@ -240,6 +240,7 @@ "test:password-policy": "ts-node --transpile-only -P tests/tsconfig.json tests/password-policy.test.ts", "test:server-app-server-role": "node tests/run-electron-node-test.cjs tests/server-app-server-role.test.ts && npm run test:server-app-maintenance-handoff", "test:server-app-maintenance-handoff": "node tests/run-electron-node-test.cjs tests/server-app-maintenance-handoff.test.ts", + "test:server-app-station-routing": "node tests/run-electron-node-test.cjs tests/server-app-station-routing.test.ts", "test:customer-soft-reactivate-bindings": "ts-node --transpile-only -P tests/tsconfig.json tests/customer-soft-reactivate-bindings.test.ts", "test:customer-phone-search": "node tests/run-electron-node-test.cjs tests/customer-phone-search.test.ts", "test:customer-pagination": "node tests/run-electron-node-test.cjs tests/customer-pagination.test.ts", diff --git a/tests/server-app-station-routing.test.ts b/tests/server-app-station-routing.test.ts new file mode 100644 index 000000000..d5ab0d61d --- /dev/null +++ b/tests/server-app-station-routing.test.ts @@ -0,0 +1,645 @@ +/** + * End-to-End Integration Test: Issue #1348 + * Multi-station KOT routing from the Waiter App (Server App -> Main API -> Thermal Transport) + * + * Verifies that when the Waiter App (server-standalone) sends an order and posts + * `/api/printers/print-kot` with no `stationName` through `main/server-app.ts`: + * 1. `main/server-app.ts` authenticates the waiter and forwards `/api/printers/print-kot` + * to `main/routes/printers.ts` on the Main API. + * 2. `main/routes/printers.ts` invokes `routeItemsToStations()` to split items across + * mapped category station printers and falls back to the default Kitchen printer for + * unmapped items. + * 3. `printKOTDetailed` / `printViaNetwork` in `main/printers/thermal.ts` dispatches + * separate ESC/POS KOT payloads to each physical station printer with only its + * own items. + * + * Usage: node tests/run-electron-node-test.cjs tests/server-app-station-routing.test.ts + */ + +import assert from 'node:assert/strict'; +import * as fs from 'node:fs'; +import * as net from 'node:net'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +const Module = require('module'); +const originalLoad = Module._load; +const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'flo-server-app-station-routing-')); + +class RasterSurfaceStub { + destroyed = false; + webContents = { + on: () => {}, + removeListener: () => {}, + send: () => {}, + loadURL: async () => {}, + }; + isDestroyed() { + return this.destroyed; + } + close() { + this.destroyed = true; + } + on() {} + removeListener() {} +} + +Module._load = function (request: string, parent: unknown, isMain: boolean) { + if (request === 'electron') { + return { + app: { isPackaged: true, getPath: () => testDir, getVersion: () => 'test' }, + ipcMain: { on: () => {}, removeListener: () => {}, handle: () => {} }, + BrowserWindow: RasterSurfaceStub, + }; + } + return originalLoad.apply(this, arguments as any); +}; + +interface RecordedDispatch { + ip: string; + port: number; + bytes: Buffer; + text: string; +} + +const DEFAULT_KITCHEN_PRINTER = { + id: 'pr-default-kitchen', + name: 'Main Kitchen Printer', + ip: '192.0.2.100', + port: 9100, +}; + +const GRILL_STATION_PRINTER = { + id: 'pr-grill', + name: 'Grill Station Printer', + ip: '192.0.2.101', + port: 9101, +}; + +const BAR_STATION_PRINTER = { + id: 'pr-bar', + name: 'Bar Station Printer', + ip: '192.0.2.102', + port: 9102, +}; + +const STUBBED_PRINTER_IPS = new Set([ + DEFAULT_KITCHEN_PRINTER.ip, + GRILL_STATION_PRINTER.ip, + BAR_STATION_PRINTER.ip, +]); + +async function getFreeTcpPort(): Promise { + const probe = net.createServer(); + await new Promise((resolve, reject) => { + probe.once('error', reject); + probe.listen(0, '127.0.0.1', resolve); + }); + const address = probe.address(); + assert(address && typeof address !== 'string'); + const port = address.port; + await new Promise((resolve, reject) => + probe.close((error) => (error ? reject(error) : resolve())), + ); + return port; +} + +async function postJson( + baseUrl: string, + pathName: string, + body: unknown, + token?: string, + extraHeaders?: Record, +) { + const response = await fetch(`${baseUrl}${pathName}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(extraHeaders || {}), + }, + body: JSON.stringify(body), + }); + const text = await response.text(); + const parsedBody = (() => { + try { + return JSON.parse(text); + } catch { + return text; + } + })(); + return { status: response.status, body: parsedBody }; +} + +function assertMultiStationRouting(dispatches: RecordedDispatch[]) { + assert.equal( + dispatches.length, + 3, + `Expected 3 separate KOT dispatches (Grill, Bar, fallback Kitchen), got ${dispatches.length}`, + ); + + const grillDispatches = dispatches.filter( + (d) => d.ip === GRILL_STATION_PRINTER.ip && d.port === GRILL_STATION_PRINTER.port, + ); + assert.equal(grillDispatches.length, 1, 'Grill station printer receives exactly 1 KOT dispatch'); + const grillText = grillDispatches[0].text; + assert.match(grillText, /GRILL/i, 'Grill KOT header identifies Grill station'); + assert.match(grillText, /Ribeye Steak/, 'Grill KOT includes Ribeye Steak'); + assert.match(grillText, /Charbroiled Burger/, 'Grill KOT includes Charbroiled Burger'); + assert.doesNotMatch(grillText, /Classic Mojito/, 'Grill KOT excludes Bar items'); + assert.doesNotMatch(grillText, /Basque Cheesecake/, 'Grill KOT excludes unmapped Dessert items'); + + const barDispatches = dispatches.filter( + (d) => d.ip === BAR_STATION_PRINTER.ip && d.port === BAR_STATION_PRINTER.port, + ); + assert.equal(barDispatches.length, 1, 'Bar station printer receives exactly 1 KOT dispatch'); + const barText = barDispatches[0].text; + assert.match(barText, /BAR/i, 'Bar KOT header identifies Bar station'); + assert.match(barText, /Classic Mojito/, 'Bar KOT includes Classic Mojito'); + assert.doesNotMatch(barText, /Ribeye Steak/, 'Bar KOT excludes Grill items'); + assert.doesNotMatch(barText, /Charbroiled Burger/, 'Bar KOT excludes Grill items'); + assert.doesNotMatch(barText, /Basque Cheesecake/, 'Bar KOT excludes unmapped Dessert items'); + + const kitchenDispatches = dispatches.filter( + (d) => d.ip === DEFAULT_KITCHEN_PRINTER.ip && d.port === DEFAULT_KITCHEN_PRINTER.port, + ); + assert.equal( + kitchenDispatches.length, + 1, + 'Default Kitchen printer receives exactly 1 fallback KOT dispatch for unmatched items', + ); + const kitchenText = kitchenDispatches[0].text; + assert.match(kitchenText, /KITCHEN/i, 'Fallback KOT header identifies Kitchen'); + assert.match(kitchenText, /Basque Cheesecake/, 'Fallback Kitchen KOT includes Basque Cheesecake'); + assert.doesNotMatch(kitchenText, /Ribeye Steak/, 'Fallback Kitchen KOT excludes Grill items'); + assert.doesNotMatch( + kitchenText, + /Charbroiled Burger/, + 'Fallback Kitchen KOT excludes Grill items', + ); + assert.doesNotMatch(kitchenText, /Classic Mojito/, 'Fallback Kitchen KOT excludes Bar items'); +} + +async function main() { + console.log('Integration Test: Server App end-to-end multi-station KOT routing (#1348)'); + console.log('='.repeat(74)); + + process.env.PORT = String(await getFreeTcpPort()); + process.env.KDS_PORT = String(await getFreeTcpPort()); + process.env.SERVER_APP_PORT = String(await getFreeTcpPort()); + + const bcrypt = require('bcryptjs'); + const { seedCategory, seedProduct } = require('./helpers/test-setup'); + const { initDatabase, getDatabase, closeDatabase, now } = await import('../main/db'); + const { startServer, stopServer } = await import('../main/server'); + const { startKdsServer, stopKdsServer } = await import('../main/kds-server'); + const { startServerApp, stopServerApp, getServerAppPort } = await import('../main/server-app'); + const { escPosToText } = await import('../main/printers/thermal'); + const { destroySharedRasterRenderer } = await import('../main/printers/raster-renderer'); + + // Stub only the physical network socket dispatch layer (`net.Socket.prototype.connect`) + // for the test printer IPs, leaving HTTP loopback traffic (Server App -> Main API) untouched. + const recordedDispatches: RecordedDispatch[] = []; + const originalSocketConnect = net.Socket.prototype.connect; + (net.Socket.prototype as any).connect = function (this: net.Socket, ...args: any[]) { + const [port, host, connectListener] = args; + if (typeof port === 'number' && typeof host === 'string' && STUBBED_PRINTER_IPS.has(host)) { + const chunks: Buffer[] = []; + let finalized = false; + const finalize = () => { + if (finalized) return; + finalized = true; + const bytes = Buffer.concat(chunks); + recordedDispatches.push({ + ip: host, + port, + bytes, + text: escPosToText(bytes), + }); + }; + this.setTimeout = () => this; + (this as any).write = ( + chunk: Buffer | Uint8Array | string, + encodingOrCb?: BufferEncoding | ((error?: Error | null) => void), + cb?: (error?: Error | null) => void, + ) => { + const buf = Buffer.isBuffer(chunk) ? Buffer.from(chunk) : Buffer.from(chunk as any); + chunks.push(buf); + const callback = typeof encodingOrCb === 'function' ? encodingOrCb : cb; + if (callback) process.nextTick(callback); + return true; + }; + (this as any).end = (cb?: () => void) => { + finalize(); + if (typeof cb === 'function') process.nextTick(cb); + return this; + }; + (this as any).destroy = () => { + finalize(); + return this; + }; + if (typeof connectListener === 'function') { + process.nextTick(connectListener); + } + return this; + } + return originalSocketConnect.apply(this, args as any); + }; + + initDatabase(); + const db = getDatabase(); + + const setSetting = (key: string, value: string) => + db + .prepare( + 'INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at', + ) + .run(key, value, now()); + + for (const [key, value] of [ + ['country', 'US'], + ['currency', 'USD'], + ['currency_symbol', '$'], + ['timezone', 'America/New_York'], + ['business_name', 'Flo Test Cafe'], + ['kot_printing_enabled', 'true'], + ] as const) { + setSetting(key, value); + } + + const passwordHash = bcrypt.hashSync('WaiterPass123!', 10); + db.prepare(` + INSERT INTO users (id, name, email, password, role, is_active, created_at, updated_at) + VALUES ('waiter-1', 'Waiter One', 'waiter@server-app.test', ?, 'server', 1, ?, ?) + `).run(passwordHash, now(), now()); + + db.prepare(` + INSERT INTO tables (id, number, capacity, status, is_active, created_at, updated_at) + VALUES ('tbl-1', 'T1', 4, 'available', 1, ?, ?) + `).run(now(), now()); + + // Seed default Kitchen printer + two station printers (Grill and Bar) + const insertPrinter = db.prepare(` + INSERT INTO printers (id, name, connection_type, ip_address, port, paper_width, is_default, created_at, updated_at) + VALUES (?, ?, 'network', ?, ?, 'cols-42', ?, ?, ?) + `); + insertPrinter.run( + DEFAULT_KITCHEN_PRINTER.id, + DEFAULT_KITCHEN_PRINTER.name, + DEFAULT_KITCHEN_PRINTER.ip, + DEFAULT_KITCHEN_PRINTER.port, + 1, + now(), + now(), + ); + insertPrinter.run( + GRILL_STATION_PRINTER.id, + GRILL_STATION_PRINTER.name, + GRILL_STATION_PRINTER.ip, + GRILL_STATION_PRINTER.port, + 0, + now(), + now(), + ); + insertPrinter.run( + BAR_STATION_PRINTER.id, + BAR_STATION_PRINTER.name, + BAR_STATION_PRINTER.ip, + BAR_STATION_PRINTER.port, + 0, + now(), + now(), + ); + + // Seed 2 mapped categories + 1 unmapped category + seedCategory(db, 'cat-grill', 'Grill'); + seedCategory(db, 'cat-bar', 'Bar'); + seedCategory(db, 'cat-dessert', 'Desserts'); + + seedProduct(db, 'prod-steak', 'cat-grill', 'Ribeye Steak', 2800); + seedProduct(db, 'prod-burger', 'cat-grill', 'Charbroiled Burger', 1600); + seedProduct(db, 'prod-mojito', 'cat-bar', 'Classic Mojito', 1200); + seedProduct(db, 'prod-cheesecake', 'cat-dessert', 'Basque Cheesecake', 900); + + // Map cat-grill -> Grill Station and cat-bar -> Bar Station; leave cat-dessert unmapped + const insertStation = db.prepare(` + INSERT INTO kitchen_stations (id, name, category_ids, printer_id, is_active, created_at, updated_at) + VALUES (?, ?, ?, ?, 1, ?, ?) + `); + insertStation.run( + 'stn-grill', + 'Grill', + JSON.stringify(['cat-grill']), + GRILL_STATION_PRINTER.id, + now(), + now(), + ); + insertStation.run( + 'stn-bar', + 'Bar', + JSON.stringify(['cat-bar']), + BAR_STATION_PRINTER.id, + now(), + now(), + ); + + await startServer(); + await startKdsServer(); + await startServerApp(); + const serverAppBaseUrl = `http://127.0.0.1:${getServerAppPort()}`; + + try { + // 1. Authenticate waiter on Server App + const loginRes = await postJson(serverAppBaseUrl, '/api/auth/login', { + email: 'waiter@server-app.test', + password: 'WaiterPass123!', + }); + assert.equal(loginRes.status, 200, 'Waiter logs into Server App'); + const waiterToken = loginRes.body.access_token as string; + assert.ok(waiterToken, 'Server App returns access_token for waiter'); + + // 2. Create order through Server App with items spanning Grill, Bar, and unmapped Desserts + const createOrderRes = await postJson( + serverAppBaseUrl, + '/api/orders', + { + table_id: 'tbl-1', + type: 'dine_in', + items: [ + { product_id: 'prod-steak', quantity: 2 }, + { product_id: 'prod-burger', quantity: 1 }, + { product_id: 'prod-mojito', quantity: 3 }, + { product_id: 'prod-cheesecake', quantity: 1 }, + ], + }, + waiterToken, + { 'Idempotency-Key': 'waiter-station-routing-order-1' }, + ); + assert.equal( + createOrderRes.status, + 201, + `Server App POST /api/orders creates order (got ${createOrderRes.status}: ${JSON.stringify(createOrderRes.body)})`, + ); + const createdOrder = createOrderRes.body.order; + assert.ok(createdOrder?.id, 'Created order has an id'); + assert.equal(createdOrder.items?.length, 4, 'Created order has all 4 items'); + + // 3. Call Server App POST /api/printers/print-kot with { orderId, items } and NO stationName, + // matching printKotForOrder() in frontend/src/app/server-standalone/page.tsx + recordedDispatches.length = 0; + const printKotWithItemsRes = await postJson( + serverAppBaseUrl, + '/api/printers/print-kot', + { + orderId: createdOrder.id, + items: createdOrder.items, + }, + waiterToken, + ); + assert.equal( + printKotWithItemsRes.status, + 200, + `Server App POST /api/printers/print-kot succeeds (got ${printKotWithItemsRes.status}: ${JSON.stringify(printKotWithItemsRes.body)})`, + ); + assert.equal(printKotWithItemsRes.body.success, true); + + assertMultiStationRouting(recordedDispatches); + console.log( + ' ✓ Waiter App print-kot (with items, no stationName) routed to Grill, Bar, and fallback Kitchen printers', + ); + + // 4. Also verify POST /api/printers/print-kot with { orderId } alone (no items, no stationName) + recordedDispatches.length = 0; + const printKotOrderOnlyRes = await postJson( + serverAppBaseUrl, + '/api/printers/print-kot', + { + orderId: createdOrder.id, + }, + waiterToken, + ); + assert.equal(printKotOrderOnlyRes.status, 200); + assert.equal(printKotOrderOnlyRes.body.success, true); + + assertMultiStationRouting(recordedDispatches); + console.log( + ' ✓ Waiter App print-kot (orderId only, no stationName) routed DB items to Grill, Bar, and fallback Kitchen printers', + ); + + // 5. Append flow: waiter appends a Bar item and an unmapped Dessert item via + // POST /api/orders/:id/items and calls POST /api/printers/print-kot with only newItems + const existingItemIds = new Set((createdOrder.items as Array<{ id: number }>).map((i) => i.id)); + const appendOrderRes = await postJson( + serverAppBaseUrl, + `/api/orders/${createdOrder.id}/items`, + { + items: [ + { product_id: 'prod-mojito', quantity: 1 }, + { product_id: 'prod-cheesecake', quantity: 2 }, + ], + }, + waiterToken, + { 'Idempotency-Key': 'waiter-station-routing-append-1' }, + ); + assert.equal(appendOrderRes.status, 200, 'Server App POST /api/orders/:id/items succeeds'); + const newItems = (appendOrderRes.body.order.items as Array<{ id: number }>).filter( + (item) => !existingItemIds.has(item.id), + ); + assert.equal(newItems.length, 2, 'Append delta contains only the 2 newly added items'); + + recordedDispatches.length = 0; + const appendPrintRes = await postJson( + serverAppBaseUrl, + '/api/printers/print-kot', + { + orderId: createdOrder.id, + items: newItems, + }, + waiterToken, + ); + assert.equal(appendPrintRes.status, 200); + assert.equal(appendPrintRes.body.success, true); + assert.equal( + recordedDispatches.length, + 2, + 'Appending Bar + unmapped Dessert items dispatches only to Bar and fallback Kitchen printers (skipping Grill)', + ); + const appendedGrillDispatches = recordedDispatches.filter( + (d) => d.ip === GRILL_STATION_PRINTER.ip && d.port === GRILL_STATION_PRINTER.port, + ); + assert.equal( + appendedGrillDispatches.length, + 0, + 'Grill printer receives no KOT when no Grill items were appended', + ); + const appendedBarDispatches = recordedDispatches.filter( + (d) => d.ip === BAR_STATION_PRINTER.ip && d.port === BAR_STATION_PRINTER.port, + ); + assert.equal( + appendedBarDispatches.length, + 1, + 'Bar station printer receives exactly 1 KOT dispatch for appended Mojito', + ); + const appendedBar = appendedBarDispatches[0]; + assert.match(appendedBar.text, /Classic Mojito/); + assert.doesNotMatch(appendedBar.text, /Basque Cheesecake/); + const appendedKitchenDispatches = recordedDispatches.filter( + (d) => d.ip === DEFAULT_KITCHEN_PRINTER.ip && d.port === DEFAULT_KITCHEN_PRINTER.port, + ); + assert.equal( + appendedKitchenDispatches.length, + 1, + 'Default Kitchen printer receives exactly 1 KOT dispatch for appended Cheesecake', + ); + const appendedKitchen = appendedKitchenDispatches[0]; + assert.match(appendedKitchen.text, /Basque Cheesecake/); + assert.doesNotMatch(appendedKitchen.text, /Classic Mojito/); + console.log( + ' ✓ Waiter App append-items print-kot routed only touched stations (Bar + fallback Kitchen)', + ); + + // 6. Inactive station fallback: when Bar station (stn-bar) is marked inactive, + // posting /api/printers/print-kot routes Grill items to GRILL_STATION_PRINTER and + // falls back both Bar (inactive station) and Dessert (unmapped) items to DEFAULT_KITCHEN_PRINTER. + db.prepare("UPDATE kitchen_stations SET is_active = 0 WHERE id = 'stn-bar'").run(); + try { + recordedDispatches.length = 0; + const inactiveStationRes = await postJson( + serverAppBaseUrl, + '/api/printers/print-kot', + { + orderId: createdOrder.id, + items: createdOrder.items, + }, + waiterToken, + ); + assert.equal(inactiveStationRes.status, 200); + assert.equal(inactiveStationRes.body.success, true); + assert.equal( + recordedDispatches.length, + 2, + 'Deactivating Bar station dispatches only to Grill and fallback Kitchen printers', + ); + + const inactiveGrillDispatches = recordedDispatches.filter( + (d) => d.ip === GRILL_STATION_PRINTER.ip && d.port === GRILL_STATION_PRINTER.port, + ); + assert.equal( + inactiveGrillDispatches.length, + 1, + 'Active Grill station printer receives 1 KOT dispatch', + ); + assert.match(inactiveGrillDispatches[0].text, /GRILL/i); + assert.match(inactiveGrillDispatches[0].text, /Ribeye Steak/); + assert.match(inactiveGrillDispatches[0].text, /Charbroiled Burger/); + assert.doesNotMatch(inactiveGrillDispatches[0].text, /Classic Mojito/); + assert.doesNotMatch(inactiveGrillDispatches[0].text, /Basque Cheesecake/); + + const inactiveBarDispatches = recordedDispatches.filter( + (d) => d.ip === BAR_STATION_PRINTER.ip && d.port === BAR_STATION_PRINTER.port, + ); + assert.equal( + inactiveBarDispatches.length, + 0, + 'Inactive Bar station printer receives 0 KOT dispatches', + ); + + const inactiveKitchenDispatches = recordedDispatches.filter( + (d) => d.ip === DEFAULT_KITCHEN_PRINTER.ip && d.port === DEFAULT_KITCHEN_PRINTER.port, + ); + assert.equal( + inactiveKitchenDispatches.length, + 1, + 'Default Kitchen printer receives 1 fallback KOT with inactive-station and unmapped items', + ); + assert.match(inactiveKitchenDispatches[0].text, /KITCHEN/i); + assert.match( + inactiveKitchenDispatches[0].text, + /Classic Mojito/, + 'Fallback Kitchen KOT includes item from inactive Bar station', + ); + assert.match( + inactiveKitchenDispatches[0].text, + /Basque Cheesecake/, + 'Fallback Kitchen KOT includes unmapped Dessert item', + ); + assert.doesNotMatch(inactiveKitchenDispatches[0].text, /Ribeye Steak/); + assert.doesNotMatch(inactiveKitchenDispatches[0].text, /Charbroiled Burger/); + console.log( + ' ✓ Inactive station (stn-bar) items fell back to Default Kitchen printer while Grill routed normally', + ); + } finally { + db.prepare("UPDATE kitchen_stations SET is_active = 1 WHERE id = 'stn-bar'").run(); + } + + // 7. Negative verification: if station routing is bypassed (e.g. an explicit stationName forces + // all items onto a single default-printer ticket, or items land on the wrong station), + // assertMultiStationRouting must fail. + recordedDispatches.length = 0; + const bypassedRes = await postJson( + serverAppBaseUrl, + '/api/printers/print-kot', + { + orderId: createdOrder.id, + items: createdOrder.items, + stationName: 'Kitchen', + }, + waiterToken, + ); + assert.equal(bypassedRes.status, 200); + assert.equal( + recordedDispatches.length, + 1, + 'Passing an explicit stationName bypasses per-station routing into 1 combined ticket', + ); + assert.throws( + () => assertMultiStationRouting(recordedDispatches), + /Expected 3 separate KOT dispatches/, + 'Routing assertion fails if station routing is bypassed into a single ticket', + ); + assert.throws( + () => + assertMultiStationRouting([ + { + ip: GRILL_STATION_PRINTER.ip, + port: GRILL_STATION_PRINTER.port, + bytes: Buffer.alloc(0), + text: 'GRILL\nRibeye Steak\nCharbroiled Burger\nClassic Mojito', + }, + { + ip: BAR_STATION_PRINTER.ip, + port: BAR_STATION_PRINTER.port, + bytes: Buffer.alloc(0), + text: 'BAR\nClassic Mojito', + }, + { + ip: DEFAULT_KITCHEN_PRINTER.ip, + port: DEFAULT_KITCHEN_PRINTER.port, + bytes: Buffer.alloc(0), + text: 'KITCHEN\nBasque Cheesecake', + }, + ]), + /Grill KOT excludes Bar items/, + 'Routing assertion fails if a Bar item leaks onto the Grill station ticket', + ); + console.log( + ' ✓ Routing assertion rejects bypassed single-ticket and cross-station dispatches', + ); + } finally { + net.Socket.prototype.connect = originalSocketConnect; + destroySharedRasterRenderer(); + await stopServerApp(); + await stopKdsServer(); + await stopServer(); + closeDatabase(); + Module._load = originalLoad; + fs.rmSync(testDir, { recursive: true, force: true }); + } + + console.log('ALL PASSED'); +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});