diff --git a/API.md b/API.md index 7dc1881..d74f44a 100644 --- a/API.md +++ b/API.md @@ -200,6 +200,14 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met - [posterminal](API.posterminal.md) - `delete(googleAuthId: number, id: number, tx?: object): Promise` - `findByPairingpin(pairingpin: string, tx?: object): Promise` + - `findByToken(token: string, tx?: object): Promise` + - `getCategoryCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` + - `getCompositeProductCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` + - `getCustomerCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` + - `getCustomerGroupCount(input: {posterminalId: number, cursor?: number}, tx?: object): Promise` + - `getDeletedEntityCount(input?: {posterminalId?: number, cursor?: number}): Promise` + - `getMaxOrderIdCount(input: {posterminalId: number}, tx?: object): Promise` + - `getProductCount(input: {posterminalId: number, cursor?: 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}>` - `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array}>` diff --git a/API.posterminal.md b/API.posterminal.md index 71c745f..d4b75ec 100644 --- a/API.posterminal.md +++ b/API.posterminal.md @@ -47,6 +47,154 @@ Finds a posterminal by its pairingpin. `Promise` — The posterminal row, or null if not found. +## `posterminal.findByToken` + +*Source: `src/store/posterminal/findByToken.js`* + +Finds a posterminal by its auth token. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `token` | `string` | The auth token to search for. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The posterminal row, or null if not found. + +## `posterminal.getCategoryCount` + +*Source: `src/store/posterminal/getCategoryCount.js`* + +Counts categories assigned to the POS terminal updated after the given unix timestamp cursor. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The count of changed records. + +## `posterminal.getCompositeProductCount` + +*Source: `src/store/posterminal/getCompositeProductCount.js`* + +Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The count of changed records. + +## `posterminal.getCustomerCount` + +*Source: `src/store/posterminal/getCustomerCount.js`* + +Counts customers of the POS terminal company updated after the given unix timestamp cursor. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The count of changed records. + +## `posterminal.getCustomerGroupCount` + +*Source: `src/store/posterminal/getCustomerGroupCount.js`* + +Counts customer groups of the POS terminal company updated after the given unix timestamp cursor. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The count of changed records. + +## `posterminal.getDeletedEntityCount` + +*Source: `src/store/posterminal/getDeletedEntityCount.js`* + +Counts deleted entities after the given cursor (currently always 0). + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `[input]` | `object` | | +| `[input.posterminalId]` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor. | + +**Returns** + +`Promise` — Always 0. + +## `posterminal.getMaxOrderIdCount` + +*Source: `src/store/posterminal/getMaxOrderIdCount.js`* + +Returns the maximum order manifest id for the POS terminal company. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — Maximum order id or 0. + +## `posterminal.getProductCount` + +*Source: `src/store/posterminal/getProductCount.js`* + +Counts products assigned to the POS terminal updated after the given unix timestamp cursor. + +**Parameters** + +| Name | Type | Description | +| --- | --- | --- | +| `input` | `object` | | +| `input.posterminalId` | `number` | The POS terminal id. | +| `[input.cursor=0]` | `number` | Unix timestamp cursor of last update. | +| `[tx]` | `object` | Optional transaction to run within. | + +**Returns** + +`Promise` — The count of changed records. + ## `posterminal.list` *Source: `src/store/posterminal/list.js`* diff --git a/src/index.js b/src/index.js index 9e62759..2f56aa0 100644 --- a/src/index.js +++ b/src/index.js @@ -102,7 +102,15 @@ import posterminalSetOnAll from './store/posterminal/setOnAll.js'; import posterminalSetOffAll from './store/posterminal/setOffAll.js'; import posterminalGetCategoryPosterminalIds from './store/posterminal/getCategoryPosterminalIds.js'; import posterminalFindByPairingpin from './store/posterminal/findByPairingpin.js'; +import posterminalFindByToken from './store/posterminal/findByToken.js'; import posterminalPair from './store/posterminal/pair.js'; +import posterminalGetProductCount from './store/posterminal/getProductCount.js'; +import posterminalGetCategoryCount from './store/posterminal/getCategoryCount.js'; +import posterminalGetCustomerCount from './store/posterminal/getCustomerCount.js'; +import posterminalGetCustomerGroupCount from './store/posterminal/getCustomerGroupCount.js'; +import posterminalGetCompositeProductCount from './store/posterminal/getCompositeProductCount.js'; +import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEntityCount.js'; +import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js'; import langCreate from './store/lang/create.js'; import langDelete from './store/lang/delete.js'; import langList from './store/lang/list.js'; @@ -367,7 +375,15 @@ export default class LibDashStore { setOffAll: posterminalSetOffAll(provider), getCategoryPosterminalIds: posterminalGetCategoryPosterminalIds(provider), findByPairingpin: posterminalFindByPairingpin(provider), + findByToken: posterminalFindByToken(provider), pair: posterminalPair(provider), + getProductCount: posterminalGetProductCount(provider), + getCategoryCount: posterminalGetCategoryCount(provider), + getCustomerCount: posterminalGetCustomerCount(provider), + getCustomerGroupCount: posterminalGetCustomerGroupCount(provider), + getCompositeProductCount: posterminalGetCompositeProductCount(provider), + getDeletedEntityCount: posterminalGetDeletedEntityCount(provider), + getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider), }; this.google_auth = { diff --git a/src/providers/mssql/index.js b/src/providers/mssql/index.js index d899d7e..29a4327 100644 --- a/src/providers/mssql/index.js +++ b/src/providers/mssql/index.js @@ -182,6 +182,16 @@ export class MssqlProvider { BEGIN ALTER TABLE propertyname ADD exclusive BIT NOT NULL CONSTRAINT DF_propertyname_exclusive DEFAULT 0; END + + IF OBJECT_ID(N'dbo.category', N'U') IS NOT NULL AND COL_LENGTH('category', 'updated_at') IS NULL + BEGIN + ALTER TABLE category ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME(); + END + + IF OBJECT_ID(N'dbo.customergroup', N'U') IS NOT NULL AND COL_LENGTH('customergroup', 'updated_at') IS NULL + BEGIN + ALTER TABLE customergroup ADD updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME(); + END `); } catch { // ignore diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 0550dd9..0a5ba1a 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -174,37 +174,37 @@ WHERE a.sku = @sku export const UPDATE_WEIGHT = ` UPDATE article -SET weight = @weight +SET weight = @weight, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; export const UPDATE_TRACK_STOCK = ` UPDATE article -SET track_stock = @trackStock +SET track_stock = @trackStock, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; export const UPDATE_BRAND = ` UPDATE article -SET brand_id = @brandId +SET brand_id = @brandId, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; export const UPDATE_UNIT = ` UPDATE article -SET unit_id = @unitId +SET unit_id = @unitId, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; export const UPDATE_UNITAMOUNT = ` UPDATE article -SET unitamount = @unitamount +SET unitamount = @unitamount, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; export const UPDATE_UNIT_AND_AMOUNT = ` UPDATE article -SET unit_id = @unitId, unitamount = @unitamount +SET unit_id = @unitId, unitamount = @unitamount, updated_at = SYSUTCDATETIME() WHERE id = @articleId `; diff --git a/src/providers/mssql/queries/category.js b/src/providers/mssql/queries/category.js index b6b0730..70405ce 100644 --- a/src/providers/mssql/queries/category.js +++ b/src/providers/mssql/queries/category.js @@ -45,6 +45,7 @@ FROM categoryarticle ca INNER JOIN article a ON a.id = ca.article_id ${ARTICLE_LIST_JOINS} WHERE ca.category_id = @categoryId + AND a.fatherarticle_id IS NULL ORDER BY a.id `; @@ -110,7 +111,7 @@ WHERE parent_id = @parentId export const UPDATE_SORT = ` UPDATE category -SET sort = @sort +SET sort = @sort, updated_at = SYSUTCDATETIME() WHERE id = @id AND parent_id = @parentId `; @@ -134,7 +135,7 @@ WHERE id = @id export const MOVE = ` UPDATE category -SET parent_id = @parentId, sort = @sort +SET parent_id = @parentId, sort = @sort, updated_at = SYSUTCDATETIME() WHERE id = @id `; diff --git a/src/providers/mssql/queries/customergroup.js b/src/providers/mssql/queries/customergroup.js index 783069c..0267055 100644 --- a/src/providers/mssql/queries/customergroup.js +++ b/src/providers/mssql/queries/customergroup.js @@ -49,14 +49,14 @@ WHERE company_id = @companyId export const UPDATE = ` UPDATE customergroup -SET name = @name +SET name = @name, updated_at = SYSUTCDATETIME() OUTPUT INSERTED.id, INSERTED.name, INSERTED.sort WHERE id = @id AND company_id = @companyId `; export const UPDATE_SORT = ` UPDATE customergroup -SET sort = @sort +SET sort = @sort, updated_at = SYSUTCDATETIME() WHERE id = @id AND company_id = @companyId `; diff --git a/src/providers/mssql/queries/posterminal.js b/src/providers/mssql/queries/posterminal.js index 49ae4b1..3e53510 100644 --- a/src/providers/mssql/queries/posterminal.js +++ b/src/providers/mssql/queries/posterminal.js @@ -112,3 +112,86 @@ FROM posterminalcategory pc INNER JOIN posterminal pt ON pt.id = pc.posterminal_id WHERE pt.company_id = @companyId `; + + +export const FIND_BY_TOKEN = ` +SELECT + id, + company_id AS companyId, + name, + token, + pairingpin, + paired_at AS pairedAt, + clientserial AS clientserial +FROM posterminal +WHERE token = @token +`; + +export const COUNT_PRODUCTS = ` +SELECT COUNT(DISTINCT a.id) AS count +FROM article a +INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id +WHERE EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id + WHERE ca.article_id = a.id + AND pc.posterminal_id = @posterminalId +) +AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor +`; + +export const COUNT_CATEGORIES = ` +SELECT COUNT(*) AS count +FROM category c +INNER JOIN posterminalcategory pc ON pc.category_id = c.id +INNER JOIN posterminal pt ON pt.id = pc.posterminal_id +INNER JOIN company co ON co.id = pt.company_id +WHERE pc.posterminal_id = @posterminalId + AND (co.root_category_id IS NULL OR c.id <> co.root_category_id) + AND DATEDIFF_BIG(second, '1970-01-01', c.updated_at) > @cursor +`; + +export const COUNT_CUSTOMERS = ` +SELECT COUNT(*) AS count +FROM entity e +INNER JOIN customergroup cg ON cg.id = e.customergroup_id +INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id +WHERE cg.name NOT IN ('Lieferanten', 'POS') + AND DATEDIFF_BIG(second, '1970-01-01', e.updated_at) > @cursor +`; + +export const COUNT_CUSTOMER_GROUPS = ` +SELECT COUNT(*) AS count +FROM customergroup cg +INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = cg.company_id +WHERE cg.name <> 'Lieferanten' + AND DATEDIFF_BIG(second, '1970-01-01', cg.updated_at) > @cursor +`; + +export const COUNT_COMPOSITE_PRODUCTS = ` +SELECT COUNT(DISTINCT a.id) AS count +FROM article a +INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = a.company_id +WHERE a.articlelist_id IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM articlelistarticle ala + WHERE ala.articlelist_id = a.articlelist_id + ) + AND EXISTS ( + SELECT 1 + FROM categoryarticle ca + INNER JOIN posterminalcategory pc ON pc.category_id = ca.category_id + WHERE ca.article_id = a.id + AND pc.posterminal_id = @posterminalId + ) + AND DATEDIFF_BIG(second, '1970-01-01', a.updated_at) > @cursor +`; + +export const MAX_ORDER_ID = ` +SELECT ISNULL(MAX(m.id), 0) AS maxOrderId +FROM manifest m +INNER JOIN posterminal pt ON pt.id = @posterminalId AND pt.company_id = m.company_id +WHERE m.[type] = 'order' +`; diff --git a/src/providers/mssql/schema.js b/src/providers/mssql/schema.js index 4d1f8c9..fc72d78 100644 --- a/src/providers/mssql/schema.js +++ b/src/providers/mssql/schema.js @@ -77,6 +77,7 @@ BEGIN id INT IDENTITY(1,1) PRIMARY KEY, parent_id INT NULL, sort INT NOT NULL CONSTRAINT DF_category_sort DEFAULT 0, + updated_at DATETIME2 NOT NULL CONSTRAINT DF_category_updated_at DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_category_parent FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE NO ACTION ); END @@ -134,6 +135,7 @@ BEGIN -- Nullable only for company-create chicken-egg (POS group first, then company, then SET). -- Always set before commit; never left null for live rows. company_id INT NULL, + updated_at DATETIME2 NOT NULL CONSTRAINT DF_customergroup_updated_at DEFAULT SYSUTCDATETIME(), CONSTRAINT FK_customergroup_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE ); END diff --git a/src/providers/sqlite3/index.js b/src/providers/sqlite3/index.js index 764863a..5ba9494 100644 --- a/src/providers/sqlite3/index.js +++ b/src/providers/sqlite3/index.js @@ -431,6 +431,18 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor { if (propertynameCols.length > 0 && !propertynameCols.includes('exclusive')) { this.db.exec(`ALTER TABLE propertyname ADD COLUMN exclusive INTEGER NOT NULL DEFAULT 0`); } + + const categoryCols = this.db.pragma('table_info("category")').map((c) => c.name); + if (categoryCols.length > 0 && !categoryCols.includes('updated_at')) { + this.db.exec(`ALTER TABLE category ADD COLUMN updated_at TEXT`); + this.db.exec(`UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE updated_at IS NULL`); + } + + const customergroupCols = this.db.pragma('table_info("customergroup")').map((c) => c.name); + if (customergroupCols.length > 0 && !customergroupCols.includes('updated_at')) { + this.db.exec(`ALTER TABLE customergroup ADD COLUMN updated_at TEXT`); + this.db.exec(`UPDATE customergroup SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE updated_at IS NULL`); + } } catch { // ignore } diff --git a/src/providers/sqlite3/queries/category.js b/src/providers/sqlite3/queries/category.js index 59d1349..8bcc6c6 100644 --- a/src/providers/sqlite3/queries/category.js +++ b/src/providers/sqlite3/queries/category.js @@ -65,6 +65,7 @@ FROM categoryarticle ca INNER JOIN article a ON a.id = ca.article_id ${ARTICLE_LIST_JOINS} WHERE ca.category_id = @categoryId + AND a.fatherarticle_id IS NULL ORDER BY a.id `; diff --git a/src/providers/sqlite3/queries/posterminal.js b/src/providers/sqlite3/queries/posterminal.js index 5e1faf0..b819d8b 100644 --- a/src/providers/sqlite3/queries/posterminal.js +++ b/src/providers/sqlite3/queries/posterminal.js @@ -6,6 +6,8 @@ export const { CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY, LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF, SET_OFF_ALL, UPDATE, FIND_BY_PAIRINGPIN, PAIR, + FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS, + COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID, } = queries; export const SET_ON = ` diff --git a/src/providers/sqlite3/queries/translate.js b/src/providers/sqlite3/queries/translate.js index 01f3bc8..d0eee34 100644 --- a/src/providers/sqlite3/queries/translate.js +++ b/src/providers/sqlite3/queries/translate.js @@ -33,6 +33,7 @@ export function sqliteQuery(sql) { let result = quoteIdentifiers(sql) .replace(/\bISNULL\s*\(/gi, 'IFNULL(') .replace(/\bSYSUTCDATETIME\s*\(\s*\)/gi, "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')") + .replace(/\bDATEDIFF_BIG\s*\(\s*second\s*,\s*'1970-01-01'\s*,\s*([^)]+?)\s*\)/gi, "CAST(strftime('%s', $1) AS INTEGER)") .replace(/\bCAST\s*\(([^()]+?)\s+AS\s+INT\s*\)/gi, 'CAST($1 AS INTEGER)') .replace(/\bCONVERT\s*\(\s*VARBINARY\s*\(\s*MAX\s*\)\s*,\s*([^)]+?)\s*\)/gi, '$1'); result = moveReturning(result); diff --git a/src/providers/sqlite3/schema.js b/src/providers/sqlite3/schema.js index 0b7108c..cc4966e 100644 --- a/src/providers/sqlite3/schema.js +++ b/src/providers/sqlite3/schema.js @@ -28,6 +28,7 @@ CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY, parent_id INTEGER, sort INTEGER NOT NULL DEFAULT 0, + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), FOREIGN KEY (parent_id) REFERENCES category(id) ON DELETE NO ACTION ); `; @@ -86,6 +87,7 @@ CREATE TABLE IF NOT EXISTS customergroup ( name TEXT NOT NULL, sort INTEGER NOT NULL DEFAULT 0, company_id INTEGER, + updated_at TEXT NOT NULL DEFAULT (${UTC_NOW}), FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE ); `; diff --git a/src/store/article/setPricesBySku.js b/src/store/article/setPricesBySku.js index 211d0c9..cb7ffef 100644 --- a/src/store/article/setPricesBySku.js +++ b/src/store/article/setPricesBySku.js @@ -82,6 +82,11 @@ export default function setPricesBySku(provider) { throw new Error('POS customer group required'); } + const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId }); + if (artCheck.rows[0]?.is_father) { + throw new Error('A father article cannot have prices'); + } + const taxIds = new Map(); for (const entry of normalized) { if (!taxIds.has(entry.tax)) { diff --git a/src/store/article/toggleFather.js b/src/store/article/toggleFather.js index 31f99a9..32210bd 100644 --- a/src/store/article/toggleFather.js +++ b/src/store/article/toggleFather.js @@ -53,6 +53,26 @@ export default function toggleFather(provider) { throw new Error('An article with options cannot become a father'); } + const stockRes = await runner.query( + 'SELECT COALESCE(SUM(l.amount), 0) AS totalStock FROM ledger l WHERE l.article_id = @articleId', + { articleId }, + ); + const totalStock = Number(stockRes.rows[0]?.totalStock ?? 0); + if (totalStock !== 0) { + throw new Error('An article with existing stock cannot become a father article'); + } + + const supRes = await runner.query( + 'SELECT COUNT(*) AS count FROM articlesupplier WHERE article_id = @articleId', + { articleId }, + ); + if ((supRes.rows[0]?.count ?? 0) > 0) { + throw new Error('An article with assigned suppliers cannot become a father article'); + } + + await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId }); + await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId }); + // If article is a child, verify attribute is not used by father or any father up the chain let currentFatherId = currentArt?.fatherarticle_id; const visited = new Set(); diff --git a/src/store/articlelist/setItemsByArticleSku.js b/src/store/articlelist/setItemsByArticleSku.js index 4118141..ec863f7 100644 --- a/src/store/articlelist/setItemsByArticleSku.js +++ b/src/store/articlelist/setItemsByArticleSku.js @@ -39,9 +39,6 @@ export default function setItemsByArticleSku(provider) { if (article.is_father) { throw new Error('A father article cannot be a Stückliste'); } - if (article.optionslist_id != null) { - throw new Error('An article with options cannot be a Stückliste'); - } // If items is empty: clear Stückliste if (items.length === 0) { diff --git a/src/store/articlesupplier/create.js b/src/store/articlesupplier/create.js index 151fcdd..ff78a50 100644 --- a/src/store/articlesupplier/create.js +++ b/src/store/articlesupplier/create.js @@ -91,6 +91,11 @@ export default function create(provider) { runner, ); + const artCheck = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { articleId }); + if (artCheck.rows[0]?.is_father) { + throw new Error('A father article cannot have suppliers'); + } + await assertSupplier(runner, companyId, entityId); const existingCheck = await runner.query(queries.CHECK_EXISTS, { diff --git a/src/store/optionslist/setOptionsByArticleSku.js b/src/store/optionslist/setOptionsByArticleSku.js index fe87bf4..8e5cb82 100644 --- a/src/store/optionslist/setOptionsByArticleSku.js +++ b/src/store/optionslist/setOptionsByArticleSku.js @@ -39,9 +39,6 @@ export default function setOptionsByArticleSku(provider) { if (article.is_father) { throw new Error('A father article cannot have options'); } - if (article.articlelist_id != null) { - throw new Error('A Stückliste article cannot have options'); - } // If options is empty: clear optionslist if (options.length === 0) { diff --git a/src/store/posterminal/findByToken.js b/src/store/posterminal/findByToken.js new file mode 100644 index 0000000..c040c13 --- /dev/null +++ b/src/store/posterminal/findByToken.js @@ -0,0 +1,23 @@ +import { FIND_BY_TOKEN as MSSQL_FIND } from '../../providers/mssql/queries/posterminal.js'; +import { FIND_BY_TOKEN as SQLITE_FIND } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +export default function findByToken(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_FIND : MSSQL_FIND; + + /** + * Finds a posterminal by its auth token. + * + * @param {string} token - The auth token to search for. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The posterminal row, or null if not found. + */ + return async (token, tx) => { + const trimmed = String(token ?? '').trim(); + if (!trimmed) return null; + const result = await makeRunner(provider, tx).query(sql, { + token: trimmed, + }); + return result.rows[0] || null; + }; +} diff --git a/src/store/posterminal/getCategoryCount.js b/src/store/posterminal/getCategoryCount.js new file mode 100644 index 0000000..5476b71 --- /dev/null +++ b/src/store/posterminal/getCategoryCount.js @@ -0,0 +1,30 @@ +import { COUNT_CATEGORIES as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { COUNT_CATEGORIES as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +function normalizeCursor(cursor) { + const n = Number(cursor) || 0; + if (n <= 0) return 0; + return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); +} + +export default function getCategoryCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Counts categories assigned to the POS terminal updated after the given unix timestamp cursor. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The count of changed records. + */ + return async ({ posterminalId, cursor = 0 } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + cursor: normalizeCursor(cursor), + }); + return Number(result.rows[0]?.count ?? 0); + }; +} diff --git a/src/store/posterminal/getCompositeProductCount.js b/src/store/posterminal/getCompositeProductCount.js new file mode 100644 index 0000000..8ba08cf --- /dev/null +++ b/src/store/posterminal/getCompositeProductCount.js @@ -0,0 +1,30 @@ +import { COUNT_COMPOSITE_PRODUCTS as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { COUNT_COMPOSITE_PRODUCTS as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +function normalizeCursor(cursor) { + const n = Number(cursor) || 0; + if (n <= 0) return 0; + return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); +} + +export default function getCompositeProductCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Counts composite products (Stücklisten) assigned to the POS terminal updated after the given unix timestamp cursor. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The count of changed records. + */ + return async ({ posterminalId, cursor = 0 } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + cursor: normalizeCursor(cursor), + }); + return Number(result.rows[0]?.count ?? 0); + }; +} diff --git a/src/store/posterminal/getCustomerCount.js b/src/store/posterminal/getCustomerCount.js new file mode 100644 index 0000000..7fa1162 --- /dev/null +++ b/src/store/posterminal/getCustomerCount.js @@ -0,0 +1,30 @@ +import { COUNT_CUSTOMERS as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { COUNT_CUSTOMERS as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +function normalizeCursor(cursor) { + const n = Number(cursor) || 0; + if (n <= 0) return 0; + return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); +} + +export default function getCustomerCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Counts customers of the POS terminal company updated after the given unix timestamp cursor. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The count of changed records. + */ + return async ({ posterminalId, cursor = 0 } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + cursor: normalizeCursor(cursor), + }); + return Number(result.rows[0]?.count ?? 0); + }; +} diff --git a/src/store/posterminal/getCustomerGroupCount.js b/src/store/posterminal/getCustomerGroupCount.js new file mode 100644 index 0000000..f8ffa8b --- /dev/null +++ b/src/store/posterminal/getCustomerGroupCount.js @@ -0,0 +1,30 @@ +import { COUNT_CUSTOMER_GROUPS as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { COUNT_CUSTOMER_GROUPS as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +function normalizeCursor(cursor) { + const n = Number(cursor) || 0; + if (n <= 0) return 0; + return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); +} + +export default function getCustomerGroupCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Counts customer groups of the POS terminal company updated after the given unix timestamp cursor. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The count of changed records. + */ + return async ({ posterminalId, cursor = 0 } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + cursor: normalizeCursor(cursor), + }); + return Number(result.rows[0]?.count ?? 0); + }; +} diff --git a/src/store/posterminal/getDeletedEntityCount.js b/src/store/posterminal/getDeletedEntityCount.js new file mode 100644 index 0000000..5d56933 --- /dev/null +++ b/src/store/posterminal/getDeletedEntityCount.js @@ -0,0 +1,11 @@ +export default function getDeletedEntityCount() { + /** + * Counts deleted entities after the given cursor (currently always 0). + * + * @param {object} [input] + * @param {number} [input.posterminalId] - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor. + * @returns {Promise} Always 0. + */ + return async () => 0; +} diff --git a/src/store/posterminal/getMaxOrderIdCount.js b/src/store/posterminal/getMaxOrderIdCount.js new file mode 100644 index 0000000..003ae1d --- /dev/null +++ b/src/store/posterminal/getMaxOrderIdCount.js @@ -0,0 +1,22 @@ +import { MAX_ORDER_ID as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { MAX_ORDER_ID as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +export default function getMaxOrderIdCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Returns the maximum order manifest id for the POS terminal company. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} Maximum order id or 0. + */ + return async ({ posterminalId } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + }); + return Number(result.rows[0]?.maxOrderId ?? 0); + }; +} diff --git a/src/store/posterminal/getProductCount.js b/src/store/posterminal/getProductCount.js new file mode 100644 index 0000000..67d6eaf --- /dev/null +++ b/src/store/posterminal/getProductCount.js @@ -0,0 +1,30 @@ +import { COUNT_PRODUCTS as MSSQL_SQL } from '../../providers/mssql/queries/posterminal.js'; +import { COUNT_PRODUCTS as SQLITE_SQL } from '../../providers/sqlite3/queries/posterminal.js'; +import makeRunner from '../runner.js'; + +function normalizeCursor(cursor) { + const n = Number(cursor) || 0; + if (n <= 0) return 0; + return n >= 1e11 ? Math.floor(n / 1000) : Math.floor(n); +} + +export default function getProductCount(provider) { + const sql = provider?.dialect === 'sqlite3' ? SQLITE_SQL : MSSQL_SQL; + + /** + * Counts products assigned to the POS terminal updated after the given unix timestamp cursor. + * + * @param {object} input + * @param {number} input.posterminalId - The POS terminal id. + * @param {number} [input.cursor=0] - Unix timestamp cursor of last update. + * @param {object} [tx] - Optional transaction to run within. + * @returns {Promise} The count of changed records. + */ + return async ({ posterminalId, cursor = 0 } = {}, tx) => { + const result = await makeRunner(provider, tx).query(sql, { + posterminalId: Number(posterminalId), + cursor: normalizeCursor(cursor), + }); + return Number(result.rows[0]?.count ?? 0); + }; +} diff --git a/src/store/stock/adjust.js b/src/store/stock/adjust.js index 31b8c2f..5bcf416 100644 --- a/src/store/stock/adjust.js +++ b/src/store/stock/adjust.js @@ -97,6 +97,13 @@ export default function adjust(provider) { const resolvedArticles = []; for (const item of normalizedLines) { const resolved = await resolveArticle(googleAuthId, item.sku, activeTx); + const artRes = await runner.query('SELECT is_father FROM article WHERE id = @articleId', { + articleId: resolved.articleId, + }); + if (artRes.rows[0]?.is_father) { + throw new Error(`A father article cannot have stock (${item.sku})`); + } + const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, { articleId: resolved.articleId, languageId: resolved.languageId,