Files
lislgosms/tools/spike/validate-gateway-queue-contract.mjs

92 lines
3.9 KiB
JavaScript

import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const examplesDir = join(process.cwd(), 'docs', 'contracts', 'examples');
const envelopeFields = ['schemaVersion', 'messageType', 'traceId', 'messageId', 'channelId', 'createdAt'];
const uplinkEnvelopeFields = envelopeFields.filter((field) => field !== 'messageId');
const requiredByType = {
SubmitCommand: [
...envelopeFields,
'tenantId',
'applicationId',
'submitId',
'phoneNumber',
'content',
'signature',
'templateId',
'billingUnits',
'route',
'cmpp',
'retry',
],
SubmitResult: [...envelopeFields, 'sequenceId', 'gatewayMessageId', 'submitStatus', 'submittedAt'],
ReceiptEvent: [...envelopeFields, 'sequenceId', 'gatewayMessageId', 'receiptStatus', 'rawStatus', 'deliveredAt'],
UplinkEvent: [...uplinkEnvelopeFields, 'sequenceId', 'gatewayMessageId', 'phoneNumber', 'destId', 'content', 'receivedAt'],
};
const validTypes = new Set(Object.keys(requiredByType));
const chinaMobileNumber = /^1[3-9][0-9]{9}$/;
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function assertIsoDateTime(value, field, file) {
assert(typeof value === 'string' && !Number.isNaN(Date.parse(value)), `${file}: ${field} must be ISO date-time`);
}
function validateExample(fileName) {
const fullPath = join(examplesDir, fileName);
const payload = JSON.parse(readFileSync(fullPath, 'utf8'));
if (fileName === 'submit-result-outbox-event.json') {
for (const field of ['schemaVersion', 'eventId', 'eventType', 'path', 'messageId', 'channelId', 'submitId', 'payload', 'createdAt']) {
assert(Object.hasOwn(payload, field), `${fileName}: missing ${field}`);
}
assert(payload.schemaVersion === 'v1', `${fileName}: schemaVersion must be v1`);
assert(['submit_result', 'submit_segment_result'].includes(payload.eventType), `${fileName}: unsupported eventType`);
assert(['/gateway/events/submit-result', '/gateway/events/submit-segment-result'].includes(payload.path), `${fileName}: unsupported path`);
assert(payload.payload?.eventId === payload.eventId, `${fileName}: payload.eventId must match eventId`);
assertIsoDateTime(payload.createdAt, 'createdAt', fileName);
return `${fileName}: ${payload.eventType} ok`;
}
const type = payload.messageType;
assert(validTypes.has(type), `${fileName}: unsupported messageType ${type}`);
assert(payload.schemaVersion === 'v1', `${fileName}: schemaVersion must be v1`);
for (const field of requiredByType[type]) {
assert(Object.hasOwn(payload, field), `${fileName}: missing ${field}`);
}
for (const dateField of ['createdAt', 'submittedAt', 'deliveredAt', 'receivedAt']) {
if (payload[dateField]) {
assertIsoDateTime(payload[dateField], dateField, fileName);
}
}
if (payload.phoneNumber) {
assert(chinaMobileNumber.test(payload.phoneNumber), `${fileName}: phoneNumber must be a mainland China mobile number`);
}
if (type === 'SubmitCommand') {
assert(payload.billingUnits >= 1, `${fileName}: billingUnits must be >= 1`);
assert(payload.route?.channelCode, `${fileName}: route.channelCode is required`);
assert(payload.route?.cmppAccountCode, `${fileName}: route.cmppAccountCode is required`);
assert([0, 1].includes(payload.cmpp?.registeredDelivery), `${fileName}: cmpp.registeredDelivery must be 0 or 1`);
assert([8, 15].includes(payload.cmpp?.msgFmt), `${fileName}: cmpp.msgFmt must be 8 or 15`);
assert(payload.retry?.maxAttempts >= 1, `${fileName}: retry.maxAttempts must be >= 1`);
}
return `${fileName}: ${type} ok`;
}
const exampleFiles = readdirSync(examplesDir).filter((file) => file.endsWith('.json')).sort();
assert(exampleFiles.length > 0, 'No queue contract examples found');
const results = exampleFiles.map(validateExample);
console.log(results.join('\n'));
console.log(`Validated ${results.length} gateway queue contract examples.`);