feat: add cassette position for physical cartridge ordering

Add position column to cassettes table (migration v5→v6) so cassettes
are ordered by physical cartridge number instead of denomination.
Update BATM3 preset to $20/$1 denominations with 400-bill capacity.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Patrick Mulligan 2026-04-01 00:36:53 -04:00
commit ec60d3f201
3 changed files with 36 additions and 13 deletions

View file

@ -516,8 +516,8 @@ app.whenReady().then(() => {
], ],
gaia: [{ denomination: 20, count: 50 }], gaia: [{ denomination: 20, count: 50 }],
batm3: [ batm3: [
{ denomination: 20, count: 500 }, { denomination: 20, count: 400 },
{ denomination: 50, count: 500 }, { denomination: 1, count: 400 },
], ],
} }
seedCassettes = presets[model] || [] seedCassettes = presets[model] || []

View file

@ -15,7 +15,7 @@ import fs from 'node:fs'
let db: Database.Database | null = null let db: Database.Database | null = null
const SCHEMA_VERSION = '5' const SCHEMA_VERSION = '6'
function getDbPath(): string { function getDbPath(): string {
const prodDir = '/var/lib/lamassu-atm' const prodDir = '/var/lib/lamassu-atm'
@ -53,7 +53,8 @@ export function initDatabase(dbPath?: string): void {
CREATE TABLE IF NOT EXISTS cassettes ( CREATE TABLE IF NOT EXISTS cassettes (
denomination INTEGER PRIMARY KEY, denomination INTEGER PRIMARY KEY,
count INTEGER NOT NULL DEFAULT 0 count INTEGER NOT NULL DEFAULT 0,
position INTEGER NOT NULL DEFAULT 0
); );
CREATE TABLE IF NOT EXISTS cashbox ( CREATE TABLE IF NOT EXISTS cashbox (
@ -198,6 +199,22 @@ export function initDatabase(dbPath?: string): void {
console.log( console.log(
'[StateStore] Migrated schema v4 → v5 (added manual_dispense type, remediated_by, operator_commands)' '[StateStore] Migrated schema v4 → v5 (added manual_dispense type, remediated_by, operator_commands)'
) )
existing.value = '5'
}
if (existing && existing.value === '5') {
// Migration v5 → v6: add position column to cassettes for physical cartridge ordering
db.exec(`ALTER TABLE cassettes ADD COLUMN position INTEGER NOT NULL DEFAULT 0`)
// Backfill position from existing row order (by denomination)
const rows = db.prepare('SELECT denomination FROM cassettes ORDER BY denomination').all() as {
denomination: number
}[]
const update = db.prepare('UPDATE cassettes SET position = ? WHERE denomination = ?')
for (let i = 0; i < rows.length; i++) {
update.run(i + 1, rows[i]!.denomination)
}
db.prepare('UPDATE meta SET value = ? WHERE key = ?').run('6', 'schema_version')
console.log('[StateStore] Migrated schema v5 → v6 (added cassette position)')
} }
const cashboxRow = db.prepare('SELECT id FROM cashbox WHERE id = 1').get() const cashboxRow = db.prepare('SELECT id FROM cashbox WHERE id = 1').get()
@ -215,30 +232,36 @@ export function initDatabase(dbPath?: string): void {
export interface CassetteRow { export interface CassetteRow {
denomination: number denomination: number
count: number count: number
position: number
} }
export function loadCassettes(): CassetteRow[] { export function loadCassettes(): CassetteRow[] {
if (!db) throw new Error('Database not initialized') if (!db) throw new Error('Database not initialized')
return db return db
.prepare('SELECT denomination, count FROM cassettes ORDER BY denomination') .prepare('SELECT denomination, count, position FROM cassettes ORDER BY position, denomination')
.all() as CassetteRow[] .all() as CassetteRow[]
} }
/** /**
* Upsert cassette counts. Replaces all existing rows. * Upsert cassette counts. Replaces all existing rows.
*/ */
export function setCassettes(cassettes: CassetteRow[]): void { export function setCassettes(
cassettes: { denomination: number; count: number; position?: number }[]
): void {
if (!db) throw new Error('Database not initialized') if (!db) throw new Error('Database not initialized')
const upsert = db.prepare( const upsert = db.prepare(
'INSERT INTO cassettes (denomination, count) VALUES (?, ?) ON CONFLICT(denomination) DO UPDATE SET count = excluded.count' 'INSERT INTO cassettes (denomination, count, position) VALUES (?, ?, ?) ON CONFLICT(denomination) DO UPDATE SET count = excluded.count, position = excluded.position'
) )
const run = db.transaction((rows: CassetteRow[]) => { const run = db.transaction(
for (const row of rows) { (rows: { denomination: number; count: number; position?: number }[]) => {
upsert.run(row.denomination, row.count) for (let i = 0; i < rows.length; i++) {
const row = rows[i]!
upsert.run(row.denomination, row.count, row.position ?? i + 1)
}
} }
}) )
run(cassettes) run(cassettes)
console.log('[StateStore] Cassettes updated:', cassettes) console.log('[StateStore] Cassettes updated:', cassettes)

View file

@ -145,8 +145,8 @@ export const MACHINE_PRESETS: Record<MachineModel, Omit<DeviceConfig, 'fiatCode'
type: 'f56', type: 'f56',
device: '/dev/ttyUSB0', device: '/dev/ttyUSB0',
cassettes: [ cassettes: [
{ denomination: 20, count: 500 }, { denomination: 20, count: 400 },
{ denomination: 50, count: 500 }, { denomination: 1, count: 400 },
], ],
}, },
}, },