diff --git a/API.md b/API.md index 51397f6..0bd2b57 100644 --- a/API.md +++ b/API.md @@ -256,8 +256,8 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `ship(provider: object): Function` - [posterminal](API.posterminal.md) - `delete(googleAuthId: number, id: number, tx?: object): Promise` - - `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` - - `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` + - `findByPairingpin(pairingpin: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` + - `findByToken(token: string, tx?: object): Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` - `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` - `getCategoryList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise>` - `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` @@ -270,17 +270,18 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise` - `getProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` - `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise>` - - `list(googleAuthId: number, tx?: object): Promise>` - - `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>` + - `list(googleAuthId: number, tx?: object): Promise>` + - `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string, clientversion?: string|null}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>` - `recordObituary(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise` - `recordSyncQueue(input: {companyId: number, entityType: string, entityId: number}, tx?: object): Promise` + - `setClientversion(id: number, clientversion: string|null, tx?: object): Promise<{id: number, clientversion: string|null}>` - `setMaxKnownObituaryId(id: number, maxknownobituaryid: number|null, tx?: object): Promise<{id: number, maxknownobituaryid: number|null}>` - `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `setOnAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` - `touchLastseen(id: number, tx?: object): Promise<{id: number}>` - - `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string}>` + - `upsert(googleAuthId: number, input: {name: string, pairingpin: string, token?: string, id?: number, sort?: number, enabled?: boolean}, tx?: object): Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>` - [printer](API.printer.md) - `create(googleAuthId: number, input: {address: string, type: string, model?: string, serial?: string, sort?: number, desc?: string}, tx?: object): Promise<{id: number, company_id: number, address: string, type: string, model: string|null, serial: string|null, sort: number, desc: string|null}>` - `delete(googleAuthId: number, id: number, tx?: object): Promise` diff --git a/API.posterminal.md b/API.posterminal.md index 87b4d09..ce16003 100644 --- a/API.posterminal.md +++ b/API.posterminal.md @@ -45,7 +45,7 @@ Finds a posterminal by its pairingpin. **Returns** -`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` — The posterminal, or null if not found. +`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found. ## `posterminal.findByToken` @@ -62,7 +62,7 @@ Finds a posterminal by its auth token. **Returns** -`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string}|null>` — The posterminal, or null if not found. +`Promise<{id: number, companyId: number, name: string, token: string|null, pairingpin: string|null, pairedAt: string|null, clientserial: string|null, maxknownobituaryid: number|null, rootCategoryId: number|null, syncRootArticles: boolean|number, lastseenAt: string|null, updatedAt: string, sort: number, enabled: boolean|number, clientversion: string|null}|null>` — The posterminal, or null if not found. ## `posterminal.getCategoryCount` @@ -311,13 +311,13 @@ Lists the posterminals of the user's selected company. **Returns** -`Promise>` — The posterminals. +`Promise>` — The posterminals. **Example** ```js const terminals = await store.posterminal.list(42); -// => [{ id, name, token, pairingpin, pairedAt, clientserial, maxknownobituaryid, rootCategoryId, syncRootArticles, lastseenAt, updatedAt }] +// => [{ id, name, token, pairingpin, pairedAt, clientserial, maxknownobituaryid, rootCategoryId, syncRootArticles, lastseenAt, updatedAt, sort, enabled, clientversion }] ``` ## `posterminal.pair` @@ -335,11 +335,12 @@ Completes POS device pairing for a terminal. | `input.token` | `string` | The auth token for the paired device. | | `[input.clientserial]` | `string` | The client name or serial identifier. | | `[input.pairedAt]` | `string` | Timestamp when paired. | +| `[input.clientversion]` | `string|null` | Client software version reported by the POS device during pairing. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>` +`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>` ## `posterminal.recordObituary` @@ -381,6 +382,31 @@ Enqueues a sync notification for all POS terminals of a company. `Promise` +## `posterminal.setClientversion` + +*Source: `src/store/posterminal/setClientversion.js`* + +Stores the software version reported by a POS terminal. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `id` | `number` | The posterminal id. | +| `clientversion` | `string|null` | Version string reported by the client (trimmed; empty → null). | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise<{id: number, clientversion: string|null}>` + +**Example** + +```js +await store.posterminal.setClientversion(7, '1.10.12.0'); +// => { id: 7, clientversion: '1.10.12.0' } +``` + ## `posterminal.setMaxKnownObituaryId` *Source: `src/store/posterminal/setMaxKnownObituaryId.js`* @@ -550,11 +576,13 @@ Creates or updates a posterminal for the user's selected company. | `input.pairingpin` | `string` | The pairing pin. | | `[input.token]` | `string` | Optional token. | | `[input.id]` | `number` | Optional id to update an existing posterminal. | +| `[input.sort]` | `number` | Display order (integer, ascending). Default 0 on create. | +| `[input.enabled]` | `boolean` | Whether the terminal may sync. Default true on create. | | `[tx]` | `object` | Optional transaction to run within. | **Returns** -`Promise<{id: number, name: string, token: string, pairingpin: string}>` — The upserted posterminal. +`Promise<{id: number, name: string, token: string, pairingpin: string, sort: number, enabled: number}>` — The upserted posterminal. **Throws** @@ -564,5 +592,5 @@ Creates or updates a posterminal for the user's selected company. ```js const terminal = await store.posterminal.upsert(42, { name: 'Kasse 1', pairingpin: '1234', token: 'abc' }); -// => { id, name, token, pairingpin } +// => { id, name, token, pairingpin, sort, enabled } ``` diff --git a/src/providers/mssql/queries/posterminal.js b/src/providers/mssql/queries/posterminal.js index 1ab6a59..9e4e0cf 100644 --- a/src/providers/mssql/queries/posterminal.js +++ b/src/providers/mssql/queries/posterminal.js @@ -63,7 +63,7 @@ WHERE pairingpin = @pairingpin export const PAIR = ` UPDATE posterminal -SET token = @token, paired_at = @pairedAt, clientserial = @clientserial, pairingpin = NULL, lastseen_at = @pairedAt, updated_at = SYSUTCDATETIME() +SET token = @token, paired_at = @pairedAt, clientserial = @clientserial, clientversion = @clientversion, pairingpin = NULL, lastseen_at = @pairedAt, updated_at = SYSUTCDATETIME() WHERE id = @id `; @@ -75,7 +75,7 @@ VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial, @maxkn export const UPDATE = ` UPDATE posterminal -SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknownobituaryid = @maxknownobituaryid, root_category_id = @rootCategoryId, sync_root_articles = @syncRootArticles, lastseen_at = @lastseenAt, sort = @sort, enabled = @enabled, updated_at = SYSUTCDATETIME() +SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknownobituaryid = @maxknownobituaryid, root_category_id = @rootCategoryId, sync_root_articles = @syncRootArticles, lastseen_at = @lastseenAt, sort = @sort, enabled = @enabled, clientversion = @clientversion, updated_at = SYSUTCDATETIME() WHERE id = @id AND company_id = @companyId `; diff --git a/src/store/posterminal/pair.js b/src/store/posterminal/pair.js index bdcc3a4..5074dd7 100644 --- a/src/store/posterminal/pair.js +++ b/src/store/posterminal/pair.js @@ -13,15 +13,18 @@ export default function pair(provider) { * @param {string} input.token - The auth token for the paired device. * @param {string} [input.clientserial] - The client name or serial identifier. * @param {string} [input.pairedAt] - Timestamp when paired. + * @param {string|null} [input.clientversion] - Client software version reported by the POS device during pairing. * @param {object} [tx] - Optional transaction to run within. - * @returns {Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>} + * @returns {Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null, clientversion: string|null}>} */ - return async (id, { token, clientserial, pairedAt }, tx) => { + return async (id, { token, clientserial, pairedAt, clientversion }, tx) => { + const versionVal = clientversion ? String(clientversion).trim() : null; await makeRunner(provider, tx).query(sql, { id, token, clientserial: clientserial ?? null, pairedAt: pairedAt ?? null, + clientversion: versionVal, }); return { @@ -29,6 +32,7 @@ export default function pair(provider) { token, clientserial: clientserial ?? null, pairedAt: pairedAt ?? null, + clientversion: versionVal, }; }; } diff --git a/src/store/posterminal/upsert.js b/src/store/posterminal/upsert.js index 60aa8f3..4247ce3 100644 --- a/src/store/posterminal/upsert.js +++ b/src/store/posterminal/upsert.js @@ -76,6 +76,7 @@ export default function upsert(provider) { const sort = input.sort !== undefined ? input.sort : existing.rows[0].sort; const enabled = input.enabled !== undefined ? (input.enabled ? 1 : 0) : (existing.rows[0].enabled ? 1 : 0); + const clientversion = input.clientversion !== undefined ? (input.clientversion ? String(input.clientversion).trim() : null) : (existing.rows[0].clientversion ?? null); const pairingpin = input.pairingpin !== undefined @@ -116,6 +117,7 @@ export default function upsert(provider) { lastseenAt, sort, enabled, + clientversion, }); return { @@ -131,7 +133,7 @@ export default function upsert(provider) { lastseenAt, sort, enabled, - clientversion: existing.rows[0].clientversion ?? null, + clientversion, }; } diff --git a/test/migration.test.js b/test/migration.test.js new file mode 100644 index 0000000..80b72ca --- /dev/null +++ b/test/migration.test.js @@ -0,0 +1,113 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import LibDashStore from '../src/index.js'; +import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; +import { createTenant } from './helpers.js'; + +test('schema migrations & posterminal version 2', async (t) => { + const tempDir = mkdtempSync(join(tmpdir(), 'dash-migration-test-')); + const dbPath = join(tempDir, 'test.db'); + const provider = new BetterSqlite3Provider({ + filename: dbPath, + storageDir: join(tempDir, 'storage'), + }); + + t.after(() => { + try { + provider.db?.close?.(); + } catch {} + rmSync(tempDir, { recursive: true, force: true }); + }); + + await t.test('fresh database is initialized at version 2', async () => { + await provider.initialize(); + const version = await provider.getSchemaVersion(); + assert.equal(version, 2, 'Schema version must be 2'); + + const cols = provider.db.pragma('table_info("posterminal")').map((c) => c.name); + assert.ok(cols.includes('sort'), 'posterminal must have sort column'); + assert.ok(cols.includes('enabled'), 'posterminal must have enabled column'); + assert.ok(cols.includes('clientversion'), 'posterminal must have clientversion column'); + }); + + let tenant; + let store; + let terminalId; + + await t.test('posterminal store operations support sort, enabled, clientversion', async () => { + store = await new LibDashStore(provider); + tenant = await createTenant(store, { name: 'Migration Tenant' }); + + const created = await store.posterminal.upsert(tenant.googleAuthId, { + name: 'Terminal 1', + sort: 5, + enabled: true, + }); + assert.equal(created.name, 'Terminal 1'); + assert.equal(created.sort, 5); + assert.equal(created.enabled, 1); + terminalId = created.id; + + // setClientversion + const versionRes = await store.posterminal.setClientversion(terminalId, '2.1.0'); + assert.equal(versionRes.clientversion, '2.1.0'); + + // list + const list = await store.posterminal.list(tenant.googleAuthId); + assert.equal(list.length, 1); + assert.equal(list[0].sort, 5); + assert.equal(list[0].enabled, 1); + assert.equal(list[0].clientversion, '2.1.0'); + + // patch update + const updated = await store.posterminal.upsert(tenant.googleAuthId, { + id: terminalId, + sort: 10, + enabled: false, + clientversion: '2.2.0', + }); + assert.equal(updated.sort, 10); + assert.equal(updated.enabled, 0); + assert.equal(updated.clientversion, '2.2.0'); + + // pair with clientversion + const paired = await store.posterminal.pair(terminalId, { + token: 'tok_abc123', + clientserial: 'POS-Device-1', + pairedAt: '2026-10-29T03:00:00.000Z', + clientversion: '3.0.0-beta', + }); + assert.equal(paired.token, 'tok_abc123'); + assert.equal(paired.clientversion, '3.0.0-beta'); + + const afterPair = await store.posterminal.findByToken('tok_abc123'); + assert.equal(afterPair.clientversion, '3.0.0-beta'); + }); + + await t.test('downgrade from version 2 to version 1', async () => { + const steps = await provider.downgradeSchema(1); + assert.equal(steps, 1); + const version = await provider.getSchemaVersion(); + assert.equal(version, 1); + + const cols = provider.db.pragma('table_info("posterminal")').map((c) => c.name); + assert.ok(!cols.includes('sort'), 'sort column should be removed'); + assert.ok(!cols.includes('enabled'), 'enabled column should be removed'); + assert.ok(!cols.includes('clientversion'), 'clientversion column should be removed'); + }); + + await t.test('upgrade from version 1 back to version 2 on re-initialize', async () => { + await provider.initialize(); + const version = await provider.getSchemaVersion(); + assert.equal(version, 2); + + const cols = provider.db.pragma('table_info("posterminal")').map((c) => c.name); + assert.ok(cols.includes('sort'), 'sort column should be restored'); + assert.ok(cols.includes('enabled'), 'enabled column should be restored'); + assert.ok(cols.includes('clientversion'), 'clientversion column should be restored'); + }); +}); +