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 }],
batm3: [
{ denomination: 20, count: 500 },
{ denomination: 50, count: 500 },
{ denomination: 20, count: 400 },
{ denomination: 1, count: 400 },
],
}
seedCassettes = presets[model] || []

View file

@ -15,7 +15,7 @@ import fs from 'node:fs'
let db: Database.Database | null = null
const SCHEMA_VERSION = '5'
const SCHEMA_VERSION = '6'
function getDbPath(): string {
const prodDir = '/var/lib/lamassu-atm'
@ -53,7 +53,8 @@ export function initDatabase(dbPath?: string): void {
CREATE TABLE IF NOT EXISTS cassettes (
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 (
@ -198,6 +199,22 @@ export function initDatabase(dbPath?: string): void {
console.log(
'[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()
@ -215,30 +232,36 @@ export function initDatabase(dbPath?: string): void {
export interface CassetteRow {
denomination: number
count: number
position: number
}
export function loadCassettes(): CassetteRow[] {
if (!db) throw new Error('Database not initialized')
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[]
}
/**
* 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')
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[]) => {
for (const row of rows) {
upsert.run(row.denomination, row.count)
const run = db.transaction(
(rows: { denomination: number; count: number; position?: number }[]) => {
for (let i = 0; i < rows.length; i++) {
const row = rows[i]!
upsert.run(row.denomination, row.count, row.position ?? i + 1)
}
})
}
)
run(cassettes)
console.log('[StateStore] Cassettes updated:', cassettes)

View file

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