import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { createTestStore, createTenant, createArticle, makePng } from './helpers.js'; describe('store.article', () => { let ctx; let store; let tenant; let other; let gid; before(async () => { ctx = await createTestStore('dash-article-'); store = ctx.store; tenant = await createTenant(store, { name: 'Article Test GmbH' }); other = await createTenant(store, { name: 'Article Other GmbH' }); gid = tenant.googleAuthId; }); after(async () => { await ctx.cleanup(); }); const get = async (sku, who = gid) => (await store.article.getBySku(who, sku)).article; const historyTexts = async (sku) => (await store.article.getHistoryBySku(gid, sku)).map((h) => h.text); describe('createUnderParent', () => { it('creates an article with auto-SKU, default unit and untracked stock', async () => { const art = await createArticle(store, tenant, { name: 'Cola Zero', desc: '

Lecker

', seo: 'cola' }); assert.equal(typeof art.id, 'number'); assert.equal(art.parentId, tenant.rootCategoryId); assert.equal(art.name, 'Cola Zero'); assert.equal(art.track_stock, false); assert.ok(typeof art.sku === 'string' && art.sku.length > 0); const detail = await get(art.sku); assert.equal(detail.id, art.id); assert.equal(detail.name, 'Cola Zero'); assert.equal(detail.slug, 'cola-zero'); assert.equal(detail.desc, '

Lecker

'); assert.equal(detail.seo, 'cola'); assert.equal(detail.unitamount, 1); assert.equal(detail.weight, 0); assert.equal(detail.track_stock, false); assert.deepEqual(detail.categoryIds, [tenant.rootCategoryId]); assert.deepEqual(detail.eans, []); assert.deepEqual(detail.prices, []); assert.deepEqual(detail.photos, []); assert.equal(detail.brand, null); assert.equal(detail.parentArticleId, null); assert.ok(detail.unit.si, 'default unit is assigned'); }); it('allocates distinct SKUs and unique slugs for equal names', async () => { const a = await createArticle(store, tenant, { name: 'Dupe Name' }); const b = await createArticle(store, tenant, { name: 'Dupe Name' }); assert.notEqual(a.sku, b.sku); assert.equal((await get(a.sku)).slug, 'dupe-name'); assert.equal((await get(b.sku)).slug, 'dupe-name-2'); }); it('accepts an explicit SKU and rejects duplicates', async () => { const art = await createArticle(store, tenant, { sku: 'EXPLICIT-1' }); assert.equal(art.sku, 'EXPLICIT-1'); await assert.rejects( () => createArticle(store, tenant, { sku: 'EXPLICIT-1' }), { message: 'SKU "EXPLICIT-1" already exists' }, ); }); it('records a creation history entry', async () => { const art = await createArticle(store, tenant, { name: 'History Born' }); const history = await store.article.getHistoryBySku(gid, art.sku); assert.ok(history.length >= 1); assert.ok(history.every((h) => typeof h.text === 'string' && h.date)); }); it('rejects a parent category of another company', async () => { await assert.rejects( () => createArticle(store, tenant, { parentId: other.rootCategoryId }), { message: 'Not authorized' }, ); }); }); describe('stock tracking', () => { it('cannot enable tracking without a warehouse', async () => { const fresh = await createTenant(store, { name: 'No Warehouse GmbH' }); const msg = { message: 'At least one warehouse is required to enable stock tracking' }; await assert.rejects(() => createArticle(store, fresh, { track_stock: true }), msg); const art = await createArticle(store, fresh); await assert.rejects(() => store.article.setTrackStockBySku(fresh.googleAuthId, art.sku, true), msg); await assert.rejects( () => store.article.updateBySku(fresh.googleAuthId, art.sku, { track_stock: true }), msg, ); }); it('setTrackStockBySku toggles the flag and validates input', async () => { await store.warehouse.create(gid, { name: 'Article WH' }); const art = await createArticle(store, tenant); const on = await store.article.setTrackStockBySku(gid, art.sku, true); assert.equal(on.track_stock, true); assert.equal((await get(art.sku)).track_stock, true); const off = await store.article.setTrackStockBySku(gid, art.sku, 0); assert.equal(off.track_stock, false); await assert.rejects( () => store.article.setTrackStockBySku(gid, art.sku, 'yes'), { message: 'Invalid track_stock' }, ); }); }); describe('updateBySku', () => { it('updates name, desc, seo and unitamount and records history', async () => { const art = await createArticle(store, tenant, { name: 'Before' }); await store.article.updateBySku(gid, art.sku, { name: 'After', desc: 'new desc', seo: 'new seo', unitamount: 0.5, }); const detail = await get(art.sku); assert.equal(detail.name, 'After'); assert.equal(detail.slug, 'after'); assert.equal(detail.desc, 'new desc'); assert.equal(detail.seo, 'new seo'); assert.equal(detail.unitamount, 0.5); const texts = await historyTexts(art.sku); assert.ok(texts.some((t) => t.includes('Name geändert von "Before" auf "After"'))); }); it('validates the patch', async () => { const art = await createArticle(store, tenant); const upd = (patch) => store.article.updateBySku(gid, art.sku, patch); await assert.rejects(() => upd({}), { message: 'Name required' }); await assert.rejects(() => upd({ name: ' ' }), { message: 'Name required' }); await assert.rejects(() => upd({ unitamount: 0 }), { message: 'Invalid unitamount' }); await assert.rejects(() => upd({ unitamount: Number.NaN }), { message: 'Invalid unitamount' }); await assert.rejects(() => upd({ track_stock: 'x' }), { message: 'Invalid track_stock' }); await assert.rejects(() => upd({ unitSi: 'does-not-exist' }), { message: 'Unit not found' }); }); it('rejects unknown SKUs', async () => { await assert.rejects( () => store.article.updateBySku(gid, 'NOPE-404', { name: 'x' }), { message: 'Article not found' }, ); }); }); describe('setEansBySku', () => { it('replaces the EAN list (trimmed)', async () => { const art = await createArticle(store, tenant); let res = await store.article.setEansBySku(gid, art.sku, [' 4001234567890 ', '4001234567891']); assert.deepEqual(res.eans, ['4001234567890', '4001234567891']); res = await store.article.setEansBySku(gid, art.sku, ['4001234567892']); assert.deepEqual(res.eans, ['4001234567892']); assert.deepEqual((await get(art.sku)).eans, ['4001234567892']); res = await store.article.setEansBySku(gid, art.sku, []); assert.deepEqual(res.eans, []); }); it('rejects invalid input', async () => { const art = await createArticle(store, tenant); const err = { message: 'Invalid eans' }; await assert.rejects(() => store.article.setEansBySku(gid, art.sku, 'x'), err); await assert.rejects(() => store.article.setEansBySku(gid, art.sku, ['']), err); await assert.rejects(() => store.article.setEansBySku(gid, art.sku, [123]), err); await assert.rejects(() => store.article.setEansBySku(gid, art.sku, ['1', ' 1 ']), err); }); it('rejects an EAN used by another article of the company', async () => { const a = await createArticle(store, tenant); const b = await createArticle(store, tenant); await store.article.setEansBySku(gid, a.sku, ['4009999999999']); await assert.rejects( () => store.article.setEansBySku(gid, b.sku, ['4009999999999']), { message: 'EAN already in use' }, ); assert.deepEqual((await get(b.sku)).eans, [], 'failed write persisted nothing'); }); }); describe('setPricesBySku', () => { it('stores net prices with tax rate and minmultiplier', async () => { const art = await createArticle(store, tenant); const res = await store.article.setPricesBySku(gid, art.sku, [ { net: 10, taxRate: 19 }, { net: 9, tax: 19, minmultiplier: 10 }, ]); assert.equal(res.prices.length, 2); assert.deepEqual( res.prices.map((p) => [p.minmultiplier, p.net, p.taxRate]), [[1, 10, 19], [10, 9, 19]], ); }); it('converts gross to net', async () => { const art = await createArticle(store, tenant); const res = await store.article.setPricesBySku(gid, art.sku, [{ gross: 11.9, taxRate: 19 }]); assert.equal(res.prices.length, 1); assert.equal(res.prices[0].net, 10); }); it('replaces previous prices', async () => { const art = await createArticle(store, tenant); await store.article.setPricesBySku(gid, art.sku, [{ net: 1, taxRate: 7 }]); const res = await store.article.setPricesBySku(gid, art.sku, [{ net: 2, taxRate: 7 }]); assert.deepEqual(res.prices.map((p) => p.net), [2]); }); it('rejects invalid prices', async () => { const art = await createArticle(store, tenant); const err = { message: 'Invalid prices' }; const set = (prices) => store.article.setPricesBySku(gid, art.sku, prices); await assert.rejects(() => set('x'), err); await assert.rejects(() => set([{ net: 1 }]), err); await assert.rejects(() => set([{ net: 1, taxRate: -1 }]), err); await assert.rejects(() => set([{ taxRate: 19 }]), err); await assert.rejects(() => set([{ net: -1, taxRate: 19 }]), err); await assert.rejects(() => set([{ net: 1, taxRate: 19, minmultiplier: 0 }]), err); await assert.rejects(() => set([{ net: 1, taxRate: 19 }, { net: 2, taxRate: 19 }]), err); }); }); describe('setCategoryIdsBySku', () => { it('replaces category membership', async () => { const art = await createArticle(store, tenant); const cat = await store.category.createUnderParent({ googleAuthId: gid, parentId: tenant.rootCategoryId, name: 'Art Cat' }); const res = await store.article.setCategoryIdsBySku(gid, art.sku, [cat.id, tenant.rootCategoryId]); assert.deepEqual([...res.categoryIds].sort(), [cat.id, tenant.rootCategoryId].sort()); await store.article.setCategoryIdsBySku(gid, art.sku, [cat.id]); assert.deepEqual((await get(art.sku)).categoryIds, [cat.id]); }); it('validates the category ids', async () => { const art = await createArticle(store, tenant); const set = (ids) => store.article.setCategoryIdsBySku(gid, art.sku, ids); await assert.rejects(() => set('x'), { message: 'Invalid categoryIds' }); await assert.rejects(() => set([]), { message: 'Article must have at least one category' }); await assert.rejects(() => set([tenant.rootCategoryId, tenant.rootCategoryId]), { message: 'Invalid categoryIds' }); await assert.rejects(() => set([other.rootCategoryId]), /Invalid categoryIds|Not authorized/); assert.deepEqual((await get(art.sku)).categoryIds, [tenant.rootCategoryId]); }); }); describe('setWeightBySku', () => { it('sets the weight and validates it', async () => { const art = await createArticle(store, tenant); const res = await store.article.setWeightBySku(gid, art.sku, 1.5); assert.equal(res.weight, 1.5); assert.equal((await get(art.sku)).weight, 1.5); await assert.rejects(() => store.article.setWeightBySku(gid, art.sku, -1), { message: 'Invalid weight' }); await assert.rejects(() => store.article.setWeightBySku(gid, art.sku, 'heavy'), { message: 'Invalid weight' }); }); }); describe('setBrandIdBySku', () => { it('sets and clears the brand', async () => { const art = await createArticle(store, tenant); const brand = await store.brand.create(gid, { name: 'Acme' }); await store.article.setBrandIdBySku(gid, art.sku, brand.id); let detail = await get(art.sku); assert.equal(detail.brand.id, brand.id); assert.equal(detail.brand.name, 'Acme'); await store.article.setBrandIdBySku(gid, art.sku, null); detail = await get(art.sku); assert.equal(detail.brand, null); }); it('rejects invalid and foreign brands', async () => { const art = await createArticle(store, tenant); const foreignBrand = await store.brand.create(other.googleAuthId, { name: 'Foreign Brand' }); await assert.rejects(() => store.article.setBrandIdBySku(gid, art.sku, 'x'), { message: 'Invalid brandId' }); await assert.rejects(() => store.article.setBrandIdBySku(gid, art.sku, 0), { message: 'Invalid brandId' }); await assert.rejects(() => store.article.setBrandIdBySku(gid, art.sku, 999999), { message: 'Brand not found' }); await assert.rejects(() => store.article.setBrandIdBySku(gid, art.sku, foreignBrand.id), { message: 'Brand not found' }); }); }); describe('getHistoryBySku', () => { it('honours the limit option', async () => { const art = await createArticle(store, tenant, { name: 'H0' }); await store.article.updateBySku(gid, art.sku, { name: 'H1' }); await store.article.updateBySku(gid, art.sku, { name: 'H2' }); const all = await store.article.getHistoryBySku(gid, art.sku); assert.ok(all.length >= 3); const limited = await store.article.getHistoryBySku(gid, art.sku, { limit: 1 }); assert.equal(limited.length, 1); }); }); describe('listSearchIndex / listExport', () => { it('includes sku, name, eans and categories', async () => { const art = await createArticle(store, tenant, { name: 'Indexed' }); await store.article.setEansBySku(gid, art.sku, ['4000000000017']); const index = await store.article.listSearchIndex(gid); const entry = index.find((e) => e.sku === art.sku); assert.ok(entry); assert.equal(entry.name, 'Indexed'); assert.deepEqual(entry.eans, ['4000000000017']); assert.deepEqual(entry.categoryIds, [tenant.rootCategoryId]); const exported = await store.article.listExport(gid, null); assert.ok(exported.some((e) => e.sku === art.sku)); }); it('scopes export to a category', async () => { const cat = await store.category.createUnderParent({ googleAuthId: gid, parentId: tenant.rootCategoryId, name: 'Export Cat' }); const inside = await createArticle(store, tenant, { parentId: cat.id }); const outside = await createArticle(store, tenant); const exported = await store.article.listExport(gid, cat.id); const skus = exported.map((e) => e.sku); assert.ok(skus.includes(inside.sku)); assert.ok(!skus.includes(outside.sku)); }); it('does not leak articles of other companies', async () => { const foreign = await createArticle(store, other, { name: 'Foreign Indexed', sku: 'FOREIGN-INDEXED' }); const index = await store.article.listSearchIndex(gid); assert.ok(!index.some((e) => e.sku === foreign.sku)); await assert.rejects(() => store.article.listExport(gid, other.rootCategoryId), /Not authorized|not found/); }); }); describe('delete', () => { it('deletes an article', async () => { const art = await createArticle(store, tenant); await store.article.delete(gid, art.sku); await assert.rejects(() => get(art.sku), { message: 'Article not found' }); }); it('rejects unknown SKUs', async () => { await assert.rejects(() => store.article.delete(gid, 'NOPE-404'), { message: 'Article not found' }); }); it('cannot delete articles of another company', async () => { const foreign = await createArticle(store, other, { sku: 'FOREIGN-DELETE' }); await assert.rejects(() => store.article.delete(gid, foreign.sku), /Article not found|Not authorized/); assert.ok(await get(foreign.sku, other.googleAuthId)); }); }); describe('variants (parent / child)', () => { let attr; before(async () => { attr = await store.attributename.create(gid, { name: 'Farbe' }); }); const makeParent = async (name = 'T-Shirt') => { const parent = await createArticle(store, tenant, { name }); await store.article.toggleParent(gid, parent.sku, { isParent: true, attributeNameId: attr.id }); return parent; }; it('toggleParent requires an attribute name', async () => { const art = await createArticle(store, tenant); await assert.rejects( () => store.article.toggleParent(gid, art.sku, { isParent: true }), { message: 'attributeNameId is required to become a parent article' }, ); }); it('addChild creates sequential child SKUs and listChildren lists them', async () => { const parent = await makeParent(); const red = await store.article.addChild(gid, parent.sku, { value: 'Rot' }); const blue = await store.article.addChild(gid, parent.sku, { value: 'Blau' }); assert.equal(red.article.sku, `${parent.sku}-1`); assert.equal(blue.article.sku, `${parent.sku}-2`); const children = await store.article.listChildren(gid, parent.sku); assert.deepEqual(children.map((c) => c.sku), [red.article.sku, blue.article.sku]); assert.deepEqual(children.map((c) => c.parentsort), [0, 1]); assert.deepEqual(children.map((c) => c.attributeValue), ['Rot', 'Blau']); const detail = await get(parent.sku); assert.equal(detail.isParent, true); assert.equal(detail.childCount, 2); assert.equal((await get(red.article.sku)).parentSku, parent.sku); }); it('addChild rejects duplicates, explicit SKUs and non-parents', async () => { const parent = await makeParent('Hoodie'); await store.article.addChild(gid, parent.sku, { value: 'Rot' }); await assert.rejects( () => store.article.addChild(gid, parent.sku, { value: 'Rot' }), { message: 'Variant with this attribute value already exists' }, ); await assert.rejects( () => store.article.addChild(gid, parent.sku, { value: 'Lila', sku: 'CUSTOM' }), /sku is not allowed when creating a child article/, ); await assert.rejects( () => store.article.addChild(gid, parent.sku, {}), { message: 'Attribute value is required for child article' }, ); const plain = await createArticle(store, tenant); await assert.rejects( () => store.article.addChild(gid, plain.sku, { value: 'Rot' }), { message: 'Article is not a parent article' }, ); await assert.rejects( () => store.article.addChild(gid, 'NOPE-404', { value: 'Rot' }), { message: 'Article not found' }, ); }); it('parent articles cannot have prices', async () => { const parent = await makeParent('Priceless'); await assert.rejects( () => store.article.setPricesBySku(gid, parent.sku, [{ net: 1, taxRate: 19 }]), { message: 'A parent article cannot have prices' }, ); }); it('removeChild unlinks (re-compacting parentsort) or deletes', async () => { const parent = await makeParent('Cap'); const c1 = await store.article.addChild(gid, parent.sku, { value: 'A' }); const c2 = await store.article.addChild(gid, parent.sku, { value: 'B' }); const c3 = await store.article.addChild(gid, parent.sku, { value: 'C' }); const unlinked = await store.article.removeChild(gid, parent.sku, c2.article.sku); assert.equal(unlinked.success, true); assert.equal(unlinked.deleted, false); assert.equal((await get(c2.article.sku)).parentArticleId, null); let children = await store.article.listChildren(gid, parent.sku); assert.deepEqual(children.map((c) => c.sku), [c1.article.sku, c3.article.sku]); assert.deepEqual(children.map((c) => c.parentsort), [0, 1]); const deleted = await store.article.removeChild(gid, parent.sku, c3.article.sku, { deleteArticle: true }); assert.equal(deleted.deleted, true); await assert.rejects(() => get(c3.article.sku), { message: 'Article not found' }); await assert.rejects( () => store.article.removeChild(gid, parent.sku, c2.article.sku), { message: 'Child article does not belong to this parent' }, ); children = await store.article.listChildren(gid, parent.sku); assert.equal(children.length, 1); }); it('attachChild attaches an existing article and guards against misuse', async () => { const parent = await makeParent('Sock'); const loose = await createArticle(store, tenant, { name: 'Loose Sock' }); const res = await store.article.attachChild(gid, parent.sku, { sku: loose.sku, value: 'Grün' }); assert.equal(res.article.sku, loose.sku); assert.equal(res.article.parentArticleId, parent.id); assert.ok(res.children.some((c) => c.sku === loose.sku)); // idempotent on same parent const again = await store.article.attachChild(gid, parent.sku, { sku: loose.sku, value: 'Grün' }); assert.equal(again.article.sku, loose.sku); await assert.rejects( () => store.article.attachChild(gid, parent.sku, {}), { message: 'sku is required to attach an existing article' }, ); await assert.rejects( () => store.article.attachChild(gid, parent.sku, { sku: parent.sku, value: 'X' }), { message: 'An article cannot be a child of itself' }, ); const otherParent = await makeParent('Other Sock'); await assert.rejects( () => store.article.attachChild(gid, otherParent.sku, { sku: loose.sku, value: 'Gelb' }), { message: 'Article already belongs to another parent' }, ); }); it('cannot disable parent mode while children exist', async () => { const parent = await makeParent('Scarf'); await store.article.addChild(gid, parent.sku, { value: 'Rot' }); await assert.rejects( () => store.article.toggleParent(gid, parent.sku, { isParent: false }), { message: 'Cannot disable parent mode on an article that still has children' }, ); }); it('delete refuses parents with children unless cascade', async () => { const parent = await makeParent('Glove'); const child = await store.article.addChild(gid, parent.sku, { value: 'Rot' }); await assert.rejects(() => store.article.delete(gid, parent.sku), { message: 'Article has children' }); await store.article.delete(gid, parent.sku, { cascade: true }); await assert.rejects(() => get(parent.sku), { message: 'Article not found' }); await assert.rejects(() => get(child.article.sku), { message: 'Article not found' }); }); it('setParentBySku links and validates the parent', async () => { const parent = await createArticle(store, tenant, { name: 'Compound V' }); const child = await createArticle(store, tenant, { name: 'Compound K' }); const linked = await store.article.setParentBySku(gid, child.sku, parent.sku); assert.equal(linked.parentArticleId, parent.id); assert.equal(linked.parentSku, parent.sku); assert.equal((await get(parent.sku)).isParent, true); await assert.rejects(() => store.article.setParentBySku(gid, child.sku, 'NOPE-404'), { message: 'Parent not found' }); await assert.rejects(() => store.article.setParentBySku(gid, child.sku, child.sku), { message: 'Invalid parent' }); }); it('setParentBySku unlinks with null', { todo: 'BUG src/store/article/setParentBySku.js:66 — insertHistorySql is not defined (ReferenceError when unlinking a child that has a parent)', }, async () => { const parent = await createArticle(store, tenant, { name: 'Unlink V' }); const child = await createArticle(store, tenant, { name: 'Unlink K' }); await store.article.setParentBySku(gid, child.sku, parent.sku); const unlinked = await store.article.setParentBySku(gid, child.sku, null); assert.equal(unlinked.parentArticleId, null); }); }); describe('importRows', () => { it('imports rows, resolves categories/brands/prices/eans and links parents in a 2nd pass', async () => { const result = await store.article.importRows({ googleAuthId: gid, rows: [ // child appears before its parent { sku: 'IMP-1-1', name: 'Imported 1g', vk: 'K', parentCategory: 'Import | Flowers', brand: 'ImportBrand', net: '11,00', tax: 19, eans: ['4262557690455'], parentSku: 'IMP-1' }, { sku: 'IMP-1', name: 'Imported', vk: 'V', parentCategory: 'Import | Flowers' }, { sku: '', name: 'No SKU' }, ], }); assert.deepEqual(result.imported.map((r) => r.sku).sort(), ['IMP-1', 'IMP-1-1']); assert.equal(result.errors.length, 1); assert.equal(result.errors[0].error, 'Invalid row'); const child = await get('IMP-1-1'); assert.equal(child.name, 'Imported 1g'); assert.equal(child.brand.name, 'ImportBrand'); assert.equal(child.prices[0].net, 11); assert.equal(child.prices[0].taxRate, 19); assert.deepEqual(child.eans, ['4262557690455']); assert.equal(child.parentSku, 'IMP-1'); const { tree } = await store.category.getTreeForCompany(gid); const leaf = tree.find((c) => c.id === child.categoryIds[0]); assert.equal(leaf.name, 'Flowers'); }); it('skips existing SKUs and requires a parent category', async () => { const existing = await createArticle(store, tenant); const result = await store.article.importRows({ googleAuthId: gid, rows: [ { sku: existing.sku, name: 'Again', parentId: tenant.rootCategoryId }, { sku: 'IMP-NOCAT', name: 'No Category' }, ], }); assert.deepEqual(result.skipped.map((s) => s.sku), [existing.sku]); assert.equal(result.errors.length, 1); assert.equal(result.errors[0].error, 'Parent category required'); assert.equal(result.imported.length, 0); }); it('rejects non-array rows', async () => { await assert.rejects( () => store.article.importRows({ googleAuthId: gid, rows: 'nope' }), { message: 'Invalid rows' }, ); }); }); describe('images', () => { it('adds, orders and deletes images', async () => { const art = await createArticle(store, tenant); const img1 = await store.article.addImage(gid, art.sku, await makePng({ r: 10 })); const img2 = await store.article.addImage(gid, art.sku, await makePng({ r: 250, width: 80 })); assert.notEqual(img1.fullHash, img2.fullHash); assert.equal(img1.full.w, 64); assert.equal(img1.full.h, 48); assert.ok(img1.thumb.w > 0 && img1.smallThumb.w > 0); let photos = (await get(art.sku)).photos; assert.deepEqual(photos.map((p) => p.fullHash), [img1.fullHash, img2.fullHash]); const ordered = await store.article.orderImages(gid, art.sku, { fullHashes: [img2.fullHash, img1.fullHash] }); assert.deepEqual(ordered.fullHashes, [img2.fullHash, img1.fullHash]); photos = (await get(art.sku)).photos; assert.deepEqual(photos.map((p) => p.fullHash), [img2.fullHash, img1.fullHash]); await assert.rejects( () => store.article.orderImages(gid, art.sku, { fullHashes: [img1.fullHash] }), { message: 'Invalid image order' }, ); await assert.rejects( () => store.article.orderImages(gid, art.sku, { fullHashes: [img1.fullHash, 'bogus'] }), { message: 'Invalid image order' }, ); await store.article.deleteImage(gid, art.sku, img2.fullHash); photos = (await get(art.sku)).photos; assert.deepEqual(photos.map((p) => p.fullHash), [img1.fullHash]); await assert.rejects( () => store.article.deleteImage(gid, art.sku, img2.fullHash), { message: 'Image not found' }, ); const history = await store.article.getHistoryBySku(gid, art.sku); const texts = history.map((h) => h.text); assert.ok(texts.some((t) => t.includes('Bild hinzugefügt'))); assert.ok(texts.some((t) => t.includes('Bildreihenfolge geändert'))); assert.ok(texts.some((t) => t.includes('Bild gelöscht'))); }); }); describe('tenant isolation', () => { it('getBySku / setters do not work across companies', async () => { const foreign = await createArticle(store, other, { name: 'Foreign', sku: 'FOREIGN-ISO' }); const denied = /Article not found|Not authorized/; await assert.rejects(() => store.article.getBySku(gid, foreign.sku), denied); await assert.rejects(() => store.article.updateBySku(gid, foreign.sku, { name: 'Hijack' }), denied); await assert.rejects(() => store.article.setEansBySku(gid, foreign.sku, ['4011111111111']), denied); await assert.rejects(() => store.article.setPricesBySku(gid, foreign.sku, [{ net: 1, taxRate: 19 }]), denied); await assert.rejects(() => store.article.setWeightBySku(gid, foreign.sku, 2), denied); const untouched = await get(foreign.sku, other.googleAuthId); assert.equal(untouched.name, 'Foreign'); assert.deepEqual(untouched.eans, []); }); }); describe('supplier incoming deliveries', () => { it('returns incoming dates and unbooked amounts for each supplier', async () => { const art = await createArticle(store, tenant, { name: 'Incoming Article', sku: 'INC-ART-1' }); const supp = await store.supplier.create(gid, { name: 'Supplier Incoming' }); await store.articlesupplier.create(gid, art.sku, { entityId: supp.id, sku: 'SUP-SKU-1', pu: 10, }); let res = await store.article.getBySku(gid, art.sku); assert.equal(res.article.suppliers.length, 1); assert.deepEqual(res.article.suppliers[0].incoming, []); const d1 = await store.delivery.create(gid, { supplierId: supp.id, date: '2026-11-01T00:00:00.000Z', lines: [{ sku: 'SUP-SKU-1', name: 'Item', pu: 10, amount: 25, net: 5, taxrate: 19 }], }); const d2 = await store.delivery.create(gid, { supplierId: supp.id, date: '2026-11-15T00:00:00.000Z', lines: [{ sku: 'SUP-SKU-1', name: 'Item', pu: 10, amount: 15, net: 5, taxrate: 19 }], }); res = await store.article.getBySku(gid, art.sku); assert.equal(res.article.suppliers[0].incoming.length, 2); assert.equal(res.article.suppliers[0].incoming[0].amount, 25); assert.equal(res.article.suppliers[0].incoming[0].date, '2026-11-01T00:00:00.000Z'); assert.equal(res.article.suppliers[0].incoming[1].amount, 15); assert.equal(res.article.suppliers[0].incoming[1].date, '2026-11-15T00:00:00.000Z'); const wh = await store.warehouse.create(gid, { name: 'Inc WH' }); await store.stock.accept(gid, { deliveryId: d1.id, warehouseId: wh.id }); res = await store.article.getBySku(gid, art.sku); assert.equal(res.article.suppliers[0].incoming.length, 1); assert.equal(res.article.suppliers[0].incoming[0].amount, 15); assert.equal(res.article.suppliers[0].incoming[0].date, '2026-11-15T00:00:00.000Z'); }); }); });