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 order shipping ledger & stock adjustment ---'); const tempDir = mkdtempSync(join(tmpdir(), 'test-order-shipping-')); 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. Setup company and user const googleAuthId = await store.google_auth.add('shipping-test@example.com'); const company = await store.company.create({ name: 'Shipping Test Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId, }); await store.google_auth.setSelectedCompany(googleAuthId, company.id); // Setup warehouse const warehouse = await store.warehouse.create(googleAuthId, { name: 'Hauptlager', }); assert(warehouse.id, 'Warehouse should have id'); // Setup units await store.unit.upsert(googleAuthId, { si: 'stk', basesi: 'stk', basesifactor: 1, symbol: 'Stk', name: 'Stück', }); // Setup customer group & customer entity const groups = await store.customergroup.list(googleAuthId); const customer = await store.entity.create(googleAuthId, { customergroupId: groups[0].id, name: 'Max Mustermann', }); // 2. Setup tracked article and untracked article const trackedArt = await store.article.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Tracked Article', track_stock: 1, }); const untrackedArt = await store.article.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Untracked Service Item', track_stock: 0, }); // Initial stock for tracked article: +10 await store.stock.adjust(googleAuthId, { sku: trackedArt.sku, warehouseId: warehouse.id, amount: 10, text: 'Initial stock intake', }); const artBefore = await store.article.getBySku(googleAuthId, trackedArt.sku); const stockBefore = artBefore.article.warehouses.reduce((sum, w) => sum + w.stock, 0); assert.equal(stockBefore, 10, 'Initial stock should be 10'); // 3. Create sales order with tracked and untracked items const order = await store.order.create(googleAuthId, { number: 'O-2026-001', date: new Date().toISOString(), entityId: customer.id, lines: [ { sku: trackedArt.sku, name: 'Tracked Article', pu: 1, amount: 3, net: 50, taxrate: 19, }, { sku: untrackedArt.sku, name: 'Untracked Service Item', pu: 1, amount: 2, net: 20, taxrate: 19, }, ], }); assert.equal(order.lines.length, 2); // 4. Create shipment and shipping with state 'open' (not shipped yet) console.log('4. Creating shipment and shipping with state "open"...'); const shipment = await store.shipment.create(googleAuthId, { orderId: order.id, lines: [ { sku: trackedArt.sku, amount: 3 }, { sku: untrackedArt.sku, amount: 2 }, ], }); const shippingOpen = await store.shipping.create(googleAuthId, { manifestId: shipment.id, type: 'parcel', state: 'open', }); assert.equal(shippingOpen.state, 'open'); // Assert stock is still 10 const artStill10 = await store.article.getBySku(googleAuthId, trackedArt.sku); assert.equal(artStill10.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 10, 'Stock should still be 10 when shipping is open'); // 5. Update shipping to state 'shipped' console.log('5. Updating shipping to state "shipped"...'); const shippingShipped = await store.shipping.update(googleAuthId, shippingOpen.id, { state: 'shipped', warehouseId: warehouse.id, }); assert.equal(shippingShipped.state, 'shipped'); // Assert stock is now 7 (10 - 3) const artAfter = await store.article.getBySku(googleAuthId, trackedArt.sku); assert.equal(artAfter.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 7, 'Stock should be reduced to 7'); // Assert ledger entries const ledgerRows = await store.ledger.listBySku(googleAuthId, trackedArt.sku); assert.equal(ledgerRows.length, 3, 'Should have 3 ledger entries for tracked article (adjust + shipment doc + warehouse)'); const shipmentDocLedger = ledgerRows.find((l) => l.transaction_type === 'SHIPMENT' && l.warehouse_id == null); assert(shipmentDocLedger, 'Shipment document ledger entry should exist with warehouse_id null'); assert.equal(shipmentDocLedger.amount, -3, 'Shipment document ledger entry should be -3'); const shipmentWhLedger = ledgerRows.find((l) => l.transaction_type === 'WAREHOUSE' && l.warehouse_id === warehouse.id); assert(shipmentWhLedger, 'Shipment warehouse ledger entry should exist with warehouse_id set'); assert.equal(shipmentWhLedger.amount, -3, 'Shipment warehouse ledger entry should be -3'); // Assert untracked article has 1 document ledger entry and 0 warehouse ledger entries const untrackedLedger = await store.ledger.listBySku(googleAuthId, untrackedArt.sku); assert.equal(untrackedLedger.length, 1, 'Untracked article should have 1 shipment document ledger entry'); assert.equal(untrackedLedger[0].transaction_type, 'SHIPMENT'); assert.equal(untrackedLedger[0].warehouse_id, null, 'Untracked article should not have warehouse ledger entry'); assert.equal(untrackedLedger[0].amount, -2); // Assert article history: order goes into history, and shipment produces history const history = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku); console.log('Tracked article history:', history.map((h) => h.text)); const orderHistory = history.find((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 3, € 59,50)'); assert(orderHistory, 'Article history should contain order creation entry with formatted gross price'); assert(orderHistory.meta, 'Article history entry should have meta'); const parsedMeta = JSON.parse(orderHistory.meta); assert.equal(parsedMeta.type, 'order'); assert.equal(parsedMeta.orderId, order.id); assert.equal(parsedMeta.gross, 59.5); const shipmentHistory = history.find((h) => h.text.startsWith(`Lieferschein ${shipment.number}`) && h.text.includes('Kunde: "Max Mustermann"')); assert(shipmentHistory, 'Article history should report the shipment document'); // Untracked article also has the order in history and shipment in history const untrackedHistory = await store.article.getHistoryBySku(googleAuthId, untrackedArt.sku); console.log('Untracked article history:', untrackedHistory.map((h) => h.text)); assert( untrackedHistory.some((h) => h.text === 'Auftrag O-2026-001 (Kunde: "Max Mustermann", Menge: 2, € 23,80)'), 'Untracked article history should contain order creation entry with formatted gross price', ); assert( untrackedHistory.some((h) => h.text.startsWith(`Lieferschein ${shipment.number}`)), 'Untracked article history should report the shipment', ); // Test order import with historical date preserves date in article history const historicalDate = '2023-04-15T10:30:00.000Z'; await store.order.create(googleAuthId, { number: 'O-HIST-001', date: historicalDate, createdAt: historicalDate, lines: [{ sku: trackedArt.sku, amount: 1, net: 10, taxrate: 19 }], }); const histWithOld = await store.article.getHistoryBySku(googleAuthId, trackedArt.sku); const oldEntry = histWithOld.find((h) => h.text.startsWith('Auftrag O-HIST-001')); assert(oldEntry, 'Historical order entry should exist in article history'); assert.equal(new Date(oldEntry.date).toISOString(), historicalDate, 'History entry should preserve historical order date'); const oldMeta = JSON.parse(oldEntry.meta); assert.equal(oldMeta.orderNumber, 'O-HIST-001'); // 6. Test immutability: once shipped, cannot be deleted console.log('6. Testing cannot delete shipped shipping...'); await assert.rejects( async () => { await store.shipping.delete(googleAuthId, shippingShipped.id); }, { message: 'Shipped shipping cannot be deleted' }, ); // 7. Test immutability: once shipped, cannot be undone console.log('7. Testing cannot undo shipped shipping...'); await assert.rejects( async () => { await store.shipping.update(googleAuthId, shippingShipped.id, { state: 'open' }); }, { message: 'Shipped shipping cannot be undone' }, ); // 8. Test order manifest cannot be used directly for shipping console.log('8. Testing order manifest cannot be used directly for shipping...'); await assert.rejects( async () => { await store.shipping.create(googleAuthId, { manifestId: order.id, type: 'express', state: 'shipped', }); }, { message: 'Manifest is not a shipment' }, ); // 9. Test direct creation with state 'shipped' on shipment manifest console.log('9. Testing direct creation with state "shipped"...'); const order2 = await store.order.create(googleAuthId, { number: 'O-2026-002', date: new Date().toISOString(), entityId: customer.id, lines: [ { sku: trackedArt.sku, name: 'Tracked Article', pu: 1, amount: 2, net: 50, taxrate: 19, }, ], }); const shipment2 = await store.shipment.create(googleAuthId, { orderId: order2.id, lines: [{ sku: trackedArt.sku, amount: 2 }], }); const directShipped = await store.shipping.create(googleAuthId, { manifestId: shipment2.id, type: 'parcel', state: 'shipped', }); assert.equal(directShipped.state, 'shipped'); const artFinal = await store.article.getBySku(googleAuthId, trackedArt.sku); assert.equal(artFinal.article.warehouses.reduce((sum, w) => sum + w.stock, 0), 5, 'Stock should now be 5 (7 - 2)'); console.log('✓ All order shipping ledger tests passed successfully!'); } finally { try { rmSync(tempDir, { recursive: true, force: true }); } catch {} } } run().catch((err) => { console.error('Test failed:', err); process.exit(1); });