diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index 1ed6abd..a284d92 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -9,16 +9,16 @@ Params: path values first, then `query:` and `body:` (`body?` = optional body). ### Reusable types -- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T18[], externalNumber?: str|null, createdAt?: str, shipping?: T20|null, shippings?: T20[], payment?: T29|null, payments?: T29[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T9[] }` -- `T2` = `{ id: int, number: str, type: str, date: str, lines: T18[], shippings: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T9[], state?: str }` -- `T3` = `{ id: int, number: str, type: str, date: str, lines: T18[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T9[] }` -- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T21[], files: T9[], externalNumber?: str|null, createdAt?: str, ledger?: T36 }` +- `T1` = `{ id: int, number: str, type: str, entityId: int, date: str, lines: T18[], externalNumber?: str|null, createdAt?: str, shipping?: T20|null, shippings?: T20[], payment?: T29|null, payments?: T29[], paid?: bool, shipped?: bool, delivered?: bool, orderTotal?: num, paidTotal?: num, orderedQuantity?: num, shippedQuantity?: num, shipments?: T2[], files?: T10[] }` +- `T2` = `{ id: int, number: str, type: str, date: str, lines: T18[], shippings: T20[], entityId?: int|null, externalNumber?: str|null, createdAt?: str|null, lineCount?: int, packageCount?: int, files?: T10[], state?: str }` +- `T3` = `{ id: int, number: str, type: str, date: str, lines: T18[], entityId?: int|null, externalNumber?: str|null, createdAt?: str, positions?: int, grossTotal?: num, totalQuantity?: num, customerName?: str|null, files?: T10[] }` +- `T4` = `{ id: int, number: str, type: str, supplierId: int, date: str, lines: T21[], files: T10[], externalNumber?: str|null, createdAt?: str, ledger?: T36 }` - `T5` = `{ name: str, path: str, totalSizeBytes: int, timestamp: str|null, manifest?: T6|null }` - `T6` = `{ id: str, timestamp: str, dateKey: str, monthKey: str, dbProvider: str, files: T11, totalSizeBytes: int, durationMs: int }` -- `T7` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T38|null, incoming?: { date: str, amount: num }[] }` -- `T8` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] }, thumb?: str|null }` -- `T9` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` -- `T10` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }` +- `T7` = `{ id: int, name: str, pairingpin?: str|null, token?: str|null, pairedAt?: str|null, clientserial?: str|null, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, lastseenAt?: str|null, updatedAt?: str|null, categoryIds?: int[] }` +- `T8` = `{ id: int, articleId: int, entityId: int, pu: num, sort: int, supplierName?: str|null, sku?: str|null, stock?: num|null, priceId?: int|null, price?: T38|null, incoming?: { date: str, amount: num }[] }` +- `T9` = `{ id: int, name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] }, thumb?: str|null }` +- `T10` = `{ id: int, manifestId: int, fileId: int, sort: int, hash: str, mimetype: str, name: str, size: int, created?: str, modified?: str, skeledash?: str|null, thumbx?: int|null, thumby?: int|null, meta?: { [k]: str }|null }` - `T11` = `{ vfs: T37, storage: T37, database: T31 }` - `T12` = `{ id: int, entityId: int, addressType: str, firstName?: str|null, name?: str|null, street?: str|null, streetNumber?: str|null, zip?: str|null, city?: str|null, county?: str|null }` - `T13` = `{ id: int, company_id: int, prompt: str, max_itr: int, cron: str, created_at: str, desc?: str|null, skills?: str|null, lastagentnote?: str|null, lastrun_at?: str|null }` @@ -88,7 +88,7 @@ _none_ - Returns: `204 void` #### Get article by SKU — `GET /article/{sku}` - Params: `sku: str` -- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T26, brand: T25|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T7[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }` +- Returns: `200 { article: { id: int, sku: str, name: str, slug: str, desc: str|null, seo: str|null, track_stock: bool, unitamount: num|null, weight: num|null, unit: T26, brand: T25|null, categoryIds: int[], photos: { sort: int, fullHash: str, fullW: int, fullH: int, fullSize: int, thumbW: int, thumbH: int, thumbHash: str }[], eans: str[], prices: { minmultiplier: num, net: num, taxRate: num, customergroupId: int }[], suppliers: T8[], undeliveredOrders: { orderId: int, orderNumber: str, date: str, amount: num, customerName?: str|null }[] } }` #### Update article by SKU — `PATCH /article/{sku}` - Params: `sku: str, body: { name?: str, desc?: str, seo?: str, unitamount?: num, unitSi?: str, track_stock?: bool }` - Returns: `204 void` @@ -163,10 +163,10 @@ _none_ - Returns: `200 { ok: bool }` #### List article suppliers — `GET /article/{sku}/supplier` - Params: `sku: str` -- Returns: `200 { suppliers: T7[] }` +- Returns: `200 { suppliers: T8[] }` #### Assign supplier to article — `POST /article/{sku}/supplier` - Params: `sku: str, body: { entityId: int, sku?: str|null, pu?: num, stock?: num|null, sort?: int|null, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` -- Returns: `201 { supplier: T7 }` +- Returns: `201 { supplier: T8 }` #### Reorder article suppliers — `PUT /article/{sku}/supplier/order` - Params: `sku: str, body: { ids: int[] }` - Returns: `200 { ids: int[] }` @@ -175,7 +175,7 @@ _none_ - Returns: `204 void` #### Update article supplier assignment — `PATCH /article/{sku}/supplier/{id}` - Params: `sku: str, id: int, body: { sku?: str|null, pu?: num, stock?: num|null, sort?: int, netPrice?: num|null, taxId?: int|null, taxRate?: num|null }` -- Returns: `200 { supplier: T7 }` +- Returns: `200 { supplier: T8 }` #### Set article track_stock flag by SKU — `PUT /article/{sku}/track_stock` - Params: `sku: str, body: { track_stock: bool }` - Returns: `204 void` @@ -424,7 +424,7 @@ _none_ - Returns: `200 str` #### Generate and attach delivery note PDF (goods receipt) — `POST /delivery/{id}/pdf` - Params: `id: int` -- Returns: `201 { attachment: T9, fileUrl: str }` +- Returns: `201 { attachment: T10, fileUrl: str }` ### Section: /entity @@ -518,19 +518,19 @@ _none_ - Returns: `200 { labeltemplates: { id: int, name: str, width: num, height: num, dpi: int, type: str, thumb?: str|null }[] }` #### Create label template — `POST /labeltemplate` - Params: `body: { name: str, width: num, height: num, dpi: int, type: str, content: { objects: T22[] } }` -- Returns: `201 { labeltemplate: T8 }` +- Returns: `201 { labeltemplate: T9 }` #### Delete label template — `DELETE /labeltemplate/{id}` - Params: `id: int` - Returns: `204 void` #### Get label template by ID — `GET /labeltemplate/{id}` - Params: `id: int` -- Returns: `200 { labeltemplate: T8 }` +- Returns: `200 { labeltemplate: T9 }` #### Update label template (partial) — `PATCH /labeltemplate/{id}` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T22[] } }` -- Returns: `200 { labeltemplate: T8 }` +- Returns: `200 { labeltemplate: T9 }` #### Update label template — `PUT /labeltemplate/{id}` - Params: `id: int, body: { name?: str, width?: num, height?: num, dpi?: int, type?: str, content?: { objects: T22[] } }` -- Returns: `200 { labeltemplate: T8 }` +- Returns: `200 { labeltemplate: T9 }` ### Section: /ledger @@ -650,7 +650,7 @@ _none_ - Returns: `201 { payment: T29 }` #### Generate and attach order PDF — `POST /order/{id}/pdf` - Params: `id: int` -- Returns: `201 { attachment: T9, fileUrl: str }` +- Returns: `201 { attachment: T10, fileUrl: str }` #### List shipments of an order — `GET /order/{id}/shipment` - Params: `id: int` - Returns: `200 { shipments: T2[] }` @@ -680,16 +680,16 @@ _none_ #### List POS terminals — `GET /posterminal` _none_ -- Returns: `200 { posterminals: T10[] }` +- Returns: `200 { posterminals: T7[] }` #### Create POS terminal — `PUT /posterminal` -- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool }` -- Returns: `200 { posterminal: T10 }` +- Params: `body: { name: str, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool }` +- Returns: `200 { posterminal: T7 }` #### Delete POS terminal — `DELETE /posterminal/{id}` - Params: `id: int` - Returns: `204 void` #### Update POS terminal — `PATCH /posterminal/{id}` -- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, pairingpin?: str|null }` -- Returns: `200 { posterminal: T10 }` +- Params: `id: int, body: { name?: str, rootCategoryId?: int|null, syncRootArticles?: bool, sort?: int, enabled?: bool, clientversion?: str|null, pairingpin?: str|null }` +- Returns: `200 { posterminal: T7 }` #### Disable all categories on terminal — `DELETE /posterminal/{id}/all` - Params: `id: int` - Returns: `200 { id: int, categoryIds: int[] }` @@ -788,7 +788,7 @@ _none_ - Returns: `201 { order: T1 }` #### Generate and attach quote PDF — `POST /quote/{id}/pdf` - Params: `id: int` -- Returns: `201 { attachment: T9, fileUrl: str }` +- Returns: `201 { attachment: T10, fileUrl: str }` ### Section: /retoure @@ -875,7 +875,7 @@ _none_ - Returns: `200 str` #### Generate and attach shipment PDF (delivery note) — `POST /shipment/{id}/pdf` - Params: `id: int` -- Returns: `201 { attachment: T9, fileUrl: str }` +- Returns: `201 { attachment: T10, fileUrl: str }` #### List packages of a shipment — `GET /shipment/{id}/shipping` - Params: `id: int` - Returns: `200 { shippings: T20[] }` @@ -982,7 +982,7 @@ _none_ - Params: `query: path: str, size?: int, w?: int, h?: int, format?: str` - Returns: `200 jpeg` #### POS discovery and pairing — `GET /v1/client` -- Params: `query: authCode: str, name?: str` +- Params: `query: authCode: str, name?: str, clientversion?: str` - Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }` #### JTL-POS customer catalog synchronization — `GET /v1/customer` - Params: `query: lastChangedCustomer?: str, limit?: int` diff --git a/docs/api/routes/pos/client.md b/docs/api/routes/pos/client.md index f27a5b5..46bc8b6 100644 --- a/docs/api/routes/pos/client.md +++ b/docs/api/routes/pos/client.md @@ -15,6 +15,7 @@ Special API for POS bind and sync. |-------|----------|------|-------------| | `authCode` | yes | string | Discovery code (1-4 digits) or pairing PIN (6 digits) | | `name` | no | string | Client device name or serial identifier (default 'JTL-POS') | +| `clientversion` | no | string | Optional client app version (for future POS clients during pairing) | ## Response `200` diff --git a/docs/api/routes/posterminal/list.md b/docs/api/routes/posterminal/list.md index 71c4f8a..b936026 100644 --- a/docs/api/routes/posterminal/list.md +++ b/docs/api/routes/posterminal/list.md @@ -22,6 +22,9 @@ Posterminal list "clientserial": "…", "rootCategoryId": 1, "syncRootArticles": true, + "sort": 1, + "enabled": true, + "clientversion": "…", "lastseenAt": "…", "updatedAt": "…", "categoryIds": [ diff --git a/docs/api/routes/posterminal/update.md b/docs/api/routes/posterminal/update.md index 3b5ae85..0aabc69 100644 --- a/docs/api/routes/posterminal/update.md +++ b/docs/api/routes/posterminal/update.md @@ -19,6 +19,9 @@ Updates a POS terminal configuration (name, root category, sync settings, or pai | `name` | no | string | | | `rootCategoryId` | no | integer | | | `syncRootArticles` | no | boolean | | +| `sort` | no | integer | | +| `enabled` | no | boolean | | +| `clientversion` | no | string | | | `pairingpin` | no | string | | ```json @@ -44,6 +47,9 @@ Updated posterminal "clientserial": "…", "rootCategoryId": 1, "syncRootArticles": true, + "sort": 1, + "enabled": true, + "clientversion": "…", "lastseenAt": "…", "updatedAt": "…", "categoryIds": [ diff --git a/docs/api/routes/posterminal/upsert.md b/docs/api/routes/posterminal/upsert.md index e6fca9c..8f1678a 100644 --- a/docs/api/routes/posterminal/upsert.md +++ b/docs/api/routes/posterminal/upsert.md @@ -15,6 +15,8 @@ The pairingpin is randomly generated server-side and returned in the response. | `name` | yes | string | | | `rootCategoryId` | no | integer | | | `syncRootArticles` | no | boolean | | +| `sort` | no | integer | | +| `enabled` | no | boolean | | ```json { @@ -37,6 +39,9 @@ Created posterminal with generated pairingpin "clientserial": "…", "rootCategoryId": 1, "syncRootArticles": true, + "sort": 1, + "enabled": true, + "clientversion": "…", "lastseenAt": "…", "updatedAt": "…", "categoryIds": [ diff --git a/package.json b/package.json index 4da27be..00be6db 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dev": "cross-env DEV_BACKEND_MONITOR=true node --watch-path=./src --watch-path=./.env --watch-path=../dashdbschema/src --watch-path=../llmorbiter/src src/index.js", "manage-users": "node scripts/manageUsers.js", "wipe-company": "node scripts/wipeCompany.js", + "downgrade": "node scripts/downgradeSchema.js", "docs:api": "node scripts/generateApiDocs.js", "api-graph": "node generate-api-graph.js", "review:functions": "node scripts/extractFunctions.js", diff --git a/scripts/downgradeSchema.js b/scripts/downgradeSchema.js new file mode 100644 index 0000000..8ddabd3 --- /dev/null +++ b/scripts/downgradeSchema.js @@ -0,0 +1,30 @@ +/** + * Downgrades the configured database (from `.env`) to a target schema version. + * + * npm run downgrade -- + * + * Uses libdashstore's `src/providers//downgrade/_to_.js`. + * Stop the API first; starting it again upgrades back to the latest version. + */ +import { createProvider } from '../src/db/provider.js'; +import env from '../src/config/env.js'; + +const [rawTarget] = process.argv.slice(2); +const target = Number(rawTarget); +if (!rawTarget || !Number.isInteger(target) || target < 1) { + console.error('Usage: npm run downgrade -- '); + process.exit(1); +} + +const provider = createProvider(); +try { + const before = await provider.getSchemaVersion(); + const steps = await provider.downgradeSchema(target); + console.log(`Schema ${env.dbProvider}: ${before} -> ${target} (${steps} step(s))`); +} catch (err) { + console.error(`Downgrade failed: ${err.message}`); + process.exitCode = 1; +} finally { + await provider.close(); +} + diff --git a/src/api/openapi.js b/src/api/openapi.js index 5bdee62..624bb5e 100644 --- a/src/api/openapi.js +++ b/src/api/openapi.js @@ -1693,6 +1693,18 @@ * nullable: true * syncRootArticles: * type: boolean + * sort: + * type: integer + * description: Display order (ascending) + * default: 0 + * enabled: + * type: boolean + * description: Whether terminal is active/allowed to sync + * default: true + * clientversion: + * type: string + * nullable: true + * description: Client software version reported by POS terminal * lastseenAt: * type: string * nullable: true diff --git a/src/api/routes/posterminal.js b/src/api/routes/posterminal.js index 6e21087..dbdcfa6 100644 --- a/src/api/routes/posterminal.js +++ b/src/api/routes/posterminal.js @@ -132,6 +132,12 @@ export function register(app, basePath = '') { * syncRootArticles: * type: boolean * default: true + * sort: + * type: integer + * default: 0 + * enabled: + * type: boolean + * default: true * example: * name: "Kassa 1" * responses: @@ -190,6 +196,11 @@ export function register(app, basePath = '') { const syncRootArticles = req.body.syncRootArticles !== undefined ? req.body.syncRootArticles : req.body.sync_root_articles; + const sort = req.body.sort !== undefined ? Number(req.body.sort) : undefined; + if (sort !== undefined && !Number.isInteger(sort)) { + return res.status(400).json({ error: 'sort must be an integer' }); + } + const enabled = req.body.enabled !== undefined ? Boolean(req.body.enabled) : undefined; try { const posterminal = await db.upsert(req.user.id, { @@ -197,6 +208,8 @@ export function register(app, basePath = '') { pairingpin, rootCategoryId, syncRootArticles, + sort, + enabled, }); res.status(200).json({ posterminal }); } catch (error) { @@ -239,6 +252,13 @@ export function register(app, basePath = '') { * nullable: true * syncRootArticles: * type: boolean + * sort: + * type: integer + * enabled: + * type: boolean + * clientversion: + * type: string + * nullable: true * pairingpin: * type: string * nullable: true @@ -312,6 +332,22 @@ export function register(app, basePath = '') { payload.syncRootArticles = Boolean(rawSync); } + if (req.body.sort !== undefined) { + const rawSort = Number(req.body.sort); + if (!Number.isInteger(rawSort)) { + return res.status(400).json({ error: 'sort must be an integer' }); + } + payload.sort = rawSort; + } + + if (req.body.enabled !== undefined) { + payload.enabled = Boolean(req.body.enabled); + } + + if (req.body.clientversion !== undefined) { + payload.clientversion = req.body.clientversion ? String(req.body.clientversion).trim() : null; + } + if (req.body.pairingpin !== undefined) { payload.pairingpin = req.body.pairingpin ? String(req.body.pairingpin).trim() : null; } diff --git a/src/db/posterminal.js b/src/db/posterminal.js index 300004d..88077bd 100644 --- a/src/db/posterminal.js +++ b/src/db/posterminal.js @@ -50,6 +50,11 @@ export async function touchLastseen(id) { return store.posterminal.touchLastseen(id); } +export async function setClientversion(id, clientversion) { + const store = await getStore(); + return store.posterminal.setClientversion(id, clientversion); +} + export async function pair(id, data) { const store = await getStore(); return store.posterminal.pair(id, data); diff --git a/src/db/provider.js b/src/db/provider.js new file mode 100644 index 0000000..73b71aa --- /dev/null +++ b/src/db/provider.js @@ -0,0 +1,41 @@ +import { MssqlProvider } from 'libdashstore/providers/mssql'; +import { BetterSqlite3Provider } from 'libdashstore/providers/better-sqlite3'; +import env from '../config/env.js'; + +function getMssqlConfig() { + const { server, database, user, password, port, encrypt, trustServerCertificate } = env.mssql; + + if (!server || !database || !user || !password) { + throw new Error('MSSQL is not configured (MSSQL_SERVER, MSSQL_DATABASE, MSSQL_USER, MSSQL_PASSWORD required)'); + } + + return { + server, + database, + user, + password, + port, + storageDir: env.storageDir, + pool: { + max: 10, + min: 0, + idleTimeoutMillis: 30_000, + }, + options: { + encrypt, + trustServerCertificate, + }, + }; +} + +/** + * Creates the configured (uninitialized) libdashstore provider from `.env`. + * @returns {MssqlProvider | BetterSqlite3Provider} + */ +export function createProvider() { + if (env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3') { + return new BetterSqlite3Provider(env.sqlite); + } + return new MssqlProvider(getMssqlConfig(), { storageDir: env.storageDir }); +} + diff --git a/src/db/store.js b/src/db/store.js index 1d1876e..1b61463 100644 --- a/src/db/store.js +++ b/src/db/store.js @@ -1,7 +1,5 @@ import LibDashStore from 'libdashstore'; -import { MssqlProvider } from 'libdashstore/providers/mssql'; -import { BetterSqlite3Provider } from 'libdashstore/providers/better-sqlite3'; -import env from '../config/env.js'; +import { createProvider } from './provider.js'; import { createLogger, chalk } from '../util/logger.js'; import { clearCachedCompanyUsers, clearCachedUser } from '../api/middleware/auth.js'; import { clearDeletedCompany, setUserCompany } from '../api/sse.js'; @@ -134,34 +132,10 @@ function wrapStoreWithTiming(rawStore) { return rawStore; } -function getMssqlConfig() { - const { server, database, user, password, port, encrypt, trustServerCertificate } = env.mssql; - - if (!server || !database || !user || !password) { - throw new Error('MSSQL is not configured (MSSQL_SERVER, MSSQL_DATABASE, MSSQL_USER, MSSQL_PASSWORD required)'); - } - - return { - server, - database, - user, - password, - port, - storageDir: env.storageDir, - pool: { - max: 10, - min: 0, - idleTimeoutMillis: 30_000, - }, - options: { - encrypt, - trustServerCertificate, - }, - }; -} - /** * Returns the initialized LibDashStore singleton. + * `provider.initialize()` applies pending schema upgrades (libdashstore + * `src/providers//upgrade/`) before the store is used. * @returns {Promise>} */ export async function getStore() { @@ -171,11 +145,7 @@ export async function getStore() { if (!connecting) { connecting = (async () => { - if (env.dbProvider === 'sqlite' || env.dbProvider === 'better-sqlite3') { - provider = new BetterSqlite3Provider(env.sqlite); - } else { - provider = new MssqlProvider(getMssqlConfig(), { storageDir: env.storageDir }); - } + provider = createProvider(); await provider.initialize(); store = wrapStoreWithTiming(await new LibDashStore(provider)); return store; diff --git a/src/v1/middleware/posAuth.js b/src/v1/middleware/posAuth.js index ac0c22d..e83eafe 100644 --- a/src/v1/middleware/posAuth.js +++ b/src/v1/middleware/posAuth.js @@ -57,6 +57,11 @@ export async function posAuthMiddleware(req, res, next) { return res.status(401).json({ Message: 'Authorization has been denied for this request.' }); } + if (terminal.enabled === 0 || terminal.enabled === false) { + console.log(chalk.yellow(` [posAuth] Terminal "${terminal.id}" is disabled`)); + return res.status(403).json({ Message: 'Authorization has been denied for this request.' }); + } + req.posterminal = terminal; db.touchLastseen(terminal.id).catch((err) => { console.error('Failed to touch lastseen for posterminal:', err); diff --git a/src/v1/routes/client.js b/src/v1/routes/client.js index e59abd6..5c91983 100644 --- a/src/v1/routes/client.js +++ b/src/v1/routes/client.js @@ -83,6 +83,13 @@ export function buildClientStep2(authCode, config) { * type: string * description: Client device name or serial identifier (default 'JTL-POS') * example: "Kasse 1" + * - in: query + * name: clientversion + * required: false + * schema: + * type: string + * description: Optional client app version (for future POS clients during pairing) + * example: "1.10.12.0" * responses: * 200: * description: Discovery or pairing successful @@ -160,11 +167,13 @@ export async function handleClient(req, res) { const token = terminal.token; const strToken = String(token || crypto.randomBytes(16).toString('hex')).trim(); const pairedAt = serverTimestamp(); + const clientversion = req.query.clientversion || req.query.version || req.query.clientVersion || null; await db.pair(terminal.id, { token: strToken, clientserial: name, pairedAt, + clientversion: clientversion ? String(clientversion).trim() : null, }); if (terminal.companyId) {