Files
dashlibstorage/scripts/test-order-shipping-ledger.js
2026-10-21 11:55:21 +02:00

273 lines
11 KiB
JavaScript

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',
});
const carrier = await store.carrier.create(googleAuthId, {
name: 'Standard Carrier',
});
// 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 },
],
shipping: { carrierId: carrier.id, type: 'parcel', state: 'open' },
});
const shippingOpen = shipment.shippings[0];
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,
entityId: customer.id,
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,
carrierId: carrier.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 }],
shipping: { carrierId: carrier.id, type: 'parcel', state: 'shipped' },
});
const directShipped = shipment2.shippings[0];
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);
});