Files
alla-allaos-fullstack/scripts/audit-template-version-integrity.ts
phaichayon fd01ebf7c7 pdf task-h
2026-06-19 15:09:58 +07:00

167 lines
5.0 KiB
TypeScript

import {
buildAuditPayload,
createSqlClient,
getActiveTemplateVersions,
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';
message: string;
context: Record<string, unknown>;
};
async function main() {
const sql = createSqlClient();
try {
const [activeTemplateVersions, versionRows, orphanMappingRows, payload] = await Promise.all([
getActiveTemplateVersions(sql),
sql`
select
v.id,
v.template_id as "templateId",
v.organization_id as "organizationId",
v.version,
v.is_active as "isActive",
t.id as "resolvedTemplateId"
from crm_document_template_versions v
left join crm_document_templates t
on t.id = v.template_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)
]);
const issues: IntegrityIssue[] = [];
const templateBuckets = new Map<string, typeof activeTemplateVersions>();
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<string, Array<Record<string, unknown>>>();
for (const row of versionRows as Array<Record<string, unknown>>) {
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 row of orphanMappingRows as Array<Record<string, unknown>>) {
issues.push({
kind: 'MAPPING_WITHOUT_VERSION',
message: `Mapping ${String(row.id)} points to a missing template version`,
context: row
});
}
if (
payload.approvedTemplateVersionId &&
!activeTemplateVersions.some((record) => record.versionId === payload.approvedTemplateVersionId)
) {
issues.push({
kind: 'INVALID_APPROVED_TEMPLATE_VERSION',
message: 'Audit quotation approvedTemplateVersionId does not resolve to an active 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,
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);
});