448 lines
22 KiB
JavaScript
448 lines
22 KiB
JavaScript
import { describe, it, before, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { createTestStore, createTenant, createArticle, createCustomer, isoNow } from './helpers.js';
|
|
import { computePrevHash, INIT_HASH } from '../src/store/ledger/add.js';
|
|
|
|
describe('store.ledger', () => {
|
|
let ctx;
|
|
let store;
|
|
let tenant;
|
|
let other;
|
|
let gid;
|
|
let supplier;
|
|
let warehouse;
|
|
let warehouse2;
|
|
let customer;
|
|
let seq = 0;
|
|
|
|
const uniqueSku = (p) => `${p}-${++seq}`;
|
|
|
|
const makeDelivery = async (sku, amount = 5, extra = {}) => {
|
|
const d = await store.delivery.create(gid, {
|
|
supplierId: supplier.id,
|
|
date: isoNow(),
|
|
lines: [{ sku, name: `Line ${sku}`, pu: 1, amount, net: 10, taxrate: 19, ...extra }],
|
|
});
|
|
return { delivery: d, lineId: d.lines[0].id };
|
|
};
|
|
|
|
const makeShipmentLine = async (sku, orderAmount, shipAmount) => {
|
|
seq += 1;
|
|
const order = await store.order.create(gid, {
|
|
number: `LG-O-${seq}`,
|
|
date: '2026-10-12',
|
|
entityId: customer.id,
|
|
lines: [{ sku, name: `Order ${sku}`, pu: 1, amount: orderAmount, net: 25, taxrate: 19 }],
|
|
});
|
|
const shipment = await store.shipment.create(gid, { orderId: order.id, lines: [{ sku, amount: shipAmount }] });
|
|
return { order, shipment, lineId: shipment.lines[0].id };
|
|
};
|
|
|
|
const stockOf = async (sku, warehouseId = warehouse.id) => {
|
|
const art = (await store.article.getBySku(gid, sku)).article;
|
|
return art.warehouses.find((w) => w.id === warehouseId)?.stock;
|
|
};
|
|
|
|
before(async () => {
|
|
ctx = await createTestStore('dash-ledger-');
|
|
store = ctx.store;
|
|
tenant = await createTenant(store, { name: 'Ledger Test GmbH' });
|
|
other = await createTenant(store, { name: 'Ledger Other GmbH' });
|
|
gid = tenant.googleAuthId;
|
|
supplier = await store.supplier.create(gid, { name: 'Ledger Supplier' });
|
|
warehouse = await store.warehouse.create(gid, { name: 'Hauptlager' });
|
|
warehouse2 = await store.warehouse.create(gid, { name: 'Nebenlager' });
|
|
customer = await createCustomer(store, tenant, 'Ledger Customer');
|
|
});
|
|
|
|
after(async () => {
|
|
await ctx.cleanup();
|
|
});
|
|
|
|
describe('add — delivery lines', () => {
|
|
it('untracked article: single ACCEPTANCE row without warehouse', async () => {
|
|
const sku = uniqueSku('UNTRACKED');
|
|
await createArticle(store, tenant, { sku });
|
|
const { delivery, lineId } = await makeDelivery(sku, 5);
|
|
|
|
const row = await store.ledger.add(gid, {
|
|
manifestarticle_id: lineId,
|
|
transaction_type: 'ACCEPTANCE',
|
|
amount: 5,
|
|
transaction_text: `Lieferung ${delivery.number}`,
|
|
});
|
|
|
|
assert.equal(typeof row.id, 'number');
|
|
assert.equal(row.manifestarticle_id, lineId);
|
|
assert.equal(row.warehouse_id, null);
|
|
assert.equal(row.transaction_type, 'ACCEPTANCE');
|
|
assert.equal(row.amount, 5);
|
|
assert.equal(row.transaction_text, `Lieferung ${delivery.number}`);
|
|
assert.equal(row.hash_version, 1);
|
|
assert.match(row.prev_hash, /^[0-9a-f]{128}$|^TODO_TSE_BASED_INITHASH$/);
|
|
assert.ok(!Number.isNaN(Date.parse(row.created_at)));
|
|
|
|
const entries = await store.ledger.listByManifestarticleId(gid, lineId);
|
|
assert.equal(entries.length, 1);
|
|
});
|
|
|
|
it('tracked article: ACCEPTANCE + WAREHOUSE rows and stock increase', async () => {
|
|
const sku = uniqueSku('TRACKED');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId } = await makeDelivery(sku, 10);
|
|
|
|
const row = await store.ledger.add(gid, {
|
|
manifestarticle_id: lineId,
|
|
warehouse_id: warehouse.id,
|
|
transaction_type: 'ACCEPTANCE',
|
|
amount: 10,
|
|
transaction_text: 'Eingang',
|
|
});
|
|
assert.equal(row.warehouse_id, null);
|
|
assert.equal(row.transaction_type, 'ACCEPTANCE');
|
|
|
|
const entries = await store.ledger.listByManifestarticleId(gid, lineId);
|
|
assert.deepEqual(entries.map((e) => e.transaction_type), ['ACCEPTANCE', 'WAREHOUSE']);
|
|
assert.deepEqual(entries.map((e) => e.warehouse_id), [null, warehouse.id]);
|
|
assert.deepEqual(entries.map((e) => e.amount), [10, 10]);
|
|
assert.equal(entries[1].id, entries[0].id + 1);
|
|
assert.equal(await stockOf(sku), 10);
|
|
});
|
|
|
|
it('uses the default warehouse when none is given', async () => {
|
|
const sku = uniqueSku('DEFWH');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId } = await makeDelivery(sku, 3);
|
|
await store.ledger.add(gid, { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: 3, transaction_text: 'x' });
|
|
|
|
const entries = await store.ledger.listByManifestarticleId(gid, lineId);
|
|
assert.equal(entries.length, 2);
|
|
assert.ok(entries[1].warehouse_id > 0);
|
|
});
|
|
|
|
it('stores acceptance amounts as positive numbers', async () => {
|
|
const sku = uniqueSku('ABS');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 4);
|
|
const row = await store.ledger.add(gid, { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: -4, transaction_text: 'x' });
|
|
assert.equal(row.amount, 4);
|
|
});
|
|
|
|
it('rejects double booking of a delivery line', async () => {
|
|
const sku = uniqueSku('DOUBLE');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 2);
|
|
const input = { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: 2, transaction_text: 'x' };
|
|
await store.ledger.add(gid, input);
|
|
await assert.rejects(() => store.ledger.add(gid, input), { message: 'Delivery line already booked on ledger' });
|
|
assert.equal((await store.ledger.listByManifestarticleId(gid, lineId)).length, 1);
|
|
});
|
|
|
|
it('rejects an unknown warehouse and rolls back', async () => {
|
|
const sku = uniqueSku('BADWH');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId } = await makeDelivery(sku, 2);
|
|
await assert.rejects(
|
|
() => store.ledger.add(gid, { manifestarticle_id: lineId, warehouse_id: 999999, transaction_type: 'ACCEPTANCE', amount: 2, transaction_text: 'x' }),
|
|
{ message: 'Warehouse not found' },
|
|
);
|
|
assert.equal((await store.ledger.listByManifestarticleId(gid, lineId)).length, 0, 'transaction rolled back');
|
|
});
|
|
|
|
it('accepts camelCase aliases', async () => {
|
|
const sku = uniqueSku('CAMEL');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 1);
|
|
const row = await store.ledger.add(gid, { manifestarticleId: lineId, transactionType: 'ACCEPTANCE', amount: 1, transactionText: 'alias' });
|
|
assert.equal(row.manifestarticle_id, lineId);
|
|
assert.equal(row.transaction_text, 'alias');
|
|
});
|
|
});
|
|
|
|
describe('add — shipment lines', () => {
|
|
it('books SHIPMENT (negative) + WAREHOUSE rows and reduces stock', async () => {
|
|
const sku = uniqueSku('SHIP');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId: inLine } = await makeDelivery(sku, 10);
|
|
await store.ledger.add(gid, { manifestarticle_id: inLine, warehouse_id: warehouse.id, transaction_type: 'ACCEPTANCE', amount: 10, transaction_text: 'in' });
|
|
|
|
const { lineId } = await makeShipmentLine(sku, 5, 3);
|
|
const row = await store.ledger.add(gid, { manifestarticle_id: lineId, warehouse_id: warehouse.id, transaction_type: 'SHIPMENT', amount: 3, transaction_text: 'out' });
|
|
|
|
assert.equal(row.transaction_type, 'SHIPMENT');
|
|
assert.equal(row.warehouse_id, null);
|
|
assert.equal(row.amount, -3);
|
|
|
|
const entries = await store.ledger.listByManifestarticleId(gid, lineId);
|
|
assert.deepEqual(entries.map((e) => [e.transaction_type, e.warehouse_id, e.amount]), [
|
|
['SHIPMENT', null, -3],
|
|
['WAREHOUSE', warehouse.id, -3],
|
|
]);
|
|
assert.equal(await stockOf(sku), 7);
|
|
});
|
|
|
|
it('rejects double booking of a shipment line', async () => {
|
|
const sku = uniqueSku('SHIPDBL');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeShipmentLine(sku, 2, 2);
|
|
const input = { manifestarticle_id: lineId, transaction_type: 'SHIPMENT', amount: 2, transaction_text: 'x' };
|
|
await store.ledger.add(gid, input);
|
|
await assert.rejects(() => store.ledger.add(gid, input), { message: 'Shipment line already booked on ledger' });
|
|
});
|
|
});
|
|
|
|
describe('add — guards', () => {
|
|
it('refuses to book quote and order lines', async () => {
|
|
const sku = uniqueSku('DOC');
|
|
await createArticle(store, tenant, { sku });
|
|
const line = { sku, name: 'x', pu: 1, amount: 1, net: 1, taxrate: 19 };
|
|
const quote = await store.quote.create(gid, { number: `LG-Q-${sku}`, date: isoNow(), entityId: customer.id, lines: [line] });
|
|
const order = await store.order.create(gid, { number: `LG-O-${sku}`, date: isoNow(), entityId: customer.id, lines: [line] });
|
|
const base = { transaction_type: 'ACCEPTANCE', amount: 1, transaction_text: 'x' };
|
|
|
|
await assert.rejects(() => store.ledger.add(gid, { ...base, manifestarticle_id: quote.lines[0].id }), { message: 'Cannot book ledger entries for quote' });
|
|
await assert.rejects(() => store.ledger.add(gid, { ...base, manifestarticle_id: order.lines[0].id }), { message: 'Cannot book ledger entries for order' });
|
|
});
|
|
|
|
it('validates the payload', async () => {
|
|
const sku = uniqueSku('VALID');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 1);
|
|
const ok = { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: 1, transaction_text: 'x' };
|
|
const bad = (patch) => store.ledger.add(gid, { ...ok, ...patch });
|
|
const err = { message: 'Invalid ledger entry' };
|
|
|
|
await assert.rejects(() => bad({ manifestarticle_id: 0 }), err);
|
|
await assert.rejects(() => bad({ manifestarticle_id: 1.5 }), err);
|
|
await assert.rejects(() => bad({ manifestarticle_id: 'x' }), err);
|
|
await assert.rejects(() => bad({ warehouse_id: -1 }), err);
|
|
await assert.rejects(() => bad({ warehouse_id: 'x' }), err);
|
|
await assert.rejects(() => bad({ transaction_type: ' ' }), err);
|
|
await assert.rejects(() => bad({ transaction_type: 'T'.repeat(101) }), err);
|
|
await assert.rejects(() => bad({ transaction_type: 5 }), err);
|
|
await assert.rejects(() => bad({ amount: 'abc' }), err);
|
|
await assert.rejects(() => bad({ amount: Number.POSITIVE_INFINITY }), err);
|
|
await assert.rejects(() => bad({ transaction_text: undefined }), err);
|
|
await assert.rejects(() => bad({ transaction_text: null }), err);
|
|
});
|
|
|
|
it('rejects unknown manifest articles and ones of other companies', async () => {
|
|
const sku = uniqueSku('FOREIGN');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 1);
|
|
const input = { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: 1, transaction_text: 'x' };
|
|
|
|
await assert.rejects(() => store.ledger.add(gid, { ...input, manifestarticle_id: 999999 }), { message: 'Manifest article not found' });
|
|
await assert.rejects(() => store.ledger.add(other.googleAuthId, input), { message: 'Manifest article not found' });
|
|
assert.equal((await store.ledger.listByManifestarticleId(gid, lineId)).length, 0);
|
|
});
|
|
});
|
|
|
|
describe('hash chain', () => {
|
|
it('chains every row to the SHA-512 hash of the previous row', async () => {
|
|
const t = await createTenant(store, { name: 'Chain GmbH' });
|
|
await store.warehouse.create(t.googleAuthId, { name: 'Chain WH' });
|
|
await createArticle(store, t, { sku: 'CHAIN-1', track_stock: true });
|
|
const rows = [];
|
|
for (let i = 1; i <= 3; i += 1) {
|
|
const d = await store.delivery.create(t.googleAuthId, {
|
|
date: isoNow(),
|
|
lines: [{ sku: 'CHAIN-1', name: 'c', pu: 1, amount: i, net: 1, taxrate: 19 }],
|
|
});
|
|
await store.ledger.add(t.googleAuthId, { manifestarticle_id: d.lines[0].id, transaction_type: 'ACCEPTANCE', amount: i, transaction_text: `c${i}` });
|
|
rows.push(...(await store.ledger.listByManifestarticleId(t.googleAuthId, d.lines[0].id)));
|
|
}
|
|
|
|
assert.equal(rows.length, 6);
|
|
for (let i = 1; i < rows.length; i += 1) {
|
|
assert.equal(rows[i].prev_hash, computePrevHash(rows[i - 1]), `row ${rows[i].id} links to row ${rows[i - 1].id}`);
|
|
assert.ok(rows[i].id > rows[i - 1].id);
|
|
assert.equal(rows[i].hash_version, 1);
|
|
}
|
|
});
|
|
|
|
it('computePrevHash returns INIT_HASH for no row and a deterministic sha512 otherwise', () => {
|
|
assert.equal(computePrevHash(null), INIT_HASH);
|
|
assert.equal(computePrevHash(undefined), INIT_HASH);
|
|
|
|
const row = {
|
|
id: 7, manifestarticle_id: 3, warehouse_id: undefined, transaction_type: 'ACCEPTANCE',
|
|
amount: 2, transaction_text: 'x', hash_version: 1, prev_hash: INIT_HASH,
|
|
created_at: new Date('2026-01-01T00:00:00.000Z'),
|
|
};
|
|
const expected = createHash('sha512').update(JSON.stringify({
|
|
id: 7, manifestarticle_id: 3, warehouse_id: null, transaction_type: 'ACCEPTANCE',
|
|
amount: 2, transaction_text: 'x', hash_version: 1, prev_hash: INIT_HASH,
|
|
created_at: '2026-01-01T00:00:00.000Z',
|
|
}), 'utf8').digest('hex');
|
|
assert.equal(computePrevHash(row), expected);
|
|
assert.equal(computePrevHash({ ...row, created_at: '2026-01-01T00:00:00.000Z' }), expected);
|
|
assert.notEqual(computePrevHash({ ...row, amount: 3 }), expected);
|
|
});
|
|
});
|
|
|
|
describe('listByManifestarticleId', () => {
|
|
it('filters by transaction type and warehouse', async () => {
|
|
const sku = uniqueSku('FILTER');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId } = await makeDelivery(sku, 4);
|
|
await store.ledger.add(gid, { manifestarticle_id: lineId, warehouse_id: warehouse2.id, transaction_type: 'ACCEPTANCE', amount: 4, transaction_text: 'x' });
|
|
|
|
const byType = await store.ledger.listByManifestarticleId(gid, lineId, 'WAREHOUSE');
|
|
assert.deepEqual(byType.map((e) => e.transaction_type), ['WAREHOUSE']);
|
|
|
|
const byObjType = await store.ledger.listByManifestarticleId(gid, lineId, { transactionType: 'ACCEPTANCE' });
|
|
assert.deepEqual(byObjType.map((e) => e.transaction_type), ['ACCEPTANCE']);
|
|
|
|
const byWh = await store.ledger.listByManifestarticleId(gid, lineId, { warehouse_id: warehouse2.id });
|
|
assert.deepEqual(byWh.map((e) => e.warehouse_id), [warehouse2.id]);
|
|
|
|
const none = await store.ledger.listByManifestarticleId(gid, lineId, { warehouseId: warehouse.id });
|
|
assert.deepEqual(none, []);
|
|
assert.ok(byType[0].sku === sku);
|
|
});
|
|
|
|
it('rejects invalid, unknown and foreign lines', async () => {
|
|
const sku = uniqueSku('LISTERR');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 1);
|
|
await assert.rejects(() => store.ledger.listByManifestarticleId(gid, 0), { message: 'Invalid manifest article id' });
|
|
await assert.rejects(() => store.ledger.listByManifestarticleId(gid, 'x'), { message: 'Invalid manifest article id' });
|
|
await assert.rejects(() => store.ledger.listByManifestarticleId(gid, 999999), { message: 'Manifest article not found' });
|
|
await assert.rejects(() => store.ledger.listByManifestarticleId(other.googleAuthId, lineId), { message: 'Manifest article not found' });
|
|
});
|
|
});
|
|
|
|
describe('listBySku', () => {
|
|
it('lists all movements of an article in booking order', async () => {
|
|
const sku = uniqueSku('BYSKU');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const { lineId } = await makeDelivery(sku, 8);
|
|
await store.ledger.add(gid, { manifestarticle_id: lineId, warehouse_id: warehouse.id, transaction_type: 'ACCEPTANCE', amount: 8, transaction_text: 'in' });
|
|
const ship = await makeShipmentLine(sku, 5, 2);
|
|
await store.ledger.add(gid, { manifestarticle_id: ship.lineId, warehouse_id: warehouse.id, transaction_type: 'SHIPMENT', amount: 2, transaction_text: 'out' });
|
|
|
|
const all = await store.ledger.listBySku(gid, sku);
|
|
assert.deepEqual(all.map((e) => e.transaction_type), ['ACCEPTANCE', 'WAREHOUSE', 'SHIPMENT', 'WAREHOUSE']);
|
|
assert.ok(all.every((e) => e.sku === sku));
|
|
assert.equal(all.reduce((sum, e) => sum + (e.warehouse_id ? e.amount : 0), 0), 6, 'warehouse rows sum to stock');
|
|
|
|
const shipments = await store.ledger.listBySku(gid, sku, 'SHIPMENT');
|
|
assert.equal(shipments.length, 1);
|
|
assert.equal(shipments[0].amount, -2);
|
|
|
|
const whRows = await store.ledger.listBySku(gid, sku, { warehouseId: warehouse.id });
|
|
assert.equal(whRows.length, 2);
|
|
});
|
|
|
|
it('trims the SKU, rejects blanks and does not leak across companies', async () => {
|
|
const sku = uniqueSku('BYSKU2');
|
|
await createArticle(store, tenant, { sku });
|
|
const { lineId } = await makeDelivery(sku, 1);
|
|
await store.ledger.add(gid, { manifestarticle_id: lineId, transaction_type: 'ACCEPTANCE', amount: 1, transaction_text: 'x' });
|
|
|
|
assert.equal((await store.ledger.listBySku(gid, ` ${sku} `)).length, 1);
|
|
assert.deepEqual(await store.ledger.listBySku(other.googleAuthId, sku), []);
|
|
await assert.rejects(() => store.ledger.listBySku(gid, ' '), { message: 'Invalid SKU' });
|
|
await assert.rejects(() => store.ledger.listBySku(gid, null), { message: 'Invalid SKU' });
|
|
assert.deepEqual(await store.ledger.listBySku(gid, 'NO-SUCH-SKU'), []);
|
|
});
|
|
});
|
|
|
|
describe('stock.adjust (adjustment manifests)', () => {
|
|
it('books a warehouse movement via the ledger and updates stock', async () => {
|
|
const sku = uniqueSku('ADJ');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
|
|
const res = await store.stock.adjust(gid, { sku, warehouseId: warehouse.id, amount: 12, text: 'Inventur' });
|
|
assert.equal(res.manifest.type, 'adjustment');
|
|
assert.equal(res.ledger.length, 1);
|
|
assert.equal(res.ledger[0].warehouse_id, warehouse.id);
|
|
assert.equal(res.ledger[0].amount, 12);
|
|
assert.equal(await stockOf(sku), 12);
|
|
|
|
await store.stock.adjust(gid, { sku, warehouseId: warehouse.id, amount: -5 });
|
|
assert.equal(await stockOf(sku), 7);
|
|
assert.equal((await store.ledger.listBySku(gid, sku)).length, 2);
|
|
});
|
|
|
|
it('rejects adjustments for untracked articles and bad amounts', async () => {
|
|
const sku = uniqueSku('ADJBAD');
|
|
await createArticle(store, tenant, { sku });
|
|
await assert.rejects(
|
|
() => store.stock.adjust(gid, { sku, warehouseId: warehouse.id, amount: 1 }),
|
|
{ message: `Stock tracking is disabled for article (${sku})` },
|
|
);
|
|
const tracked = uniqueSku('ADJBAD2');
|
|
await createArticle(store, tenant, { sku: tracked, track_stock: true });
|
|
await assert.rejects(() => store.stock.adjust(gid, { sku: tracked, warehouseId: warehouse.id, amount: 0 }), /non-zero/);
|
|
});
|
|
|
|
it('disabling tracking writes a zeroing ledger entry', async () => {
|
|
const sku = uniqueSku('ADJOFF');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
await store.stock.adjust(gid, { sku, warehouseId: warehouse.id, amount: 9 });
|
|
assert.equal(await stockOf(sku), 9);
|
|
|
|
await store.article.setTrackStockBySku(gid, sku, false);
|
|
assert.equal(await stockOf(sku), 0);
|
|
});
|
|
});
|
|
|
|
describe('stock.accept / stock.ship', () => {
|
|
it('accept books positive lines as ACCEPTANCE and negative lines as RETURN', async () => {
|
|
const sku = uniqueSku('ACCEPT');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
const d = await store.delivery.create(gid, {
|
|
supplierId: supplier.id,
|
|
date: isoNow(),
|
|
lines: [
|
|
{ sku, name: 'in', pu: 1, amount: 6, net: 10, taxrate: 19 },
|
|
{ sku, name: 'ret', pu: 1, amount: -2, net: 10, taxrate: 19 },
|
|
],
|
|
});
|
|
|
|
const pos = await store.stock.accept(gid, { manifestarticleId: d.lines[0].id, warehouseId: warehouse.id });
|
|
assert.equal(pos.document_ledger.transaction_type, 'ACCEPTANCE');
|
|
assert.equal(pos.document_ledger.amount, 6);
|
|
assert.equal(pos.adjust_ledger.amount, 6);
|
|
assert.equal(pos.adjust_ledger.warehouse_id, warehouse.id);
|
|
|
|
const neg = await store.stock.accept(gid, { manifestarticleId: d.lines[1].id, warehouseId: warehouse.id });
|
|
assert.equal(neg.document_ledger.transaction_type, 'RETURN');
|
|
assert.equal(neg.document_ledger.amount, -2);
|
|
assert.equal(await stockOf(sku), 4);
|
|
});
|
|
|
|
it('ship books positive lines as SHIPMENT and negative lines as RETOURE', async () => {
|
|
const sku = uniqueSku('SHIPSTK');
|
|
await createArticle(store, tenant, { sku, track_stock: true });
|
|
await store.stock.adjust(gid, { sku, warehouseId: warehouse.id, amount: 20 });
|
|
|
|
seq += 1;
|
|
const order = await store.order.create(gid, {
|
|
number: `LG-SO-${seq}`, date: '2026-10-12', entityId: customer.id,
|
|
lines: [{ sku, name: 'o', pu: 1, amount: 10, net: 1, taxrate: 19 }],
|
|
});
|
|
const shipment = await store.shipment.create(gid, { orderId: order.id, lines: [{ sku, amount: 4 }, { sku, amount: -1 }] });
|
|
|
|
const pos = await store.stock.ship(gid, { manifestarticleId: shipment.lines[0].id, warehouseId: warehouse.id });
|
|
assert.equal(pos.document_ledger.transaction_type, 'SHIPMENT');
|
|
assert.equal(pos.document_ledger.amount, -4);
|
|
|
|
await assert.rejects(
|
|
store.stock.ship(gid, { manifestarticleId: shipment.lines[1].id, warehouseId: warehouse.id }),
|
|
/Amount must be positive/,
|
|
);
|
|
assert.equal(await stockOf(sku), 16);
|
|
});
|
|
});
|
|
|
|
describe('append-only', () => {
|
|
it('exposes no mutating API besides add', () => {
|
|
assert.deepEqual(Object.keys(store.ledger).sort(), ['add', 'listByManifestarticleId', 'listBySku']);
|
|
});
|
|
});
|
|
});
|