u
This commit is contained in:
@@ -181,6 +181,8 @@ Returns the change history of an article by its SKU.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `googleAuthId` | `number` | The id of the Google auth user. |
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
| `sku` | `string` | The article SKU. |
|
| `sku` | `string` | The article SKU. |
|
||||||
|
| `[opts]` | `object` | Optional options. |
|
||||||
|
| `[opts.limit]` | `number` | Maximum number of entries to return (newest first). |
|
||||||
| `[tx]` | `object` | Optional transaction. |
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
**Returns**
|
**Returns**
|
||||||
@@ -192,6 +194,9 @@ Returns the change history of an article by its SKU.
|
|||||||
```js
|
```js
|
||||||
const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||||
// => [{ id, companyuserId, date, text }]
|
// => [{ id, companyuserId, date, text }]
|
||||||
|
|
||||||
|
const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
||||||
|
// => up to 5 most recent entries
|
||||||
```
|
```
|
||||||
|
|
||||||
## `article.importRows`
|
## `article.importRows`
|
||||||
@@ -362,6 +367,27 @@ Removes a child article from a father article, either by unlinking or deleting i
|
|||||||
|
|
||||||
`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
`Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
||||||
|
|
||||||
|
## `article.resolveUniqueSlug`
|
||||||
|
|
||||||
|
*Source: `src/store/article/resolveUniqueSlug.js`*
|
||||||
|
|
||||||
|
Resolves a company- and language-unique slug for an article. Disambiguates duplicate slugs with a numeric suffix (-2, -3, ...).
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `runner` | `object` | DB runner or tx. |
|
||||||
|
| `params` | `object` | |
|
||||||
|
| `params.companyId` | `number` | The company ID. |
|
||||||
|
| `params.languageId` | `number` | The language ID. |
|
||||||
|
| `params.name` | `string` | The article name. |
|
||||||
|
| `[params.excludeArticleId]` | `number` | Optional article ID to exclude (e.g. for updates). |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<string>` — The unique slug.
|
||||||
|
|
||||||
## `article.setBrandIdBySku`
|
## `article.setBrandIdBySku`
|
||||||
|
|
||||||
*Source: `src/store/article/setBrandIdBySku.js`*
|
*Source: `src/store/article/setBrandIdBySku.js`*
|
||||||
|
|||||||
40
API.categoryattributevalue.md
Normal file
40
API.categoryattributevalue.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# categoryattributevalue
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `categoryattributevalue.listByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryattributevalue/listByCategoryId.js`*
|
||||||
|
|
||||||
|
Lists the attribute values assigned to a category by its ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
|
||||||
|
## `categoryattributevalue.setByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryattributevalue/setByCategoryId.js`*
|
||||||
|
|
||||||
|
Replaces all attribute values for a category by its ID. Validates company ownership and enforces the `exclusive` flag on attributes.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `values` | `Array<number|{attributeValueId: number, sort?: number}>` | Attribute value IDs or items. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
59
API.categoryproperty.md
Normal file
59
API.categoryproperty.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# categoryproperty
|
||||||
|
|
||||||
|
This document is auto-generated from the JSDoc comments in `src/store` by running `npm run api-doc`.
|
||||||
|
|
||||||
|
## `categoryproperty.delete`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/delete.js`*
|
||||||
|
|
||||||
|
Deletes a property from a category by categoryId and propertyId.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `propertyId` | `number` | The property id. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<void>`
|
||||||
|
|
||||||
|
## `categoryproperty.listByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/listByCategoryId.js`*
|
||||||
|
|
||||||
|
Lists the properties of a category by its ID.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
|
|
||||||
|
## `categoryproperty.setByCategoryId`
|
||||||
|
|
||||||
|
*Source: `src/store/categoryproperty/setByCategoryId.js`*
|
||||||
|
|
||||||
|
Replaces all properties for a category by its ID. Enforces exclusive flag on property names.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `googleAuthId` | `number` | The id of the Google auth user. |
|
||||||
|
| `categoryId` | `number` | The category ID. |
|
||||||
|
| `properties` | `Array<{propertyId: number, value: string, sort?: number}>` | The properties to set. |
|
||||||
|
| `[tx]` | `object` | Optional transaction. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
11
API.md
11
API.md
@@ -89,13 +89,14 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
|
- `delete(googleAuthId: number, sku: string, optionsOrTx?: object|*, maybeTx?: object): Promise<void>`
|
||||||
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
|
- `deleteImage(googleAuthId: number, sku: string, fullHash: string, tx?: object): Promise<void>`
|
||||||
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>`
|
- `getBySku(googleAuthId: number, sku: string, tx?: object): Promise<{article: {id: number, sku: string, name: string, slug: string, unitamount: number|null, weight: number|null, track_stock: boolean, unit: {si: string|null, basesi: string|null, basesifactor: number|null, symbol: string|null, name: string|null}, brand: {id: number|null, name: string, fullHash: string|null, thumbW: number|null, thumbH: number|null, thumbHash: string|null}|null, categoryIds: number[], photos: Array<{sort: number, fullHash: string, fullW: number, fullH: number, fullSize: number, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[], prices: Array<{minmultiplier: number, net: number, taxRate: number, customergroupId: number}>, suppliers: Array<{id: number, articleId: number, entityId: number, supplierName: string|null, sku: string|null, pu: number, stock: number|null, sort: number, priceId: number|null, price: {net: number, taxId: number, taxRate?: number}|null}>, optionsCount: number, articleListCount: number, childCount: number, fatherArticleId: number|null, fatherSku: string|null}}>`
|
||||||
- `getHistoryBySku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
- `getHistoryBySku(googleAuthId: number, sku: string, opts?: {limit?: number}, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
|
||||||
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
|
- `importRows(input: {googleAuthId: number, rows: Array<object>}, tx?: object): Promise<{imported: Array<object>, skipped: Array<object>, errors: Array<object>}>`
|
||||||
- `listChildren(googleAuthId: number, fatherSku: string, tx?: object): Promise<Array<{id: number, sku: string, fathersort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
|
- `listChildren(googleAuthId: number, fatherSku: string, tx?: object): Promise<Array<{id: number, sku: string, fathersort: number, trackStock: boolean, unitamount: number, weight: number, name: string|null, slug: string|null, attributeValueId: number|null, attributeValue: string|null}>>`
|
||||||
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
|
- `listExport(googleAuthId: number, categoryId?: number|null, tx?: object): Promise<Array<object>>`
|
||||||
- `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>`
|
- `listSearchIndex(googleAuthId: number, tx?: object): Promise<Array<{id: number, sku: string, name: string, eans: Array<string>, categoryIds: Array<number>}>>`
|
||||||
- `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>`
|
- `orderImages(googleAuthId: number, sku: string, input: {fullHashes: Array<string>}, tx?: object): Promise<{fullHashes: Array<string>}>`
|
||||||
- `removeChild(googleAuthId: number, fatherSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
- `removeChild(googleAuthId: number, fatherSku: string, childSku: string, options?: {deleteArticle?: boolean, cascade?: boolean}, tx?: object): Promise<{success: boolean, fatherSku: string, childSku: string, deleted: boolean}>`
|
||||||
|
- `resolveUniqueSlug(runner: object, params: {companyId: number, languageId: number, name: string, excludeArticleId?: number}): Promise<string>`
|
||||||
- `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise<object>`
|
- `setBrandIdBySku(googleAuthId: number, sku: string, brandId: number|null, tx?: object): Promise<object>`
|
||||||
- `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array<number>, tx?: object): Promise<object>`
|
- `setCategoryIdsBySku(googleAuthId: number, sku: string, categoryIds: Array<number>, tx?: object): Promise<object>`
|
||||||
- `setEansBySku(googleAuthId: number, sku: string, eans: Array<string>, tx?: object): Promise<object>`
|
- `setEansBySku(googleAuthId: number, sku: string, eans: Array<string>, tx?: object): Promise<object>`
|
||||||
@@ -212,6 +213,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
- `getProductList(input: {posterminalId: number, cursor?: number, limit?: number}, tx?: object): Promise<Array<object>>`
|
||||||
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string, pairingpin: string}>>`
|
- `list(googleAuthId: number, tx?: object): Promise<Array<{id: number, name: string, token: string, pairingpin: string}>>`
|
||||||
- `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
- `pair(id: number, input: {token: string, clientserial?: string, pairedAt?: string}, tx?: object): Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
||||||
|
- `setMaxKnownTombstoneId(id: number, maxknowntombstoneid: number|null, tx?: object): Promise<{id: number, maxknowntombstoneid: number|null}>`
|
||||||
- `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOff(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOffAll(googleAuthId: number, posterminalId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
- `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
- `setOn(googleAuthId: number, posterminalId: number, categoryId: number, tx?: object): Promise<{id: number, categoryIds: Array<number>}>`
|
||||||
@@ -272,6 +274,13 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
|
|||||||
- [articlelist](API.articlelist.md)
|
- [articlelist](API.articlelist.md)
|
||||||
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
- `getItemsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||||
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
- `setItemsByArticleSku(googleAuthId: number, sku: string, items?: Array<{articleId?: number, sku?: string, count?: number, sort?: number}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number}>>`
|
||||||
|
- [categoryattributevalue](API.categoryattributevalue.md)
|
||||||
|
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
- `setByCategoryId(googleAuthId: number, categoryId: number, values: Array<number|{attributeValueId: number, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>`
|
||||||
|
- [categoryproperty](API.categoryproperty.md)
|
||||||
|
- `delete(googleAuthId: number, categoryId: number, propertyId: number, tx?: object): Promise<void>`
|
||||||
|
- `listByCategoryId(googleAuthId: number, categoryId: number, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
|
- `setByCategoryId(googleAuthId: number, categoryId: number, properties: Array<{propertyId: number, value: string, sort?: number}>, tx?: object): Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>`
|
||||||
- [optionslist](API.optionslist.md)
|
- [optionslist](API.optionslist.md)
|
||||||
- `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
- `getOptionsByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||||
- `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
- `setOptionsByArticleSku(googleAuthId: number, sku: string, options?: Array<{articleId?: number, sku?: string, count?: number, sort?: number, mandated?: boolean}>, tx?: object): Promise<Array<{id: number, articleId: number, sku: string, name: string|null, count: number, sort: number, mandated: boolean}>>`
|
||||||
|
|||||||
@@ -280,6 +280,24 @@ Completes POS device pairing for a terminal.
|
|||||||
|
|
||||||
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
`Promise<{id: number, token: string, clientserial: string|null, pairedAt: string|null}>`
|
||||||
|
|
||||||
|
## `posterminal.setMaxKnownTombstoneId`
|
||||||
|
|
||||||
|
*Source: `src/store/posterminal/setMaxKnownTombstoneId.js`*
|
||||||
|
|
||||||
|
Updates the max known tombstone ID for a POS terminal.
|
||||||
|
|
||||||
|
**Parameters**
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `number` | The posterminal id. |
|
||||||
|
| `maxknowntombstoneid` | `number|null` | The highest synced tombstone ID. |
|
||||||
|
| `[tx]` | `object` | Optional transaction to run within. |
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
`Promise<{id: number, maxknowntombstoneid: number|null}>`
|
||||||
|
|
||||||
## `posterminal.setOff`
|
## `posterminal.setOff`
|
||||||
|
|
||||||
*Source: `src/store/posterminal/setOff.js`*
|
*Source: `src/store/posterminal/setOff.js`*
|
||||||
|
|||||||
@@ -170,6 +170,9 @@ function addInferredEdges(tables, edges) {
|
|||||||
const match = column.name.match(/^(.+)_id$/i);
|
const match = column.name.match(/^(.+)_id$/i);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
|
|
||||||
|
// Tombstone tables store references to deleted entities, not active foreign keys.
|
||||||
|
if (tableName.includes('tombstone')) continue;
|
||||||
|
|
||||||
const candidate = match[1];
|
const candidate = match[1];
|
||||||
if (!tables.has(candidate) || candidate === tableName) continue;
|
if (!tables.has(candidate) || candidate === tableName) continue;
|
||||||
if (explicitlyConstrained.has(`${tableName}.${column.name}`)) continue;
|
if (explicitlyConstrained.has(`${tableName}.${column.name}`)) continue;
|
||||||
@@ -351,6 +354,7 @@ const CLUSTERS = [
|
|||||||
'article',
|
'article',
|
||||||
'articleean',
|
'articleean',
|
||||||
'articlehistory',
|
'articlehistory',
|
||||||
|
'articletombstone',
|
||||||
'articlelist',
|
'articlelist',
|
||||||
'articlelistarticle',
|
'articlelistarticle',
|
||||||
'optionslist',
|
'optionslist',
|
||||||
@@ -374,7 +378,20 @@ const CLUSTERS = [
|
|||||||
tables: ['tax', 'price'],
|
tables: ['tax', 'price'],
|
||||||
},
|
},
|
||||||
{ id: 'units', label: 'Units', tables: ['unit'] },
|
{ id: 'units', label: 'Units', tables: ['unit'] },
|
||||||
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer'] },
|
{ id: 'pos', label: 'POS', tables: ['posterminal', 'posterminalcategory', 'printer', 'labeltemplate'] },
|
||||||
|
{
|
||||||
|
id: 'attributes',
|
||||||
|
label: 'Properties & Attributes',
|
||||||
|
tables: [
|
||||||
|
'propertyname',
|
||||||
|
'articleproperty',
|
||||||
|
'categoryproperty',
|
||||||
|
'attributename',
|
||||||
|
'attributevalue',
|
||||||
|
'articleattributevalue',
|
||||||
|
'categoryattributevalue',
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const COLORS = {
|
const COLORS = {
|
||||||
|
|||||||
187
schema-graph.dot
187
schema-graph.dot
File diff suppressed because one or more lines are too long
4116
schema-graph.svg
4116
schema-graph.svg
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 272 KiB |
175
scripts/test-categoryproperty-and-attrvalue.js
Normal file
175
scripts/test-categoryproperty-and-attrvalue.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing CategoryProperty & CategoryAttributeValue ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-cat-prop-attr-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create a subcategory under root
|
||||||
|
const category = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Test Category',
|
||||||
|
});
|
||||||
|
assert.ok(category.id > 0);
|
||||||
|
|
||||||
|
// 1. Test CategoryProperty
|
||||||
|
console.log('1. Testing categoryproperty CRUD...');
|
||||||
|
const prop1 = await store.propertyname.create(googleAuthId, {
|
||||||
|
name: 'Material Care',
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
const prop2 = await store.propertyname.create(googleAuthId, {
|
||||||
|
name: 'Season',
|
||||||
|
exclusive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set properties
|
||||||
|
const setProps = await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ propertyId: prop1.id, value: 'Dry Clean Only', sort: 10 },
|
||||||
|
{ propertyId: prop2.id, value: 'Summer 2026', sort: 20 },
|
||||||
|
]);
|
||||||
|
assert.equal(setProps.length, 2);
|
||||||
|
|
||||||
|
// List properties
|
||||||
|
const listedProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedProps.length, 2);
|
||||||
|
assert.equal(listedProps[0].name, 'Material Care');
|
||||||
|
assert.equal(listedProps[0].value, 'Dry Clean Only');
|
||||||
|
assert.equal(listedProps[0].exclusive, true);
|
||||||
|
assert.equal(listedProps[1].name, 'Season');
|
||||||
|
assert.equal(listedProps[1].value, 'Summer 2026');
|
||||||
|
assert.equal(listedProps[1].exclusive, false);
|
||||||
|
|
||||||
|
// Test exclusive violation on categoryproperty
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.categoryproperty.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ propertyId: prop1.id, value: 'Val 1' },
|
||||||
|
{ propertyId: prop1.id, value: 'Val 2' },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
/exclusive/i,
|
||||||
|
'Should reject multiple values for exclusive property'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete single property
|
||||||
|
await store.categoryproperty.delete(googleAuthId, category.id, prop1.id);
|
||||||
|
const afterDeleteProps = await store.categoryproperty.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(afterDeleteProps.length, 1);
|
||||||
|
assert.equal(afterDeleteProps[0].propertyId, prop2.id);
|
||||||
|
|
||||||
|
console.log('CategoryProperty tests passed!');
|
||||||
|
|
||||||
|
// 2. Test CategoryAttributeValue
|
||||||
|
console.log('2. Testing categoryattributevalue CRUD...');
|
||||||
|
const attrExclusive = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Department',
|
||||||
|
exclusive: true,
|
||||||
|
});
|
||||||
|
const valDeptMen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
|
||||||
|
value: 'Men',
|
||||||
|
});
|
||||||
|
const valDeptWomen = await store.attributevalue.create(googleAuthId, attrExclusive.id, {
|
||||||
|
value: 'Women',
|
||||||
|
});
|
||||||
|
|
||||||
|
const attrMulti = await store.attributename.create(googleAuthId, {
|
||||||
|
name: 'Tags',
|
||||||
|
exclusive: false,
|
||||||
|
});
|
||||||
|
const valTagSale = await store.attributevalue.create(googleAuthId, attrMulti.id, {
|
||||||
|
value: 'Sale',
|
||||||
|
});
|
||||||
|
const valTagNew = await store.attributevalue.create(googleAuthId, attrMulti.id, {
|
||||||
|
value: 'New',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk set with numeric array
|
||||||
|
const setAttrValues = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
valDeptMen.id,
|
||||||
|
valTagSale.id,
|
||||||
|
valTagNew.id,
|
||||||
|
]);
|
||||||
|
assert.equal(setAttrValues.length, 3);
|
||||||
|
|
||||||
|
// List attribute values
|
||||||
|
const listedAttrValues = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedAttrValues.length, 3);
|
||||||
|
const deptMatch = listedAttrValues.find((v) => v.attributeName === 'Department');
|
||||||
|
assert.ok(deptMatch);
|
||||||
|
assert.equal(deptMatch.value, 'Men');
|
||||||
|
assert.equal(deptMatch.exclusive, true);
|
||||||
|
|
||||||
|
// Test exclusive violation
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
valDeptMen.id,
|
||||||
|
valDeptWomen.id,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
/exclusive/i,
|
||||||
|
'Should reject multiple values for exclusive attribute'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test flexible object shapes in setByCategoryId
|
||||||
|
const setWithObjs = await store.categoryattributevalue.setByCategoryId(googleAuthId, category.id, [
|
||||||
|
{ attributeValueId: valDeptWomen.id, sort: 5 },
|
||||||
|
{ id: valTagSale.id, sort: 15 },
|
||||||
|
]);
|
||||||
|
assert.equal(setWithObjs.length, 2);
|
||||||
|
const listedAfterObjSet = await store.categoryattributevalue.listByCategoryId(googleAuthId, category.id);
|
||||||
|
assert.equal(listedAfterObjSet.length, 2);
|
||||||
|
assert.equal(listedAfterObjSet[0].value, 'Women');
|
||||||
|
assert.equal(listedAfterObjSet[0].sort, 5);
|
||||||
|
|
||||||
|
// 3. Test cascade deletion on category delete
|
||||||
|
console.log('3. Testing cascade deletion when category is deleted...');
|
||||||
|
await store.category.delete({ googleAuthId, id: category.id });
|
||||||
|
|
||||||
|
// Directly query database to ensure categoryproperty and categoryattributevalue rows were deleted by FK cascade
|
||||||
|
const remainingProps = await provider.query('SELECT * FROM categoryproperty WHERE category_id = @id', { id: category.id });
|
||||||
|
assert.equal(remainingProps.rows.length, 0);
|
||||||
|
|
||||||
|
const remainingAttrValues = await provider.query('SELECT * FROM categoryattributevalue WHERE category_id = @id', { id: category.id });
|
||||||
|
assert.equal(remainingAttrValues.rows.length, 0);
|
||||||
|
|
||||||
|
console.log('All CategoryProperty and CategoryAttributeValue tests passed successfully!');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
121
scripts/test-history-and-attrvalue.js
Normal file
121
scripts/test-history-and-attrvalue.js
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing Father History & AttributeValue Shapes ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-hist-attr-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Test Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
console.log('1. Testing setFatherBySku history logging...');
|
||||||
|
const father = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Father Shirt',
|
||||||
|
});
|
||||||
|
const child = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Child Shirt Red',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check history before linking
|
||||||
|
let fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
|
||||||
|
assert.equal(fatherHist.length, 1);
|
||||||
|
assert.match(fatherHist[0].text, /angelegt/);
|
||||||
|
|
||||||
|
// Link child to father
|
||||||
|
await store.article.setFatherBySku(googleAuthId, child.sku, father.sku);
|
||||||
|
|
||||||
|
// Father history must now contain the conversion entry and the child entry!
|
||||||
|
fatherHist = await store.article.getHistoryBySku(googleAuthId, father.sku);
|
||||||
|
const hasConversion = fatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
|
const hasVariantAdded = fatherHist.some((h) => h.text.includes('hinzugefügt'));
|
||||||
|
assert.ok(hasConversion, `Father history missing conversion row: ${JSON.stringify(fatherHist)}`);
|
||||||
|
assert.ok(hasVariantAdded, `Father history missing variant added row: ${JSON.stringify(fatherHist)}`);
|
||||||
|
|
||||||
|
// Child history must contain the assignment entry
|
||||||
|
const childHist = await store.article.getHistoryBySku(googleAuthId, child.sku);
|
||||||
|
const hasAssigned = childHist.some((h) => h.text.includes('als Variante von'));
|
||||||
|
assert.ok(hasAssigned, `Child history missing variant assigned row: ${JSON.stringify(childHist)}`);
|
||||||
|
|
||||||
|
console.log('2. Testing importRows Pass 2 history logging...');
|
||||||
|
const importRes = await store.article.importRows({
|
||||||
|
googleAuthId,
|
||||||
|
rows: [
|
||||||
|
{ sku: 'IMP-FATHER', name: 'Imported Father', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-CHILD', name: 'Imported Child', parentId: company.rootCategoryId, fatherSku: 'IMP-FATHER' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(importRes.errors.length, 0);
|
||||||
|
|
||||||
|
const impFatherHist = await store.article.getHistoryBySku(googleAuthId, 'IMP-FATHER');
|
||||||
|
const impHasConversion = impFatherHist.some((h) => h.text.includes('Artikel zu Vaterartikel konvertiert'));
|
||||||
|
assert.ok(impHasConversion, `Imported father missing conversion row: ${JSON.stringify(impFatherHist)}`);
|
||||||
|
|
||||||
|
console.log('3. Testing flexible attributevalue request shapes in setByArticleSku...');
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
const val1 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Rot' });
|
||||||
|
const val2 = await store.attributevalue.create(googleAuthId, attrName.id, { value: 'Blau' });
|
||||||
|
|
||||||
|
// Test A: Array of IDs [val1.id, val2.id]
|
||||||
|
let res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [val1.id]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
// Test B: Array of objects with .id [{ id: val1.id }]
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ id: val2.id }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val2.id);
|
||||||
|
|
||||||
|
// Test C: Array of objects with .attributeValueId
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeValueId: val1.id }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
// Test D: Array of objects with attributeId and value
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeId: attrName.id, value: 'Blau' }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val2.id);
|
||||||
|
|
||||||
|
// Test E: Array of objects with attributeName and value
|
||||||
|
res = await store.articleattributevalue.setByArticleSku(googleAuthId, child.sku, [{ attributeName: 'Farbe', value: 'Rot' }]);
|
||||||
|
assert.equal(res.length, 1);
|
||||||
|
assert.equal(res[0].attributeValueId, val1.id);
|
||||||
|
|
||||||
|
console.log('✅ ALL TESTS PASSED SUCCESSFULLY');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -80,20 +80,29 @@ async function run() {
|
|||||||
assert.equal(wh1BundleStock, 5, 'Bundle stock in Hauptlager should be 5 (min(10/1, 10/2))');
|
assert.equal(wh1BundleStock, 5, 'Bundle stock in Hauptlager should be 5 (min(10/1, 10/2))');
|
||||||
assert.equal(wh2BundleStock, 0, 'Bundle stock in Filiale should be 0');
|
assert.equal(wh2BundleStock, 0, 'Bundle stock in Filiale should be 0');
|
||||||
|
|
||||||
// Book in 3x Stückliste bundle into Filiale
|
// Verify store.stock.adjust rejects adjustment on Stückliste
|
||||||
const adjustResult = await store.stock.adjust(googleAuthId, {
|
await assert.rejects(
|
||||||
|
() =>
|
||||||
|
store.stock.adjust(googleAuthId, {
|
||||||
sku: bundle.sku,
|
sku: bundle.sku,
|
||||||
warehouseId: wh2.id,
|
warehouseId: wh2.id,
|
||||||
amount: 3,
|
amount: 3,
|
||||||
text: 'Production bundle book-in',
|
text: 'Attempt to adjust Stückliste directly',
|
||||||
});
|
}),
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /A Stückliste cannot have manual stock adjustments/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'Should reject stock adjust on Stückliste bundle'
|
||||||
|
);
|
||||||
|
|
||||||
// Verify adjust result lines
|
// Book components directly into Filiale: 3x comp1, 6x comp2
|
||||||
assert.equal(adjustResult.lines.length, 2, 'Should expand into 2 component lines');
|
await store.stock.adjust(googleAuthId, {
|
||||||
const comp1Line = adjustResult.lines.find((l) => l.sku === comp1.sku);
|
lines: [
|
||||||
const comp2Line = adjustResult.lines.find((l) => l.sku === comp2.sku);
|
{ sku: comp1.sku, warehouseId: wh2.id, amount: 3, text: 'Direct component 1 book-in' },
|
||||||
assert.equal(comp1Line.amount, 3, 'Component 1 should get 3 * 1 = 3');
|
{ sku: comp2.sku, warehouseId: wh2.id, amount: 6, text: 'Direct component 2 book-in' },
|
||||||
assert.equal(comp2Line.amount, 6, 'Component 2 should get 3 * 2 = 6');
|
],
|
||||||
|
});
|
||||||
|
|
||||||
// Verify bundle article itself has NO entries in the ledger
|
// Verify bundle article itself has NO entries in the ledger
|
||||||
const bundleLedger = await store.ledger.listBySku(googleAuthId, bundle.sku);
|
const bundleLedger = await store.ledger.listBySku(googleAuthId, bundle.sku);
|
||||||
@@ -112,17 +121,17 @@ async function run() {
|
|||||||
const wh2AfterBookIn = afterBundleBookIn.article.warehouses.find((w) => w.id === wh2.id).stock;
|
const wh2AfterBookIn = afterBundleBookIn.article.warehouses.find((w) => w.id === wh2.id).stock;
|
||||||
assert.equal(wh2AfterBookIn, 3, 'Bundle stock in Filiale should be 3');
|
assert.equal(wh2AfterBookIn, 3, 'Bundle stock in Filiale should be 3');
|
||||||
|
|
||||||
// Test booking out 1x bundle from Filiale
|
// Test booking out components from Filiale: -1x comp1, -2x comp2 (equivalent to 1 bundle sale)
|
||||||
await store.stock.adjust(googleAuthId, {
|
await store.stock.adjust(googleAuthId, {
|
||||||
sku: bundle.sku,
|
lines: [
|
||||||
warehouseId: wh2.id,
|
{ sku: comp1.sku, warehouseId: wh2.id, amount: -1, text: 'Component 1 book-out' },
|
||||||
amount: -1,
|
{ sku: comp2.sku, warehouseId: wh2.id, amount: -2, text: 'Component 2 book-out' },
|
||||||
text: 'Sale bundle book-out',
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const afterRemoval = await store.article.getBySku(googleAuthId, bundle.sku);
|
const afterRemoval = await store.article.getBySku(googleAuthId, bundle.sku);
|
||||||
const wh2AfterRemoval = afterRemoval.article.warehouses.find((w) => w.id === wh2.id).stock;
|
const wh2AfterRemoval = afterRemoval.article.warehouses.find((w) => w.id === wh2.id).stock;
|
||||||
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after -1 removal');
|
assert.equal(wh2AfterRemoval, 2, 'Bundle stock in Filiale should be 2 after component removal');
|
||||||
|
|
||||||
// Verify components stock after removal in Filiale: comp1 = 2, comp2 = 4
|
// Verify components stock after removal in Filiale: comp1 = 2, comp2 = 4
|
||||||
const comp1After = await store.article.getBySku(googleAuthId, comp1.sku);
|
const comp1After = await store.article.getBySku(googleAuthId, comp1.sku);
|
||||||
|
|||||||
210
scripts/test-tombstone.js
Normal file
210
scripts/test-tombstone.js
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing articletombstone table and deletion flow ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-tombstone-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Verify table exists and has expected columns
|
||||||
|
console.log('1. Checking articletombstone schema...');
|
||||||
|
const tableInfo = provider.db.pragma('table_info("articletombstone")');
|
||||||
|
const colNames = tableInfo.map((c) => c.name);
|
||||||
|
console.log('articletombstone columns:', colNames);
|
||||||
|
assert.deepEqual(colNames, [
|
||||||
|
'id',
|
||||||
|
'company_id',
|
||||||
|
'article_id',
|
||||||
|
'sku',
|
||||||
|
'name',
|
||||||
|
'taxrate',
|
||||||
|
'net',
|
||||||
|
'deleted_at',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Setup company first
|
||||||
|
console.log('2. Setting up base data...');
|
||||||
|
const googleAuthId = await store.google_auth.add('tombstone-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Tombstone Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// 3. Test CHECK constraint (article_id IS NOT NULL OR sku IS NOT NULL)
|
||||||
|
console.log('3. Testing CHECK constraint...');
|
||||||
|
// Setting both article_id and sku to NULL should fail
|
||||||
|
assert.throws(() => {
|
||||||
|
provider.db.prepare(`
|
||||||
|
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
||||||
|
VALUES (?, NULL, NULL, 'Test')
|
||||||
|
`).run(company.id);
|
||||||
|
}, /CHECK constraint failed/);
|
||||||
|
|
||||||
|
// article_id present, sku null -> should succeed
|
||||||
|
const r1 = provider.db.prepare(`
|
||||||
|
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
||||||
|
VALUES (?, 999, NULL, 'Only ID')
|
||||||
|
`).run(company.id);
|
||||||
|
assert.ok(r1.lastInsertRowid);
|
||||||
|
|
||||||
|
// sku present, article_id null -> should succeed
|
||||||
|
const r2 = provider.db.prepare(`
|
||||||
|
INSERT INTO articletombstone (company_id, article_id, sku, name)
|
||||||
|
VALUES (?, NULL, 'SKU-ONLY', 'Only SKU')
|
||||||
|
`).run(company.id);
|
||||||
|
assert.ok(r2.lastInsertRowid);
|
||||||
|
|
||||||
|
// Clean up manual test entries
|
||||||
|
provider.db.prepare('DELETE FROM articletombstone').run();
|
||||||
|
|
||||||
|
// 4. Test article deletion flow
|
||||||
|
console.log('4. Creating test article...');
|
||||||
|
|
||||||
|
const article = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Coffee Mug',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set prices for the article
|
||||||
|
await store.article.setPricesBySku(googleAuthId, article.sku, [
|
||||||
|
{ price: 12.50, tax: 19, minmultiplier: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Verify article exists
|
||||||
|
const detailBefore = await store.article.getBySku(googleAuthId, article.sku);
|
||||||
|
assert.equal(detailBefore.article.name, 'Coffee Mug');
|
||||||
|
assert.equal(detailBefore.article.prices[0].net, 12.50);
|
||||||
|
assert.equal(detailBefore.article.prices[0].taxRate, 19);
|
||||||
|
|
||||||
|
// Delete the article
|
||||||
|
console.log('4. Deleting article and checking tombstone creation...');
|
||||||
|
await store.article.delete(googleAuthId, article.sku);
|
||||||
|
|
||||||
|
// Verify article is deleted
|
||||||
|
await assert.rejects(
|
||||||
|
() => store.article.getBySku(googleAuthId, article.sku),
|
||||||
|
/Article not found/
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify tombstone record
|
||||||
|
const tombstones = provider.db.prepare('SELECT * FROM articletombstone WHERE sku = ?').all(article.sku);
|
||||||
|
assert.equal(tombstones.length, 1);
|
||||||
|
const tomb = tombstones[0];
|
||||||
|
console.log('Created tombstone:', tomb);
|
||||||
|
assert.equal(tomb.company_id, company.id);
|
||||||
|
assert.equal(tomb.article_id, article.id);
|
||||||
|
assert.equal(tomb.sku, article.sku);
|
||||||
|
assert.equal(tomb.name, 'Coffee Mug');
|
||||||
|
assert.equal(tomb.net, 12.50);
|
||||||
|
assert.equal(tomb.taxrate, 19);
|
||||||
|
assert.ok(tomb.deleted_at);
|
||||||
|
|
||||||
|
// 5. Test cascading deletion with father and variants
|
||||||
|
console.log('5. Testing father with variant child deletion...');
|
||||||
|
const father = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Pants',
|
||||||
|
});
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
|
await store.article.toggleFather(googleAuthId, father.sku, {
|
||||||
|
isFather: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
const child = await store.article.addChild(googleAuthId, father.sku, { value: 'M' });
|
||||||
|
await store.article.setPricesBySku(googleAuthId, child.article.sku, [
|
||||||
|
{ price: 49.99, tax: 19, minmultiplier: 1 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete child via removeChild with deleteArticle
|
||||||
|
console.log('Deleting child via removeChild({ deleteArticle: true })...');
|
||||||
|
await store.article.removeChild(googleAuthId, father.sku, child.article.sku, { deleteArticle: true });
|
||||||
|
|
||||||
|
// Add another child and delete father with cascade: true
|
||||||
|
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'L' });
|
||||||
|
console.log('Deleting father with cascade: true...');
|
||||||
|
await store.article.delete(googleAuthId, father.sku, { cascade: true });
|
||||||
|
|
||||||
|
const allTombstones = provider.db.prepare('SELECT * FROM articletombstone ORDER BY id ASC').all();
|
||||||
|
assert.equal(allTombstones.length, 4);
|
||||||
|
|
||||||
|
// 1st tombstone: Coffee Mug
|
||||||
|
assert.equal(allTombstones[0].name, 'Coffee Mug');
|
||||||
|
assert.equal(allTombstones[0].sku, '1');
|
||||||
|
assert.equal(allTombstones[0].net, 12.5);
|
||||||
|
assert.equal(allTombstones[0].taxrate, 19);
|
||||||
|
|
||||||
|
// 2nd tombstone: Pants - M (deleted via removeChild)
|
||||||
|
assert.equal(allTombstones[1].name, 'Pants - M');
|
||||||
|
assert.equal(allTombstones[1].sku, child.article.sku);
|
||||||
|
assert.equal(allTombstones[1].net, 49.99);
|
||||||
|
assert.equal(allTombstones[1].taxrate, 19);
|
||||||
|
|
||||||
|
// 3rd tombstone: Pants - L (child deleted in cascade)
|
||||||
|
assert.equal(allTombstones[2].name, 'Pants - L');
|
||||||
|
assert.equal(allTombstones[2].sku, child2.article.sku);
|
||||||
|
|
||||||
|
// 4th tombstone: Pants (father deleted in cascade)
|
||||||
|
assert.equal(allTombstones[3].name, 'Pants');
|
||||||
|
assert.equal(allTombstones[3].sku, father.sku);
|
||||||
|
|
||||||
|
// 6. Test posterminal maxknowntombstoneid column and updates
|
||||||
|
console.log('6. Testing posterminal maxknowntombstoneid column...');
|
||||||
|
const posterminalCols = provider.db.pragma('table_info("posterminal")').map((c) => c.name);
|
||||||
|
assert.ok(posterminalCols.includes('maxknowntombstoneid'), 'posterminal should have maxknowntombstoneid column');
|
||||||
|
|
||||||
|
const terminal = await store.posterminal.upsert(googleAuthId, {
|
||||||
|
name: 'POS Terminal 1',
|
||||||
|
pairingpin: '1234',
|
||||||
|
});
|
||||||
|
assert.equal(terminal.maxknowntombstoneid, null);
|
||||||
|
|
||||||
|
// Pair terminal
|
||||||
|
await store.posterminal.pair(terminal.id, {
|
||||||
|
token: 'tok-123',
|
||||||
|
clientserial: 'SN-001',
|
||||||
|
pairedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const foundByToken = await store.posterminal.findByToken('tok-123');
|
||||||
|
assert.equal(foundByToken.id, terminal.id);
|
||||||
|
assert.equal(foundByToken.maxknowntombstoneid, null);
|
||||||
|
|
||||||
|
// Update maxknowntombstoneid
|
||||||
|
await store.posterminal.setMaxKnownTombstoneId(terminal.id, 4);
|
||||||
|
|
||||||
|
const foundAfterUpdate = await store.posterminal.findByToken('tok-123');
|
||||||
|
assert.equal(foundAfterUpdate.maxknowntombstoneid, 4);
|
||||||
|
|
||||||
|
console.log('✅ ALL TOMBSTONE TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
147
scripts/test-unique-slugs.js
Normal file
147
scripts/test-unique-slugs.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing Unique Slugs Disambiguation ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-slugs-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('slug-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Slug Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
console.log('1. Testing createUnderParent duplicate slugs...');
|
||||||
|
const art1 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Father Shirt',
|
||||||
|
});
|
||||||
|
const art1Res = await store.article.getBySku(googleAuthId, art1.sku);
|
||||||
|
assert.equal(art1Res.article.slug, 'root-father-shirt', `Expected slug 'root-father-shirt', got '${art1Res.article.slug}'`);
|
||||||
|
|
||||||
|
const art2 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Father Shirt',
|
||||||
|
});
|
||||||
|
const art2Res = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(art2Res.article.slug, 'root-father-shirt-2', `Expected slug 'root-father-shirt-2', got '${art2Res.article.slug}'`);
|
||||||
|
|
||||||
|
const art3 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Root Father Shirt',
|
||||||
|
});
|
||||||
|
const art3Res = await store.article.getBySku(googleAuthId, art3.sku);
|
||||||
|
assert.equal(art3Res.article.slug, 'root-father-shirt-3', `Expected slug 'root-father-shirt-3', got '${art3Res.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('2. Testing updateBySku self-collision and renaming...');
|
||||||
|
// Update art2 without changing name
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { unitamount: 2 });
|
||||||
|
let updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected unchanged slug, got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Update art2 with same name
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Root Father Shirt' });
|
||||||
|
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'root-father-shirt-2', `Expected slug to remain 'root-father-shirt-2', got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Rename art2 to something else
|
||||||
|
await store.article.updateBySku(googleAuthId, art2.sku, { name: 'Unique Shirt' });
|
||||||
|
updated2 = await store.article.getBySku(googleAuthId, art2.sku);
|
||||||
|
assert.equal(updated2.article.slug, 'unique-shirt', `Expected slug 'unique-shirt', got '${updated2.article.slug}'`);
|
||||||
|
|
||||||
|
// Rename art3 to 'Root Father Shirt' again: should take root-father-shirt-2 now that it is free
|
||||||
|
await store.article.updateBySku(googleAuthId, art3.sku, { name: 'Root Father Shirt' });
|
||||||
|
const updated3 = await store.article.getBySku(googleAuthId, art3.sku);
|
||||||
|
assert.equal(updated3.article.slug, 'root-father-shirt-2', `Expected slug to fill gap 'root-father-shirt-2', got '${updated3.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('3. Testing addChild duplicate slugs...');
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Größe' });
|
||||||
|
await store.article.toggleFather(googleAuthId, art1.sku, {
|
||||||
|
isFather: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
const child1 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
|
value: 'M',
|
||||||
|
});
|
||||||
|
assert.equal(child1.article.slug, 'root-father-shirt-m', `Expected child slug 'root-father-shirt-m', got '${child1.article.slug}'`);
|
||||||
|
|
||||||
|
// Add another child with name that yields same slug
|
||||||
|
const child2 = await store.article.addChild(googleAuthId, art1.sku, {
|
||||||
|
value: 'L',
|
||||||
|
name: 'Root Father Shirt - M',
|
||||||
|
});
|
||||||
|
assert.equal(child2.article.slug, 'root-father-shirt-m-2', `Expected child slug 'root-father-shirt-m-2', got '${child2.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('4. Testing importRows duplicate slugs in same batch...');
|
||||||
|
const importRes = await store.article.importRows({
|
||||||
|
googleAuthId,
|
||||||
|
rows: [
|
||||||
|
{ sku: 'IMP-1', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-2', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
{ sku: 'IMP-3', name: 'Batch Shirt', parentId: company.rootCategoryId },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
assert.equal(importRes.errors.length, 0, `Import had errors: ${JSON.stringify(importRes.errors)}`);
|
||||||
|
assert.equal(importRes.imported.length, 3);
|
||||||
|
|
||||||
|
const imp1 = await store.article.getBySku(googleAuthId, 'IMP-1');
|
||||||
|
const imp2 = await store.article.getBySku(googleAuthId, 'IMP-2');
|
||||||
|
const imp3 = await store.article.getBySku(googleAuthId, 'IMP-3');
|
||||||
|
|
||||||
|
assert.equal(imp1.article.slug, 'batch-shirt');
|
||||||
|
assert.equal(imp2.article.slug, 'batch-shirt-2');
|
||||||
|
assert.equal(imp3.article.slug, 'batch-shirt-3');
|
||||||
|
|
||||||
|
console.log('5. Testing company isolation...');
|
||||||
|
const company2 = await store.company.create({
|
||||||
|
name: 'Other Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company2.id);
|
||||||
|
|
||||||
|
const artCompany2 = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company2.rootCategoryId,
|
||||||
|
name: 'Root Father Shirt',
|
||||||
|
});
|
||||||
|
const artCompany2Detail = await store.article.getBySku(googleAuthId, artCompany2.sku);
|
||||||
|
assert.equal(artCompany2Detail.article.slug, 'root-father-shirt', `Expected 'root-father-shirt' in company 2, got '${artCompany2Detail.article.slug}'`);
|
||||||
|
|
||||||
|
console.log('✅ ALL SLUG TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
130
scripts/test-variant-flow.js
Normal file
130
scripts/test-variant-flow.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
console.log('--- Testing Variant Flow: Next-Free SKU, Fathersort Reclaim, and Duplicate Protection ---');
|
||||||
|
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-variants-'));
|
||||||
|
const dbFile = join(tempDir, 'test.db');
|
||||||
|
const storageDir = join(tempDir, 'storage');
|
||||||
|
|
||||||
|
const provider = new BetterSqlite3Provider({
|
||||||
|
filename: dbFile,
|
||||||
|
storageDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
await provider.initialize();
|
||||||
|
const store = await new LibDashStore(provider);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const googleAuthId = await store.google_auth.add('variant-tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Variant Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create father article
|
||||||
|
const father = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'T-Shirt',
|
||||||
|
});
|
||||||
|
const attrName = await store.attributename.create(googleAuthId, { name: 'Farbe' });
|
||||||
|
await store.article.toggleFather(googleAuthId, father.sku, {
|
||||||
|
isFather: true,
|
||||||
|
attributeNameId: attrName.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('1. Add initial children (Rot, Blau, Grün)...');
|
||||||
|
const child1 = await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
|
||||||
|
const child2 = await store.article.addChild(googleAuthId, father.sku, { value: 'Blau' });
|
||||||
|
const child3 = await store.article.addChild(googleAuthId, father.sku, { value: 'Grün' });
|
||||||
|
|
||||||
|
assert.equal(child1.article.sku, `${father.sku}-1`);
|
||||||
|
assert.equal(child2.article.sku, `${father.sku}-2`);
|
||||||
|
assert.equal(child3.article.sku, `${father.sku}-3`);
|
||||||
|
|
||||||
|
let list = await store.article.listChildren(googleAuthId, father.sku);
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
|
||||||
|
|
||||||
|
console.log('2. Testing Duplicate Variant Protection (Defect 3)...');
|
||||||
|
await assert.rejects(
|
||||||
|
async () => {
|
||||||
|
await store.article.addChild(googleAuthId, father.sku, { value: 'Rot' });
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
assert.match(err.message, /Variant with this attribute value already exists/);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
'Expected duplicate variant attribute value to be rejected'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('3. Unlink middle child (child2 / SKU-2) -> leaves orphan holding SKU-2 (Bug A & B)...');
|
||||||
|
await store.article.removeChild(googleAuthId, father.sku, child2.article.sku, {
|
||||||
|
deleteArticle: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check that remaining children's fathersort was recompacted
|
||||||
|
list = await store.article.listChildren(googleAuthId, father.sku);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
assert.deepEqual(list.map((c) => c.sku), [child1.article.sku, child3.article.sku]);
|
||||||
|
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
|
||||||
|
|
||||||
|
// Check orphan still exists in DB
|
||||||
|
const orphan = await store.article.getBySku(googleAuthId, child2.article.sku);
|
||||||
|
assert.equal(orphan.article.fatherArticleId, null);
|
||||||
|
assert.equal(orphan.article.sku, `${father.sku}-2`);
|
||||||
|
|
||||||
|
console.log('4. Add new child (Gelb) -> SKU must NOT collide with orphan (Bug A: next free index)...');
|
||||||
|
const child4 = await store.article.addChild(googleAuthId, father.sku, { value: 'Gelb' });
|
||||||
|
// Since SKU-1, SKU-2 (orphan), SKU-3 exist, next free index is 4
|
||||||
|
assert.equal(child4.article.sku, `${father.sku}-4`);
|
||||||
|
|
||||||
|
list = await store.article.listChildren(googleAuthId, father.sku);
|
||||||
|
assert.equal(list.length, 3);
|
||||||
|
assert.deepEqual(list.map((c) => c.fathersort), [0, 1, 2]);
|
||||||
|
|
||||||
|
console.log('5. Testing gaps in fathersort (Evidence: father with child at fathersort: 1)...');
|
||||||
|
// Unlink child3 and child4 so only child1 remains
|
||||||
|
await store.article.removeChild(googleAuthId, father.sku, child3.article.sku, { deleteArticle: true });
|
||||||
|
await store.article.removeChild(googleAuthId, father.sku, child4.article.sku, { deleteArticle: true });
|
||||||
|
|
||||||
|
// Manually force child1's fathersort: 1 to simulate pre-existing gap state (like father 94)
|
||||||
|
await provider.query(
|
||||||
|
`UPDATE article SET fathersort = 1 WHERE sku = @sku`,
|
||||||
|
{ sku: child1.article.sku }
|
||||||
|
);
|
||||||
|
list = await store.article.listChildren(googleAuthId, father.sku);
|
||||||
|
assert.equal(list.length, 1);
|
||||||
|
assert.equal(list[0].fathersort, 1); // exactly like father 94
|
||||||
|
|
||||||
|
// Now adding a variant MUST NOT hit UNIQUE constraint failed (409)
|
||||||
|
console.log('Adding variant to father with child at fathersort: 1...');
|
||||||
|
const child5 = await store.article.addChild(googleAuthId, father.sku, { value: 'Schwarz' });
|
||||||
|
assert.ok(child5);
|
||||||
|
list = await store.article.listChildren(googleAuthId, father.sku);
|
||||||
|
assert.equal(list.length, 2);
|
||||||
|
// Fathersort should have picked 0 (next free non-negative integer)
|
||||||
|
assert.deepEqual(list.map((c) => c.fathersort), [0, 1]);
|
||||||
|
|
||||||
|
console.log('✅ ALL VARIANT FLOW TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
18
src/index.js
18
src/index.js
@@ -113,6 +113,7 @@ import posterminalGetDeletedEntityCount from './store/posterminal/getDeletedEnti
|
|||||||
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
|
import posterminalGetMaxOrderIdCount from './store/posterminal/getMaxOrderIdCount.js';
|
||||||
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
|
import posterminalGetCategoryList from './store/posterminal/getCategoryList.js';
|
||||||
import posterminalGetProductList from './store/posterminal/getProductList.js';
|
import posterminalGetProductList from './store/posterminal/getProductList.js';
|
||||||
|
import posterminalSetMaxKnownTombstoneId from './store/posterminal/setMaxKnownTombstoneId.js';
|
||||||
import langCreate from './store/lang/create.js';
|
import langCreate from './store/lang/create.js';
|
||||||
import langDelete from './store/lang/delete.js';
|
import langDelete from './store/lang/delete.js';
|
||||||
import langList from './store/lang/list.js';
|
import langList from './store/lang/list.js';
|
||||||
@@ -181,6 +182,11 @@ import attributevalueOrder from './store/attributevalue/order.js';
|
|||||||
import attributevalueDelete from './store/attributevalue/delete.js';
|
import attributevalueDelete from './store/attributevalue/delete.js';
|
||||||
import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js';
|
import articleattributevalueListByArticleSku from './store/articleattributevalue/listByArticleSku.js';
|
||||||
import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js';
|
import articleattributevalueSetByArticleSku from './store/articleattributevalue/setByArticleSku.js';
|
||||||
|
import categorypropertyListByCategoryId from './store/categoryproperty/listByCategoryId.js';
|
||||||
|
import categorypropertySetByCategoryId from './store/categoryproperty/setByCategoryId.js';
|
||||||
|
import categorypropertyDelete from './store/categoryproperty/delete.js';
|
||||||
|
import categoryattributevalueListByCategoryId from './store/categoryattributevalue/listByCategoryId.js';
|
||||||
|
import categoryattributevalueSetByCategoryId from './store/categoryattributevalue/setByCategoryId.js';
|
||||||
import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
|
import articlelistGetItems from './store/articlelist/getItemsByArticleSku.js';
|
||||||
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
|
import articlelistSetItems from './store/articlelist/setItemsByArticleSku.js';
|
||||||
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
|
import optionslistGetOptions from './store/optionslist/getOptionsByArticleSku.js';
|
||||||
@@ -388,6 +394,7 @@ export default class LibDashStore {
|
|||||||
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
getMaxOrderIdCount: posterminalGetMaxOrderIdCount(provider),
|
||||||
getCategoryList: posterminalGetCategoryList(provider),
|
getCategoryList: posterminalGetCategoryList(provider),
|
||||||
getProductList: posterminalGetProductList(provider),
|
getProductList: posterminalGetProductList(provider),
|
||||||
|
setMaxKnownTombstoneId: posterminalSetMaxKnownTombstoneId(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.google_auth = {
|
this.google_auth = {
|
||||||
@@ -490,6 +497,17 @@ export default class LibDashStore {
|
|||||||
setByArticleSku: articleattributevalueSetByArticleSku(provider),
|
setByArticleSku: articleattributevalueSetByArticleSku(provider),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.categoryproperty = {
|
||||||
|
listByCategoryId: categorypropertyListByCategoryId(provider),
|
||||||
|
setByCategoryId: categorypropertySetByCategoryId(provider),
|
||||||
|
delete: categorypropertyDelete(provider),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.categoryattributevalue = {
|
||||||
|
listByCategoryId: categoryattributevalueListByCategoryId(provider),
|
||||||
|
setByCategoryId: categoryattributevalueSetByCategoryId(provider),
|
||||||
|
};
|
||||||
|
|
||||||
this.articlelist = {
|
this.articlelist = {
|
||||||
getItemsByArticleSku: articlelistGetItems(provider),
|
getItemsByArticleSku: articlelistGetItems(provider),
|
||||||
setItemsByArticleSku: articlelistSetItems(provider),
|
setItemsByArticleSku: articlelistSetItems(provider),
|
||||||
|
|||||||
@@ -17,6 +17,29 @@ WHERE id = @articleId
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
||||||
|
export const CREATE_TOMBSTONE = `
|
||||||
|
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
|
||||||
|
SELECT
|
||||||
|
a.company_id,
|
||||||
|
a.id AS article_id,
|
||||||
|
a.sku,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
||||||
|
(SELECT TOP 1 at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC)
|
||||||
|
) AS name,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
||||||
|
(SELECT TOP 1 t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC)
|
||||||
|
) AS taxrate,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
||||||
|
(SELECT TOP 1 pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC)
|
||||||
|
) AS net
|
||||||
|
FROM article a
|
||||||
|
INNER JOIN company co ON co.id = a.company_id
|
||||||
|
WHERE a.id = @articleId
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_PICS_BY_ARTICLE = `
|
export const DELETE_PICS_BY_ARTICLE = `
|
||||||
DELETE p
|
DELETE p
|
||||||
FROM pic p
|
FROM pic p
|
||||||
|
|||||||
@@ -9,3 +9,10 @@ FROM articlehistory
|
|||||||
WHERE article_id = @articleId
|
WHERE article_id = @articleId
|
||||||
ORDER BY [date] DESC, id DESC
|
ORDER BY [date] DESC, id DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIST_BY_ARTICLE_ID_LIMITED = `
|
||||||
|
SELECT TOP (@limit) id, companyuser_id AS companyuserId, [date], [text]
|
||||||
|
FROM articlehistory
|
||||||
|
WHERE article_id = @articleId
|
||||||
|
ORDER BY [date] DESC, id DESC
|
||||||
|
`;
|
||||||
|
|||||||
@@ -8,3 +8,13 @@ UPDATE articletext
|
|||||||
SET name = @name, slug = @slug
|
SET name = @name, slug = @slug
|
||||||
WHERE article_id = @articleId AND language_id = @languageId
|
WHERE article_id = @articleId AND language_id = @languageId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LIST_SLUGS_BY_COMPANY_AND_LANGUAGE = `
|
||||||
|
SELECT at.slug, at.article_id AS articleId
|
||||||
|
FROM articletext at
|
||||||
|
INNER JOIN article a ON a.id = at.article_id
|
||||||
|
WHERE a.company_id = @companyId
|
||||||
|
AND at.language_id = @languageId
|
||||||
|
AND (at.slug = @baseSlug OR at.slug LIKE @likePattern)
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
41
src/providers/mssql/queries/categoryattributevalue.js
Normal file
41
src/providers/mssql/queries/categoryattributevalue.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export const LIST_BY_CATEGORY_ID = `
|
||||||
|
SELECT
|
||||||
|
cav.id,
|
||||||
|
cav.category_id AS categoryId,
|
||||||
|
an.id AS attributeId,
|
||||||
|
an.name AS attributeName,
|
||||||
|
an.exclusive,
|
||||||
|
av.id AS attributeValueId,
|
||||||
|
av.value,
|
||||||
|
cav.sort
|
||||||
|
FROM categoryattributevalue cav
|
||||||
|
INNER JOIN attributevalue av ON av.id = cav.attributevalue_id
|
||||||
|
INNER JOIN attributename an ON an.id = av.attribute_id
|
||||||
|
WHERE cav.category_id = @categoryId
|
||||||
|
ORDER BY an.sort ASC, an.id ASC, cav.sort ASC, cav.id ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DELETE_BY_CATEGORY_ID = `
|
||||||
|
DELETE FROM categoryattributevalue
|
||||||
|
WHERE category_id = @categoryId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INSERT_ROW = `
|
||||||
|
INSERT INTO categoryattributevalue (category_id, attributevalue_id, sort)
|
||||||
|
OUTPUT INSERTED.id, INSERTED.category_id AS categoryId, INSERTED.attributevalue_id AS attributeValueId, INSERTED.sort
|
||||||
|
VALUES (@categoryId, @attributeValueId, @sort)
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const GET_VALUE_DETAIL = `
|
||||||
|
SELECT
|
||||||
|
av.id AS attributeValueId,
|
||||||
|
av.value,
|
||||||
|
av.sort,
|
||||||
|
an.id AS attributeId,
|
||||||
|
an.name AS attributeName,
|
||||||
|
an.exclusive,
|
||||||
|
an.company_id AS companyId
|
||||||
|
FROM attributevalue av
|
||||||
|
INNER JOIN attributename an ON an.id = av.attribute_id
|
||||||
|
WHERE av.id = @attributeValueId
|
||||||
|
`;
|
||||||
30
src/providers/mssql/queries/categoryproperty.js
Normal file
30
src/providers/mssql/queries/categoryproperty.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export const LIST_BY_CATEGORY_ID = `
|
||||||
|
SELECT
|
||||||
|
cp.id,
|
||||||
|
cp.category_id AS categoryId,
|
||||||
|
cp.property_id AS propertyId,
|
||||||
|
pn.name AS name,
|
||||||
|
pn.exclusive,
|
||||||
|
cp.value,
|
||||||
|
cp.sort
|
||||||
|
FROM categoryproperty cp
|
||||||
|
INNER JOIN propertyname pn ON pn.id = cp.property_id
|
||||||
|
WHERE cp.category_id = @categoryId
|
||||||
|
ORDER BY cp.sort ASC, cp.id ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DELETE_BY_CATEGORY_ID = `
|
||||||
|
DELETE FROM categoryproperty
|
||||||
|
WHERE category_id = @categoryId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const DELETE_BY_CATEGORY_AND_PROPERTY_ID = `
|
||||||
|
DELETE FROM categoryproperty
|
||||||
|
WHERE category_id = @categoryId AND property_id = @propertyId
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INSERT_ROW = `
|
||||||
|
INSERT INTO categoryproperty (category_id, property_id, value, sort)
|
||||||
|
OUTPUT INSERTED.id, INSERTED.category_id AS categoryId, INSERTED.property_id AS propertyId, INSERTED.value, INSERTED.sort
|
||||||
|
VALUES (@categoryId, @propertyId, @value, @sort)
|
||||||
|
`;
|
||||||
@@ -108,3 +108,28 @@ VALUES (
|
|||||||
@taxrate
|
@taxrate
|
||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CHECK_ARTICLE_IS_FATHER = `
|
||||||
|
SELECT
|
||||||
|
a.id,
|
||||||
|
a.is_father AS isFather,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
||||||
|
(SELECT MIN(at2.name) FROM articletext at2 WHERE at2.article_id = a.id),
|
||||||
|
a.sku
|
||||||
|
) AS articleName,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT MIN(asup.pu) FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku),
|
||||||
|
(SELECT MIN(asup2.pu) FROM articlesupplier asup2 WHERE asup2.article_id = a.id),
|
||||||
|
a.unitamount,
|
||||||
|
1
|
||||||
|
) AS pu
|
||||||
|
FROM article a
|
||||||
|
INNER JOIN manifest m ON m.company_id = a.company_id
|
||||||
|
INNER JOIN company co ON co.id = m.company_id
|
||||||
|
WHERE m.id = @manifestId
|
||||||
|
AND (a.sku = @sku OR EXISTS (
|
||||||
|
SELECT 1 FROM articlesupplier asup WHERE asup.article_id = a.id AND asup.sku = @sku
|
||||||
|
))
|
||||||
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ SELECT
|
|||||||
token,
|
token,
|
||||||
pairingpin,
|
pairingpin,
|
||||||
paired_at AS pairedAt,
|
paired_at AS pairedAt,
|
||||||
clientserial AS clientserial
|
clientserial AS clientserial,
|
||||||
|
maxknowntombstoneid AS maxknowntombstoneid
|
||||||
FROM posterminal
|
FROM posterminal
|
||||||
WHERE company_id = @companyId
|
WHERE company_id = @companyId
|
||||||
ORDER BY id
|
ORDER BY id
|
||||||
@@ -19,7 +20,8 @@ SELECT
|
|||||||
token,
|
token,
|
||||||
pairingpin,
|
pairingpin,
|
||||||
paired_at AS pairedAt,
|
paired_at AS pairedAt,
|
||||||
clientserial AS clientserial
|
clientserial AS clientserial,
|
||||||
|
maxknowntombstoneid AS maxknowntombstoneid
|
||||||
FROM posterminal
|
FROM posterminal
|
||||||
WHERE id = @id
|
WHERE id = @id
|
||||||
`;
|
`;
|
||||||
@@ -32,7 +34,8 @@ SELECT
|
|||||||
token,
|
token,
|
||||||
pairingpin,
|
pairingpin,
|
||||||
paired_at AS pairedAt,
|
paired_at AS pairedAt,
|
||||||
clientserial AS clientserial
|
clientserial AS clientserial,
|
||||||
|
maxknowntombstoneid AS maxknowntombstoneid
|
||||||
FROM posterminal
|
FROM posterminal
|
||||||
WHERE pairingpin = @pairingpin
|
WHERE pairingpin = @pairingpin
|
||||||
`;
|
`;
|
||||||
@@ -44,17 +47,23 @@ WHERE id = @id
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const CREATE = `
|
export const CREATE = `
|
||||||
INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial)
|
INSERT INTO posterminal (company_id, name, token, pairingpin, paired_at, clientserial, maxknowntombstoneid)
|
||||||
OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial
|
OUTPUT INSERTED.id, INSERTED.name, INSERTED.token, INSERTED.pairingpin, INSERTED.paired_at AS pairedAt, INSERTED.clientserial AS clientserial, INSERTED.maxknowntombstoneid AS maxknowntombstoneid
|
||||||
VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial)
|
VALUES (@companyId, @name, @token, @pairingpin, @pairedAt, @clientserial, @maxknowntombstoneid)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const UPDATE = `
|
export const UPDATE = `
|
||||||
UPDATE posterminal
|
UPDATE posterminal
|
||||||
SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial
|
SET name = @name, token = @token, pairingpin = @pairingpin, paired_at = @pairedAt, clientserial = @clientserial, maxknowntombstoneid = @maxknowntombstoneid
|
||||||
WHERE id = @id AND company_id = @companyId
|
WHERE id = @id AND company_id = @companyId
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const SET_MAX_KNOWN_TOMBSTONE_ID = `
|
||||||
|
UPDATE posterminal
|
||||||
|
SET maxknowntombstoneid = @maxknowntombstoneid
|
||||||
|
WHERE id = @id
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_BY_COMPANY_AND_ID = `
|
export const DELETE_BY_COMPANY_AND_ID = `
|
||||||
DELETE FROM posterminal
|
DELETE FROM posterminal
|
||||||
WHERE id = @id AND company_id = @companyId
|
WHERE id = @id AND company_id = @companyId
|
||||||
@@ -122,7 +131,8 @@ SELECT
|
|||||||
token,
|
token,
|
||||||
pairingpin,
|
pairingpin,
|
||||||
paired_at AS pairedAt,
|
paired_at AS pairedAt,
|
||||||
clientserial AS clientserial
|
clientserial AS clientserial,
|
||||||
|
maxknowntombstoneid AS maxknowntombstoneid
|
||||||
FROM posterminal
|
FROM posterminal
|
||||||
WHERE token = @token
|
WHERE token = @token
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -352,11 +352,12 @@ BEGIN
|
|||||||
ALTER TABLE article ADD fatherattributename_id INT NULL;
|
ALTER TABLE article ADD fatherattributename_id INT NULL;
|
||||||
ALTER TABLE article ADD CONSTRAINT FK_article_fatherattributename FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
|
ALTER TABLE article ADD CONSTRAINT FK_article_fatherattributename FOREIGN KEY (fatherattributename_id) REFERENCES attributename(id) ON DELETE NO ACTION;
|
||||||
END
|
END
|
||||||
END
|
|
||||||
ELSE IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('article') AND name = 'track_stock')
|
||||||
BEGIN
|
BEGIN
|
||||||
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
|
ALTER TABLE article ADD track_stock BIT NOT NULL CONSTRAINT DF_article_track_stock DEFAULT 1;
|
||||||
END
|
END
|
||||||
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLEHISTORY = `
|
export const INIT_ARTICLEHISTORY = `
|
||||||
@@ -374,6 +375,27 @@ BEGIN
|
|||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_ARTICLETOMBSTONE = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articletombstone')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE articletombstone (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
company_id INT NOT NULL,
|
||||||
|
article_id INT NULL,
|
||||||
|
sku NVARCHAR(100) NULL,
|
||||||
|
name NVARCHAR(255) NULL,
|
||||||
|
taxrate DECIMAL(18,4) NULL,
|
||||||
|
net FLOAT NULL,
|
||||||
|
deleted_at DATETIME2 NOT NULL CONSTRAINT DF_articletombstone_deleted_at DEFAULT SYSUTCDATETIME(),
|
||||||
|
CONSTRAINT CK_articletombstone_article_or_sku CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
|
||||||
|
CONSTRAINT FK_articletombstone_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IX_articletombstone_company_deleted
|
||||||
|
ON articletombstone (company_id, deleted_at);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ARTICLEEAN = `
|
export const INIT_ARTICLEEAN = `
|
||||||
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'articleean')
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -791,10 +813,18 @@ BEGIN
|
|||||||
pairingpin NVARCHAR(50) NOT NULL,
|
pairingpin NVARCHAR(50) NOT NULL,
|
||||||
paired_at DATETIME2 NULL,
|
paired_at DATETIME2 NULL,
|
||||||
clientserial NVARCHAR(255) NULL,
|
clientserial NVARCHAR(255) NULL,
|
||||||
|
maxknowntombstoneid INT NULL,
|
||||||
CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
CONSTRAINT FK_posterminal_company FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX UQ_posterminal_token ON posterminal(token) WHERE token IS NOT NULL;
|
CREATE UNIQUE INDEX UQ_posterminal_token ON posterminal(token) WHERE token IS NOT NULL;
|
||||||
END
|
END
|
||||||
|
ELSE
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID('posterminal') AND name = 'maxknowntombstoneid')
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE posterminal ADD maxknowntombstoneid INT NULL;
|
||||||
|
END
|
||||||
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const INIT_POSTERMINALCATEGORY = `
|
export const INIT_POSTERMINALCATEGORY = `
|
||||||
@@ -980,6 +1010,36 @@ BEGIN
|
|||||||
END
|
END
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_CATEGORYPROPERTY = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryproperty')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE categoryproperty (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
category_id INT NOT NULL,
|
||||||
|
property_id INT NOT NULL,
|
||||||
|
value NVARCHAR(MAX) NOT NULL,
|
||||||
|
sort INT NOT NULL CONSTRAINT DF_categoryproperty_sort DEFAULT 0,
|
||||||
|
CONSTRAINT FK_categoryproperty_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_categoryproperty_property FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INIT_CATEGORYATTRIBUTEVALUE = `
|
||||||
|
IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'categoryattributevalue')
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE categoryattributevalue (
|
||||||
|
id INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
category_id INT NOT NULL,
|
||||||
|
attributevalue_id INT NOT NULL,
|
||||||
|
sort INT NOT NULL CONSTRAINT DF_categoryattributevalue_sort DEFAULT 0,
|
||||||
|
CONSTRAINT FK_categoryattributevalue_category FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_categoryattributevalue_attributevalue FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT UQ_categoryattributevalue_category_attributevalue UNIQUE (category_id, attributevalue_id)
|
||||||
|
);
|
||||||
|
END
|
||||||
|
`;
|
||||||
|
|
||||||
/** DDL scripts in dependency order for schema init. */
|
/** DDL scripts in dependency order for schema init. */
|
||||||
export const SCHEMA_SCRIPTS = [
|
export const SCHEMA_SCRIPTS = [
|
||||||
INIT_META,
|
INIT_META,
|
||||||
@@ -1017,6 +1077,8 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_ARTICLE_SUPPLIER,
|
INIT_ARTICLE_SUPPLIER,
|
||||||
INIT_ARTICLEPROPERTY,
|
INIT_ARTICLEPROPERTY,
|
||||||
INIT_ARTICLEATTRIBUTEVALUE,
|
INIT_ARTICLEATTRIBUTEVALUE,
|
||||||
|
INIT_CATEGORYPROPERTY,
|
||||||
|
INIT_CATEGORYATTRIBUTEVALUE,
|
||||||
INIT_MANIFEST,
|
INIT_MANIFEST,
|
||||||
INIT_MANIFESTARTICLE,
|
INIT_MANIFESTARTICLE,
|
||||||
INIT_WAREHOUSE,
|
INIT_WAREHOUSE,
|
||||||
@@ -1026,6 +1088,7 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
|
INIT_PEEK_NEXT_NUMBER_WITH_PREFIX_AND_PADDING,
|
||||||
INIT_COMPANYUSER,
|
INIT_COMPANYUSER,
|
||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
|
INIT_ARTICLETOMBSTONE,
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
INIT_SYNCPOS,
|
INIT_SYNCPOS,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
|
|||||||
@@ -426,6 +426,9 @@ export class BetterSqlite3Provider extends BetterSqlite3Executor {
|
|||||||
if (posterminalCols.length > 0 && !posterminalCols.includes('clientserial')) {
|
if (posterminalCols.length > 0 && !posterminalCols.includes('clientserial')) {
|
||||||
this.db.exec(`ALTER TABLE posterminal ADD COLUMN clientserial TEXT`);
|
this.db.exec(`ALTER TABLE posterminal ADD COLUMN clientserial TEXT`);
|
||||||
}
|
}
|
||||||
|
if (posterminalCols.length > 0 && !posterminalCols.includes('maxknowntombstoneid')) {
|
||||||
|
this.db.exec(`ALTER TABLE posterminal ADD COLUMN maxknowntombstoneid INTEGER`);
|
||||||
|
}
|
||||||
|
|
||||||
const propertynameCols = this.db.pragma('table_info("propertyname")').map((c) => c.name);
|
const propertynameCols = this.db.pragma('table_info("propertyname")').map((c) => c.name);
|
||||||
if (propertynameCols.length > 0 && !propertynameCols.includes('exclusive')) {
|
if (propertynameCols.length > 0 && !propertynameCols.includes('exclusive')) {
|
||||||
|
|||||||
@@ -7,6 +7,29 @@ export const {
|
|||||||
GET_CHILDREN_IDS, GET_LIST_IDS,
|
GET_CHILDREN_IDS, GET_LIST_IDS,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|
||||||
|
export const CREATE_TOMBSTONE = `
|
||||||
|
INSERT INTO articletombstone (company_id, article_id, sku, name, taxrate, net)
|
||||||
|
SELECT
|
||||||
|
a.company_id,
|
||||||
|
a.id AS article_id,
|
||||||
|
a.sku,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT at.name FROM articletext at WHERE at.article_id = a.id AND at.language_id = co.default_language_id),
|
||||||
|
(SELECT at.name FROM articletext at WHERE at.article_id = a.id ORDER BY at.id ASC LIMIT 1)
|
||||||
|
) AS name,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
||||||
|
(SELECT t.rate FROM price pr INNER JOIN tax t ON t.id = pr.tax_id WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
|
||||||
|
) AS taxrate,
|
||||||
|
COALESCE(
|
||||||
|
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id AND pr.customergroup_id = co.poscustomergroup_id AND pr.minmultiplier = 1),
|
||||||
|
(SELECT pr.net FROM price pr WHERE pr.article_id = a.id ORDER BY pr.id ASC LIMIT 1)
|
||||||
|
) AS net
|
||||||
|
FROM article a
|
||||||
|
INNER JOIN company co ON co.id = a.company_id
|
||||||
|
WHERE a.id = @articleId
|
||||||
|
`;
|
||||||
|
|
||||||
export const DELETE_PICS_BY_ARTICLE = `
|
export const DELETE_PICS_BY_ARTICLE = `
|
||||||
DELETE FROM pic
|
DELETE FROM pic
|
||||||
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)
|
WHERE id IN (SELECT pic_id FROM articlepic WHERE article_id = @articleId)
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ import { translateQueries } from './translate.js';
|
|||||||
|
|
||||||
const queries = translateQueries(source, 'articlehistory');
|
const queries = translateQueries(source, 'articlehistory');
|
||||||
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
|
export const { INSERT, LIST_BY_ARTICLE_ID } = queries;
|
||||||
|
|
||||||
|
export const LIST_BY_ARTICLE_ID_LIMITED = `
|
||||||
|
SELECT id, companyuser_id AS companyuserId, date, text
|
||||||
|
FROM articlehistory
|
||||||
|
WHERE article_id = @articleId
|
||||||
|
ORDER BY date DESC, id DESC
|
||||||
|
LIMIT @limit
|
||||||
|
`;
|
||||||
@@ -2,4 +2,8 @@ import * as source from '../../mssql/queries/articletext.js';
|
|||||||
import { translateQueries } from './translate.js';
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
const queries = translateQueries(source, 'articletext');
|
const queries = translateQueries(source, 'articletext');
|
||||||
export const { CREATE, UPDATE_BY_ARTICLE_AND_LANGUAGE } = queries;
|
export const {
|
||||||
|
CREATE,
|
||||||
|
UPDATE_BY_ARTICLE_AND_LANGUAGE,
|
||||||
|
LIST_SLUGS_BY_COMPANY_AND_LANGUAGE,
|
||||||
|
} = queries;
|
||||||
10
src/providers/sqlite3/queries/categoryattributevalue.js
Normal file
10
src/providers/sqlite3/queries/categoryattributevalue.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as source from '../../mssql/queries/categoryattributevalue.js';
|
||||||
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
|
const queries = translateQueries(source, 'categoryattributevalue');
|
||||||
|
export const {
|
||||||
|
LIST_BY_CATEGORY_ID,
|
||||||
|
DELETE_BY_CATEGORY_ID,
|
||||||
|
INSERT_ROW,
|
||||||
|
GET_VALUE_DETAIL,
|
||||||
|
} = queries;
|
||||||
10
src/providers/sqlite3/queries/categoryproperty.js
Normal file
10
src/providers/sqlite3/queries/categoryproperty.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import * as source from '../../mssql/queries/categoryproperty.js';
|
||||||
|
import { translateQueries } from './translate.js';
|
||||||
|
|
||||||
|
const queries = translateQueries(source, 'categoryproperty');
|
||||||
|
export const {
|
||||||
|
LIST_BY_CATEGORY_ID,
|
||||||
|
DELETE_BY_CATEGORY_ID,
|
||||||
|
DELETE_BY_CATEGORY_AND_PROPERTY_ID,
|
||||||
|
INSERT_ROW,
|
||||||
|
} = queries;
|
||||||
@@ -3,7 +3,7 @@ import { translateQueries } from './translate.js';
|
|||||||
|
|
||||||
const queries = translateQueries(source, 'manifestDoc');
|
const queries = translateQueries(source, 'manifestDoc');
|
||||||
export const {
|
export const {
|
||||||
CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST, DELETE_MANIFEST,
|
CHECK_ARTICLE_IS_FATHER, CREATE_LINE, CREATE_MANIFEST, DELETE_LINES_BY_MANIFEST,
|
||||||
GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS, ORDER_TYPE,
|
DELETE_MANIFEST, GET_MANIFEST_HEADER, GET_MANIFEST_LINES, LIST_MANIFESTS,
|
||||||
QUOTE_TYPE, UPDATE_MANIFEST,
|
ORDER_TYPE, QUOTE_TYPE, UPDATE_MANIFEST,
|
||||||
} = queries;
|
} = queries;
|
||||||
@@ -5,7 +5,7 @@ const queries = translateQueries(source, 'posterminal');
|
|||||||
export const {
|
export const {
|
||||||
CREATE, DELETE_BY_COMPANY_AND_ID, GET_BY_ID, LIST_BY_COMPANY,
|
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,
|
LIST_CATEGORY_IDS, POSTERMINAL_IDS_BY_COMPANY, SET_OFF, SET_OFF_ALL, UPDATE,
|
||||||
FIND_BY_PAIRINGPIN, PAIR,
|
FIND_BY_PAIRINGPIN, PAIR, SET_MAX_KNOWN_TOMBSTONE_ID,
|
||||||
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
FIND_BY_TOKEN, COUNT_PRODUCTS, COUNT_CATEGORIES, COUNT_CUSTOMERS,
|
||||||
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
COUNT_CUSTOMER_GROUPS, COUNT_COMPOSITE_PRODUCTS, MAX_ORDER_ID,
|
||||||
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
LIST_POS_PRODUCT_CATEGORIES, LIST_POS_CUSTOMER_GROUP_IDS, LIST_POS_PRODUCT_PRICES,
|
||||||
|
|||||||
@@ -495,6 +495,23 @@ CREATE TABLE IF NOT EXISTS articlehistory (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_ARTICLETOMBSTONE = `
|
||||||
|
CREATE TABLE IF NOT EXISTS articletombstone (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
company_id INTEGER NOT NULL,
|
||||||
|
article_id INTEGER,
|
||||||
|
sku TEXT COLLATE NOCASE,
|
||||||
|
name TEXT,
|
||||||
|
taxrate NUMERIC,
|
||||||
|
net REAL,
|
||||||
|
deleted_at TEXT NOT NULL DEFAULT (${UTC_NOW}),
|
||||||
|
CHECK (article_id IS NOT NULL OR sku IS NOT NULL),
|
||||||
|
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS IX_articletombstone_company_deleted
|
||||||
|
ON articletombstone (company_id, deleted_at);
|
||||||
|
`;
|
||||||
|
|
||||||
export const INIT_ENTITYHISTORY = `
|
export const INIT_ENTITYHISTORY = `
|
||||||
CREATE TABLE IF NOT EXISTS entityhistory (
|
CREATE TABLE IF NOT EXISTS entityhistory (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
@@ -539,6 +556,7 @@ CREATE TABLE IF NOT EXISTS posterminal (
|
|||||||
pairingpin TEXT NOT NULL,
|
pairingpin TEXT NOT NULL,
|
||||||
paired_at TEXT,
|
paired_at TEXT,
|
||||||
clientserial TEXT,
|
clientserial TEXT,
|
||||||
|
maxknowntombstoneid INTEGER,
|
||||||
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
FOREIGN KEY (company_id) REFERENCES company(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
CREATE UNIQUE INDEX IF NOT EXISTS UQ_posterminal_token
|
CREATE UNIQUE INDEX IF NOT EXISTS UQ_posterminal_token
|
||||||
@@ -684,6 +702,30 @@ CREATE TABLE IF NOT EXISTS articleattributevalue (
|
|||||||
);
|
);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const INIT_CATEGORYPROPERTY = `
|
||||||
|
CREATE TABLE IF NOT EXISTS categoryproperty (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
property_id INTEGER NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (property_id) REFERENCES propertyname(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const INIT_CATEGORYATTRIBUTEVALUE = `
|
||||||
|
CREATE TABLE IF NOT EXISTS categoryattributevalue (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
attributevalue_id INTEGER NOT NULL,
|
||||||
|
sort INTEGER NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (attributevalue_id) REFERENCES attributevalue(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE (category_id, attributevalue_id)
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
/** DDL scripts in dependency order for SQLite schema initialization. */
|
/** DDL scripts in dependency order for SQLite schema initialization. */
|
||||||
export const SCHEMA_SCRIPTS = [
|
export const SCHEMA_SCRIPTS = [
|
||||||
INIT_META,
|
INIT_META,
|
||||||
@@ -720,6 +762,8 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_ARTICLE_SUPPLIER,
|
INIT_ARTICLE_SUPPLIER,
|
||||||
INIT_ARTICLEPROPERTY,
|
INIT_ARTICLEPROPERTY,
|
||||||
INIT_ARTICLEATTRIBUTEVALUE,
|
INIT_ARTICLEATTRIBUTEVALUE,
|
||||||
|
INIT_CATEGORYPROPERTY,
|
||||||
|
INIT_CATEGORYATTRIBUTEVALUE,
|
||||||
INIT_MANIFEST,
|
INIT_MANIFEST,
|
||||||
INIT_MANIFESTARTICLE,
|
INIT_MANIFESTARTICLE,
|
||||||
INIT_WAREHOUSE,
|
INIT_WAREHOUSE,
|
||||||
@@ -727,6 +771,7 @@ export const SCHEMA_SCRIPTS = [
|
|||||||
INIT_NUMBERS,
|
INIT_NUMBERS,
|
||||||
INIT_COMPANYUSER,
|
INIT_COMPANYUSER,
|
||||||
INIT_ARTICLEHISTORY,
|
INIT_ARTICLEHISTORY,
|
||||||
|
INIT_ARTICLETOMBSTONE,
|
||||||
INIT_ENTITYHISTORY,
|
INIT_ENTITYHISTORY,
|
||||||
INIT_SYNCPOS,
|
INIT_SYNCPOS,
|
||||||
INIT_COMPANYSETTING,
|
INIT_COMPANYSETTING,
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries
|
|||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import getBySku from './getBySku.js';
|
import getBySku from './getBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||||
|
|
||||||
export default function addChild(provider) {
|
export default function addChild(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = getBySku(provider);
|
const load = getBySku(provider);
|
||||||
|
const resolveSlug = resolveUniqueSlug(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
@@ -117,10 +118,46 @@ export default function addChild(provider) {
|
|||||||
throw new Error('Attribute value is required for child article');
|
throw new Error('Attribute value is required for child article');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute child SKU: father.sku + '-' + finalValueId
|
// Check if father already has a variant with this attribute value
|
||||||
const childSku = `${father.sku}-${finalValueId}`;
|
const existingVariantRes = await runner.query(
|
||||||
|
`SELECT a.id
|
||||||
|
FROM article a
|
||||||
|
INNER JOIN articleattributevalue aav ON aav.article_id = a.id
|
||||||
|
WHERE a.fatherarticle_id = @fatherId
|
||||||
|
AND aav.attributevalue_id = @finalValueId`,
|
||||||
|
{ fatherId, finalValueId },
|
||||||
|
);
|
||||||
|
if (existingVariantRes.rows.length > 0) {
|
||||||
|
throw new Error('Variant with this attribute value already exists');
|
||||||
|
}
|
||||||
|
|
||||||
// Check if child SKU already exists
|
// Compute child SKU:
|
||||||
|
// If options.sku is provided, use it.
|
||||||
|
// Otherwise, derive using next free index: fatherSku-1, fatherSku-2, ...
|
||||||
|
let childSku = (typeof options.sku === 'string' && options.sku.trim()) || null;
|
||||||
|
if (!childSku) {
|
||||||
|
const existingSkusRes = await runner.query(
|
||||||
|
'SELECT sku FROM article WHERE company_id = @companyId AND sku LIKE @prefix',
|
||||||
|
{ companyId, prefix: `${father.sku}-%` },
|
||||||
|
);
|
||||||
|
const usedIndices = new Set();
|
||||||
|
const prefix = `${father.sku}-`;
|
||||||
|
for (const row of existingSkusRes.rows) {
|
||||||
|
if (row.sku && row.sku.startsWith(prefix)) {
|
||||||
|
const numPart = row.sku.slice(prefix.length);
|
||||||
|
if (/^\d+$/.test(numPart)) {
|
||||||
|
usedIndices.add(parseInt(numPart, 10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let nextIndex = 1;
|
||||||
|
while (usedIndices.has(nextIndex)) {
|
||||||
|
nextIndex++;
|
||||||
|
}
|
||||||
|
childSku = `${father.sku}-${nextIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if child SKU already exists (e.g. orphan holding SKU or custom SKU)
|
||||||
const existingChildRes = await runner.query(
|
const existingChildRes = await runner.query(
|
||||||
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
|
'SELECT id FROM article WHERE sku = @childSku AND company_id = @companyId',
|
||||||
{ childSku, companyId },
|
{ childSku, companyId },
|
||||||
@@ -129,9 +166,16 @@ export default function addChild(provider) {
|
|||||||
throw new Error(`Child article "${childSku}" already exists`);
|
throw new Error(`Child article "${childSku}" already exists`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next fathersort
|
// Next free fathersort (smallest non-negative integer not used by any child of this father)
|
||||||
const countRes = await runner.query(getChildCountSql, { articleId: fatherId });
|
const existingSortsRes = await runner.query(
|
||||||
const fathersort = countRes.rows[0]?.count ?? 0;
|
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||||
|
{ fatherId },
|
||||||
|
);
|
||||||
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||||
|
let fathersort = 0;
|
||||||
|
while (usedSorts.has(fathersort)) {
|
||||||
|
fathersort++;
|
||||||
|
}
|
||||||
|
|
||||||
// Create child article
|
// Create child article
|
||||||
const rawTrack = options.track_stock ?? options.trackStock;
|
const rawTrack = options.track_stock ?? options.trackStock;
|
||||||
@@ -186,7 +230,11 @@ export default function addChild(provider) {
|
|||||||
|
|
||||||
// Create article text
|
// Create article text
|
||||||
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
|
const childName = options.name?.trim() || `${fatherName} - ${finalValue}`;
|
||||||
const childSlug = slugify(childName);
|
const childSlug = await resolveSlug(runner, {
|
||||||
|
companyId,
|
||||||
|
languageId,
|
||||||
|
name: childName,
|
||||||
|
});
|
||||||
await runner.query(createArticleTextSql, {
|
await runner.query(createArticleTextSql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
languageId,
|
languageId,
|
||||||
@@ -194,13 +242,20 @@ export default function addChild(provider) {
|
|||||||
slug: childSlug,
|
slug: childSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
// History
|
// History on child
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
|
text: `Artikel "${childName}" (${childSku}) als Variante von "${father.sku}" angelegt`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// History on father
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${childSku}) hinzugefügt`,
|
||||||
|
});
|
||||||
|
|
||||||
return load(googleAuthId, childSku, runner);
|
return load(googleAuthId, childSku, runner);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,13 @@ import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
|||||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||||
import nextSku from '../numbers/nextSku.js';
|
import nextSku from '../numbers/nextSku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||||
|
|
||||||
export default function createUnderParent(provider) {
|
export default function createUnderParent(provider) {
|
||||||
const access = resolveCategoryAccess(provider);
|
const access = resolveCategoryAccess(provider);
|
||||||
const ensureUnit = ensureDefaultUnit(provider);
|
const ensureUnit = ensureDefaultUnit(provider);
|
||||||
const allocateSku = nextSku(provider);
|
const allocateSku = nextSku(provider);
|
||||||
|
const resolveSlug = resolveUniqueSlug(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
const createArticleSql = isSqlite ? SQLITE_CREATE_ARTICLE : MSSQL_CREATE_ARTICLE;
|
||||||
@@ -53,7 +54,7 @@ export default function createUnderParent(provider) {
|
|||||||
|
|
||||||
const unitId = await ensureUnit(runner, languageId, companyId);
|
const unitId = await ensureUnit(runner, languageId, companyId);
|
||||||
const sku = await allocateSku(companyId, runner);
|
const sku = await allocateSku(companyId, runner);
|
||||||
const slug = slugify(name);
|
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||||
|
|
||||||
let article;
|
let article;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
|
import * as mssqlQueries from '../../providers/mssql/queries/articleDelete.js';
|
||||||
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
|
import * as sqliteQueries from '../../providers/sqlite3/queries/articleDelete.js';
|
||||||
|
import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
||||||
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
@@ -7,6 +9,7 @@ export default function del(provider) {
|
|||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
const queries = isSqlite ? sqliteQueries : mssqlQueries;
|
||||||
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an article by its SKU, refusing when it still has children unless cascade is true.
|
* Deletes an article by its SKU, refusing when it still has children unless cascade is true.
|
||||||
@@ -40,6 +43,8 @@ export default function del(provider) {
|
|||||||
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
|
const listIds = await runner.query(queries.GET_LIST_IDS, { articleId: id });
|
||||||
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
const { articlelistId, optionslistId } = listIds.rows[0] ?? {};
|
||||||
|
|
||||||
|
await runner.query(queries.CREATE_TOMBSTONE, { articleId: id });
|
||||||
|
|
||||||
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
|
await runner.query(queries.DELETE_PICS_BY_ARTICLE, { articleId: id });
|
||||||
|
|
||||||
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
|
const deleted = await runner.query(queries.DELETE_BY_ID, { articleId: id });
|
||||||
@@ -64,16 +69,38 @@ export default function del(provider) {
|
|||||||
await deleteArticleAndLists(id);
|
await deleteArticleAndLists(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fatherRes = await runner.query(
|
||||||
|
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
||||||
|
{ articleId },
|
||||||
|
);
|
||||||
|
const fatherArticleId = fatherRes.rows[0]?.fatherarticle_id;
|
||||||
|
|
||||||
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
|
const childCount = await runner.query(queries.GET_CHILD_COUNT, { articleId });
|
||||||
if (childCount.rows[0].count > 0) {
|
if (childCount.rows[0].count > 0) {
|
||||||
if (!cascade) {
|
if (!cascade) {
|
||||||
throw new Error('Article has children');
|
throw new Error('Article has children');
|
||||||
}
|
}
|
||||||
await deleteSubtree(articleId);
|
await deleteSubtree(articleId);
|
||||||
return;
|
} else {
|
||||||
|
await deleteArticleAndLists(articleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
await deleteArticleAndLists(articleId);
|
if (fatherArticleId != null) {
|
||||||
|
const remainingChildrenRes = await runner.query(
|
||||||
|
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherArticleId ORDER BY fathersort ASC, id ASC',
|
||||||
|
{ fatherArticleId },
|
||||||
|
);
|
||||||
|
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||||
|
const childRow = remainingChildrenRes.rows[i];
|
||||||
|
if (Number(childRow.fathersort) !== i) {
|
||||||
|
await runner.query(articleQueries.SET_FATHER_SORT, {
|
||||||
|
articleId: childRow.id,
|
||||||
|
fatherArticleId,
|
||||||
|
fathersort: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
|
|||||||
@@ -1,28 +1,50 @@
|
|||||||
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY } from '../../providers/mssql/queries/articlehistory.js';
|
import { LIST_BY_ARTICLE_ID as MSSQL_QUERY, LIST_BY_ARTICLE_ID_LIMITED as MSSQL_QUERY_LIMITED } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/articlehistory.js';
|
import { LIST_BY_ARTICLE_ID as SQLITE_QUERY, LIST_BY_ARTICLE_ID_LIMITED as SQLITE_QUERY_LIMITED } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function getHistoryBySku(provider) {
|
export default function getHistoryBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const sql = isSqlite ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
const sqlLimited = isSqlite ? SQLITE_QUERY_LIMITED : MSSQL_QUERY_LIMITED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the change history of an article by its SKU.
|
* Returns the change history of an article by its SKU.
|
||||||
*
|
*
|
||||||
* @param {number} googleAuthId - The id of the Google auth user.
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
* @param {string} sku - The article SKU.
|
* @param {string} sku - The article SKU.
|
||||||
|
* @param {object} [opts] - Optional options.
|
||||||
|
* @param {number} [opts.limit] - Maximum number of entries to return (newest first).
|
||||||
* @param {object} [tx] - Optional transaction.
|
* @param {object} [tx] - Optional transaction.
|
||||||
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
* @returns {Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>} The article history entries.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
* const history = await store.article.getHistoryBySku(42, 'SKU-1');
|
||||||
* // => [{ id, companyuserId, date, text }]
|
* // => [{ id, companyuserId, date, text }]
|
||||||
|
*
|
||||||
|
* const recent = await store.article.getHistoryBySku(42, 'SKU-1', { limit: 5 });
|
||||||
|
* // => up to 5 most recent entries
|
||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, tx) => {
|
return async (googleAuthId, sku, opts, tx) => {
|
||||||
const { articleId } = await resolve(googleAuthId, sku, tx);
|
// Support legacy call signature: getHistoryBySku(googleAuthId, sku, tx)
|
||||||
const runner = makeRunner(provider, tx);
|
let limit = null;
|
||||||
const result = await runner.query(sql, { articleId });
|
let resolvedTx = tx;
|
||||||
|
if (opts != null && typeof opts === 'object' && !opts._isTx) {
|
||||||
|
limit = opts.limit != null ? Number(opts.limit) : null;
|
||||||
|
} else if (opts != null) {
|
||||||
|
resolvedTx = opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = makeRunner(provider, resolvedTx);
|
||||||
|
const { articleId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
|
let result;
|
||||||
|
if (limit != null && Number.isInteger(limit) && limit > 0) {
|
||||||
|
result = await runner.query(sqlLimited, { articleId, limit });
|
||||||
|
} else {
|
||||||
|
result = await runner.query(sql, { articleId });
|
||||||
|
}
|
||||||
|
|
||||||
return result.rows.map((row) => ({
|
return result.rows.map((row) => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
@@ -32,4 +54,3 @@ export default function getHistoryBySku(provider) {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import resolveSelectedCompanyAccess from '../companyuser/resolveSelectedCompanyA
|
|||||||
import ensureByPath from '../category/ensureByPath.js';
|
import ensureByPath from '../category/ensureByPath.js';
|
||||||
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
import ensureDefaultUnit from '../unit/ensureDefault.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||||
|
|
||||||
// Normalizes a numeric value that may arrive as a string using German decimal
|
// Normalizes a numeric value that may arrive as a string using German decimal
|
||||||
// notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty.
|
// notation (e.g. "11,00"), a JS number, or null/undefined. Returns null when empty.
|
||||||
@@ -49,6 +49,7 @@ export default function importRows(provider) {
|
|||||||
const companyAccess = resolveSelectedCompanyAccess(provider);
|
const companyAccess = resolveSelectedCompanyAccess(provider);
|
||||||
const ensureUnit = ensureDefaultUnit(provider);
|
const ensureUnit = ensureDefaultUnit(provider);
|
||||||
const ensureCategoryByPath = ensureByPath(provider);
|
const ensureCategoryByPath = ensureByPath(provider);
|
||||||
|
const resolveSlug = resolveUniqueSlug(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
@@ -167,7 +168,7 @@ export default function importRows(provider) {
|
|||||||
try {
|
try {
|
||||||
await runInTx(async (runner) => {
|
await runInTx(async (runner) => {
|
||||||
const unitId = await ensureUnit(runner, companyId, languageId);
|
const unitId = await ensureUnit(runner, companyId, languageId);
|
||||||
const slug = slugify(name);
|
const slug = await resolveSlug(runner, { companyId, languageId, name });
|
||||||
|
|
||||||
const rawTrack = row.track_stock ?? row.trackStock;
|
const rawTrack = row.track_stock ?? row.trackStock;
|
||||||
const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
|
const trackStock = rawTrack == null ? 1 : (rawTrack ? 1 : 0);
|
||||||
@@ -286,22 +287,52 @@ export default function importRows(provider) {
|
|||||||
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
const child = await runner.query(articleQueries.GET_ID_BY_SKU, { sku, companyId });
|
||||||
const childId = child.rows[0]?.articleId;
|
const childId = child.rows[0]?.articleId;
|
||||||
if (childId) {
|
if (childId) {
|
||||||
const father = await runner.query(articleQueries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
|
const father = await runner.query(
|
||||||
const fatherId = father.rows[0]?.articleId;
|
'SELECT id, is_father FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||||
|
{ sku: fatherSku, companyId },
|
||||||
|
);
|
||||||
|
const fatherRow = father.rows[0];
|
||||||
|
const fatherId = fatherRow?.id;
|
||||||
if (fatherId && fatherId !== childId) {
|
if (fatherId && fatherId !== childId) {
|
||||||
const childCount = await runner.query(getChildCountSql, {
|
const wasFather = Boolean(fatherRow.is_father);
|
||||||
articleId: fatherId,
|
|
||||||
});
|
const existingSortsRes = await runner.query(
|
||||||
const count = childCount.rows[0].count;
|
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||||
|
{ fatherId },
|
||||||
|
);
|
||||||
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||||
|
let fathersort = 0;
|
||||||
|
while (usedSorts.has(fathersort)) {
|
||||||
|
fathersort++;
|
||||||
|
}
|
||||||
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
await runner.query(articleQueries.UPDATE_FATHER_BY_SKU, {
|
||||||
sku,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: fatherId,
|
fatherArticleId: fatherId,
|
||||||
fathersort: count,
|
fathersort,
|
||||||
});
|
});
|
||||||
await runner.query(articleQueries.SET_IS_FATHER, {
|
await runner.query(articleQueries.SET_IS_FATHER, {
|
||||||
articleId: fatherId,
|
articleId: fatherId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!wasFather) {
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: 'Artikel zu Vaterartikel konvertiert',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const childName = String(row?.name ?? sku).trim();
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||||
|
});
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: childId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Artikel "${childName}" (${sku}) als Variante von "${fatherSku}" zugewiesen`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function removeChild(provider) {
|
|||||||
*/
|
*/
|
||||||
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
|
return async (googleAuthId, fatherSku, childSku, options = {}, tx) => {
|
||||||
const run = async (runner) => {
|
const run = async (runner) => {
|
||||||
const { articleId: fatherId, companyuserId } = await resolve(
|
const { articleId: fatherId, languageId, companyuserId } = await resolve(
|
||||||
googleAuthId,
|
googleAuthId,
|
||||||
fatherSku,
|
fatherSku,
|
||||||
runner,
|
runner,
|
||||||
@@ -45,9 +45,45 @@ export default function removeChild(provider) {
|
|||||||
throw new Error('Child article does not belong to this father');
|
throw new Error('Child article does not belong to this father');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch child name before any deletion (needed for history regardless of path)
|
||||||
|
const childNameRes = await runner.query(
|
||||||
|
`SELECT at.name FROM articletext at
|
||||||
|
INNER JOIN article a ON a.id = at.article_id
|
||||||
|
WHERE a.id = @childId AND at.language_id = @languageId`,
|
||||||
|
{ childId, languageId },
|
||||||
|
);
|
||||||
|
const childName = childNameRes.rows[0]?.name || childSku;
|
||||||
|
|
||||||
|
const recompactFatherSort = async () => {
|
||||||
|
const remainingChildrenRes = await runner.query(
|
||||||
|
'SELECT id, fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC, id ASC',
|
||||||
|
{ fatherId },
|
||||||
|
);
|
||||||
|
for (let i = 0; i < remainingChildrenRes.rows.length; i++) {
|
||||||
|
const childRow = remainingChildrenRes.rows[i];
|
||||||
|
if (Number(childRow.fathersort) !== i) {
|
||||||
|
await runner.query(queries.SET_FATHER_SORT, {
|
||||||
|
articleId: childRow.id,
|
||||||
|
fatherArticleId: fatherId,
|
||||||
|
fathersort: i,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (options.deleteArticle) {
|
if (options.deleteArticle) {
|
||||||
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
|
const cascade = options.cascade !== undefined ? Boolean(options.cascade) : true;
|
||||||
await del(googleAuthId, childSku, { cascade }, runner);
|
await del(googleAuthId, childSku, { cascade }, runner);
|
||||||
|
|
||||||
|
await recompactFatherSort();
|
||||||
|
|
||||||
|
// History on father (child article is gone so we can only log on father)
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${childSku}) gelöscht`,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fatherSku,
|
fatherSku,
|
||||||
@@ -81,13 +117,22 @@ export default function removeChild(provider) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add history
|
await recompactFatherSort();
|
||||||
|
|
||||||
|
// Add history on child
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId: childId,
|
articleId: childId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
|
text: `Artikel "${childSku}" von Vaterartikel "${fatherSku}" getrennt`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add history on father
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${childSku}) getrennt`,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
fatherSku,
|
fatherSku,
|
||||||
|
|||||||
51
src/store/article/resolveUniqueSlug.js
Normal file
51
src/store/article/resolveUniqueSlug.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as MSSQL_LIST_SLUGS } from '../../providers/mssql/queries/articletext.js';
|
||||||
|
import { LIST_SLUGS_BY_COMPANY_AND_LANGUAGE as SQLITE_LIST_SLUGS } from '../../providers/sqlite3/queries/articletext.js';
|
||||||
|
import slugify from '../slugify.js';
|
||||||
|
|
||||||
|
export default function resolveUniqueSlug(provider) {
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const listSlugsSql = isSqlite ? SQLITE_LIST_SLUGS : MSSQL_LIST_SLUGS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a company- and language-unique slug for an article.
|
||||||
|
* Disambiguates duplicate slugs with a numeric suffix (-2, -3, ...).
|
||||||
|
*
|
||||||
|
* @param {object} runner - DB runner or tx.
|
||||||
|
* @param {object} params
|
||||||
|
* @param {number} params.companyId - The company ID.
|
||||||
|
* @param {number} params.languageId - The language ID.
|
||||||
|
* @param {string} params.name - The article name.
|
||||||
|
* @param {number} [params.excludeArticleId] - Optional article ID to exclude (e.g. for updates).
|
||||||
|
* @returns {Promise<string>} The unique slug.
|
||||||
|
*/
|
||||||
|
return async (runner, { companyId, languageId, name, excludeArticleId }) => {
|
||||||
|
const baseSlug = slugify(name) || 'article';
|
||||||
|
const likePattern = `${baseSlug}-%`;
|
||||||
|
|
||||||
|
const res = await runner.query(listSlugsSql, {
|
||||||
|
companyId,
|
||||||
|
languageId,
|
||||||
|
baseSlug,
|
||||||
|
likePattern,
|
||||||
|
});
|
||||||
|
|
||||||
|
const existingSlugs = new Set();
|
||||||
|
for (const row of res.rows) {
|
||||||
|
const artId = row.articleId ?? row.article_id;
|
||||||
|
if (excludeArticleId != null && artId === excludeArticleId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
existingSlugs.add(row.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingSlugs.has(baseSlug)) {
|
||||||
|
return baseSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
let suffix = 2;
|
||||||
|
while (existingSlugs.has(`${baseSlug}-${suffix}`)) {
|
||||||
|
suffix++;
|
||||||
|
}
|
||||||
|
return `${baseSlug}-${suffix}`;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -47,25 +47,29 @@ export default function setBrandIdBySku(provider) {
|
|||||||
throw new Error('Invalid brandId');
|
throw new Error('Invalid brandId');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read old brand for history
|
// Read old brand name (null = no brand assigned)
|
||||||
const oldBrandRes = await runner.query(
|
const oldBrandRes = await runner.query(
|
||||||
`SELECT b.name FROM article a LEFT JOIN brand b ON b.id = a.brand_id WHERE a.id = @articleId`,
|
`SELECT b.name FROM article a LEFT JOIN brand b ON b.id = a.brand_id WHERE a.id = @articleId`,
|
||||||
{ articleId },
|
{ articleId },
|
||||||
);
|
);
|
||||||
const oldBrandName = oldBrandRes.rows[0]?.name ?? '';
|
const oldBrandName = oldBrandRes.rows[0]?.name ?? null;
|
||||||
|
|
||||||
let resolvedBrandId = null;
|
let resolvedBrandId = null;
|
||||||
let newBrandName = '';
|
let newBrandName = null;
|
||||||
if (brandId !== null) {
|
if (brandId !== null) {
|
||||||
const brand = await runner.query(getBrandSql, {
|
// Verify brand belongs to this company
|
||||||
brandId,
|
const brandCheck = await runner.query(getBrandSql, { brandId, companyId });
|
||||||
companyId,
|
if (!brandCheck.rows[0]) {
|
||||||
});
|
|
||||||
if (!brand.rows[0]) {
|
|
||||||
throw new Error('Brand not found');
|
throw new Error('Brand not found');
|
||||||
}
|
}
|
||||||
resolvedBrandId = brandId;
|
resolvedBrandId = brandId;
|
||||||
newBrandName = brand.rows[0].name ?? '';
|
|
||||||
|
// GET_BRAND_BY_COMPANY_AND_ID only selects `id`, so fetch name separately
|
||||||
|
const brandNameRes = await runner.query(
|
||||||
|
'SELECT name FROM brand WHERE id = @brandId',
|
||||||
|
{ brandId },
|
||||||
|
);
|
||||||
|
newBrandName = brandNameRes.rows[0]?.name ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await runner.query(updateBrandSql, {
|
const result = await runner.query(updateBrandSql, {
|
||||||
@@ -77,12 +81,12 @@ export default function setBrandIdBySku(provider) {
|
|||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write history if brand changed
|
// null = no brand; treat as distinct sentinel so first assignment is logged
|
||||||
if (oldBrandName !== newBrandName) {
|
if (oldBrandName !== newBrandName) {
|
||||||
await runner.query(insertHistorySql, {
|
await runner.query(insertHistorySql, {
|
||||||
articleId,
|
articleId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
text: `Marke geändert von "${oldBrandName}" auf "${newBrandName}"`,
|
text: `Marke ge\u00e4ndert von "${oldBrandName ?? ''}" auf "${newBrandName ?? ''}"`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,4 +98,3 @@ export default function setBrandIdBySku(provider) {
|
|||||||
return article;
|
return article;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import * as mssqlArticle from '../../providers/mssql/queries/article.js';
|
|||||||
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
import * as sqliteArticle from '../../providers/sqlite3/queries/article.js';
|
||||||
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/queries/articleDelete.js';
|
||||||
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
import { GET_CHILD_COUNT as SQLITE_GET_CHILD_COUNT } from '../../providers/sqlite3/queries/articleDelete.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
@@ -13,6 +15,7 @@ export default function setFatherBySku(provider) {
|
|||||||
|
|
||||||
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
const queries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
const getChildCountSql = isSqlite ? SQLITE_GET_CHILD_COUNT : MSSQL_GET_CHILD_COUNT;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Links an article to a father article (V/K compound) by their SKUs.
|
* Links an article to a father article (V/K compound) by their SKUs.
|
||||||
@@ -30,14 +33,47 @@ export default function setFatherBySku(provider) {
|
|||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, fatherSku, tx) => {
|
return async (googleAuthId, sku, fatherSku, tx) => {
|
||||||
const runSet = async (runner) => {
|
const runSet = async (runner) => {
|
||||||
const { articleId, languageId, companyId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, languageId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
|
const childTextRes = await runner.query(
|
||||||
|
'SELECT name FROM articletext WHERE article_id = @articleId AND language_id = @languageId',
|
||||||
|
{ articleId, languageId },
|
||||||
|
);
|
||||||
|
const childName = childTextRes.rows[0]?.name || sku;
|
||||||
|
|
||||||
if (fatherSku == null || fatherSku === '') {
|
if (fatherSku == null || fatherSku === '') {
|
||||||
|
const prevFatherRes = await runner.query(
|
||||||
|
'SELECT fatherarticle_id FROM article WHERE id = @articleId',
|
||||||
|
{ articleId },
|
||||||
|
);
|
||||||
|
const prevFatherId = prevFatherRes.rows[0]?.fatherarticle_id;
|
||||||
|
|
||||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||||
sku,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: null,
|
fatherArticleId: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (prevFatherId) {
|
||||||
|
const prevFatherTextRes = await runner.query(
|
||||||
|
'SELECT sku FROM article WHERE id = @prevFatherId',
|
||||||
|
{ prevFatherId },
|
||||||
|
);
|
||||||
|
const prevFatherSku = prevFatherTextRes.rows[0]?.sku || String(prevFatherId);
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Artikel "${sku}" von Vaterartikel "${prevFatherSku}" getrennt`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: prevFatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${sku}) getrennt`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const unlinked = await load(articleId, languageId, runner);
|
const unlinked = await load(articleId, languageId, runner);
|
||||||
if (!unlinked) {
|
if (!unlinked) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
@@ -45,8 +81,12 @@ export default function setFatherBySku(provider) {
|
|||||||
return unlinked;
|
return unlinked;
|
||||||
}
|
}
|
||||||
|
|
||||||
const father = await runner.query(queries.GET_ID_BY_SKU, { sku: fatherSku, companyId });
|
const father = await runner.query(
|
||||||
const fatherId = father.rows[0]?.articleId;
|
'SELECT id, is_father, sku FROM article WHERE sku = @sku AND (@companyId IS NULL OR company_id = @companyId)',
|
||||||
|
{ sku: fatherSku, companyId },
|
||||||
|
);
|
||||||
|
const fatherRow = father.rows[0];
|
||||||
|
const fatherId = fatherRow?.id;
|
||||||
if (!fatherId) {
|
if (!fatherId) {
|
||||||
throw new Error('Father not found');
|
throw new Error('Father not found');
|
||||||
}
|
}
|
||||||
@@ -54,22 +94,49 @@ export default function setFatherBySku(provider) {
|
|||||||
throw new Error('Invalid father');
|
throw new Error('Invalid father');
|
||||||
}
|
}
|
||||||
|
|
||||||
const childCount = await runner.query(getChildCountSql, {
|
const wasFather = Boolean(fatherRow.is_father);
|
||||||
articleId: fatherId,
|
|
||||||
});
|
const existingSortsRes = await runner.query(
|
||||||
const count = childCount.rows[0].count;
|
'SELECT fathersort FROM article WHERE fatherarticle_id = @fatherId ORDER BY fathersort ASC',
|
||||||
|
{ fatherId },
|
||||||
|
);
|
||||||
|
const usedSorts = new Set(existingSortsRes.rows.map((r) => Number(r.fathersort)));
|
||||||
|
let fathersort = 0;
|
||||||
|
while (usedSorts.has(fathersort)) {
|
||||||
|
fathersort++;
|
||||||
|
}
|
||||||
|
|
||||||
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
await runner.query(queries.UPDATE_FATHER_BY_SKU, {
|
||||||
sku,
|
sku,
|
||||||
companyId,
|
companyId,
|
||||||
fatherArticleId: fatherId,
|
fatherArticleId: fatherId,
|
||||||
fathersort: count,
|
fathersort,
|
||||||
});
|
});
|
||||||
|
|
||||||
await runner.query(queries.SET_IS_FATHER, {
|
await runner.query(queries.SET_IS_FATHER, {
|
||||||
articleId: fatherId,
|
articleId: fatherId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!wasFather) {
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: 'Artikel zu Vaterartikel konvertiert',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId: fatherId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Variante "${childName}" (${sku}) hinzugefügt`,
|
||||||
|
});
|
||||||
|
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Artikel "${sku}" als Variante von "${fatherSku}" zugewiesen`,
|
||||||
|
});
|
||||||
|
|
||||||
const article = await load(articleId, languageId, runner);
|
const article = await load(articleId, languageId, runner);
|
||||||
if (!article) {
|
if (!article) {
|
||||||
throw new Error('Article not found');
|
throw new Error('Article not found');
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { GET_FULL_BY_SI as SQLITE_GET_UNIT } from '../../providers/sqlite3/queri
|
|||||||
import queryBySku from './queryBySku.js';
|
import queryBySku from './queryBySku.js';
|
||||||
import resolveArticleBySku from './resolveArticleBySku.js';
|
import resolveArticleBySku from './resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import slugify from '../slugify.js';
|
import resolveUniqueSlug from './resolveUniqueSlug.js';
|
||||||
|
|
||||||
function formatUnitLabel(amount, symbol) {
|
function formatUnitLabel(amount, symbol) {
|
||||||
const sym = symbol ?? '';
|
const sym = symbol ?? '';
|
||||||
@@ -24,6 +24,7 @@ function formatUnitLabel(amount, symbol) {
|
|||||||
export default function updateBySku(provider) {
|
export default function updateBySku(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const load = queryBySku(provider);
|
const load = queryBySku(provider);
|
||||||
|
const resolveSlug = resolveUniqueSlug(provider);
|
||||||
const isSqlite = provider?.dialect === 'sqlite3';
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
|
||||||
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
const articleQueries = isSqlite ? sqliteArticle : mssqlArticle;
|
||||||
@@ -144,7 +145,12 @@ export default function updateBySku(provider) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasName) {
|
if (hasName) {
|
||||||
const slug = slugify(patch.name);
|
const slug = await resolveSlug(runner, {
|
||||||
|
companyId,
|
||||||
|
languageId,
|
||||||
|
name: patch.name,
|
||||||
|
excludeArticleId: articleId,
|
||||||
|
});
|
||||||
const update = await runner.query(updateTextSql, {
|
const update = await runner.query(updateTextSql, {
|
||||||
articleId,
|
articleId,
|
||||||
languageId,
|
languageId,
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export default function setByArticleSku(provider) {
|
|||||||
`SELECT aav.attributevalue_id AS attributeValueId, an.name AS attributeName, av.value
|
`SELECT aav.attributevalue_id AS attributeValueId, an.name AS attributeName, av.value
|
||||||
FROM articleattributevalue aav
|
FROM articleattributevalue aav
|
||||||
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
INNER JOIN attributevalue av ON av.id = aav.attributevalue_id
|
||||||
INNER JOIN attributename an ON an.id = av.attributename_id
|
INNER JOIN attributename an ON an.id = av.attribute_id
|
||||||
WHERE aav.article_id = @articleId
|
WHERE aav.article_id = @articleId
|
||||||
ORDER BY aav.sort ASC, aav.id ASC`,
|
ORDER BY aav.sort ASC, aav.id ASC`,
|
||||||
{ articleId },
|
{ articleId },
|
||||||
@@ -57,7 +57,36 @@ export default function setByArticleSku(provider) {
|
|||||||
|
|
||||||
for (let i = 0; i < values.length; i++) {
|
for (let i = 0; i < values.length; i++) {
|
||||||
const item = values[i];
|
const item = values[i];
|
||||||
const valId = typeof item === 'object' && item !== null ? Number(item.attributeValueId) : Number(item);
|
let rawId;
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
rawId = item.attributeValueId ?? item.attributevalueId ?? item.attribute_value_id ?? item.valueId ?? item.id;
|
||||||
|
} else {
|
||||||
|
rawId = item;
|
||||||
|
}
|
||||||
|
let valId = Number(rawId);
|
||||||
|
|
||||||
|
if ((!Number.isInteger(valId) || valId <= 0) && typeof item === 'object' && item !== null && typeof item.value === 'string' && item.value.trim()) {
|
||||||
|
const valStr = item.value.trim();
|
||||||
|
let targetAttrId = item.attributeId != null ? Number(item.attributeId) : null;
|
||||||
|
if (!targetAttrId && (item.attributeName || item.name)) {
|
||||||
|
const attrName = String(item.attributeName || item.name).trim();
|
||||||
|
const nameRes = await runner.query(
|
||||||
|
'SELECT id FROM attributename WHERE company_id = @companyId AND name = @attrName',
|
||||||
|
{ companyId, attrName },
|
||||||
|
);
|
||||||
|
targetAttrId = nameRes.rows[0]?.id;
|
||||||
|
}
|
||||||
|
if (targetAttrId) {
|
||||||
|
const valRes = await runner.query(
|
||||||
|
'SELECT id FROM attributevalue WHERE attribute_id = @attributeId AND value = @value',
|
||||||
|
{ attributeId: targetAttrId, value: valStr },
|
||||||
|
);
|
||||||
|
if (valRes.rows[0]?.id) {
|
||||||
|
valId = valRes.rows[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(valId) || valId <= 0) {
|
if (!Number.isInteger(valId) || valId <= 0) {
|
||||||
throw new Error('Invalid attributeValueId');
|
throw new Error('Invalid attributeValueId');
|
||||||
}
|
}
|
||||||
@@ -80,8 +109,8 @@ export default function setByArticleSku(provider) {
|
|||||||
exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName);
|
exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sort = typeof item === 'object' && item !== null && item.sort != null && Number.isInteger(item.sort)
|
const sort = typeof item === 'object' && item !== null && (item.sort != null || item.sort_order != null) && Number.isInteger(Number(item.sort ?? item.sort_order))
|
||||||
? item.sort
|
? Number(item.sort ?? item.sort_order)
|
||||||
: i;
|
: i;
|
||||||
|
|
||||||
validated.push({
|
validated.push({
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ import {
|
|||||||
import {
|
import {
|
||||||
DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE,
|
DELETE_BY_ARTICLE_AND_PROPERTY_ID as SQLITE_DELETE,
|
||||||
} from '../../providers/sqlite3/queries/articleproperty.js';
|
} from '../../providers/sqlite3/queries/articleproperty.js';
|
||||||
|
import { INSERT as MSSQL_INSERT_HISTORY } from '../../providers/mssql/queries/articlehistory.js';
|
||||||
|
import { INSERT as SQLITE_INSERT_HISTORY } from '../../providers/sqlite3/queries/articlehistory.js';
|
||||||
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
import resolveArticleBySku from '../article/resolveArticleBySku.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
export default function deleteArticleProperty(provider) {
|
export default function deleteArticleProperty(provider) {
|
||||||
const resolve = resolveArticleBySku(provider);
|
const resolve = resolveArticleBySku(provider);
|
||||||
const deleteSql = provider?.dialect === 'sqlite3' ? SQLITE_DELETE : MSSQL_DELETE;
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes a property from an article by SKU and propertyId.
|
* Deletes a property from an article by SKU and propertyId.
|
||||||
@@ -22,7 +26,26 @@ export default function deleteArticleProperty(provider) {
|
|||||||
*/
|
*/
|
||||||
return async (googleAuthId, sku, propertyId, tx) => {
|
return async (googleAuthId, sku, propertyId, tx) => {
|
||||||
const runner = makeRunner(provider, tx);
|
const runner = makeRunner(provider, tx);
|
||||||
const { articleId } = await resolve(googleAuthId, sku, runner);
|
const { articleId, companyuserId } = await resolve(googleAuthId, sku, runner);
|
||||||
|
|
||||||
|
// Read the property name + value before deleting (for the history entry)
|
||||||
|
const propRes = await runner.query(
|
||||||
|
`SELECT ap.value, pn.name
|
||||||
|
FROM articleproperty ap
|
||||||
|
INNER JOIN propertyname pn ON pn.id = ap.property_id
|
||||||
|
WHERE ap.article_id = @articleId AND ap.property_id = @propertyId`,
|
||||||
|
{ articleId, propertyId },
|
||||||
|
);
|
||||||
|
const prop = propRes.rows[0];
|
||||||
|
|
||||||
await runner.query(deleteSql, { articleId, propertyId });
|
await runner.query(deleteSql, { articleId, propertyId });
|
||||||
|
|
||||||
|
if (prop) {
|
||||||
|
await runner.query(insertHistorySql, {
|
||||||
|
articleId,
|
||||||
|
companyuserId,
|
||||||
|
text: `Eigenschaft "${prop.name}" (${prop.value}) gelöscht`,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/store/categoryattributevalue/listByCategoryId.js
Normal file
34
src/store/categoryattributevalue/listByCategoryId.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { LIST_BY_CATEGORY_ID as MSSQL_QUERY } from '../../providers/mssql/queries/categoryattributevalue.js';
|
||||||
|
import { LIST_BY_CATEGORY_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/categoryattributevalue.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function listByCategoryId(provider) {
|
||||||
|
const resolve = resolveCategoryAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the attribute values assigned to a category by its ID.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} categoryId - The category ID.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
await resolve(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
|
const result = await runner.query(sql, { categoryId });
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
categoryId: row.categoryId,
|
||||||
|
attributeId: row.attributeId,
|
||||||
|
attributeName: row.attributeName,
|
||||||
|
exclusive: Boolean(row.exclusive),
|
||||||
|
attributeValueId: row.attributeValueId,
|
||||||
|
value: row.value,
|
||||||
|
sort: row.sort,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
155
src/store/categoryattributevalue/setByCategoryId.js
Normal file
155
src/store/categoryattributevalue/setByCategoryId.js
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_ID as MSSQL_DELETE,
|
||||||
|
INSERT_ROW as MSSQL_INSERT,
|
||||||
|
GET_VALUE_DETAIL as MSSQL_GET_DETAIL,
|
||||||
|
} from '../../providers/mssql/queries/categoryattributevalue.js';
|
||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_ID as SQLITE_DELETE,
|
||||||
|
INSERT_ROW as SQLITE_INSERT,
|
||||||
|
GET_VALUE_DETAIL as SQLITE_GET_DETAIL,
|
||||||
|
} from '../../providers/sqlite3/queries/categoryattributevalue.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function setByCategoryId(provider) {
|
||||||
|
const resolve = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||||
|
const getDetailSql = isSqlite ? SQLITE_GET_DETAIL : MSSQL_GET_DETAIL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all attribute values for a category by its ID.
|
||||||
|
* Validates company ownership and enforces the `exclusive` flag on attributes.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} categoryId - The category ID.
|
||||||
|
* @param {Array<number|{attributeValueId: number, sort?: number}>} values - Attribute value IDs or items.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, categoryId: number, attributeId: number, attributeName: string, exclusive: boolean, attributeValueId: number, value: string, sort: number}>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, categoryId, values = [], tx) => {
|
||||||
|
if (!Array.isArray(values)) {
|
||||||
|
throw new Error('Invalid attribute values array');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSet = async (runner) => {
|
||||||
|
const { companyId } = await resolve(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
|
const validated = [];
|
||||||
|
const seenValues = new Set();
|
||||||
|
const exclusiveAttributesSeen = new Map(); // attributeId -> attributeName
|
||||||
|
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
const item = values[i];
|
||||||
|
let rawId;
|
||||||
|
if (typeof item === 'object' && item !== null) {
|
||||||
|
rawId = item.attributeValueId ?? item.attributevalueId ?? item.attribute_value_id ?? item.valueId ?? item.id;
|
||||||
|
} else {
|
||||||
|
rawId = item;
|
||||||
|
}
|
||||||
|
let valId = Number(rawId);
|
||||||
|
|
||||||
|
if ((!Number.isInteger(valId) || valId <= 0) && typeof item === 'object' && item !== null && typeof item.value === 'string' && item.value.trim()) {
|
||||||
|
const valStr = item.value.trim();
|
||||||
|
let targetAttrId = item.attributeId != null ? Number(item.attributeId) : null;
|
||||||
|
if (!targetAttrId && (item.attributeName || item.name)) {
|
||||||
|
const attrName = String(item.attributeName || item.name).trim();
|
||||||
|
const nameRes = await runner.query(
|
||||||
|
'SELECT id FROM attributename WHERE company_id = @companyId AND name = @attrName',
|
||||||
|
{ companyId, attrName },
|
||||||
|
);
|
||||||
|
targetAttrId = nameRes.rows[0]?.id;
|
||||||
|
}
|
||||||
|
if (targetAttrId) {
|
||||||
|
const valRes = await runner.query(
|
||||||
|
'SELECT id FROM attributevalue WHERE attribute_id = @targetAttrId AND value = @valStr',
|
||||||
|
{ targetAttrId, valStr },
|
||||||
|
);
|
||||||
|
if (valRes.rows[0]?.id) {
|
||||||
|
valId = valRes.rows[0].id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(valId) || valId <= 0) {
|
||||||
|
throw new Error(`Invalid attributeValueId at index ${i}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seenValues.has(valId)) {
|
||||||
|
continue; // de-duplicate identical value assignments
|
||||||
|
}
|
||||||
|
seenValues.add(valId);
|
||||||
|
|
||||||
|
const detailRes = await runner.query(getDetailSql, { attributeValueId: valId });
|
||||||
|
const detail = detailRes.rows[0];
|
||||||
|
if (!detail) {
|
||||||
|
throw new Error(`Attribute value not found: ${valId}`);
|
||||||
|
}
|
||||||
|
if (detail.companyId !== companyId) {
|
||||||
|
throw new Error(`Attribute value does not belong to company: ${valId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExclusive = Boolean(detail.exclusive);
|
||||||
|
if (isExclusive) {
|
||||||
|
if (exclusiveAttributesSeen.has(detail.attributeId)) {
|
||||||
|
const attrName = exclusiveAttributesSeen.get(detail.attributeId);
|
||||||
|
throw new Error(`Attribute "${attrName}" is exclusive and can only have one value`);
|
||||||
|
}
|
||||||
|
exclusiveAttributesSeen.set(detail.attributeId, detail.attributeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = typeof item === 'object' && item !== null && item.sort != null && Number.isInteger(item.sort)
|
||||||
|
? item.sort
|
||||||
|
: i;
|
||||||
|
|
||||||
|
validated.push({
|
||||||
|
attributeValueId: valId,
|
||||||
|
attributeId: detail.attributeId,
|
||||||
|
attributeName: detail.attributeName,
|
||||||
|
exclusive: isExclusive,
|
||||||
|
value: detail.value,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(deleteSql, { categoryId });
|
||||||
|
|
||||||
|
const inserted = [];
|
||||||
|
for (const item of validated) {
|
||||||
|
const res = await runner.query(insertSql, {
|
||||||
|
categoryId,
|
||||||
|
attributeValueId: item.attributeValueId,
|
||||||
|
sort: item.sort,
|
||||||
|
});
|
||||||
|
const id = res.rows[0]?.id ?? res.insertId;
|
||||||
|
inserted.push({
|
||||||
|
id,
|
||||||
|
categoryId,
|
||||||
|
attributeId: item.attributeId,
|
||||||
|
attributeName: item.attributeName,
|
||||||
|
exclusive: item.exclusive,
|
||||||
|
attributeValueId: item.attributeValueId,
|
||||||
|
value: item.value,
|
||||||
|
sort: item.sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch category updated_at
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/store/categoryproperty/delete.js
Normal file
36
src/store/categoryproperty/delete.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_AND_PROPERTY_ID as MSSQL_DELETE,
|
||||||
|
} from '../../providers/mssql/queries/categoryproperty.js';
|
||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_AND_PROPERTY_ID as SQLITE_DELETE,
|
||||||
|
} from '../../providers/sqlite3/queries/categoryproperty.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function deleteCategoryProperty(provider) {
|
||||||
|
const resolve = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a property from a category by categoryId and propertyId.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} categoryId - The category ID.
|
||||||
|
* @param {number} propertyId - The property id.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, categoryId, propertyId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
await resolve(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
|
await runner.query(deleteSql, { categoryId, propertyId });
|
||||||
|
|
||||||
|
// Touch category updated_at
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
};
|
||||||
|
}
|
||||||
33
src/store/categoryproperty/listByCategoryId.js
Normal file
33
src/store/categoryproperty/listByCategoryId.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { LIST_BY_CATEGORY_ID as MSSQL_QUERY } from '../../providers/mssql/queries/categoryproperty.js';
|
||||||
|
import { LIST_BY_CATEGORY_ID as SQLITE_QUERY } from '../../providers/sqlite3/queries/categoryproperty.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function listByCategoryId(provider) {
|
||||||
|
const resolve = resolveCategoryAccess(provider);
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_QUERY : MSSQL_QUERY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists the properties of a category by its ID.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} categoryId - The category ID.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, categoryId, tx) => {
|
||||||
|
const runner = makeRunner(provider, tx);
|
||||||
|
await resolve(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
|
const result = await runner.query(sql, { categoryId });
|
||||||
|
return result.rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
categoryId: row.categoryId,
|
||||||
|
propertyId: row.propertyId,
|
||||||
|
name: row.name,
|
||||||
|
exclusive: Boolean(row.exclusive),
|
||||||
|
value: row.value,
|
||||||
|
sort: row.sort,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
117
src/store/categoryproperty/setByCategoryId.js
Normal file
117
src/store/categoryproperty/setByCategoryId.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_ID as MSSQL_DELETE,
|
||||||
|
INSERT_ROW as MSSQL_INSERT,
|
||||||
|
} from '../../providers/mssql/queries/categoryproperty.js';
|
||||||
|
import {
|
||||||
|
DELETE_BY_CATEGORY_ID as SQLITE_DELETE,
|
||||||
|
INSERT_ROW as SQLITE_INSERT,
|
||||||
|
} from '../../providers/sqlite3/queries/categoryproperty.js';
|
||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID as MSSQL_GET_PROP,
|
||||||
|
} from '../../providers/mssql/queries/propertyname.js';
|
||||||
|
import {
|
||||||
|
GET_BY_COMPANY_AND_ID as SQLITE_GET_PROP,
|
||||||
|
} from '../../providers/sqlite3/queries/propertyname.js';
|
||||||
|
import resolveCategoryAccess from '../category/resolveCategoryAccess.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function setByCategoryId(provider) {
|
||||||
|
const resolve = resolveCategoryAccess(provider);
|
||||||
|
const isSqlite = provider?.dialect === 'sqlite3';
|
||||||
|
const deleteSql = isSqlite ? SQLITE_DELETE : MSSQL_DELETE;
|
||||||
|
const insertSql = isSqlite ? SQLITE_INSERT : MSSQL_INSERT;
|
||||||
|
const getPropSql = isSqlite ? SQLITE_GET_PROP : MSSQL_GET_PROP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces all properties for a category by its ID.
|
||||||
|
* Enforces exclusive flag on property names.
|
||||||
|
*
|
||||||
|
* @param {number} googleAuthId - The id of the Google auth user.
|
||||||
|
* @param {number} categoryId - The category ID.
|
||||||
|
* @param {Array<{propertyId: number, value: string, sort?: number}>} properties - The properties to set.
|
||||||
|
* @param {object} [tx] - Optional transaction.
|
||||||
|
* @returns {Promise<Array<{id: number, categoryId: number, propertyId: number, name: string, exclusive: boolean, value: string, sort: number}>>}
|
||||||
|
*/
|
||||||
|
return async (googleAuthId, categoryId, properties = [], tx) => {
|
||||||
|
if (!Array.isArray(properties)) {
|
||||||
|
throw new Error('Invalid properties array');
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSet = async (runner) => {
|
||||||
|
const { companyId } = await resolve(googleAuthId, categoryId, runner);
|
||||||
|
|
||||||
|
const validated = [];
|
||||||
|
const seenExclusiveProps = new Set();
|
||||||
|
|
||||||
|
for (let i = 0; i < properties.length; i++) {
|
||||||
|
const item = properties[i];
|
||||||
|
const propertyId = Number(item.propertyId);
|
||||||
|
if (!Number.isInteger(propertyId) || propertyId <= 0) {
|
||||||
|
throw new Error('Invalid propertyId');
|
||||||
|
}
|
||||||
|
|
||||||
|
const propRes = await runner.query(getPropSql, { id: propertyId, companyId });
|
||||||
|
const prop = propRes.rows[0];
|
||||||
|
if (!prop) {
|
||||||
|
throw new Error(`Property not found: ${propertyId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExclusive = Boolean(prop.exclusive);
|
||||||
|
if (isExclusive) {
|
||||||
|
if (seenExclusiveProps.has(propertyId)) {
|
||||||
|
throw new Error(`Property "${prop.name}" is exclusive and can only have one value`);
|
||||||
|
}
|
||||||
|
seenExclusiveProps.add(propertyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = item.value != null ? String(item.value).trim() : '';
|
||||||
|
const sort = item.sort != null && Number.isInteger(item.sort) ? item.sort : i;
|
||||||
|
validated.push({
|
||||||
|
propertyId,
|
||||||
|
name: prop.name,
|
||||||
|
exclusive: isExclusive,
|
||||||
|
value,
|
||||||
|
sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await runner.query(deleteSql, { categoryId });
|
||||||
|
|
||||||
|
const inserted = [];
|
||||||
|
for (const item of validated) {
|
||||||
|
const res = await runner.query(insertSql, {
|
||||||
|
categoryId,
|
||||||
|
propertyId: item.propertyId,
|
||||||
|
value: item.value,
|
||||||
|
sort: item.sort,
|
||||||
|
});
|
||||||
|
const id = res.rows[0]?.id ?? res.insertId;
|
||||||
|
inserted.push({
|
||||||
|
id,
|
||||||
|
categoryId,
|
||||||
|
propertyId: item.propertyId,
|
||||||
|
name: item.name,
|
||||||
|
exclusive: item.exclusive,
|
||||||
|
value: item.value,
|
||||||
|
sort: item.sort,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch category updated_at
|
||||||
|
const touchSql = isSqlite
|
||||||
|
? "UPDATE category SET updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') WHERE id = @categoryId"
|
||||||
|
: 'UPDATE category SET updated_at = SYSUTCDATETIME() WHERE id = @categoryId';
|
||||||
|
await runner.query(touchSql, { categoryId });
|
||||||
|
|
||||||
|
return inserted;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tx) {
|
||||||
|
return runSet(makeRunner(provider, tx));
|
||||||
|
}
|
||||||
|
if (typeof provider?.transaction === 'function') {
|
||||||
|
return provider.transaction(runSet);
|
||||||
|
}
|
||||||
|
return runSet(makeRunner(provider));
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { CREATE_LINE as MSSQL_CREATE_LINE } from '../providers/mssql/queries/manifestDoc.js';
|
import {
|
||||||
import { CREATE_LINE as SQLITE_CREATE_LINE } from '../providers/sqlite3/queries/manifestDoc.js';
|
CHECK_ARTICLE_IS_FATHER as MSSQL_CHECK_ARTICLE_IS_FATHER,
|
||||||
|
CREATE_LINE as MSSQL_CREATE_LINE,
|
||||||
|
} from '../providers/mssql/queries/manifestDoc.js';
|
||||||
|
import {
|
||||||
|
CHECK_ARTICLE_IS_FATHER as SQLITE_CHECK_ARTICLE_IS_FATHER,
|
||||||
|
CREATE_LINE as SQLITE_CREATE_LINE,
|
||||||
|
} from '../providers/sqlite3/queries/manifestDoc.js';
|
||||||
import makeRunner from './runner.js';
|
import makeRunner from './runner.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,7 +30,7 @@ export function normalizeLines(lines) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pu =
|
const pu =
|
||||||
entry?.pu === undefined || entry?.pu === null ? 1 : entry.pu;
|
entry?.pu === undefined || entry?.pu === null ? null : entry.pu;
|
||||||
const amount = entry?.amount;
|
const amount = entry?.amount;
|
||||||
const net = entry?.net;
|
const net = entry?.net;
|
||||||
const rebate =
|
const rebate =
|
||||||
@@ -36,7 +42,7 @@ export function normalizeLines(lines) {
|
|||||||
: String(entry.comment);
|
: String(entry.comment);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0 ||
|
(pu !== null && (typeof pu !== 'number' || !Number.isFinite(pu) || pu <= 0)) ||
|
||||||
typeof amount !== 'number' || !Number.isFinite(amount) ||
|
typeof amount !== 'number' || !Number.isFinite(amount) ||
|
||||||
typeof net !== 'number' || !Number.isFinite(net) ||
|
typeof net !== 'number' || !Number.isFinite(net) ||
|
||||||
typeof rebate !== 'number' || !Number.isFinite(rebate) ||
|
typeof rebate !== 'number' || !Number.isFinite(rebate) ||
|
||||||
@@ -116,8 +122,32 @@ export async function insertLines(
|
|||||||
const { manifestId, lines, ensureTax } = params;
|
const { manifestId, lines, ensureTax } = params;
|
||||||
const isSqlite = runner?.dialect === 'sqlite3';
|
const isSqlite = runner?.dialect === 'sqlite3';
|
||||||
const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE;
|
const createLineSql = isSqlite ? SQLITE_CREATE_LINE : MSSQL_CREATE_LINE;
|
||||||
|
const checkFatherSql = isSqlite ? SQLITE_CHECK_ARTICLE_IS_FATHER : MSSQL_CHECK_ARTICLE_IS_FATHER;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
let resolvedName = line.name;
|
||||||
|
let resolvedPu = line.pu;
|
||||||
|
|
||||||
|
if (line.sku) {
|
||||||
|
const fatherCheck = await runner.query(checkFatherSql, {
|
||||||
|
manifestId,
|
||||||
|
sku: line.sku,
|
||||||
|
});
|
||||||
|
const art = fatherCheck.rows[0];
|
||||||
|
if (art?.isFather) {
|
||||||
|
throw new Error(`A father article cannot be booked on documents (${line.sku})`);
|
||||||
|
}
|
||||||
|
if (!resolvedName && art?.articleName) {
|
||||||
|
resolvedName = art.articleName;
|
||||||
|
}
|
||||||
|
if (resolvedPu == null && art?.pu != null) {
|
||||||
|
resolvedPu = art.pu;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedName = resolvedName || line.sku || null;
|
||||||
|
resolvedPu = resolvedPu ?? 1;
|
||||||
|
|
||||||
if (ensureTax) {
|
if (ensureTax) {
|
||||||
await ensureTax(line.taxrate, runner);
|
await ensureTax(line.taxrate, runner);
|
||||||
}
|
}
|
||||||
@@ -125,8 +155,8 @@ export async function insertLines(
|
|||||||
await runner.query(createLineSql, {
|
await runner.query(createLineSql, {
|
||||||
manifestId,
|
manifestId,
|
||||||
sku: line.sku ?? null,
|
sku: line.sku ?? null,
|
||||||
name: line.name ?? null,
|
name: resolvedName,
|
||||||
pu: line.pu ?? 1,
|
pu: resolvedPu,
|
||||||
amount: line.amount,
|
amount: line.amount,
|
||||||
net: line.net,
|
net: line.net,
|
||||||
comment: line.comment,
|
comment: line.comment,
|
||||||
|
|||||||
27
src/store/posterminal/setMaxKnownTombstoneId.js
Normal file
27
src/store/posterminal/setMaxKnownTombstoneId.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { SET_MAX_KNOWN_TOMBSTONE_ID as MSSQL_SET } from '../../providers/mssql/queries/posterminal.js';
|
||||||
|
import { SET_MAX_KNOWN_TOMBSTONE_ID as SQLITE_SET } from '../../providers/sqlite3/queries/posterminal.js';
|
||||||
|
import makeRunner from '../runner.js';
|
||||||
|
|
||||||
|
export default function setMaxKnownTombstoneId(provider) {
|
||||||
|
const sql = provider?.dialect === 'sqlite3' ? SQLITE_SET : MSSQL_SET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the max known tombstone ID for a POS terminal.
|
||||||
|
*
|
||||||
|
* @param {number} id - The posterminal id.
|
||||||
|
* @param {number|null} maxknowntombstoneid - The highest synced tombstone ID.
|
||||||
|
* @param {object} [tx] - Optional transaction to run within.
|
||||||
|
* @returns {Promise<{id: number, maxknowntombstoneid: number|null}>}
|
||||||
|
*/
|
||||||
|
return async (id, maxknowntombstoneid, tx) => {
|
||||||
|
await makeRunner(provider, tx).query(sql, {
|
||||||
|
id,
|
||||||
|
maxknowntombstoneid: maxknowntombstoneid != null ? Number(maxknowntombstoneid) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
maxknowntombstoneid: maxknowntombstoneid != null ? Number(maxknowntombstoneid) : null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -60,6 +60,11 @@ export default function upsert(provider) {
|
|||||||
throw new Error('Posterminal not found');
|
throw new Error('Posterminal not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxknowntombstoneid =
|
||||||
|
input.maxknowntombstoneid !== undefined
|
||||||
|
? input.maxknowntombstoneid
|
||||||
|
: (existing.rows[0].maxknowntombstoneid ?? null);
|
||||||
|
|
||||||
await runner.query(updateSql, {
|
await runner.query(updateSql, {
|
||||||
id,
|
id,
|
||||||
companyId,
|
companyId,
|
||||||
@@ -68,11 +73,13 @@ export default function upsert(provider) {
|
|||||||
pairingpin,
|
pairingpin,
|
||||||
pairedAt,
|
pairedAt,
|
||||||
clientserial,
|
clientserial,
|
||||||
|
maxknowntombstoneid,
|
||||||
});
|
});
|
||||||
|
|
||||||
return { id, name, token, pairingpin, pairedAt, clientserial };
|
return { id, name, token, pairingpin, pairedAt, clientserial, maxknowntombstoneid };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxknowntombstoneid = input.maxknowntombstoneid ?? null;
|
||||||
const created = await runner.query(createSql, {
|
const created = await runner.query(createSql, {
|
||||||
companyId,
|
companyId,
|
||||||
name,
|
name,
|
||||||
@@ -80,6 +87,7 @@ export default function upsert(provider) {
|
|||||||
pairingpin,
|
pairingpin,
|
||||||
pairedAt,
|
pairedAt,
|
||||||
clientserial,
|
clientserial,
|
||||||
|
maxknowntombstoneid,
|
||||||
});
|
});
|
||||||
|
|
||||||
const row = created.rows[0] ?? {
|
const row = created.rows[0] ?? {
|
||||||
@@ -89,6 +97,7 @@ export default function upsert(provider) {
|
|||||||
pairingpin,
|
pairingpin,
|
||||||
pairedAt,
|
pairedAt,
|
||||||
clientserial,
|
clientserial,
|
||||||
|
maxknowntombstoneid,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -98,6 +107,7 @@ export default function upsert(provider) {
|
|||||||
pairingpin: row.pairingpin,
|
pairingpin: row.pairingpin,
|
||||||
pairedAt: row.pairedAt ?? null,
|
pairedAt: row.pairedAt ?? null,
|
||||||
clientserial: row.clientserial ?? null,
|
clientserial: row.clientserial ?? null,
|
||||||
|
maxknowntombstoneid: row.maxknowntombstoneid ?? null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,47 +123,10 @@ export default function adjust(provider) {
|
|||||||
throw new Error(`A father article cannot have stock (${item.sku})`);
|
throw new Error(`A father article cannot have stock (${item.sku})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const articlelistId = artRes.rows[0]?.articlelist_id;
|
if (artRes.rows[0]?.articlelist_id != null) {
|
||||||
if (articlelistId != null) {
|
throw new Error(`A Stückliste cannot have manual stock adjustments (${item.sku})`);
|
||||||
const compRes = await runner.query(`
|
|
||||||
SELECT
|
|
||||||
ala.article_id AS articleId,
|
|
||||||
ala.count,
|
|
||||||
a.sku,
|
|
||||||
at.name AS articleName
|
|
||||||
FROM articlelistarticle ala
|
|
||||||
INNER JOIN article a ON a.id = ala.article_id
|
|
||||||
LEFT JOIN articletext at ON at.article_id = a.id AND at.language_id = @languageId
|
|
||||||
WHERE ala.articlelist_id = @articlelistId
|
|
||||||
ORDER BY ala.sort ASC, ala.id ASC
|
|
||||||
`, {
|
|
||||||
articlelistId,
|
|
||||||
languageId: resolved.languageId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (compRes.rows.length === 0) {
|
|
||||||
throw new Error(`Stückliste has no items (${item.sku})`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const comp of compRes.rows) {
|
|
||||||
const compCount = Number(comp.count || 1);
|
|
||||||
const compAmount = item.amount * compCount;
|
|
||||||
const compText = item.text
|
|
||||||
? `${item.text} (${item.sku})`
|
|
||||||
: (compAmount > 0
|
|
||||||
? `Manual stock addition (+${compAmount}) [${item.sku}]`
|
|
||||||
: `Manual stock removal (${compAmount}) [${item.sku}]`);
|
|
||||||
|
|
||||||
resolvedArticles.push({
|
|
||||||
sku: comp.sku,
|
|
||||||
warehouseId: item.warehouseId,
|
|
||||||
amount: compAmount,
|
|
||||||
text: compText,
|
|
||||||
articleId: comp.articleId,
|
|
||||||
articleName: comp.articleName || comp.sku,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, {
|
const nameRes = await runner.query(GET_ARTICLE_NAME_SQL, {
|
||||||
articleId: resolved.articleId,
|
articleId: resolved.articleId,
|
||||||
languageId: resolved.languageId,
|
languageId: resolved.languageId,
|
||||||
@@ -171,7 +134,6 @@ export default function adjust(provider) {
|
|||||||
const articleName = nameRes.rows[0]?.name ?? item.sku;
|
const articleName = nameRes.rows[0]?.name ?? item.sku;
|
||||||
resolvedArticles.push({ ...item, ...resolved, articleName });
|
resolvedArticles.push({ ...item, ...resolved, articleName });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Allocate unique adjustment number using the dedicated adjustment number scheme
|
// Allocate unique adjustment number using the dedicated adjustment number scheme
|
||||||
const number = await allocateNumber(companyId, ADJUSTMENT_TYPE, activeTx);
|
const number = await allocateNumber(companyId, ADJUSTMENT_TYPE, activeTx);
|
||||||
|
|||||||
Reference in New Issue
Block a user