import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { createTestStore, createTenant, createArticle, isoNow } from './helpers.js'; describe('store.delivery', () => { let ctx; let store; let tenant; let other; let gid; let supplier; let warehouse; let tracked; const line = (sku, amount = 5, extra = {}) => ({ sku, name: `Line ${sku}`, pu: 1, amount, net: 10, taxrate: 19, ...extra }); const create = (body = {}) => store.delivery.create(gid, { supplierId: supplier.id, date: isoNow(), lines: [], ...body }); before(async () => { ctx = await createTestStore('dash-delivery-'); store = ctx.store; tenant = await createTenant(store, { name: 'Delivery Test GmbH' }); other = await createTenant(store, { name: 'Delivery Other GmbH' }); gid = tenant.googleAuthId; supplier = await store.supplier.create(gid, { name: 'Supplier One' }); warehouse = await store.warehouse.create(gid, { name: 'Hauptlager' }); tracked = await createArticle(store, tenant, { name: 'Tracked', sku: 'DEL-TRACKED', track_stock: true }); }); after(async () => { await ctx.cleanup(); }); describe('create / getById', () => { it('creates a delivery with number, lines and an unbooked summary', async () => { const d = await create({ externalNumber: ' EXT-1 ', lines: [line('DEL-TRACKED', 5), { name: 'Free text', amount: 2, net: 3.5, taxrate: 7 }], }); assert.equal(typeof d.id, 'number'); assert.equal(d.type, 'delivery'); assert.ok(d.number); assert.equal(d.externalNumber, 'EXT-1'); assert.equal(d.supplierId, supplier.id); assert.equal(d.lines.length, 2); assert.equal(d.lines[0].sku, 'DEL-TRACKED'); assert.equal(d.lines[0].amount, 5); assert.equal(d.lines[1].sku, null); assert.equal(d.lines[1].name, 'Free text'); assert.equal(d.lines[1].pu, 1); assert.equal(d.positions, 2); assert.equal(d.isBooked, false); assert.deepEqual(d.files, []); const fetched = await store.delivery.getById(gid, d.id); assert.equal(fetched.id, d.id); assert.equal(fetched.number, d.number); assert.equal(fetched.lines.length, 2); }); it('allocates distinct sequential numbers', async () => { const a = await create(); const b = await create(); assert.notEqual(a.number, b.number); }); it('rejects a delivery without supplier', async () => { await assert.rejects( () => store.delivery.create(gid, { date: isoNow(), lines: [] }), { message: 'No supplier selected' }, ); }); it('validates input', async () => { await assert.rejects(() => create({ date: 'garbage' }), { message: 'Invalid delivery' }); await assert.rejects(() => create({ createdAt: 'garbage' }), { message: 'Invalid delivery' }); await assert.rejects(() => create({ supplierId: null }), { message: 'No supplier selected' }); await assert.rejects(() => create({ supplierId: '1' }), { message: 'Invalid supplierId' }); await assert.rejects(() => create({ supplierId: 999999 }), { message: 'Supplier not found' }); await assert.rejects(() => create({ lines: 'x' }), { message: 'Invalid lines' }); await assert.rejects(() => create({ lines: [{ amount: 1, net: 1, taxrate: 19 }] }), { message: 'Invalid lines' }); await assert.rejects(() => create({ lines: [line('DEL-TRACKED', 'many')] }), { message: 'Invalid lines' }); await assert.rejects(() => create({ lines: [line('DEL-TRACKED', 1, { taxrate: -1 })] }), { message: 'Invalid lines' }); }); it('rejects a supplier of another company', async () => { const foreign = await store.supplier.create(other.googleAuthId, { name: 'Foreign Supplier' }); await assert.rejects(() => create({ supplierId: foreign.id }), { message: 'Supplier not found' }); }); it('rejects booking a parent article on a document', async () => { const attr = await store.attributename.create(gid, { name: 'Delivery Attr' }); const parent = await createArticle(store, tenant, { name: 'Parent', sku: 'DEL-PARENT' }); await store.article.toggleParent(gid, parent.sku, { isParent: true, attributeNameId: attr.id }); await assert.rejects( () => create({ lines: [line('DEL-PARENT')] }), /A parent article cannot be booked on documents \(DEL-PARENT\)/, ); }); it('getById rejects unknown and foreign deliveries', async () => { const d = await create(); await assert.rejects(() => store.delivery.getById(gid, 999999), { message: 'Delivery not found' }); await assert.rejects(() => store.delivery.getById(other.googleAuthId, d.id), { message: 'Delivery not found' }); }); }); describe('update', () => { it('updates header fields', async () => { const d = await create({ externalNumber: 'OLD' }); const upd = await store.delivery.update(gid, d.id, { externalNumber: 'NEW', number: 'MY-NUMBER' }); assert.equal(upd.externalNumber, 'NEW'); assert.equal(upd.number, 'MY-NUMBER'); const cleared = await store.delivery.update(gid, d.id, { externalNumber: ' ' }); assert.equal(cleared.externalNumber, null); await assert.rejects( () => store.delivery.update(gid, d.id, { supplierId: null }), { message: 'No supplier selected' }, ); }); it('replaces lines of an unbooked delivery', async () => { const d = await create({ lines: [line('DEL-TRACKED', 1)] }); const upd = await store.delivery.update(gid, d.id, { lines: [line('DEL-TRACKED', 7), line('DEL-TRACKED', 3)] }); assert.deepEqual(upd.lines.map((l) => l.amount), [7, 3]); }); it('refuses to replace lines that are already booked', async () => { const d = await create({ lines: [line('DEL-TRACKED', 4)] }); await store.delivery.book(gid, { id: d.id, warehouseId: warehouse.id }); await assert.rejects( () => store.delivery.update(gid, d.id, { lines: [line('DEL-TRACKED', 9)] }), { message: 'Delivery lines already booked on ledger' }, ); assert.equal((await store.delivery.getById(gid, d.id)).lines[0].amount, 4); }); it('refuses duplicate delivery numbers on update', async () => { const d1 = await create(); const d2 = await create(); await assert.rejects( () => store.delivery.update(gid, d2.id, { number: d1.number }), { message: 'Delivery number already exists' }, ); }); it('validates input', async () => { const d = await create(); await assert.rejects(() => store.delivery.update(gid, d.id, {}), { message: 'Invalid delivery' }); await assert.rejects(() => store.delivery.update(gid, d.id, { number: ' ' }), { message: 'Invalid delivery' }); await assert.rejects(() => store.delivery.update(gid, d.id, { date: 'x' }), { message: 'Invalid delivery' }); await assert.rejects(() => store.delivery.update(gid, d.id, { createdAt: 'x' }), { message: 'Invalid delivery' }); await assert.rejects(() => store.delivery.update(gid, d.id, { supplierId: 999999 }), { message: 'Supplier not found' }); await assert.rejects(() => store.delivery.update(gid, 999999, { number: 'X' }), { message: 'Delivery not found' }); await assert.rejects(() => store.delivery.update(gid, Number.NaN, { number: 'X' }), { message: 'Delivery not found' }); }); it('cannot update deliveries of another company', async () => { const d = await create(); await assert.rejects( () => store.delivery.update(other.googleAuthId, d.id, { number: 'HIJACK' }), { message: 'Delivery not found' }, ); }); }); describe('book', () => { it('books all lines and marks the delivery as booked', async () => { const d = await create({ lines: [line('DEL-TRACKED', 6), line('DEL-TRACKED', 2)] }); const booked = await store.delivery.book(gid, { id: d.id, warehouseId: warehouse.id }); assert.equal(booked.isBooked, true); assert.equal(booked.ledger.isBooked, true); assert.equal(booked.ledger.completelyOnLedgerPositions, 2); for (const l of booked.lines) { const entries = await store.ledger.listByManifestarticleId(gid, l.id); assert.deepEqual(entries.map((e) => e.transaction_type), ['ACCEPTANCE', 'WAREHOUSE']); assert.equal(entries[1].warehouse_id, warehouse.id); } }); it('accepts a plain id and rejects double booking', async () => { const d = await create({ lines: [line('DEL-TRACKED', 1)] }); await store.delivery.book(gid, d.id); await assert.rejects( () => store.delivery.book(gid, d.id), /Delivery line(s)? already (accepted|booked) on ledger/, ); }); it('rejects invalid, unknown and empty deliveries', async () => { await assert.rejects(() => store.delivery.book(gid, {}), { message: 'Delivery not found' }); await assert.rejects(() => store.delivery.book(gid, 999999), { message: 'Delivery not found' }); const empty = await create(); await assert.rejects(() => store.delivery.book(gid, empty.id), { message: 'Delivery has no lines' }); }); it('cannot book a delivery of another company', async () => { const d = await create({ lines: [line('DEL-TRACKED', 1)] }); await assert.rejects(() => store.delivery.book(other.googleAuthId, d.id), { message: 'Delivery not found' }); }); }); describe('delete', () => { it('deletes an unbooked delivery', async () => { const d = await create({ lines: [line('DEL-TRACKED', 1)] }); await store.delivery.delete(gid, d.id); await assert.rejects(() => store.delivery.getById(gid, d.id), { message: 'Delivery not found' }); }); it('refuses to delete a booked delivery', async () => { const d = await create({ lines: [line('DEL-TRACKED', 1)] }); await store.delivery.book(gid, d.id); await assert.rejects(() => store.delivery.delete(gid, d.id), { message: 'Delivery in use in ledger' }); assert.ok(await store.delivery.getById(gid, d.id)); }); it('rejects unknown ids and foreign deliveries', async () => { const d = await create(); await assert.rejects(() => store.delivery.delete(gid, 999999), { message: 'Delivery not found' }); await assert.rejects(() => store.delivery.delete(gid, Number.NaN), { message: 'Delivery not found' }); await assert.rejects(() => store.delivery.delete(other.googleAuthId, d.id), { message: 'Delivery not found' }); assert.ok(await store.delivery.getById(gid, d.id)); }); }); describe('list', () => { let lt; // isolated tenant so counts are exact let ls1; let ls2; let d1; let d2; let d3; before(async () => { lt = await createTenant(store, { name: 'Delivery List GmbH' }); await store.warehouse.create(lt.googleAuthId, { name: 'List WH' }); ls1 = await store.supplier.create(lt.googleAuthId, { name: 'Alpha Trading' }); ls2 = await store.supplier.create(lt.googleAuthId, { name: 'Beta Supplies' }); await createArticle(store, lt, { sku: 'LST-1', track_stock: true }); const mk = (body) => store.delivery.create(lt.googleAuthId, { lines: [line('LST-1', 2)], ...body }); d1 = await mk({ supplierId: ls1.id, externalNumber: 'EXT-ALPHA', date: '2026-01-10T00:00:00Z' }); d2 = await mk({ supplierId: ls2.id, externalNumber: 'EXT-BETA', date: '2026-02-10T00:00:00Z' }); d3 = await mk({ supplierId: ls2.id, externalNumber: 'EXT-BETA-2', date: '2026-03-10T00:00:00Z' }); await store.delivery.book(lt.googleAuthId, d1.id); }); it('returns a plain array without options, newest date first', async () => { const all = await store.delivery.list(lt.googleAuthId); assert.ok(Array.isArray(all)); assert.deepEqual(all.map((d) => d.id), [d3.id, d2.id, d1.id]); assert.equal(all[2].isBooked, true); assert.equal(all[0].isBooked, false); }); it('paginates', async () => { const page = await store.delivery.list(lt.googleAuthId, { offset: 1, limit: 1 }); assert.equal(page.total, 3); assert.equal(page.unfilteredTotal, 3); assert.equal(page.offset, 1); assert.equal(page.limit, 1); assert.deepEqual(page.deliveries.map((d) => d.id), [d2.id]); }); it('filters by search, supplier and unbooked state', async () => { const bySearch = await store.delivery.list(lt.googleAuthId, { search: 'EXT-BETA' }); assert.equal(bySearch.total, 2); assert.equal(bySearch.unfilteredTotal, 3); const bySupplierName = await store.delivery.list(lt.googleAuthId, { search: 'Alpha' }); assert.deepEqual(bySupplierName.deliveries.map((d) => d.id), [d1.id]); const bySupplier = await store.delivery.list(lt.googleAuthId, { supplierId: ls1.id }); assert.deepEqual(bySupplier.deliveries.map((d) => d.id), [d1.id]); const unbooked = await store.delivery.list(lt.googleAuthId, { onlyUnbooked: true }); assert.deepEqual(unbooked.deliveries.map((d) => d.id).sort(), [d2.id, d3.id].sort()); }); it('validates options', async () => { const l = (o) => store.delivery.list(lt.googleAuthId, o); await assert.rejects(() => l({ offset: -1 }), { message: 'Invalid offset' }); await assert.rejects(() => l({ limit: 0 }), { message: 'Invalid limit' }); await assert.rejects(() => l({ seekMonths: '1' }), { message: 'Invalid seekMonths' }); await assert.rejects(() => l({ supplierId: 0 }), { message: 'Invalid supplierId' }); }); it('does not leak deliveries of other companies', async () => { const mine = await store.delivery.list(gid); assert.ok(!mine.some((d) => [d1.id, d2.id, d3.id].includes(d.id))); }); }); describe('rebuildSummaries', () => { it('keeps list results stable after a rebuild', async () => { const rt = await createTenant(store, { name: 'Delivery Rebuild GmbH' }); await store.warehouse.create(rt.googleAuthId, { name: 'Rebuild WH' }); await createArticle(store, rt, { sku: 'RB-1', track_stock: true }); const rs = await store.supplier.create(rt.googleAuthId, { name: 'Rebuild Supplier' }); const d = await store.delivery.create(rt.googleAuthId, { supplierId: rs.id, date: isoNow(), lines: [line('RB-1', 3)] }); await store.delivery.book(rt.googleAuthId, d.id); const before = await store.delivery.list(rt.googleAuthId); await store.delivery.rebuildSummaries(rt.googleAuthId); const after = await store.delivery.list(rt.googleAuthId); assert.equal(after.length, before.length); assert.equal(after[0].isBooked, true); assert.equal(after[0].positions, before[0].positions); assert.equal(after[0].bookedQuantity, before[0].bookedQuantity); }); }); });