This document explains the architecture, key modules, endpoints, event flow, and implementation details of the eftkey multi-terminal application.
- Stack: Node.js + TypeScript, Express, ts-node + nodemon for dev
- PayTec library: Official GitHub package
github:PayTecAG/ecritf(POSTerminal instances) - Transport: Cloud via SMQ WebSocket
wss://ecritf.paytec.ch/smq.lsp(handled by the PayTec lib) - Persistence: Per-terminal JSON files
.data/pairings/{terminalId}.json - Results persistence: NDJSON
.data/transactions.ndjsonwith terminal ID and full outcome payloads - GUI: static HTML/JS served from
public/with ID-filtered SSE log streaming - Multi-terminal: Each terminal ID maintains its own pairing, connection state, and loop configurations
src/server.ts— Express app, ID-based endpoints, SSE log streaming, per-terminal loop logicsrc/terminalManager.ts— Multi-terminal registry, per-id persistence, id-tagged logs, terminal state managementpublic/—index.htmlGUI: terminal selection, pairing form, status, actions, loop controls, live log panel.data/pairings/{terminalId}.json— persisted pairing info per terminal.data/transactions.ndjson— transaction outcomes with terminal IDpackage.json— dependencies includinggithub:PayTecAG/ecritf
- Multi-terminal registry with per-ID POSTerminal instances
- Loads official PayTec library:
github:PayTecAG/ecritf - Constructs
new POSTerminal(pairing, options)per terminal ID with:- AutoConnect, AutoReconnect
- Tuned intervals/timeouts to reduce disconnects
- Exposes multi-terminal API:
getOrCreateTerminal(id)- creates/retrieves terminal instancepairTerminal(id, code, name)- pairs specific terminalgetPairing(id)- retrieves pairing info for terminallistTerminals()- returns all known terminal IDsgetTerminalState(id)- returns loop configuration state
- Log event bus:
subscribeAll(cb); emits ID-tagged events from PayTec callbacks:{ id: string, type: string, payload?: any }- Events:
connected,disconnected,pairingSucceeded,pairingFailed,status,messageSent,messageReceived,error,activationSucceeded,activationFailed,transactionApproved,transactionDeclined,transactionAborted,transactionTimedOut,transactionConfirmationSucceeded,transactionConfirmationFailed,receipt
- Per-terminal persistence:
.data/pairings/{terminalId}.json
GET /healthz→ health checkGET /terminals→{ ids: string[] }- list all known terminal IDs
GET /pairing/:id→ returns pairing info for specific terminalPOST /pair/:id→{ code }pairs specific terminal usingpairTerminal(id, code, name)POST /activate/:id→ activates specific terminalPOST /transaction/:id/account-verification→ starts ACCOUNT_VERIFICATION on specific terminalPOST /transaction/:id/purchase→ starts PURCHASE with{ AmtAuth, TrxCurrC?, RecOrderRef? }on specific terminalGET /logs/:id→ Server-Sent Events stream (SSE) filtered by terminal ID
GET /loop/:id→{ enabled: boolean, delayMs: number }- ACCOUNT_VERIFICATION loop configPOST /loop/:id→{ enabled?: boolean, delayMs?: number }- configure ACCOUNT_VERIFICATION loopGET /loop/:id/purchase→{ enabled: boolean, amount: number, currency: number, delayMs: number }- PURCHASE loop configPOST /loop/:id/purchase→{ enabled?: boolean, amount?: number, currency?: number, delayMs?: number }- configure PURCHASE loop
GET /diagnostics→{ ok: boolean, serverTime: string, uptimeSec: number, cloud: object }- global connectivity testGET /diagnostics/terminal/:id→{ ok: boolean, id: string, paired: boolean, status: number, ready: boolean }- terminal-specific status
- Writes
event: <type>and JSONdata:lines for each log event - Consumed by the GUI via
EventSource('/logs/:id')(filtered by terminal ID) - All events are ID-tagged from
terminalManager.subscribeAll
- ACCOUNT_VERIFICATION loop: repeatedly run ACCOUNT_VERIFICATION as soon as the terminal is ready after the previous one
- PURCHASE loop: repeatedly run PURCHASE with configured amount/currency after completion
- Per-terminal state management:
loopAVEnabled,loopAVDelayMs,loopAVPending(ACCOUNT_VERIFICATION)loopPurchaseEnabled,loopPurchaseAmount,loopPurchaseTrxCurrC,loopPurchaseDelayMs,loopPurchasePending(PURCHASE)lastStatus,cooldownUntil(shared)
- Loop triggers on completion outcomes: approved/declined/aborted/timed out or receipt
- Waits for terminal ready state (SHIFT_OPEN set and BUSY not set) plus cooldown before next transaction
- Each terminal maintains its own pairing state and connection
- Pairing info is automatically loaded from
.data/pairings/{terminalId}.jsonon startup - Failed pairings can be retried by sending a new pairing code
- Subscribes to the ID-tagged log bus and persists only outcome events to
.data/transactions.ndjson:- types: approved, declined, aborted, timed out, confirmation succeeded/failed
- record:
{ ts: string, terminalId: string, type: string, status: number, payload: object }
- Uses terminal status flags from PayTec:
- SHIFT_OPEN =
0x00000001 - BUSY =
0x00000004
- SHIFT_OPEN =
- Ready =
(TrmStatus & SHIFT_OPEN) && !(TrmStatus & BUSY) - Each terminal tracks its own status independently
- Terminal selection: Enter terminal ID and click "Laden" to load configuration
- Pairing form: Posts to
/pair/:idfor specific terminal - Status card: Shows pairing info, live connection state, and connection diagnostics
- Actions (per terminal):
- Activate button →
/activate/:id - ACCOUNT_VERIFICATION button →
/transaction/:id/account-verification - PURCHASE form →
/transaction/:id/purchase
- Activate button →
- Loop controls (per terminal):
- ACCOUNT_VERIFICATION loop: checkbox, delay (ms), save to
/loop/:id - PURCHASE loop: checkbox, amount, currency, delay (ms), save to
/loop/:id/purchase
- ACCOUNT_VERIFICATION loop: checkbox, delay (ms), save to
- Connection diagnostics: "Verbindung testen" button tests cloud connectivity
- Live log pane: Subscribes to SSE
/logs/:idand appends ID-filtered events with timestamps
- Pairing data:
.data/pairings/{terminalId}.json- one file per terminal ID - Transaction outcomes:
.data/transactions.ndjson- all outcomes with terminal ID - Terminal state: In-memory per-terminal loop configurations and status tracking
- Loaded on startup; app attempts to auto-connect each known terminal
- Dev:
npm run dev(nodemon + ts-node) - Build:
npm run build - Start:
npm start
- Each terminal ID maintains its own pairing state and connection; pairing info in
.data/pairings/{terminalId}.jsonshows as "Gepairt" in the GUI - Live connection state is shown separately and can be tested via diagnostics
- The cloud transport and device status transitions are handled by the PayTec library; intermittent disconnects can happen depending on network/device
- Loop modes avoid sending while terminal is busy; if you still see "Terminal is busy", increase
delayMsor wait for ready status - Channel IDs can change when terminals restart or are re-paired; this is normal behavior
- Multi-terminal support allows independent operation of different terminals with separate loop configurations
- Add more endpoints as needed (e.g., receipts retrieval, other transaction types)
- Enhance GUI (framework, UX) or expose WebSocket for richer client updates
- Extend terminal state persistence to include loop settings in config files
- Add terminal grouping or batch operations for multiple terminals
- Implement terminal health monitoring and automatic reconnection strategies