diff --git a/scratch/test-inventory-touch.js b/scratch/test-inventory-touch.js new file mode 100644 index 0000000..ec51060 --- /dev/null +++ b/scratch/test-inventory-touch.js @@ -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); +}); diff --git a/src/providers/mssql/queries/article.js b/src/providers/mssql/queries/article.js index 31b5649..b469db9 100644 --- a/src/providers/mssql/queries/article.js +++ b/src/providers/mssql/queries/article.js @@ -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 = ` INSERT INTO article (company_id, sku, unit_id, unitamount, weight, track_stock) OUTPUT INSERTED.id diff --git a/src/providers/sqlite3/queries/article.js b/src/providers/sqlite3/queries/article.js index 68cf2eb..5f597df 100644 --- a/src/providers/sqlite3/queries/article.js +++ b/src/providers/sqlite3/queries/article.js @@ -12,6 +12,7 @@ export const { LIST_CHILDREN_BY_PARENT_ID, LIST_PAIRED_POSTERMINAL_IDS_BY_ARTICLE, SET_PARENT_SORT, 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, UPDATE_UNITAMOUNT, UPDATE_UNIT_AND_AMOUNT, UPDATE_WEIGHT, } = queries; diff --git a/src/store/articlewarehousesummary/syncSummary.js b/src/store/articlewarehousesummary/syncSummary.js index a462350..7c8a169 100644 --- a/src/store/articlewarehousesummary/syncSummary.js +++ b/src/store/articlewarehousesummary/syncSummary.js @@ -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. * @@ -142,6 +151,11 @@ export async function syncArticleWarehouseSummaryForLedger(runner, { companyId, warehouseId, 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 }); } } diff --git a/src/store/ledger/add.js b/src/store/ledger/add.js index e6b0988..92fb17e 100644 --- a/src/store/ledger/add.js +++ b/src/store/ledger/add.js @@ -11,6 +11,14 @@ import { recordWarehouseHistory, } from '../articlehistory/record.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'; @@ -202,6 +210,10 @@ export default function add(provider) { // Record history for acceptance 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, { articleId: match.id, companyuserId, @@ -287,6 +299,10 @@ export default function add(provider) { }); 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, { articleId: match.id, companyuserId, diff --git a/src/store/stock/adjust.js b/src/store/stock/adjust.js index 36f4f7b..3546bd1 100644 --- a/src/store/stock/adjust.js +++ b/src/store/stock/adjust.js @@ -11,6 +11,14 @@ import ledgerAdd from '../ledger/add.js'; import makeRunner from '../runner.js'; import { parseDate } from '../manifestLines.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'; @@ -160,6 +168,11 @@ export default function adjust(provider) { 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 if (manifestArticle.manifest_type === 'delivery') { await recordWarehouseHistory(runner, { @@ -369,6 +382,10 @@ export default function adjust(provider) { // Write article history for the adjusted article 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, { articleId: item.articleId, companyuserId,