import { buildAuditPayload, createSqlClient, getActiveTemplateVersions, loadTemplateSourceByOrganizationName, summarizeIssues, writeAuditArtifact } from './pdf-audit-utils.ts'; type IntegrityIssue = { kind: | 'DUPLICATE_ACTIVE_TEMPLATE' | 'DUPLICATE_ACTIVE_VERSION' | 'ACTIVE_VERSION_WITHOUT_TEMPLATE' | 'MAPPING_WITHOUT_VERSION' | 'INVALID_APPROVED_TEMPLATE_VERSION' | 'ACTIVE_VERSION_SCHEMA_DRIFT' | 'MISSING_RETAINED_INACTIVE_VERSION'; message: string; context: Record; }; async function main() { const sql = createSqlClient(); try { const [ activeTemplateVersions, versionRows, orphanMappingRows, payload, templateRows ] = await Promise.all([ getActiveTemplateVersions(sql), sql` select v.id, v.template_id as "templateId", v.organization_id as "organizationId", o.name as "organizationName", v.version, v.is_active as "isActive", v.schema_json as "schemaJson", t.id as "resolvedTemplateId" from crm_document_template_versions v left join crm_document_templates t on t.id = v.template_id left join organizations o on o.id = v.organization_id where v.deleted_at is null `, sql` select m.id, m.template_version_id as "templateVersionId", m.placeholder_key as "placeholderKey" from crm_document_template_mappings m left join crm_document_template_versions v on v.id = m.template_version_id where m.deleted_at is null and (v.id is null or v.deleted_at is not null) `, buildAuditPayload(sql), sql` select t.id, t.organization_id as "organizationId", o.name as "organizationName", t.template_name as "templateName" from crm_document_templates t inner join organizations o on o.id = t.organization_id where t.deleted_at is null and t.document_type = 'quotation' and t.file_type = 'pdfme' ` ]); const issues: IntegrityIssue[] = []; const templateBuckets = new Map(); for (const record of activeTemplateVersions) { const bucketKey = [ record.organizationId, record.documentType, record.productType, record.fileType ].join(':'); const items = templateBuckets.get(bucketKey) ?? []; items.push(record); templateBuckets.set(bucketKey, items); } for (const [bucketKey, records] of templateBuckets.entries()) { const templateIds = [...new Set(records.map((record) => record.templateId))]; if (templateIds.length > 1) { issues.push({ kind: 'DUPLICATE_ACTIVE_TEMPLATE', message: `More than one active template exists for ${bucketKey}`, context: { bucketKey, templateIds, templates: records.map((record) => ({ templateId: record.templateId, templateName: record.templateName, versionId: record.versionId, version: record.version })) } }); } } const versionBuckets = new Map>>(); for (const row of versionRows as Array>) { if (!row.isActive) { continue; } const items = versionBuckets.get(String(row.templateId)) ?? []; items.push(row); versionBuckets.set(String(row.templateId), items); if (!row.resolvedTemplateId) { issues.push({ kind: 'ACTIVE_VERSION_WITHOUT_TEMPLATE', message: `Active version ${String(row.id)} has no parent template`, context: row }); } } for (const [templateId, rows] of versionBuckets.entries()) { if (rows.length > 1) { issues.push({ kind: 'DUPLICATE_ACTIVE_VERSION', message: `Template ${templateId} has more than one active version`, context: { templateId, versions: rows.map((row) => ({ id: row.id, version: row.version })) } }); } } for (const templateRow of templateRows as Array>) { const templateId = String(templateRow.id); const rows = (versionRows as Array>).filter( (row) => String(row.templateId) === templateId ); const activeRows = rows.filter((row) => Boolean(row.isActive)); const inactiveRows = rows.filter((row) => !row.isActive); const templateSource = loadTemplateSourceByOrganizationName(String(templateRow.organizationName)); if (templateSource && activeRows.length === 1) { const activeSchema = typeof activeRows[0].schemaJson === 'string' ? JSON.parse(String(activeRows[0].schemaJson)) : activeRows[0].schemaJson; if (JSON.stringify(activeSchema) !== JSON.stringify(templateSource.schemaJson)) { issues.push({ kind: 'ACTIVE_VERSION_SCHEMA_DRIFT', message: `Active template version does not match source JSON for ${String(templateRow.templateName)}`, context: { templateId, organizationName: templateRow.organizationName, activeVersionId: activeRows[0].id, activeVersion: activeRows[0].version, sourceFile: templateSource.relativePath } }); } } if (rows.length > 1 && inactiveRows.length === 0) { issues.push({ kind: 'MISSING_RETAINED_INACTIVE_VERSION', message: `Template ${String(templateRow.templateName)} has no retained inactive historical version`, context: { templateId, organizationName: templateRow.organizationName } }); } } for (const row of orphanMappingRows as Array>) { issues.push({ kind: 'MAPPING_WITHOUT_VERSION', message: `Mapping ${String(row.id)} points to a missing template version`, context: row }); } const approvedTemplateVersionExists = payload.approvedTemplateVersionId ? (versionRows as Array>).some( (row) => String(row.id) === payload.approvedTemplateVersionId ) : true; if (payload.approvedTemplateVersionId && !approvedTemplateVersionExists) { issues.push({ kind: 'INVALID_APPROVED_TEMPLATE_VERSION', message: 'Audit quotation approvedTemplateVersionId does not resolve to a retained template version', context: { quotationCode: payload.quotationCode, approvedTemplateVersionId: payload.approvedTemplateVersionId } }); } const status = summarizeIssues(issues.length ? ['FAIL'] : ['PASS']); const report = { audit: 'template-version-integrity', overallStatus: status, activeTemplateCount: activeTemplateVersions.length, quotationCode: payload.quotationCode, approvedTemplateVersionId: payload.approvedTemplateVersionId, approvedTemplateVersionIsActive: activeTemplateVersions.some( (record) => record.versionId === payload.approvedTemplateVersionId ), issues }; const artifactPath = await writeAuditArtifact('template-version-report.json', report); console.log(JSON.stringify({ ...report, artifactPath }, null, 2)); } finally { await sql.end({ timeout: 5 }); } } main().catch((error) => { console.error(`[audit-template-version-integrity] ${error.message}`); process.exit(1); });