const fs = require('node:fs'); const path = require('node:path'); const postgres = require('postgres'); function loadEnvFile(filePath) { if (!fs.existsSync(filePath)) { return; } const content = fs.readFileSync(filePath, 'utf8'); for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim(); if (!line || line.startsWith('#')) { continue; } const separatorIndex = line.indexOf('='); if (separatorIndex === -1) { continue; } const key = line.slice(0, separatorIndex).trim(); let value = line.slice(separatorIndex + 1).trim(); if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1); } if (!(key in process.env)) { process.env[key] = value; } } } function loadLocalEnv() { loadEnvFile(path.resolve(process.cwd(), '.env.local')); loadEnvFile(path.resolve(process.cwd(), '.env')); } function assert(condition, message) { if (!condition) { throw new Error(message); } } function pickSnapshotValue(snapshot, pathSegments) { return pathSegments.reduce((value, segment) => { if (!value || typeof value !== 'object') { return undefined; } return value[segment]; }, snapshot); } async function main() { loadLocalEnv(); const databaseUrl = process.env.DATABASE_URL?.trim(); assert(databaseUrl, 'Missing DATABASE_URL'); const sql = postgres(databaseUrl, { max: 1 }); try { const mappingRows = await sql` select placeholder_key, source_path from crm_document_template_mappings where placeholder_key in ('app1', 'app1_position', 'app2', 'app2_position', 'app3', 'app3_position') and deleted_at is null order by placeholder_key asc `; const mappingMap = new Map(mappingRows.map((row) => [row.placeholder_key, row.source_path])); assert(mappingMap.get('app1') === 'signatures.preparedBy.name', 'app1 mapping is incorrect'); assert( mappingMap.get('app1_position') === 'signatures.preparedBy.position', 'app1_position mapping is incorrect' ); assert(mappingMap.get('app2') === 'signatures.approvedBy.name', 'app2 mapping is incorrect'); assert( mappingMap.get('app2_position') === 'signatures.approvedBy.position', 'app2_position mapping is incorrect' ); assert(mappingMap.get('app3') === 'signatures.authorizedBy.name', 'app3 mapping is incorrect'); assert( mappingMap.get('app3_position') === 'signatures.authorizedBy.position', 'app3_position mapping is incorrect' ); const [snapshotRow] = await sql` select q.id, q.code, q.organization_id, q.approved_snapshot from crm_quotations q where q.approved_snapshot is not null and q.deleted_at is null order by q.updated_at desc limit 1 `; assert( snapshotRow, 'No quotation with approved_snapshot was found. Generate at least one approved PDF before running verify-task-h3.' ); const snapshot = snapshotRow.approved_snapshot; const signatures = pickSnapshotValue(snapshot, ['documentData', 'signatures']); const templateInput = pickSnapshotValue(snapshot, ['templateInput']); assert(signatures && typeof signatures === 'object', 'approvedSnapshot is missing signatures'); if ( typeof signatures.preparedBy === 'string' || typeof signatures.salesManager === 'string' || typeof signatures.topManager === 'string' ) { throw new Error( 'approvedSnapshot still uses the pre-H3 signature shape. Regenerate one approved PDF to backfill snapshot signatures.' ); } for (const key of ['preparedBy', 'approvedBy', 'authorizedBy']) { const block = signatures[key]; assert(block && typeof block === 'object', `approvedSnapshot is missing ${key}`); assert( typeof block.name === 'string' && block.name.trim().length > 0 && block.name !== '-', `approvedSnapshot ${key}.name is missing` ); assert( typeof block.position === 'string' && block.position.trim().length > 0 && block.position !== '-', `approvedSnapshot ${key}.position is missing` ); } assert(templateInput && typeof templateInput === 'object', 'approvedSnapshot is missing templateInput'); assert(templateInput.app1 === signatures.preparedBy.name, 'templateInput.app1 does not match preparedBy'); assert( templateInput.app1_position === signatures.preparedBy.position, 'templateInput.app1_position does not match preparedBy.position' ); assert(templateInput.app2 === signatures.approvedBy.name, 'templateInput.app2 does not match approvedBy'); assert( templateInput.app2_position === signatures.approvedBy.position, 'templateInput.app2_position does not match approvedBy.position' ); assert( templateInput.app3 === signatures.authorizedBy.name, 'templateInput.app3 does not match authorizedBy' ); assert( templateInput.app3_position === signatures.authorizedBy.position, 'templateInput.app3_position does not match authorizedBy.position' ); const jobTitleRows = await sql` select code, label from ms_options where organization_id = ${snapshotRow.organization_id} and category = 'crm_job_title' and deleted_at is null and is_active = true order by sort_order asc `; assert(jobTitleRows.length >= 5, 'crm_job_title master options are incomplete'); console.log( JSON.stringify( { success: true, quotationId: snapshotRow.id, quotationCode: snapshotRow.code, verifiedMappings: [...mappingMap.entries()], verifiedJobTitles: jobTitleRows }, null, 2 ) ); } finally { await sql.end({ timeout: 5 }); } } main().catch((error) => { console.error(`[verify-task-h3] ${error.message}`); process.exit(1); });