fix(hal): seed the validator's fiat code from config, or it rejects every note

The Apex returned every bill inserted. Cause is not wiring or DIP
switches: the driver had no fiat code, so no credit channel could resolve
to a value.

ApexValidator initialises `fiatCode` to null and only assigns it in
setFiatCode(). NOTHING in this repo calls setFiatCode() — not
hal-service, not the store, nothing. It is declared on the BillValidator
interface and implemented three times, and it is dead code.

So the resolver installed in run():

    this.rs232.setDenomResolver((ch) => denomForChannel(this.fiatCode, ch))

is always called with null, and denomForChannel bails on its first line
(`if (!fiatCode || channel < 1) return null`). Every note is read,
resolves to null denomination, hits the `!bill.denomination` branch, and
is handed straight back.

id003 is unaffected because it never relies on the field: run() threads
`config.fiatCode` into the rs232 config, so the value reaches the layer
that needs it regardless. Apex and EBDS both read `this.fiatCode`
instead, so both are broken the same way. EBDS is fixed here too — it has
the identical dead-field dependency in _denominations() and would fail
identically the first time it met hardware.

Both constructors now seed from `config.fiatCode ?? config.rs232.fiatCode`,
which is what the callers have been passing all along. setFiatCode() stays
as a later override rather than the only path in.

Also makes the failure loud, because the old log line is what sent us
looking at the acceptor instead of the driver. "Bill rejected:
unsupported/unmapped channel" reads as a dataset/hardware mismatch and
gives no hint that the driver simply has no currency. It now names which
of the two causes it is, and run() logs an error up front when there is no
fiat code at all, since in that state every note is guaranteed to be
returned.
This commit is contained in:
Padreug 2026-09-29 21:08:24 +02:00
commit dd542eda88
2 changed files with 31 additions and 1 deletions

View file

@ -48,6 +48,11 @@ export class ApexValidator extends EventEmitter implements BillValidator {
constructor(config: ValidatorConfig) { constructor(config: ValidatorConfig) {
super() super()
this.config = config this.config = config
// Seed from config, as id003 effectively does by threading config.fiatCode
// into its rs232 config. Nothing in the app calls setFiatCode(), so a
// driver that relies on it alone resolves every credit channel to null and
// rejects every note. setFiatCode() stays available as a later override.
this.fiatCode = config.fiatCode ?? config.rs232.fiatCode ?? null
this._throttledError = throttle((err: Error) => this.emit('error', err), 2000) this._throttledError = throttle((err: Error) => this.emit('error', err), 2000)
} }
@ -76,7 +81,20 @@ export class ApexValidator extends EventEmitter implements BillValidator {
this.fsm.on('billsAccepted', () => this.emit('billsAccepted')) this.fsm.on('billsAccepted', () => this.emit('billsAccepted'))
this.fsm.on('billsRead', (bill: { denomination: number | null; code: string }) => { this.fsm.on('billsRead', (bill: { denomination: number | null; code: string }) => {
if (!bill.denomination) { if (!bill.denomination) {
console.log('[APEX] Bill rejected: unsupported/unmapped channel') // Say WHICH of the two causes this is. "unmapped channel" alone reads
// as a hardware/dataset mismatch and sent us looking at DIP switches
// when the real cause was a null fiat code rejecting every note.
if (!this.fiatCode) {
console.error(
'[APEX] Bill rejected: no fiat code set on the driver, so NO channel ' +
'can resolve to a value. Every note will be returned until this is fixed.'
)
} else {
console.log(
`[APEX] Bill rejected: channel ${bill.code || '?'} is not mapped in the ` +
`${this.fiatCode} dataset — check the acceptor's configuration card`
)
}
this.rs232?.reject() this.rs232?.reject()
return return
} }
@ -92,6 +110,13 @@ export class ApexValidator extends EventEmitter implements BillValidator {
this.fsm.on('standby', () => this.emit('standby')) this.fsm.on('standby', () => this.emit('standby'))
this.fsm.on('error', (err: Error) => this.emit('error', err)) this.fsm.on('error', (err: Error) => this.emit('error', err))
if (!this.fiatCode) {
console.error(
'[APEX] starting with NO fiat code — denomination lookup will return null ' +
'for every credit channel and the acceptor will reject every note.'
)
}
this.rs232.open((err) => { this.rs232.open((err) => {
if (err) return cb(err) if (err) return cb(err)
this.rs232!.reset() // start disabled this.rs232!.reset() // start disabled

View file

@ -53,6 +53,11 @@ export class EbdsValidator extends EventEmitter implements BillValidator {
constructor(config: ValidatorConfig) { constructor(config: ValidatorConfig) {
super() super()
this.config = config this.config = config
// Seed from config, as id003 effectively does by threading config.fiatCode
// into its rs232 config. Nothing in the app calls setFiatCode(), so a
// driver that relies on it alone resolves every credit channel to null and
// rejects every note. setFiatCode() stays available as a later override.
this.fiatCode = config.fiatCode ?? config.rs232.fiatCode ?? null
this._throttledError = throttle((err: Error) => this.emit('error', err), 2000) this._throttledError = throttle((err: Error) => this.emit('error', err), 2000)
} }