import assert from 'node:assert/strict'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createHash } from 'node:crypto'; import LibDashStore from '../src/index.js'; import { BetterSqlite3Provider } from '../src/providers/sqlite3/index.js'; import { INIT_HASH, computePrevHash } from '../src/store/ledger/add.js'; async function run() { console.log('--- Testing store.ledger (add, created_at, list queries) ---'); const tempDir = mkdtempSync(join(tmpdir(), 'test-ledger-')); 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 user and company const googleAuthId = await store.google_auth.add('ledger-test@example.com'); const company = await store.company.create({ name: 'Ledger Test Corp', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId, }); await store.google_auth.setSelectedCompany(googleAuthId, company.id); await store.unit.upsert(googleAuthId, { si: 'stk', basesi: 'stk', basesifactor: 1, symbol: 'Stk', name: 'Stück', }); const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier X' }); const article = await store.article.createUnderParent({ googleAuthId, parentId: company.rootCategoryId, name: 'Ledger Linked Article', }); assert.equal(article.track_stock, false, 'Newly created article must have track_stock disabled'); await assert.rejects( async () => store.article.setTrackStockBySku(googleAuthId, article.sku, true), /At least one warehouse is required to enable stock tracking/, 'Should reject enabling stock tracking when no warehouse exists', ); await store.articlesupplier.create(googleAuthId, article.sku, { entityId: supplier.id, sku: 'SKU-ABC', pu: 1, }); // Create a delivery manifest with lines const delivery = await store.delivery.create(googleAuthId, { supplierId: supplier.id, externalNumber: 'DEL-1', date: new Date().toISOString(), lines: [ { sku: 'SKU-ABC', name: 'Ledger Item 1', pu: 1, amount: 5, net: 10, rebate: 0, taxrate: 19, }, { sku: 'SKU-ABC', name: 'Ledger Item 1 Batch 2', pu: 1, amount: 10, net: 20, rebate: 0, taxrate: 19, }, ], }); const manifestarticleId1 = delivery.lines[0].id; const manifestarticleId2 = delivery.lines[1].id; assert(manifestarticleId1, 'manifestarticle line 1 should have an id'); assert(manifestarticleId2, 'manifestarticle line 2 should have an id'); // 2. Test first row (genesis row) console.log('1. Inserting genesis ledger row...'); const row1 = await store.ledger.add(googleAuthId, { manifestarticle_id: manifestarticleId1, transaction_type: 'GOODS_RECEIPT', amount: 5, transaction_text: 'Initial delivery movement', }); assert.equal(row1.id, 1); assert.equal(row1.manifestarticle_id, manifestarticleId1); assert.equal(row1.transaction_type, 'GOODS_RECEIPT'); assert.equal(row1.amount, 5); assert.equal(row1.transaction_text, 'Initial delivery movement'); assert.equal(row1.hash_version, 1); assert.equal(row1.prev_hash, INIT_HASH); assert(row1.created_at, 'created_at should exist'); assert(!Number.isNaN(new Date(row1.created_at).getTime()), 'created_at should be valid date string'); console.log('✓ Row 1 created_at:', row1.created_at); // Verify manual hash computation of row 1 (including created_at) const expectedHash2 = computePrevHash(row1); const manualHash2 = createHash('sha512') .update( JSON.stringify({ id: 1, manifestarticle_id: manifestarticleId1, warehouse_id: null, transaction_type: 'GOODS_RECEIPT', amount: 5, transaction_text: 'Initial delivery movement', hash_version: 1, prev_hash: INIT_HASH, created_at: new Date(row1.created_at).toISOString(), }), 'utf8', ) .digest('hex'); assert.equal(expectedHash2, manualHash2); assert.equal(manualHash2.length, 128); // 3. Test second row (line 1, different transaction type) console.log('2. Inserting second ledger row...'); const row2 = await store.ledger.add(googleAuthId, { manifestarticleId: manifestarticleId1, transactionType: 'INVENTORY_ADJUST', amount: -1, transactionText: 'Damaged item written off', }); assert.equal(row2.id, 2); assert.equal(row2.hash_version, 1); assert.equal(row2.prev_hash, expectedHash2); assert(row2.created_at, 'row 2 created_at should exist'); console.log('✓ Row 2 prev_hash matches SHA-512 of Row 1 with created_at'); // 4. Test third row (line 2, same SKU SKU-ABC) console.log('3. Inserting third ledger row on line 2...'); const expectedHash3 = computePrevHash(row2); const row3 = await store.ledger.add(googleAuthId, { manifestarticle_id: manifestarticleId2, transaction_type: 'GOODS_RECEIPT', amount: 10, transaction_text: 'Second line goods receipt', }); assert.equal(row3.id, 3); assert.equal(row3.hash_version, 1); assert.equal(row3.prev_hash, expectedHash3); // 5. Test query by manifestarticle_id console.log('4. Testing listByManifestarticleId...'); const listLine1All = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1); assert.equal(listLine1All.length, 2); assert.equal(listLine1All[0].id, 1); assert.equal(listLine1All[1].id, 2); assert.equal(listLine1All[0].sku, 'SKU-ABC'); const listLine1GoodsReceipt = await store.ledger.listByManifestarticleId( googleAuthId, manifestarticleId1, 'GOODS_RECEIPT', ); assert.equal(listLine1GoodsReceipt.length, 1); assert.equal(listLine1GoodsReceipt[0].id, 1); const listLine1Adjust = await store.ledger.listByManifestarticleId( googleAuthId, manifestarticleId1, { transaction_type: 'INVENTORY_ADJUST' }, ); assert.equal(listLine1Adjust.length, 1); assert.equal(listLine1Adjust[0].id, 2); const listLine1None = await store.ledger.listByManifestarticleId( googleAuthId, manifestarticleId1, 'NON_EXISTENT', ); assert.equal(listLine1None.length, 0); // 6. Test query by SKU console.log('5. Testing listBySku...'); const listSkuAll = await store.ledger.listBySku(googleAuthId, 'SKU-ABC'); assert.equal(listSkuAll.length, 3); assert.equal(listSkuAll[0].id, 1); assert.equal(listSkuAll[1].id, 2); assert.equal(listSkuAll[2].id, 3); const listSkuGoodsReceipt = await store.ledger.listBySku( googleAuthId, 'SKU-ABC', 'GOODS_RECEIPT', ); assert.equal(listSkuGoodsReceipt.length, 2); assert.equal(listSkuGoodsReceipt[0].id, 1); assert.equal(listSkuGoodsReceipt[1].id, 3); const listSkuAdjust = await store.ledger.listBySku( googleAuthId, 'SKU-ABC', 'INVENTORY_ADJUST', ); assert.equal(listSkuAdjust.length, 1); assert.equal(listSkuAdjust[0].id, 2); const listUnknownSku = await store.ledger.listBySku(googleAuthId, 'UNKNOWN-SKU'); assert.equal(listUnknownSku.length, 0); // 8. Test article history created from ledger console.log('7. Testing article history entries from ledger rows...'); const history = await store.article.getHistoryBySku(googleAuthId, article.sku); const historyTexts = history.map((h) => h.text); console.log('Article history texts:', historyTexts); assert( historyTexts.some((t) => t.includes('Initial delivery movement') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')), 'History should include initial delivery movement with supplier, EK brutto, and amount', ); assert(historyTexts.includes('Damaged item written off'), 'History should include undamaged adjustment memo as is'); assert( historyTexts.some((t) => t.includes('Second line goods receipt') && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')), 'History should include second line goods receipt with details', ); // 9. Test warehouse CRUD, sort, reorder, and linking with ledger console.log('8. Testing warehouse CRUD, sort, reorder, and ledger association...'); const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' }); assert.equal(wh1.name, 'Hauptlager'); assert.equal(wh1.sort, 0); assert(wh1.id > 0); const wh2 = await store.warehouse.create(googleAuthId, { name: 'Filiale Nord' }); assert.equal(wh2.name, 'Filiale Nord'); assert.equal(wh2.sort, 1); const whList = await store.warehouse.list(googleAuthId); assert.equal(whList.length, 2); assert.equal(whList[0].id, wh1.id); assert.equal(whList[0].sort, 0); assert.equal(whList[1].id, wh2.id); assert.equal(whList[1].sort, 1); const whGet = await store.warehouse.getById(googleAuthId, wh1.id); assert.equal(whGet.name, 'Hauptlager'); assert.equal(whGet.sort, 0); const whUpdated = await store.warehouse.update(googleAuthId, wh2.id, { name: 'Filiale Süd', sort: 5 }); assert.equal(whUpdated.name, 'Filiale Süd'); assert.equal(whUpdated.sort, 5); // Test warehouse order await store.warehouse.order(googleAuthId, { warehouseIds: [wh2.id, wh1.id] }); const whListReordered = await store.warehouse.list(googleAuthId); assert.equal(whListReordered[0].id, wh2.id); assert.equal(whListReordered[0].sort, 0); assert.equal(whListReordered[1].id, wh1.id); assert.equal(whListReordered[1].sort, 1); // Test invalid warehouse order await assert.rejects( async () => store.warehouse.order(googleAuthId, { warehouseIds: [wh1.id] }), /Invalid warehouse order/, ); // Verify article detail includes warehouses with sort const articleDetail = await store.article.getBySku(googleAuthId, article.sku); assert(Array.isArray(articleDetail.article.warehouses), 'Article should have warehouses array'); assert.equal(articleDetail.article.warehouses.length, 2); assert.equal(articleDetail.article.warehouses[0].id, wh2.id); assert.equal(articleDetail.article.warehouses[0].sort, 0); assert.equal(articleDetail.article.warehouses[1].id, wh1.id); assert.equal(articleDetail.article.warehouses[1].sort, 1); // Enable stock tracking now that warehouses exist await store.article.setTrackStockBySku(googleAuthId, article.sku, true); // Add ledger row with warehouse const rowWithWh = await store.ledger.add(googleAuthId, { manifestarticle_id: manifestarticleId1, warehouse_id: wh1.id, transaction_type: 'DELIVERY', amount: 25, transaction_text: 'Delivered directly to Hauptlager', }); assert.equal(rowWithWh.warehouse_id, wh1.id); assert.equal(rowWithWh.amount, 25); // Filter by warehouse in listBySku and listByManifestarticleId const whRowsBySku = await store.ledger.listBySku(googleAuthId, 'SKU-ABC', { warehouse_id: wh1.id }); assert.equal(whRowsBySku.length, 1); assert.equal(whRowsBySku[0].id, rowWithWh.id); assert.equal(whRowsBySku[0].warehouse_id, wh1.id); const whRowsByLine = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1, { warehouse_id: wh1.id }); assert.equal(whRowsByLine.length, 1); assert.equal(whRowsByLine[0].id, rowWithWh.id); // Verify warehouse is recorded in article history const historyWithWh = await store.article.getHistoryBySku(googleAuthId, article.sku); assert( historyWithWh.some((h) => h.text.includes('Lager: "Hauptlager"') && h.text.includes('Lieferant: "Supplier X"')), 'History should include warehouse name (Lager: "Hauptlager")', ); // Trying to delete wh1 (used in ledger) must fail await assert.rejects( async () => store.warehouse.delete(googleAuthId, wh1.id), /Warehouse in use in ledger/, 'Should reject deleting warehouse referenced in ledger', ); // Deleting wh2 (not used in ledger) must succeed await store.warehouse.delete(googleAuthId, wh2.id); const whListAfterDelete = await store.warehouse.list(googleAuthId); assert.equal(whListAfterDelete.length, 1); assert.equal(whListAfterDelete[0].id, wh1.id); // Test last warehouse guard with track_stock articles const freshCompany = await store.company.create({ name: 'Warehouse Test Co', iso639: 'de', taxrate: 19, reducedtaxrate: 7, googleAuthId, }); await store.google_auth.setSelectedCompany(googleAuthId, freshCompany.id); const testWh = await store.warehouse.create(googleAuthId, { name: 'Only Warehouse' }); const testArt = await store.article.createUnderParent({ googleAuthId, parentId: freshCompany.rootCategoryId, name: 'Tracked Article', track_stock: true, }); assert.equal(testArt.track_stock, true); await assert.rejects( async () => store.warehouse.delete(googleAuthId, testWh.id), /Cannot delete the last warehouse while articles have stock tracking enabled/, 'Should reject deleting last warehouse when articles have track_stock enabled', ); // Disable track_stock on the article, then deleting the last warehouse must succeed await store.article.setTrackStockBySku(googleAuthId, testArt.sku, false); await store.warehouse.delete(googleAuthId, testWh.id); const remainingWh = await store.warehouse.list(googleAuthId); assert.equal(remainingWh.length, 0); // Switch back to original company await store.google_auth.setSelectedCompany(googleAuthId, company.id); console.log('✓ Warehouse CRUD and ledger enforcement passed'); console.log('✓ All ledger tests passed successfully!'); } finally { try { rmSync(tempDir, { recursive: true, force: true }); } catch { // ignore } } } run().catch((err) => { console.error('Test failed:', err); process.exit(1); });