u
This commit is contained in:
159
scratch/test-inventory-touch.js
Normal file
159
scratch/test-inventory-touch.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
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 main() {
|
||||||
|
console.log('--- Testing updated_at touch on all inventory movements ---');
|
||||||
|
const tempDir = mkdtempSync(join(tmpdir(), 'test-touch-'));
|
||||||
|
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('tester@example.com');
|
||||||
|
const company = await store.company.create({
|
||||||
|
name: 'Inventory Corp',
|
||||||
|
iso639: 'de',
|
||||||
|
taxrate: 19,
|
||||||
|
reducedtaxrate: 7,
|
||||||
|
googleAuthId,
|
||||||
|
});
|
||||||
|
await store.google_auth.setSelectedCompany(googleAuthId, company.id);
|
||||||
|
|
||||||
|
// Create warehouse
|
||||||
|
const wh = await store.warehouse.create(googleAuthId, { name: 'Hauptlager' });
|
||||||
|
|
||||||
|
const cat = await store.category.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: company.rootCategoryId,
|
||||||
|
name: 'Waren',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create article with past updated_at
|
||||||
|
const art = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
name: 'Test Article',
|
||||||
|
track_stock: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldTime = '2020-01-01T00:00:00.000Z';
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
const getUpdatedAt = async (articleId) => {
|
||||||
|
const res = await provider.query(`SELECT updated_at FROM article WHERE id = @articleId`, { articleId });
|
||||||
|
return res.rows[0]?.updated_at;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.equal(initialUpdatedAt, oldTime, 'Should start with old updated_at');
|
||||||
|
|
||||||
|
// 1. Test store.stock.adjust
|
||||||
|
console.log('1. Testing store.stock.adjust touches article.updated_at...');
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 2,
|
||||||
|
// reason omitted so it defaults to "Manual stock addition (+2)"
|
||||||
|
});
|
||||||
|
|
||||||
|
const afterAdjustUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(afterAdjustUpdatedAt, oldTime, 'stock.adjust should touch updated_at');
|
||||||
|
assert.ok(new Date(afterAdjustUpdatedAt).getTime() > new Date(oldTime).getTime());
|
||||||
|
|
||||||
|
// Check history was recorded with exact text
|
||||||
|
const historyRes = await provider.query(`SELECT text FROM articlehistory WHERE article_id = @id`, { id: art.id });
|
||||||
|
const adjustmentEntry = historyRes.rows.find((h) => h.text.includes('Bestandskorrektur +2 (Manual stock addition (+2))'));
|
||||||
|
assert.ok(adjustmentEntry, 'Should have exact article history entry');
|
||||||
|
|
||||||
|
// Reset updated_at to old time
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
// 2. Test Stückliste component touch
|
||||||
|
console.log('2. Testing Stückliste parent is touched when component has stock adjustment...');
|
||||||
|
const bundle = await store.article.createUnderParent({
|
||||||
|
googleAuthId,
|
||||||
|
parentId: cat.id,
|
||||||
|
sku: 'BUNDLE-1',
|
||||||
|
name: 'Bundle 1',
|
||||||
|
track_stock: 0,
|
||||||
|
});
|
||||||
|
await store.articlelist.setItemsByArticleSku(googleAuthId, 'BUNDLE-1', [
|
||||||
|
{ sku: 'TEST-SKU', count: 1 },
|
||||||
|
]);
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: bundle.id });
|
||||||
|
|
||||||
|
await store.stock.adjust(googleAuthId, {
|
||||||
|
sku: 'TEST-SKU',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
amount: 5,
|
||||||
|
text: 'Restock component',
|
||||||
|
});
|
||||||
|
|
||||||
|
const bundleUpdatedAt = await getUpdatedAt(bundle.id);
|
||||||
|
assert.notEqual(bundleUpdatedAt, oldTime, 'Stückliste bundle updated_at should be touched when component changes stock');
|
||||||
|
|
||||||
|
// 3. Test delivery acceptance touches article.updated_at
|
||||||
|
console.log('3. Testing delivery touches article.updated_at...');
|
||||||
|
const supplier = await store.supplier.create(googleAuthId, { name: 'Supplier Inc' });
|
||||||
|
const delivery = await store.delivery.create(googleAuthId, {
|
||||||
|
supplierId: supplier.id,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 10, net: 5, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
await store.delivery.book(googleAuthId, { id: delivery.id, warehouseId: wh.id });
|
||||||
|
const deliveryUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(deliveryUpdatedAt, oldTime, 'delivery.book should touch article.updated_at');
|
||||||
|
|
||||||
|
// 4. Test customer shipment touches article.updated_at
|
||||||
|
console.log('4. Testing shipment touches article.updated_at...');
|
||||||
|
const groups = await store.customergroup.list(googleAuthId);
|
||||||
|
const customer = await store.entity.create(googleAuthId, { name: 'Customer A', customergroupId: groups[0].id });
|
||||||
|
const order = await store.order.create(googleAuthId, {
|
||||||
|
number: 'AUF-000001',
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
entityId: customer.id,
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 3, net: 10, taxrate: 19 }],
|
||||||
|
});
|
||||||
|
const carrier = await store.carrier.create(googleAuthId, { name: 'DHL' });
|
||||||
|
await provider.query(`UPDATE article SET updated_at = @oldTime WHERE id = @id`, { oldTime, id: art.id });
|
||||||
|
|
||||||
|
await store.shipment.create(googleAuthId, {
|
||||||
|
orderId: order.id,
|
||||||
|
lines: [{ sku: 'TEST-SKU', amount: 3 }],
|
||||||
|
warehouseId: wh.id,
|
||||||
|
shipping: {
|
||||||
|
carrierId: carrier.id,
|
||||||
|
type: 'standard',
|
||||||
|
state: 'shipped',
|
||||||
|
warehouseId: wh.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shipmentUpdatedAt = await getUpdatedAt(art.id);
|
||||||
|
assert.notEqual(shipmentUpdatedAt, oldTime, 'shipment.create should touch article.updated_at');
|
||||||
|
|
||||||
|
console.log('✅ ALL INVENTORY TOUCH TESTS PASSED');
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Test failed:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -239,6 +239,17 @@ WHERE optionslist_id IN (
|
|||||||
)
|
)
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT = `
|
||||||
|
UPDATE article
|
||||||
|
SET updated_at = SYSUTCDATETIME()
|
||||||
|
WHERE articlelist_id IN (
|
||||||
|
SELECT ala.articlelist_id
|
||||||
|
FROM articlelistarticle ala
|
||||||
|
WHERE ala.article_id = @articleId
|
||||||
|
)
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
export const CREATE = `
|
export const CREATE = `
|
||||||
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
|
INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock)
|
||||||
OUTPUT INSERTED.id
|
OUTPUT INSERTED.id
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const {
|
|||||||
LIST_CHILDREN_BY_PARENT_ID, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE,
|
LIST_CHILDREN_BY_PARENT_ID, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE,
|
||||||
SET_PARENT_SORT,
|
SET_PARENT_SORT,
|
||||||
SET_IS_PARENT, TOGGLE_PARENT, TOUCH_UPDATED_AT, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT,
|
SET_IS_PARENT, TOGGLE_PARENT, TOUCH_UPDATED_AT, TOUCH_OPTION_PARENT_ARTICLES_UPDATED_AT,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT,
|
||||||
UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
|
UNLINK_CHILD, UPDATE_BRAND, UPDATE_PARENT_BY_SKU, UPDATE_TRACK_STOCK, UPDATE_UNIT,
|
||||||
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
|
UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT,
|
||||||
} = queries;
|
} = queries;
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as MSSQL_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as SQLITE_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table.
|
* Centralized sync and rebuild logic for the materialized `articlewarehousesummary` table.
|
||||||
*
|
*
|
||||||
@@ -142,6 +151,11 @@ export async function syncArticleWarehouseSummaryForLedger(runner, { companyId,
|
|||||||
warehouseId,
|
warehouseId,
|
||||||
stock,
|
stock,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||||
|
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
|
||||||
|
await runner.query(touchSql, { articleId });
|
||||||
|
await runner.query(touchStuecklisteSql, { articleId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import {
|
|||||||
recordWarehouseHistory,
|
recordWarehouseHistory,
|
||||||
} from '../articlehistory/record.js';
|
} from '../articlehistory/record.js';
|
||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as MSSQL_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as SQLITE_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
|
|
||||||
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
|
export const INIT_HASH = 'TODO_TSE_BASED_INITHASH';
|
||||||
|
|
||||||
@@ -202,6 +210,10 @@ export default function add(provider) {
|
|||||||
|
|
||||||
// Record history for acceptance
|
// Record history for acceptance
|
||||||
for (const match of articleMatches) {
|
for (const match of articleMatches) {
|
||||||
|
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||||
|
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
|
||||||
|
await runner.query(touchSql, { articleId: match.id });
|
||||||
|
await runner.query(touchStuecklisteSql, { articleId: match.id });
|
||||||
await recordAcceptanceHistory(runner, {
|
await recordAcceptanceHistory(runner, {
|
||||||
articleId: match.id,
|
articleId: match.id,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
@@ -287,6 +299,10 @@ export default function add(provider) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const match of articleMatches) {
|
for (const match of articleMatches) {
|
||||||
|
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||||
|
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
|
||||||
|
await runner.query(touchSql, { articleId: match.id });
|
||||||
|
await runner.query(touchStuecklisteSql, { articleId: match.id });
|
||||||
await recordShipmentHistory(runner, {
|
await recordShipmentHistory(runner, {
|
||||||
articleId: match.id,
|
articleId: match.id,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import ledgerAdd from '../ledger/add.js';
|
|||||||
import makeRunner from '../runner.js';
|
import makeRunner from '../runner.js';
|
||||||
import { parseDate } from '../manifestLines.js';
|
import { parseDate } from '../manifestLines.js';
|
||||||
import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js';
|
import { syncArticleWarehouseSummaryForLedger } from '../articlewarehousesummary/syncSummary.js';
|
||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as MSSQL_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as MSSQL_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/mssql/queries/article.js';
|
||||||
|
import {
|
||||||
|
TOUCH_UPDATED_AT as SQLITE_TOUCH,
|
||||||
|
TOUCH_STUECKLISTE_ARTICLES_UPDATED_AT as SQLITE_TOUCH_STUECKLISTE,
|
||||||
|
} from '../../providers/sqlite3/queries/article.js';
|
||||||
|
|
||||||
const ADJUSTMENT_TYPE = 'adjustment';
|
const ADJUSTMENT_TYPE = 'adjustment';
|
||||||
|
|
||||||
@@ -160,6 +168,11 @@ export default function adjust(provider) {
|
|||||||
warehouseId,
|
warehouseId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||||
|
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
|
||||||
|
await runner.query(touchSql, { articleId: art.id });
|
||||||
|
await runner.query(touchStuecklisteSql, { articleId: art.id });
|
||||||
|
|
||||||
// Write warehouse history for tracked article
|
// Write warehouse history for tracked article
|
||||||
if (manifestArticle.manifest_type === 'delivery') {
|
if (manifestArticle.manifest_type === 'delivery') {
|
||||||
await recordWarehouseHistory(runner, {
|
await recordWarehouseHistory(runner, {
|
||||||
@@ -369,6 +382,10 @@ export default function adjust(provider) {
|
|||||||
|
|
||||||
// Write article history for the adjusted article
|
// Write article history for the adjusted article
|
||||||
if (item.articleId) {
|
if (item.articleId) {
|
||||||
|
const touchSql = isSqlite ? SQLITE_TOUCH : MSSQL_TOUCH;
|
||||||
|
const touchStuecklisteSql = isSqlite ? SQLITE_TOUCH_STUECKLISTE : MSSQL_TOUCH_STUECKLISTE;
|
||||||
|
await runner.query(touchSql, { articleId: item.articleId });
|
||||||
|
await runner.query(touchStuecklisteSql, { articleId: item.articleId });
|
||||||
await recordStockAdjustmentHistory(runner, {
|
await recordStockAdjustmentHistory(runner, {
|
||||||
articleId: item.articleId,
|
articleId: item.articleId,
|
||||||
companyuserId,
|
companyuserId,
|
||||||
|
|||||||
Reference in New Issue
Block a user