fix(cassettes): say when the counts are unverified instead of reporting a guess
When the dispenser throws or the dispense times out there is no per-bay report, so nothing is debited — not the cassette rows, not HAL's bays. Bills may well have reached the customer, and both counters then read high with nothing to indicate it. The machine went on treating a number it had reason to doubt as measurement. A dispense that ends with no report now latches a countsUncertainSince flag, which rides along in the state document as counts_uncertain_since so the operator can see the numbers need a recount. The field is additive: a consumer reading positions ignores it, so this needs no coordinated release. An operator config apply clears the flag inside the same transaction, since asserting authoritative counts is precisely what a recount is. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
54c59fadcc
commit
474903c38b
7 changed files with 130 additions and 2 deletions
|
|
@ -429,6 +429,48 @@ export function getLastStatePublishedAt(): number | null {
|
|||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the bay counts are known to be unverified, and since when.
|
||||
*
|
||||
* Set when a dispense ends without the dispenser reporting what it moved — a
|
||||
* driver throw, or the dispense timeout. Bills may well have reached the
|
||||
* customer, but nothing knows how many, so neither the rows here nor HAL's
|
||||
* bays were debited and both now read high. Reporting that number as fact is
|
||||
* the worst option available; saying the number is unverified is honest and
|
||||
* tells the operator to open the machine and recount.
|
||||
*
|
||||
* Cleared when an operator asserts authoritative counts (a config apply),
|
||||
* which is precisely what a recount is. Uses an upsert so no migration is
|
||||
* needed for machines whose meta table predates the key.
|
||||
*/
|
||||
export function getCountsUncertainSince(): number | null {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('countsUncertainSince') as
|
||||
| { value: string }
|
||||
| undefined
|
||||
if (!row || row.value === '') return null
|
||||
const n = Number(row.value)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
|
||||
/** Flag the counts as unverified. Keeps the earliest time it went bad. */
|
||||
export function markCountsUncertain(unixTimestamp: number): void {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
if (getCountsUncertainSince() !== null) return
|
||||
db.prepare(
|
||||
'INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
||||
).run('countsUncertainSince', String(unixTimestamp))
|
||||
console.warn('[StateStore] Cassette counts flagged unverified at', unixTimestamp)
|
||||
}
|
||||
|
||||
/** Clear the flag — an operator has asserted real counts. */
|
||||
export function clearCountsUncertain(): void {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
db.prepare(
|
||||
'INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
||||
).run('countsUncertainSince', '')
|
||||
}
|
||||
|
||||
/** Record the `created_at` just published, as the next publish's floor. */
|
||||
export function markStatePublished(unixTimestamp: number): void {
|
||||
if (!db) throw new Error('Database not initialized')
|
||||
|
|
@ -661,11 +703,18 @@ export function applyOperatorCassettesConfig(
|
|||
)
|
||||
const setWatermark = db.prepare('UPDATE meta SET value = ? WHERE key = ?')
|
||||
|
||||
const clearUncertain = db.prepare(
|
||||
'INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value'
|
||||
)
|
||||
|
||||
const run = db.transaction(() => {
|
||||
for (const [posKey, entry] of Object.entries(payload.positions)) {
|
||||
updateCassette.run(entry.denomination, entry.count, Number(posKey))
|
||||
}
|
||||
setWatermark.run(String(eventCreatedAt), 'lastKnownConfigCreatedAt')
|
||||
// The operator just asserted real counts, which is what a recount is.
|
||||
// Whatever made the old numbers untrustworthy no longer applies.
|
||||
clearUncertain.run('countsUncertainSince', '')
|
||||
})
|
||||
|
||||
run()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue