[verified] fix(ordrestyring): require hours provenance readiness

This commit is contained in:
alexpolo1
2026-10-04 21:37:32 +02:00
parent f14f54dad8
commit 42579dbc7f
4 changed files with 13 additions and 7 deletions
@@ -1,12 +1,13 @@
const fs = require('fs');
const path = require('path');
const { assertOrdrestyringReadiness } = require('../ordrestyringPreflight');
const tables = ['ordrestyring_cases_current', 'ordrestyring_debtors_current', 'ordrestyring_material_semantics', 'ordrestyring_projection_state'];
const tables = ['ordrestyring_cases_current', 'ordrestyring_debtors_current', 'ordrestyring_material_semantics', 'ordrestyring_projection_state', 'ordrestyring_hour_mirror_ids'];
function fixture() {
const states = ['cases', 'debtors', 'materials'].map(projection_kind => ({ projection_kind, source_account: 'account', row_count: 2, sync_run_id: 'run', mirrored_at: '2026-10-04' }));
const counts = { actual_count: 2, account_count: 2 };
const db = { execute: jest.fn(async sql => {
if (sql.includes('INFORMATION_SCHEMA')) return [tables.map(table_name => ({ table_name }))];
if (sql.includes('ordrestyring_hour_mirror_ids')) return [[{ tracked_count: 2 }]];
if (sql.includes('FROM ordrestyring_local.ordrestyring_projection_state')) return [states];
return [[counts]];
}) };
@@ -124,7 +124,7 @@ test.each([
test('benchmark SQL requires both export state and tracked upstream identity', () => {
expect(hoursRelation).toMatch(/exported\s*=\s*0/);
expect(hoursRelation).toMatch(/exported\s+IS\s+NOT\s+NULL[\s\S]*exported\s*<>\s*-1/);
expect(hoursRelation).toMatch(/EXISTS\s*\(SELECT.*ordrestyring_hour_mirror_ids.*hour_id\s*=\s*\w+\.id/s);
expect(planningHoursRelation).not.toMatch(/exported|ordrestyring_hour_mirror_ids/);
});
@@ -148,8 +148,8 @@ test('benchmark hours exclude local and untracked rows while planning retains lo
INSERT INTO ordrestyring_local.ordrestyring_hour_mirror_ids VALUES (1), (2), (5), (6), (7);`);
const model = new OrdrestyringReadModel({ query: async (sql, params) => db.prepare(sql).all(...params) });
const hours = await model.hours(['A']);
expect(hours.get('A').map(row => row.id)).toEqual([1, 6]);
expect(db.prepare(`SELECT SUM((stop_time - start_time) / 3600) AS total FROM ${hoursRelation}`).get().total).toBe(2);
expect(hours.get('A').map(row => row.id)).toEqual([1, 2, 6]);
expect(db.prepare(`SELECT SUM((stop_time - start_time) / 3600) AS total FROM ${hoursRelation}`).get().total).toBe(4);
expect(db.prepare(`SELECT id FROM ${planningHoursRelation} ORDER BY id`).all().map(row => row.id)).toEqual([1, 2, 3, 4, 6, 7]);
} finally {
db.close();
@@ -16,10 +16,15 @@ async function assertOrdrestyringReadiness(connection, env = process.env) {
throw new Error('Ordrestyring preflight requires ORDRESTYRING_SOURCE_ACCOUNT');
}
const fail = reason => { throw new Error(`Ordrestyring canonical preflight failed: ${reason}. Run the explicit canonical migration before starting.`); };
const tables = [...Object.values(projections), 'ordrestyring_projection_state'];
const tables = [...Object.values(projections), 'ordrestyring_projection_state', 'ordrestyring_hour_mirror_ids'];
const rows = await model.rows(`SELECT TABLE_NAME AS table_name FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA = 'ordrestyring_local' AND TABLE_NAME IN (?, ?, ?, ?)`, tables);
WHERE TABLE_SCHEMA = 'ordrestyring_local' AND TABLE_NAME IN (?, ?, ?, ?, ?)`, tables);
if (!tables.every(table => rows.some(row => row.table_name === table))) fail('missing canonical tables');
const [trackedHours] = await model.rows(`SELECT COUNT(*) AS tracked_count
FROM ordrestyring_local.ordrestyring_hour_mirror_ids`);
if (!Number.isSafeInteger(Number(trackedHours?.tracked_count)) || Number(trackedHours.tracked_count) <= 0) {
fail('hours provenance is not ready');
}
const states = await model.rows(`SELECT projection_kind, source_account, row_count, sync_run_id, mirrored_at
FROM ordrestyring_local.ordrestyring_projection_state`);
for (const [kind, table] of Object.entries(projections)) {
@@ -18,7 +18,7 @@ const fields = (names) => names.map(name => `NULLIF(JSON_UNQUOTE(JSON_EXTRACT(so
const casesRelation = `(SELECT case_number, case_number AS id, ${fields(['customer_number', 'description', 'remarks', 'creation_date', 'created_at', 'updated_at', 'status', 'case_type', 'main_technician', 'additional_technicians', 'contact', 'delivery_address', 'yourref', 'work_done', 'offer_number'])} FROM ordrestyring_local.ordrestyring_cases_current)`;
const debtorsRelation = `(SELECT customer_number, ${fields(['customer_name', 'customer_address', 'customer_telephone', 'customer_email'])} FROM ordrestyring_local.ordrestyring_debtors_current)`;
const seconds = name => `(CASE WHEN ${name} > 100000000000 THEN ${name} / 1000 ELSE ${name} END)`;
const hoursRelation = `(SELECT id, emp_id, new_case_number, remark, hour_type, ${seconds('start_time')} AS start_time, ${seconds('stop_time')} AS stop_time FROM ordrestyring_local.hours h WHERE exported = 0 AND EXISTS (SELECT 1 FROM ordrestyring_local.ordrestyring_hour_mirror_ids mirror WHERE mirror.hour_id = h.id) AND start_time > 0 AND ${seconds('stop_time')} > ${seconds('start_time')})`;
const hoursRelation = `(SELECT id, emp_id, new_case_number, remark, hour_type, ${seconds('start_time')} AS start_time, ${seconds('stop_time')} AS stop_time FROM ordrestyring_local.hours h WHERE exported IS NOT NULL AND exported <> -1 AND EXISTS (SELECT 1 FROM ordrestyring_local.ordrestyring_hour_mirror_ids mirror WHERE mirror.hour_id = h.id) AND start_time > 0 AND ${seconds('stop_time')} > ${seconds('start_time')})`;
// Planning also represents local allocations whose end is not known yet.
const planningHoursRelation = `(SELECT id, emp_id, new_case_number, remark, hour_type, ${seconds('start_time')} AS start_time, ${seconds('stop_time')} AS stop_time FROM ordrestyring_local.hours WHERE start_time > 0 AND (stop_time = 0 OR ${seconds('stop_time')} > ${seconds('start_time')}))`;
const parse = value => typeof value === 'string' ? JSON.parse(value) : value;