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.`);