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 Shipment Manifest & Multi-Package Shipping ---'); const tempDir = mkdtempSync(join(tmpdir(), 'test-shipment-')); 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('shipment-test@example.com'); const company = await store.company.create({ name: 'Shipment Test Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId, }); await store.google_auth.setSelectedCompany(googleAuthId, company.id); // Create a warehouse const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); // Create customer entity const groups = await store.customergroup.list(googleAuthId); const customer = await store.entity.create(googleAuthId, { customergroupId: groups[0].id, name: 'Max Mustermann', }); // Create Item A and Item B const itemA = await store.article.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Item A', sku: 'ITEM-A', track_stock: true, }); const itemB = await store.article.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Item B', sku: 'ITEM-B', track_stock: true, }); // Stock up Item A (20) and Item B (20) await store.stock.adjust(googleAuthId, { sku: 'ITEM-A', warehouseId: wh.id, amount: 20, comment: 'Initial Stock', }); await store.stock.adjust(googleAuthId, { sku: 'ITEM-B', warehouseId: wh.id, amount: 20, comment: 'Initial Stock', }); // Create Order with 10x Item A, 5x Item B const order = await store.order.create(googleAuthId, { number: 'AU-1001', date: '2026-10-12', entityId: customer.id, lines: [ { sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 }, { sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 }, ], }); assert.equal(order.number, 'AU-1001'); console.log('Order created:', order.number); // 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1 const shipment1 = await store.shipment.create(googleAuthId, { orderId: order.id, lines: [ { sku: 'ITEM-A', amount: 6 }, { sku: 'ITEM-B', amount: 3 }, ], }); assert.equal(shipment1.number, 'AU-1001-L-1'); assert.equal(shipment1.lines.length, 2); console.log('PASS: Shipment 1 created with number:', shipment1.number); // 2. Create Shipment 2: 5x Item A -> Fails with ceiling violation (6 + 5 > 10) let ceilingErrorThrown = false; try { await store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-A', amount: 5 }], }); } catch (err) { ceilingErrorThrown = true; assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/); console.log('PASS: Ceiling violation caught:', err.message); } assert.ok(ceilingErrorThrown, 'Expected ceiling violation error to be thrown'); // Also test foreign article rejection let foreignArticleErrorThrown = false; try { await store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-UNKNOWN', amount: 1 }], }); } catch (err) { foreignArticleErrorThrown = true; assert.match(err.message, /Article \(ITEM-UNKNOWN\) is not part of order/); console.log('PASS: Foreign article rejected:', err.message); } assert.ok(foreignArticleErrorThrown, 'Expected foreign article error to be thrown'); // 3. Create Shipment 2: 4x Item A, 2x Item B -> AU-1001-L-2 const shipment2 = await store.shipment.create(googleAuthId, { orderId: order.id, lines: [ { sku: 'ITEM-A', amount: 4 }, { sku: 'ITEM-B', amount: 2 }, ], }); assert.equal(shipment2.number, 'AU-1001-L-2'); assert.equal(shipment2.lines.length, 2); console.log('PASS: Shipment 2 created with number:', shipment2.number); // 4. Try creating another shipment (now 10/10 of Item A and 5/5 of Item B are shipped) let exhaustedErrorThrown = false; try { await store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-A', amount: 1 }], }); } catch (err) { exhaustedErrorThrown = true; assert.match(err.message, /Shipment amount exceeds ordered amount for article \(ITEM-A\)/); console.log('PASS: Exhausted quantity violation caught:', err.message); } assert.ok(exhaustedErrorThrown, 'Expected exhausted error to be thrown'); // 5. Add packages to Shipment 1 with tracking codes and carrier const carrier = await store.carrier.create(googleAuthId, { name: 'DHL', hasPackages: true, isReverse: false, }); assert.equal(carrier.name, 'DHL'); assert.equal(carrier.hasPackages, true); assert.equal(carrier.isReverse, false); const pkg1 = await store.shipping.create(googleAuthId, { manifestId: shipment1.id, carrierId: carrier.id, type: 'parcel', state: 'draft', trackingCode: 'TRACK-111', }); assert.equal(pkg1.trackingCode, 'TRACK-111'); assert.equal(pkg1.carrierId, carrier.id); assert.equal(pkg1.carrier, 'DHL'); assert.equal(pkg1.state, 'draft'); console.log('PASS: Package 1 created for Shipment 1:', pkg1.number, pkg1.trackingCode); const pkg2 = await store.shipping.create(googleAuthId, { manifestId: shipment1.id, carrierId: carrier.id, type: 'parcel', state: 'draft', trackingCode: 'TRACK-222', }); assert.equal(pkg2.trackingCode, 'TRACK-222'); assert.equal(pkg2.carrierId, carrier.id); console.log('PASS: Package 2 created for Shipment 1:', pkg2.number, pkg2.trackingCode); // Verify carrier in use prevents deletion await assert.rejects( () => store.carrier.delete(googleAuthId, carrier.id), { message: 'Carrier in use' }, ); console.log('PASS: Carrier delete rejected while referenced in shipping'); // Check listByOrderId const orderShipments = await store.shipment.listByOrderId(googleAuthId, order.id); assert.equal(orderShipments.length, 2); assert.equal(orderShipments[0].shippings.length, 2); console.log('PASS: store.shipment.listByOrderId returned both shipments'); // Check getById const fetchedShipment1 = await store.shipment.getById(googleAuthId, shipment1.id); assert.equal(fetchedShipment1.id, shipment1.id); assert.equal(fetchedShipment1.shippings.length, 2); console.log('PASS: store.shipment.getById returned shipment with shippings'); // 6. Transition Package 1 to shipped (should deduct ledger stock for Shipment 1 lines: 6 of Item A, 3 of Item B) const updatedPkg1 = await store.shipping.update(googleAuthId, pkg1.id, { state: 'shipped', }); assert.equal(updatedPkg1.state, 'shipped'); console.log('PASS: Package 1 transitioned to shipped'); // Transition Package 2 to shipped (stock already booked for shipment1, should not double-deduct) const updatedPkg2 = await store.shipping.update(googleAuthId, pkg2.id, { state: 'shipped', }); assert.equal(updatedPkg2.state, 'shipped'); console.log('PASS: Package 2 transitioned to shipped'); // 7. Verify cannot delete shipped shipment let deleteErrorThrown = false; try { await store.shipment.delete(googleAuthId, shipment1.id); } catch (err) { deleteErrorThrown = true; assert.match(err.message, /Shipped shipment cannot be deleted/); console.log('PASS: Shipped shipment delete rejected:', err.message); } assert.ok(deleteErrorThrown, 'Expected shipped shipment delete error'); // 8. Delete unshipped shipment 2 await store.shipment.delete(googleAuthId, shipment2.id); const shipmentsAfterDel = await store.shipment.listByOrderId(googleAuthId, order.id); assert.equal(shipmentsAfterDel.length, 1); console.log('PASS: Unshipped shipment 2 successfully deleted'); // 9. Test order2: One item, some items, all items, and automatic ordersummary updates const order2 = await store.order.create(googleAuthId, { number: 'AU-1002', date: '2026-10-15', entityId: customer.id, lines: [ { sku: 'ITEM-A', amount: 10, net: 100, taxrate: 19 }, { sku: 'ITEM-B', amount: 5, net: 50, taxrate: 19 }, ], }); // Check initial ordersummary for order2 let sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id); assert.equal(sum2.ordered_quantity, 15); assert.equal(sum2.shipped_quantity, 0); assert.equal(sum2.is_shipped, 0); // 9a. Ship ONE item (3x ITEM-A) via store.order.createShipment const shOne = await store.order.createShipment(googleAuthId, order2.id, { lines: [{ sku: 'ITEM-A', amount: 3 }], }); assert.equal(shOne.number, 'AU-1002-L-1'); assert.equal(shOne.lines.length, 1); assert.equal(shOne.lines[0].sku, 'ITEM-A'); assert.equal(shOne.lines[0].amount, 3); // Verify ordersummary updated: shipped_quantity = 3, is_shipped = 0 sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id); assert.equal(sum2.shipped_quantity, 3); assert.equal(sum2.is_shipped, 0); console.log('PASS: 9a. Shipped ONE item; ordersummary updated with shipped_quantity=3, is_shipped=0'); // 9b. Ship SOME items with omitted amount (e.g. ITEM-A with no amount -> defaults to remaining 7) const shSome = await store.order.createShipment(googleAuthId, order2.id, { lines: [{ sku: 'ITEM-A' }], }); assert.equal(shSome.number, 'AU-1002-L-2'); assert.equal(shSome.lines.length, 1); assert.equal(shSome.lines[0].sku, 'ITEM-A'); assert.equal(shSome.lines[0].amount, 7); // Verify ordersummary updated: shipped_quantity = 10 (3 + 7), is_shipped = 0 sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id); assert.equal(sum2.shipped_quantity, 10); assert.equal(sum2.is_shipped, 0); console.log('PASS: 9b. Shipped SOME items with omitted amount; ordersummary updated with shipped_quantity=10, is_shipped=0'); // 9c. Ship ALL remaining items by omitting lines entirely ({}) const shAll = await store.order.createShipment(googleAuthId, order2.id, {}); assert.equal(shAll.number, 'AU-1002-L-3'); assert.equal(shAll.lines.length, 1); assert.equal(shAll.lines[0].sku, 'ITEM-B'); assert.equal(shAll.lines[0].amount, 5); // Verify ordersummary updated: shipped_quantity = 15, is_shipped = 1 (order is fully shipped!) sum2 = provider.db.prepare('SELECT * FROM ordersummary WHERE manifest_id = ?').get(order2.id); assert.equal(sum2.shipped_quantity, 15); assert.equal(sum2.is_shipped, 1); console.log('PASS: 9c. Shipped ALL remaining items with omitted lines; ordersummary updated with shipped_quantity=15, is_shipped=1'); // 9d. Calling createShipment when all items already shipped throws 'All items already shipped' await assert.rejects( async () => { await store.order.createShipment(googleAuthId, order2.id); }, { message: 'All items already shipped' } ); console.log('PASS: 9d. Re-shipping fully shipped order rejected with "All items already shipped"'); // 10. Verify peek next number works for entity and other types without error const nextEntity = await store.entity.peekNextNumber(googleAuthId); assert.ok(typeof nextEntity === 'string' && nextEntity.length > 0); console.log('PASS: store.entity.peekNextNumber succeeded:', nextEntity); const numList = await store.numbers.list(googleAuthId); assert.ok(Array.isArray(numList) && numList.length > 0); const shipmentSeq = numList.find((n) => n.type === 'shipment'); assert.ok(shipmentSeq, 'Expected shipment sequence in numbers.list'); // 11. Test store.shipment.list, store.shipment.update, and rebuildSummaries const listAll = await store.shipment.list(googleAuthId, { offset: 0, limit: 10 }); assert.ok(listAll.total >= 3, `Expected at least 3 shipments, got ${listAll.total}`); assert.equal(listAll.limit, 10); assert.equal(listAll.offset, 0); const s1 = listAll.shipments.find((s) => s.id === shOne.id); assert.ok(s1, 'Expected shOne in shipment list'); assert.equal(s1.orderNumber, 'AU-1002'); assert.equal(s1.orderId, order2.id); assert.equal(s1.state, 'new'); assert.equal(s1.positions, 1); assert.equal(s1.totalQuantity, 3); console.log('PASS: store.shipment.list returned shipments with order info and state'); // 11a. Search by order number const listSearchOrder = await store.shipment.list(googleAuthId, { search: 'AU-1002', offset: 0, limit: 10 }); assert.equal(listSearchOrder.total, 3); console.log('PASS: store.shipment.list filtered by orderNumber search'); // 11b. Update shipment state to 'delivered' const updatedSh = await store.shipment.update(googleAuthId, shOne.id, { state: 'delivered' }); assert.equal(updatedSh.state, 'delivered'); // 11c. Filter by state = 'delivered' const listDelivered = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 }); assert.equal(listDelivered.total, 1); assert.equal(listDelivered.shipments[0].id, shOne.id); console.log('PASS: store.shipment.update set state="delivered" and filtered correctly'); // 11d. Rebuild shipment summaries await store.shipment.rebuildSummaries(googleAuthId); const listAfterRebuild = await store.shipment.list(googleAuthId, { state: 'delivered', offset: 0, limit: 10 }); assert.equal(listAfterRebuild.total, 1); console.log('PASS: store.shipment.rebuildSummaries preserved delivered state and rebuilt correctly'); 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); });