This commit is contained in:
seb
2026-09-30 23:11:04 +02:00
parent 74ed74fc68
commit 9c770d765e
8 changed files with 202 additions and 56 deletions

View File

@@ -85,7 +85,7 @@ const article = await store.article.createUnderParent({ googleAuthId: 42, parent
*Source: `src/store/article/delete.js`* *Source: `src/store/article/delete.js`*
Deletes an article by its SKU, refusing when it still has children. Deletes an article by its SKU, refusing when it still has children unless cascade is true.
**Parameters** **Parameters**
@@ -93,7 +93,8 @@ Deletes an article by its SKU, refusing when it still has children.
| --- | --- | --- | | --- | --- | --- |
| `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. |
| `[tx]` | `object` | Optional transaction to run within. | | `[optionsOrTx]` | `object|*` | Options object ({ cascade?: boolean }) or optional transaction. |
| `[maybeTx]` | `object` | Optional transaction if options was passed. |
**Returns** **Returns**
@@ -101,12 +102,13 @@ Deletes an article by its SKU, refusing when it still has children.
**Throws** **Throws**
`Error` — If the article has children or is not found. `Error` — If the article has children (when cascade=false) or is not found.
**Example** **Example**
```js ```js
await store.article.delete(42, 'SKU-1'); await store.article.delete(42, 'SKU-1');
await store.article.delete(42, 'SKU-1', { cascade: true });
``` ```
## `article.deleteImage` ## `article.deleteImage`
@@ -353,6 +355,7 @@ Removes a child article from a father article, either by unlinking or deleting i
| `childSku` | `string` | The child article SKU. | | `childSku` | `string` | The child article SKU. |
| `[options]` | `object` | | | `[options]` | `object` | |
| `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. | | `[options.deleteArticle=false]` | `boolean` | Whether to delete the child article entity entirely. |
| `[options.cascade=true]` | `boolean` | Whether to cascade delete child subtrees when deleteArticle is true. |
| `[tx]` | `object` | Optional transaction. | | `[tx]` | `object` | Optional transaction. |
**Returns** **Returns**

View File

@@ -107,7 +107,7 @@ const suppliers = await store.articlesupplier.listByArticleSku(42, 'SKU-1');
*Source: `src/store/articlesupplier/listByEntityAndSkus.js`* *Source: `src/store/articlesupplier/listByEntityAndSkus.js`*
Lists article-supplier assignments for a given supplier (entityId) and SKU list, including linked article details (name, slug, photos/articlepic, eans/articleean). Lists article-supplier assignments for a given supplier (entityId) and SKU list, including linked article details (name, slug, photos, eans).
**Parameters** **Parameters**
@@ -120,7 +120,7 @@ Lists article-supplier assignments for a given supplier (entityId) and SKU list,
**Returns** **Returns**
`Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>` — The supplier assignments with article details. `Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[]}}>>` — The supplier assignments with article details.
**Throws** **Throws**

View File

@@ -76,7 +76,7 @@ Deletes a category, refusing to delete the company's root category.
**Throws** **Throws**
`Error` — If the category is the root category. `Error` — If the category is the root category or contains articles/subcategories.
**Example** **Example**

6
API.md
View File

@@ -86,7 +86,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `addChild(googleAuthId: number, fatherSku: string, options: {attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<object>` - `addChild(googleAuthId: number, fatherSku: string, options: {attributeValueId?: number, value?: string, name?: string, unitamount?: number, weight?: number, track_stock?: boolean|number}, tx?: object): Promise<object>`
- `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>` - `addImage(googleAuthId: number, sku: string, image: Buffer, tx?: object): Promise<{picId: number, sort: number, full: {w: number, h: number, size: number}, thumb: {w: number, h: number}, fullHash: string, thumbHash: string}>`
- `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>` - `createUnderParent(input: {googleAuthId: number, parentId: number, name: string, track_stock?: boolean|number}, tx?: object): Promise<{id: number, parentId: number, name: string, sku: string, track_stock: boolean}>`
- `delete(googleAuthId: number, sku: string, tx?: 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, tx?: object): Promise<Array<{id: number, companyuserId: number|null, date: string, text: string}>>`
@@ -95,7 +95,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `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}, 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}>`
- `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>`
@@ -162,7 +162,7 @@ The store is exposed as a `LibDashStore` instance (see `src/index.js`). Each met
- `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>` - `create(googleAuthId: number, sku: string, input: {entityId: number, sku?: string, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>`
- `delete(googleAuthId: number, sku: string, id: number, tx?: object): Promise<void>` - `delete(googleAuthId: number, sku: string, id: number, tx?: object): Promise<void>`
- `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<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}>>` - `listByArticleSku(googleAuthId: number, sku: string, tx?: object): Promise<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}>>`
- `listByEntityAndSkus(googleAuthId: number, entityIdOrOptions: number|{entityId?: number, supplierId?: number, skus?: string[]|string}, maybeSkus?: string[]|string, tx?: object): Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, articlepic: Array<{sort: number, thumbhash: string, hash: string, thumbw: number, thumbh: number}>, eans: string[], articleean: string[]}}>>` - `listByEntityAndSkus(googleAuthId: number, entityIdOrOptions: number|{entityId?: number, supplierId?: number, skus?: string[]|string}, maybeSkus?: string[]|string, tx?: object): Promise<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, article: {id: number, sku: string, name: string|null, slug: string|null, photos: Array<{sort: number, fullHash: string, thumbW: number, thumbH: number, thumbHash: string}>, eans: string[]}}>>`
- `order(googleAuthId: number, sku: string, input: {ids: Array<number>}, tx?: object): Promise<{ids: Array<number>}>` - `order(googleAuthId: number, sku: string, input: {ids: Array<number>}, tx?: object): Promise<{ids: Array<number>}>`
- `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>` - `update(googleAuthId: number, sku: string, id: number, patch: {sku?: string|null, pu?: number, stock?: number|null, sort?: number, netPrice?: number|null, taxId?: number, taxRate?: number}, tx?: object): Promise<{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}>`
- [delivery](API.delivery.md) - [delivery](API.delivery.md)

View File

@@ -4,6 +4,8 @@ import { GET_CHILD_COUNT as MSSQL_GET_CHILD_COUNT } from '../../providers/mssql/
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 * as mssqlAttr from '../../providers/mssql/queries/attributename.js'; import * as mssqlAttr from '../../providers/mssql/queries/attributename.js';
import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.js'; import * as sqliteAttr from '../../providers/sqlite3/queries/attributename.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 './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';
@@ -16,6 +18,7 @@ export default function toggleFather(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 attrQueries = isSqlite ? sqliteAttr : mssqlAttr; const attrQueries = isSqlite ? sqliteAttr : mssqlAttr;
const insertHistorySql = isSqlite ? SQLITE_INSERT_HISTORY : MSSQL_INSERT_HISTORY;
/** /**
* Toggles an article into or out of father mode. * Toggles an article into or out of father mode.
@@ -31,7 +34,7 @@ export default function toggleFather(provider) {
*/ */
return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => { return async (googleAuthId, sku, { isFather, attributeNameId, fatherAttributeNameId } = {}, tx) => {
const run = async (runner) => { const run = async (runner) => {
const { articleId, companyId } = await resolve(googleAuthId, sku, runner); const { articleId, companyId, companyuserId } = await resolve(googleAuthId, sku, runner);
const targetIsFather = Boolean(isFather); const targetIsFather = Boolean(isFather);
const targetAttrId = attributeNameId ?? fatherAttributeNameId; const targetAttrId = attributeNameId ?? fatherAttributeNameId;
@@ -78,6 +81,18 @@ export default function toggleFather(provider) {
throw new Error('An article with assigned suppliers cannot become a father article'); throw new Error('An article with assigned suppliers cannot become a father article');
} }
// Fetch existing prices before deletion to record in articlehistory
const existingPricesRes = await runner.query(
`SELECT pr.net, pr.minmultiplier, t.rate AS taxRate, cg.name AS customerGroupName
FROM price pr
LEFT JOIN tax t ON t.id = pr.tax_id
LEFT JOIN customergroup cg ON cg.id = pr.customergroup_id
WHERE pr.article_id = @articleId
ORDER BY pr.customergroup_id, pr.minmultiplier`,
{ articleId },
);
const existingPrices = existingPricesRes.rows;
await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId }); await runner.query('DELETE FROM price WHERE article_id = @articleId', { articleId });
await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId }); await runner.query('UPDATE article SET track_stock = 0 WHERE id = @articleId', { articleId });
@@ -107,6 +122,7 @@ export default function toggleFather(provider) {
if (!attrRes.rows[0]) { if (!attrRes.rows[0]) {
throw new Error('Attribute name not found'); throw new Error('Attribute name not found');
} }
const attributeName = attrRes.rows[0].name;
// Verify article does not have an attribute value for this attribute // Verify article does not have an attribute value for this attribute
const existingValRes = await runner.query( const existingValRes = await runner.query(
@@ -124,6 +140,35 @@ export default function toggleFather(provider) {
isFather: 1, isFather: 1,
fatherAttributeNameId: Number(targetAttrId), fatherAttributeNameId: Number(targetAttrId),
}); });
// Add history entry recording father conversion and deleted prices
const historyLines = [
`Artikel zu Vaterartikel konvertiert (Varianten-Attribut: "${attributeName}")`,
];
if (existingPrices.length > 0) {
const priceParts = existingPrices.map((p) => {
let str = `${p.net} €`;
if (p.taxRate != null) {
str += ` (${p.taxRate}% MwSt.)`;
}
if (p.minmultiplier > 1) {
str += ` ab ${p.minmultiplier} Stk.`;
}
if (p.customerGroupName) {
str += ` [${p.customerGroupName}]`;
}
return str;
});
const label = existingPrices.length === 1 ? 'Bisheriger Preis gelöscht' : 'Bisherige Preise gelöscht';
historyLines.push(`${label}: ${priceParts.join(', ')}`);
}
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: historyLines.join('\n'),
});
} else { } else {
// Disabling father: verify zero children // Disabling father: verify zero children
const childCountRes = await runner.query(getChildCountSql, { articleId }); const childCountRes = await runner.query(getChildCountSql, { articleId });
@@ -137,6 +182,12 @@ export default function toggleFather(provider) {
isFather: 0, isFather: 0,
fatherAttributeNameId: null, fatherAttributeNameId: null,
}); });
await runner.query(insertHistorySql, {
articleId,
companyuserId,
text: 'Vaterartikel-Modus deaktiviert',
});
} }
return load(googleAuthId, sku, runner); return load(googleAuthId, sku, runner);

View File

@@ -94,17 +94,42 @@ export default function setItemsByArticleSku(provider) {
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const item = items[i]; const item = items[i];
let itemArticleId = item?.articleId != null ? Number(item.articleId) : null; if (!item || typeof item !== 'object') {
throw new Error(`Item at index ${i} must be an object`);
if (itemArticleId == null && item?.sku) {
const res = await runner.query(
'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId',
{ itemSku: item.sku, companyId },
);
itemArticleId = res.rows[0]?.id;
} }
if (!itemArticleId || Number.isNaN(itemArticleId) || itemArticleId <= 0) { let itemArticleId = null;
if (item.articleId !== undefined && item.articleId !== null) {
if (
typeof item.articleId !== 'number' ||
!Number.isInteger(item.articleId) ||
item.articleId <= 0
) {
throw new Error(`Item at index ${i} must specify a valid articleId or sku`);
}
const res = await runner.query(
'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId',
{ itemArticleId: item.articleId, companyId },
);
if (!res.rows[0]) {
throw new Error(`Article not found for component: ${item.articleId}`);
}
itemArticleId = res.rows[0].id;
} else if (item.sku !== undefined && item.sku !== null) {
if (typeof item.sku !== 'string' || !item.sku.trim()) {
throw new Error(`Item at index ${i} must specify a valid articleId or sku`);
}
const itemSku = item.sku.trim();
const res = await runner.query(
'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId',
{ itemSku, companyId },
);
if (!res.rows[0]) {
throw new Error(`Article not found for component: ${itemSku}`);
}
itemArticleId = res.rows[0].id;
} else {
throw new Error(`Item at index ${i} must specify a valid articleId or sku`); throw new Error(`Item at index ${i} must specify a valid articleId or sku`);
} }
@@ -117,16 +142,7 @@ export default function setItemsByArticleSku(provider) {
} }
seenArticleIds.add(itemArticleId); seenArticleIds.add(itemArticleId);
// Verify component article exists in same company const rawCount = item?.count === undefined ? 1 : item.count;
const itemArtRes = await runner.query(
'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId',
{ itemArticleId, companyId },
);
if (!itemArtRes.rows[0]) {
throw new Error(`Article not found for component: ${itemArticleId}`);
}
const rawCount = item?.count;
if ( if (
typeof rawCount !== 'number' || typeof rawCount !== 'number' ||
!Number.isInteger(rawCount) || !Number.isInteger(rawCount) ||
@@ -136,7 +152,13 @@ export default function setItemsByArticleSku(provider) {
throw new Error(`Invalid count for Stückliste item at index ${i}: must be a positive integer`); throw new Error(`Invalid count for Stückliste item at index ${i}: must be a positive integer`);
} }
const count = rawCount; const count = rawCount;
let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i;
if (item.sort !== undefined && item.sort !== null) {
if (typeof item.sort !== 'number' || !Number.isInteger(item.sort)) {
throw new Error(`Invalid sort for Stückliste item at index ${i}: must be an integer`);
}
}
let sort = (typeof item?.sort === 'number' && Number.isInteger(item.sort)) ? item.sort : i;
while (seenSorts.has(sort)) { while (seenSorts.has(sort)) {
sort++; sort++;

View File

@@ -15,18 +15,63 @@ export default function del(provider) {
* @param {number} input.id - The id of the category to delete. * @param {number} input.id - The id of the category to delete.
* @param {object} [tx] - Optional transaction to run within. * @param {object} [tx] - Optional transaction to run within.
* @returns {Promise<void>} * @returns {Promise<void>}
* @throws {Error} If the category is the root category. * @throws {Error} If the category is the root category or contains articles/subcategories.
* *
* @example * @example
* await store.category.delete({ googleAuthId: 42, id: 5 }); * await store.category.delete({ googleAuthId: 42, id: 5 });
*/ */
return async ({ googleAuthId, id }, tx) => { return async ({ googleAuthId, id }, tx) => {
const { rootCategoryId } = await access(googleAuthId, id, tx); const run = async (runner) => {
const { rootCategoryId } = await access(googleAuthId, id, runner);
if (id === rootCategoryId) { if (id === rootCategoryId) {
throw new Error('Cannot delete root category'); throw new Error('Cannot delete root category');
}
const childRes = await runner.query(
'SELECT id FROM category WHERE parent_id = @id',
{ id },
);
if (childRes.rows?.length > 0) {
throw new Error('Category has subcategories');
}
const artRes = await runner.query(
'SELECT article_id FROM categoryarticle WHERE category_id = @id',
{ id },
);
if (artRes.rows?.length > 0) {
throw new Error('Category has articles');
}
const posRes = await runner.query(
'SELECT posterminal_id FROM posterminalcategory WHERE category_id = @id',
{ id },
);
if (posRes.rows?.length > 0) {
throw new Error('Category is assigned to POS terminals');
}
try {
await runner.query(sql, { id });
} catch (err) {
if (
err.message?.includes('FOREIGN KEY constraint failed') ||
err.message?.includes('foreign key constraint') ||
err.message?.includes('REFERENCE constraint')
) {
throw new Error('Category has articles');
}
throw err;
}
};
if (tx) {
return run(makeRunner(provider, tx));
} }
if (typeof provider?.transaction === 'function') {
await makeRunner(provider, tx).query(sql, { id }); return provider.transaction(run);
}
return run(makeRunner(provider));
}; };
} }

View File

@@ -94,17 +94,42 @@ export default function setOptionsByArticleSku(provider) {
for (let i = 0; i < options.length; i++) { for (let i = 0; i < options.length; i++) {
const item = options[i]; const item = options[i];
let itemArticleId = item?.articleId != null ? Number(item.articleId) : null; if (!item || typeof item !== 'object') {
throw new Error(`Option at index ${i} must be an object`);
if (itemArticleId == null && item?.sku) {
const res = await runner.query(
'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId',
{ itemSku: item.sku, companyId },
);
itemArticleId = res.rows[0]?.id;
} }
if (!itemArticleId || Number.isNaN(itemArticleId) || itemArticleId <= 0) { let itemArticleId = null;
if (item.articleId !== undefined && item.articleId !== null) {
if (
typeof item.articleId !== 'number' ||
!Number.isInteger(item.articleId) ||
item.articleId <= 0
) {
throw new Error(`Option at index ${i} must specify a valid articleId or sku`);
}
const res = await runner.query(
'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId',
{ itemArticleId: item.articleId, companyId },
);
if (!res.rows[0]) {
throw new Error(`Article not found for option: ${item.articleId}`);
}
itemArticleId = res.rows[0].id;
} else if (item.sku !== undefined && item.sku !== null) {
if (typeof item.sku !== 'string' || !item.sku.trim()) {
throw new Error(`Option at index ${i} must specify a valid articleId or sku`);
}
const itemSku = item.sku.trim();
const res = await runner.query(
'SELECT id FROM article WHERE sku = @itemSku AND company_id = @companyId',
{ itemSku, companyId },
);
if (!res.rows[0]) {
throw new Error(`Article not found for option: ${itemSku}`);
}
itemArticleId = res.rows[0].id;
} else {
throw new Error(`Option at index ${i} must specify a valid articleId or sku`); throw new Error(`Option at index ${i} must specify a valid articleId or sku`);
} }
@@ -117,16 +142,7 @@ export default function setOptionsByArticleSku(provider) {
} }
seenArticleIds.add(itemArticleId); seenArticleIds.add(itemArticleId);
// Verify component article exists in same company const rawCount = item?.count === undefined ? 1 : item.count;
const itemArtRes = await runner.query(
'SELECT id FROM article WHERE id = @itemArticleId AND company_id = @companyId',
{ itemArticleId, companyId },
);
if (!itemArtRes.rows[0]) {
throw new Error(`Article not found for option: ${itemArticleId}`);
}
const rawCount = item?.count;
if ( if (
typeof rawCount !== 'number' || typeof rawCount !== 'number' ||
!Number.isInteger(rawCount) || !Number.isInteger(rawCount) ||
@@ -136,13 +152,22 @@ export default function setOptionsByArticleSku(provider) {
throw new Error(`Invalid count for options item at index ${i}: must be a positive integer`); throw new Error(`Invalid count for options item at index ${i}: must be a positive integer`);
} }
const count = rawCount; const count = rawCount;
let sort = item?.sort != null && Number.isInteger(Number(item.sort)) ? Number(item.sort) : i;
if (item.sort !== undefined && item.sort !== null) {
if (typeof item.sort !== 'number' || !Number.isInteger(item.sort)) {
throw new Error(`Invalid sort for options item at index ${i}: must be an integer`);
}
}
let sort = (typeof item?.sort === 'number' && Number.isInteger(item.sort)) ? item.sort : i;
while (seenSorts.has(sort)) { while (seenSorts.has(sort)) {
sort++; sort++;
} }
seenSorts.add(sort); seenSorts.add(sort);
if (item.mandated !== undefined && item.mandated !== null && typeof item.mandated !== 'boolean') {
throw new Error(`Invalid mandated for options item at index ${i}: must be a boolean`);
}
const mandated = Boolean(item?.mandated); const mandated = Boolean(item?.mandated);
validated.push({ validated.push({