94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
import {
|
|
buildAuditPayload,
|
|
buildComputedSnapshotPayload,
|
|
computePayloadHash,
|
|
createSqlClient,
|
|
summarizeIssues,
|
|
writeAuditArtifact
|
|
} from './pdf-audit-utils.ts';
|
|
|
|
function pickComparablePayload(payload: Record<string, unknown>) {
|
|
return {
|
|
templateInput: payload.templateInput,
|
|
topicInputKeys:
|
|
payload.topicInputs && typeof payload.topicInputs === 'object'
|
|
? Object.keys(payload.topicInputs as Record<string, unknown>).sort()
|
|
: []
|
|
};
|
|
}
|
|
|
|
async function main() {
|
|
const sql = createSqlClient();
|
|
|
|
try {
|
|
const payload = await buildAuditPayload(sql);
|
|
const previewPayload = pickComparablePayload({
|
|
templateInput: payload.templateInput,
|
|
topicInputs: payload.topicInputs
|
|
});
|
|
const generationPayload = pickComparablePayload({
|
|
templateInput: payload.templateInput,
|
|
topicInputs: payload.topicInputs
|
|
});
|
|
const computedSnapshot = buildComputedSnapshotPayload(payload, {
|
|
generatedAt: 'audit-generated',
|
|
generatedBy: 'audit-suite'
|
|
});
|
|
const snapshotPayload = pickComparablePayload({
|
|
templateInput: computedSnapshot.templateInput,
|
|
topicInputs: payload.topicInputs
|
|
});
|
|
const previewHash = computePayloadHash(previewPayload);
|
|
const generationHash = computePayloadHash(generationPayload);
|
|
const snapshotHash = computePayloadHash(snapshotPayload);
|
|
const mismatches = [
|
|
previewHash !== generationHash ? 'preview:generation' : null,
|
|
generationHash !== snapshotHash ? 'generation:snapshot' : null,
|
|
previewHash !== snapshotHash ? 'preview:snapshot' : null
|
|
].filter(Boolean);
|
|
const signatureKeys = ['preparedBy', 'approvedBy', 'authorizedBy'];
|
|
const signatureParity = signatureKeys.every((key) => {
|
|
const signatures = payload.documentData.signatures as Record<string, unknown>;
|
|
return key in signatures;
|
|
});
|
|
const priceTable = payload.templateInput.quotation_price_data;
|
|
const firstPriceRow = Array.isArray(priceTable) && Array.isArray(priceTable[0]) ? priceTable[0] : null;
|
|
const priceTableParity =
|
|
Array.isArray(priceTable) &&
|
|
Array.isArray(firstPriceRow) &&
|
|
firstPriceRow[0] === 'Price (Exclude VAT)' &&
|
|
typeof firstPriceRow[1] === 'string' &&
|
|
typeof firstPriceRow[3] === 'string';
|
|
const status = summarizeIssues([
|
|
mismatches.length ? 'FAIL' : 'PASS',
|
|
signatureParity ? 'PASS' : 'FAIL',
|
|
priceTableParity ? 'PASS' : 'FAIL'
|
|
]);
|
|
const report = {
|
|
audit: 'payload-parity',
|
|
overallStatus: status,
|
|
quotationCode: payload.quotationCode,
|
|
previewHash,
|
|
generationHash,
|
|
snapshotHash,
|
|
mismatches,
|
|
comparableFieldCount: Object.keys(payload.templateInput).length,
|
|
topicInputKeyCount: Object.keys(payload.topicInputs).length,
|
|
signatureParity,
|
|
priceTableParity,
|
|
priceTableRowCount: Array.isArray(priceTable) ? priceTable.length : 0,
|
|
priceTableCurrency: Array.isArray(firstPriceRow) ? firstPriceRow[3] ?? null : null
|
|
};
|
|
const artifactPath = await writeAuditArtifact('payload-parity-report.json', report);
|
|
|
|
console.log(JSON.stringify({ ...report, artifactPath }, null, 2));
|
|
} finally {
|
|
await sql.end({ timeout: 5 });
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(`[audit-payload-parity] ${error.message}`);
|
|
process.exit(1);
|
|
});
|