This commit is contained in:
seb
2026-10-16 22:09:12 +02:00
parent 796f6a3272
commit 7635821dad
38 changed files with 6191 additions and 3350 deletions

View File

@@ -5,12 +5,10 @@ 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';
import { computePrevHash, INIT_HASH } 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 tempDir = mkdtempSync(join(tmpdir(), 'dash-ledger-test-'));
const dbFile = join(tempDir, 'test.db');
const storageDir = join(tempDir, 'storage');
@@ -18,50 +16,85 @@ async function run() {
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');
console.log('--- Testing store.ledger (acceptance, warehouse, shipment, guards) ---');
const googleAuthId = await store.google_auth.add('ledger-tester@example.com');
const company = await store.company.create({
name: 'Ledger Test Corp',
name: 'Ledger Test GmbH',
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 supplier = await store.supplier.create(googleAuthId, { name: 'Supplier X' });
// Create an article with track_stock = false initially
const article = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Ledger Linked Article',
track_stock: false,
});
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 customer1 = await store.entity.create(googleAuthId, {
name: 'Customer Quote Test',
isCustomer: true,
customergroupId: company.rootCategoryId,
});
// Create a quote and verify ledger bookings are rejected
const quote = await store.quote.create(googleAuthId, {
number: 'Q-1',
entityId: customer1.id,
date: new Date().toISOString(),
lines: [{ sku: 'SKU-ABC', name: 'Quote item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
});
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: quote.lines[0].id,
transaction_type: 'ACCEPTANCE',
amount: 2,
transaction_text: 'Should fail for quote',
}),
/Cannot book ledger entries for quote/,
'Should reject booking ledger entries for quote manifest',
);
// Create an order and verify ledger bookings are rejected
const order = await store.order.create(googleAuthId, {
number: 'O-1',
entityId: customer1.id,
date: new Date().toISOString(),
lines: [{ sku: 'SKU-ABC', name: 'Order item', pu: 1, amount: 2, net: 10, taxrate: 19 }],
});
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: order.lines[0].id,
transaction_type: 'ACCEPTANCE',
amount: 2,
transaction_text: 'Should fail for order',
}),
/Cannot book ledger entries for order/,
'Should reject booking ledger entries for order manifest',
);
// 1. Create a delivery manifest with lines
const delivery = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
externalNumber: 'DEL-1',
@@ -78,7 +111,7 @@ async function run() {
},
{
sku: 'SKU-ABC',
name: 'Ledger Item 1 Batch 2',
name: 'Ledger Item 2',
pu: 1,
amount: 10,
net: 20,
@@ -90,278 +123,311 @@ async function run() {
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...');
// 2. Book line 1 for untracked article (acceptance only, warehouse_id is null)
console.log('1. Booking acceptance for untracked article line 1...');
const row1 = await store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
transaction_type: 'GOODS_RECEIPT',
transaction_type: 'ACCEPTANCE',
amount: 5,
transaction_text: 'Initial delivery movement',
transaction_text: `Lieferung ${delivery.number}`,
});
assert.equal(row1.id, 1);
assert.equal(row1.manifestarticle_id, manifestarticleId1);
assert.equal(row1.transaction_type, 'GOODS_RECEIPT');
assert.equal(row1.warehouse_id, null);
assert.equal(row1.transaction_type, 'ACCEPTANCE');
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)
// Attempting to book line 1 again must be rejected
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
transaction_type: 'ACCEPTANCE',
amount: 5,
transaction_text: 'Double booking attempt',
}),
/Delivery line already booked on ledger/,
'Should reject booking an already booked delivery line',
);
// 3. Create warehouse and enable tracking on the article
console.log('2. Creating warehouse and enabling stock tracking...');
const wh1 = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
// Even though tracking is now enabled, line 1 is already accepted so it cannot be booked again
await assert.rejects(
async () => store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
warehouse_id: wh1.id,
transaction_type: 'ACCEPTANCE',
amount: 5,
transaction_text: 'Late booking attempt after tracking enabled',
}),
/Delivery line already booked on ledger/,
'Cannot book a delivery line later once stock tracking is enabled if already accepted',
);
// 4. Book line 2 (now tracked): books ACCEPTANCE (row 2) + WAREHOUSE (row 3) in one transaction
console.log('3. Booking line 2 with tracking enabled (dual entries: ACCEPTANCE + WAREHOUSE)...');
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',
manifestarticle_id: manifestarticleId2,
warehouse_id: wh1.id,
transaction_type: 'ACCEPTANCE',
amount: 10,
transaction_text: `Lieferung ${delivery.number}`,
});
assert.equal(row2.id, 2);
assert.equal(row2.hash_version, 1);
assert.equal(row2.manifestarticle_id, manifestarticleId2);
assert.equal(row2.warehouse_id, null);
assert.equal(row2.transaction_type, 'ACCEPTANCE');
assert.equal(row2.amount, 10);
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',
});
// Query ledger lines for manifestarticle 2 to verify row 3 (WAREHOUSE entry)
const line2Entries = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId2);
assert.equal(line2Entries.length, 2, 'Line 2 must have 2 ledger entries (ACCEPTANCE + WAREHOUSE)');
assert.equal(line2Entries[0].transaction_type, 'ACCEPTANCE');
assert.equal(line2Entries[0].warehouse_id, null);
assert.equal(line2Entries[1].transaction_type, 'WAREHOUSE');
assert.equal(line2Entries[1].warehouse_id, wh1.id);
assert.equal(line2Entries[1].amount, 10);
assert.equal(line2Entries[1].id, 3);
assert.equal(line2Entries[1].prev_hash, computePrevHash(line2Entries[0]));
assert.equal(row3.id, 3);
assert.equal(row3.hash_version, 1);
assert.equal(row3.prev_hash, expectedHash3);
// 5. Check warehouse stock for article
console.log('4. Checking warehouse stock calculation...');
const artDetail = await store.article.getBySku(googleAuthId, article.sku);
const whStock = artDetail.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(whStock, 10, 'Warehouse stock must be 10 (from line 2 WAREHOUSE ledger entry only)');
// 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');
// 6. Check deliverysummary is_booked
const updatedDelivery = await store.delivery.getById(googleAuthId, delivery.id);
assert.equal(updatedDelivery.ledger.isBooked, true, 'Delivery should be fully booked');
assert.equal(updatedDelivery.ledger.completelyOnLedgerPositions, 2);
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...');
// 7. Check article history
console.log('5. Verifying article history...');
const history = await store.article.getHistoryBySku(googleAuthId, article.sku);
const historyTexts = history.map((h) => h.text);
console.log('Article history texts:', historyTexts);
console.log('History entries:', 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',
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 11.90') && t.includes('Menge: 5')),
'History must include acceptance of line 1 with Lieferung number, supplier, EK brutto, 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',
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lieferant: "Supplier X"') && t.includes('EK brutto: 23.80') && t.includes('Menge: 10')),
'History must include acceptance of line 2',
);
assert(
historyTexts.some((t) => t.includes(`Lieferung ${delivery.number}`) && t.includes('Lager: "Hauptlager"') && t.includes('Menge: 10')),
'History must include warehouse entry for tracked line 2',
);
// 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);
// 8. Test store.delivery.book on a fresh delivery
console.log('6. Testing store.delivery.book batch booking...');
const delivery2 = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
externalNumber: 'DEL-2',
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Batch Item 1', pu: 1, amount: 4, net: 15, taxrate: 19 },
],
});
const bookedDelivery2 = await store.delivery.book(googleAuthId, { id: delivery2.id, warehouseId: wh1.id });
assert.equal(bookedDelivery2.ledger.isBooked, true);
assert.equal(bookedDelivery2.ledger.completelyOnLedgerPositions, 1);
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
// Double booking delivery2 must reject
await assert.rejects(
async () => store.warehouse.order(googleAuthId, { warehouseIds: [wh1.id] }),
/Invalid warehouse order/,
async () => store.delivery.book(googleAuthId, { id: delivery2.id }),
/Delivery line(s)? already (accepted|booked) on ledger/,
'Should reject booking already booked delivery',
);
// 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);
// 9. Test shipment booking
console.log('7. Testing shipment booking (SHIPMENT + WAREHOUSE dual entries)...');
const customer = await store.entity.create(googleAuthId, {
name: 'Customer Y',
isCustomer: true,
customergroupId: company.rootCategoryId,
});
// Enable stock tracking now that warehouses exist
await store.article.setTrackStockBySku(googleAuthId, article.sku, true);
const order2 = await store.order.create(googleAuthId, {
number: 'O-2',
entityId: customer.id,
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Ordered Item', pu: 1, amount: 3, net: 25, taxrate: 19 },
],
});
// Add ledger row with warehouse
const rowWithWh = await store.ledger.add(googleAuthId, {
manifestarticle_id: manifestarticleId1,
const shipment = await store.shipment.create(googleAuthId, {
orderId: order2.id,
lines: [{ sku: 'SKU-ABC', amount: 3 }],
});
const shipmentLineId = shipment.lines[0].id;
const shipRow = await store.ledger.add(googleAuthId, {
manifestarticle_id: shipmentLineId,
warehouse_id: wh1.id,
transaction_type: 'DELIVERY',
amount: 25,
transaction_text: 'Delivered directly to Hauptlager',
transaction_type: 'SHIPMENT',
amount: 3,
transaction_text: `Lieferschein ${shipment.number}`,
});
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);
assert.equal(shipRow.transaction_type, 'SHIPMENT');
assert.equal(shipRow.warehouse_id, null);
assert.equal(shipRow.amount, -3);
const whRowsByLine = await store.ledger.listByManifestarticleId(googleAuthId, manifestarticleId1, { warehouse_id: wh1.id });
assert.equal(whRowsByLine.length, 1);
assert.equal(whRowsByLine[0].id, rowWithWh.id);
const shipEntries = await store.ledger.listByManifestarticleId(googleAuthId, shipmentLineId);
assert.equal(shipEntries.length, 2, 'Shipment line must have 2 entries (SHIPMENT + WAREHOUSE)');
assert.equal(shipEntries[0].transaction_type, 'SHIPMENT');
assert.equal(shipEntries[0].warehouse_id, null);
assert.equal(shipEntries[0].amount, -3);
assert.equal(shipEntries[1].transaction_type, 'WAREHOUSE');
assert.equal(shipEntries[1].warehouse_id, wh1.id);
assert.equal(shipEntries[1].amount, -3);
// Verify warehouse is recorded in article history
const historyWithWh = await store.article.getHistoryBySku(googleAuthId, article.sku);
// Stock should now be: 10 (line 2) + 4 (delivery2) - 3 (shipment) = 11
const artDetailAfterShip = await store.article.getBySku(googleAuthId, article.sku);
const whStockAfterShip = artDetailAfterShip.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(whStockAfterShip, 11, 'Warehouse stock must be 11 (10 + 4 - 3)');
// Verify shipment history
const historyAfterShip = await store.article.getHistoryBySku(googleAuthId, article.sku);
const shipHistoryTexts = historyAfterShip.map((h) => h.text);
assert(
historyWithWh.some((h) => h.text.includes('Lager: "Hauptlager"') && h.text.includes('Lieferant: "Supplier X"')),
'History should include warehouse name (Lager: "Hauptlager")',
shipHistoryTexts.some((t) => t.includes(`Lieferschein ${shipment.number}`) && t.includes('Menge: 3')),
'Article history should record outgoing shipment',
);
// 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,
// 8. Test store.stock.accept with deliverymanifestitem
console.log('8. Testing store.stock.accept (positive acceptance & negative return)...');
const delivery3 = await store.delivery.create(googleAuthId, {
supplierId: supplier.id,
externalNumber: 'DEL-3',
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Positive Delivery Line', pu: 1, amount: 6, net: 10, taxrate: 19 },
{ sku: 'SKU-ABC', name: 'Negative Delivery Line (Return)', pu: 1, amount: -2, net: 10, taxrate: 19 },
],
});
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,
const posDeliveryLineId = delivery3.lines[0].id;
const negDeliveryLineId = delivery3.lines[1].id;
const acceptPosResult = await store.stock.accept(googleAuthId, {
manifestarticleId: posDeliveryLineId,
warehouseId: wh1.id,
});
assert.equal(testArt.track_stock, true);
assert.equal(acceptPosResult.document_ledger.transaction_type, 'ACCEPTANCE');
assert.equal(acceptPosResult.document_ledger.amount, 6);
assert.equal(acceptPosResult.document_ledger.warehouse_id, null);
assert.equal(acceptPosResult.adjust_ledger.amount, 6);
assert.equal(acceptPosResult.adjust_ledger.warehouse_id, wh1.id);
// Negative delivery line: return of a delivery (outgoing)
const acceptNegResult = await store.stock.accept(googleAuthId, {
manifestarticleId: negDeliveryLineId,
warehouseId: wh1.id,
});
assert.equal(acceptNegResult.document_ledger.transaction_type, 'RETURN');
assert.equal(acceptNegResult.document_ledger.amount, -2);
assert.equal(acceptNegResult.document_ledger.warehouse_id, null);
assert.equal(acceptNegResult.adjust_ledger.amount, -2);
assert.equal(acceptNegResult.adjust_ledger.warehouse_id, wh1.id);
// 9. Test store.stock.ship with shipmentmanifestitem
console.log('9. Testing store.stock.ship (positive shipment & negative retoure)...');
const order3 = await store.order.create(googleAuthId, {
number: 'O-3',
entityId: customer.id,
date: new Date().toISOString(),
lines: [
{ sku: 'SKU-ABC', name: 'Shipment Item Normal', pu: 1, amount: 5, net: 10, taxrate: 19 },
],
});
const shipment3 = await store.shipment.create(googleAuthId, {
orderId: order3.id,
lines: [
{ sku: 'SKU-ABC', amount: 4 },
{ sku: 'SKU-ABC', amount: -1 },
],
});
const posShipLineId = shipment3.lines[0].id;
const negShipLineId = shipment3.lines[1].id;
const shipPosResult = await store.stock.ship(googleAuthId, {
manifestarticleId: posShipLineId,
warehouseId: wh1.id,
});
assert.equal(shipPosResult.document_ledger.transaction_type, 'SHIPMENT');
assert.equal(shipPosResult.document_ledger.amount, -4);
assert.equal(shipPosResult.adjust_ledger.amount, -4);
// Negative shipment line: retoure of a shipment (incoming)
const shipNegResult = await store.stock.ship(googleAuthId, {
manifestarticleId: negShipLineId,
warehouseId: wh1.id,
});
assert.equal(shipNegResult.document_ledger.transaction_type, 'RETOURE');
assert.equal(shipNegResult.document_ledger.amount, 1);
assert.equal(shipNegResult.adjust_ledger.amount, 1);
// 10. Test store.stock.adjust
console.log('10. Testing store.stock.adjust failure on untracked article...');
const untrackedArt = await store.article.createUnderParent({
googleAuthId,
parentId: company.rootCategoryId,
name: 'Pure Untracked Article',
track_stock: false,
});
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',
async () => store.stock.adjust(googleAuthId, {
sku: untrackedArt.sku,
warehouseId: wh1.id,
amount: 5,
}),
/Stock tracking is disabled for article/,
'Adjust must fail when article has no stock tracking',
);
// 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);
// 11. Test before stock tracking is disabled, adjust to 0 has to be ledgerized
console.log('11. Testing zero-adjustment before stock tracking is disabled...');
const artBeforeDisable = await store.article.getBySku(googleAuthId, article.sku);
const stockBeforeDisable = artBeforeDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
console.log('Current stock before disabling tracking:', stockBeforeDisable);
assert(stockBeforeDisable > 0, 'Article must have non-zero stock before disabling');
console.log('✓ Warehouse CRUD and ledger enforcement passed');
// Disable stock tracking
await store.article.setTrackStockBySku(googleAuthId, article.sku, false);
const artAfterDisable = await store.article.getBySku(googleAuthId, article.sku);
assert.equal(artAfterDisable.article.track_stock, false, 'Article track_stock must now be false');
const stockAfterDisable = artAfterDisable.article.warehouses.find((w) => w.id === wh1.id)?.stock;
assert.equal(stockAfterDisable, 0, 'Warehouse stock must be adjusted to 0 on the ledger');
// Calling adjust now must fail
await assert.rejects(
async () => store.stock.adjust(googleAuthId, {
sku: article.sku,
warehouseId: wh1.id,
amount: 1,
}),
/Stock tracking is disabled for article/,
'Subsequent adjust must fail once tracking is disabled',
);
console.log('✓ All ledger tests passed successfully!');
} finally {