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

378 lines
15 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 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);
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 shipping = { carrierId: carrier.id, type: 'parcel', state: 'draft' };
// 0. Shipment without shipping is rejected
await assert.rejects(
() => store.shipment.create(googleAuthId, { orderId: order.id, lines: [{ sku: 'ITEM-A', amount: 1 }] }),
{ message: 'Shipping is required' },
);
console.log('PASS: Shipment without shipping rejected');
// 1. Create Shipment 1: 6x Item A, 3x Item B -> AU-1001-L-1
const shipment1 = await store.shipment.create(googleAuthId, {
shipping,
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, {
shipping,
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, {
shipping,
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, {
shipping,
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, {
shipping,
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 further packages to Shipment 1 with tracking codes
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, 3);
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, 3);
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, {
shipping,
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, {
shipping,
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 remaining items with explicit lines
const shAll = await store.order.createShipment(googleAuthId, order2.id, { shipping, lines: [{ sku: 'ITEM-B', amount: 5 }] });
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 remaining items with explicit lines; ordersummary updated with shipped_quantity=15, is_shipped=1');
// 9d. Calling createShipment without lines is rejected with "Invalid lines"
await assert.rejects(
async () => {
await store.order.createShipment(googleAuthId, order2.id);
},
{ message: 'Invalid lines' }
);
console.log('PASS: 9d. Creating shipment without lines rejected with "Invalid lines"');
// 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);
});