[verified] fix(ordrestyring): require hours provenance readiness
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user