feat: add phone frequency controls and modularize codebase
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import ts from 'typescript';
|
||||
|
||||
const root = process.cwd();
|
||||
const servicePath = path.join(root, 'api/src/send-chain/send-chain.service.ts');
|
||||
const submissionPath = path.join(root, 'api/src/send-chain/send-submission.service.ts');
|
||||
const contract = JSON.parse(
|
||||
fs.readFileSync(path.join(root, 'docs/contracts/send-chain-r9-submission.json'), 'utf8'),
|
||||
);
|
||||
const serviceSource = fs.readFileSync(servicePath, 'utf8');
|
||||
const submissionSource = fs.readFileSync(submissionPath, 'utf8');
|
||||
const parse = (filePath, source) =>
|
||||
ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
||||
const serviceFile = parse(servicePath, serviceSource);
|
||||
const submissionFile = parse(submissionPath, submissionSource);
|
||||
const serviceClass = serviceFile.statements.find(
|
||||
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendChainService',
|
||||
);
|
||||
const submissionClass = submissionFile.statements.find(
|
||||
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === 'SendSubmissionService',
|
||||
);
|
||||
if (!serviceClass || !submissionClass) throw new Error('R9 send-chain facade or submission domain is missing');
|
||||
|
||||
const methodsByName = (classNode) => new Map(
|
||||
classNode.members
|
||||
.filter((member) => ts.isMethodDeclaration(member) && member.name && ts.isIdentifier(member.name))
|
||||
.map((member) => [member.name.text, member]),
|
||||
);
|
||||
const facadeMethods = methodsByName(serviceClass);
|
||||
const submissionMethods = methodsByName(submissionClass);
|
||||
const domainSources = new Map();
|
||||
const domainMethods = new Map();
|
||||
for (const domain of contract.domains) {
|
||||
const filePath = path.join(root, 'api/src/send-chain', domain.file);
|
||||
const source = fs.readFileSync(filePath, 'utf8');
|
||||
const file = parse(filePath, source);
|
||||
const classNode = file.statements.find(
|
||||
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === domain.className,
|
||||
);
|
||||
if (!classNode) throw new Error(`R9 domain class missing: ${domain.className}`);
|
||||
const methods = methodsByName(classNode);
|
||||
const migratedCount = contract.methods.filter((method) => method.file === domain.file).length;
|
||||
if (migratedCount !== domain.methodCount) {
|
||||
throw new Error(`R9 domain contract count changed: ${domain.file}`);
|
||||
}
|
||||
domainSources.set(domain.file, { source, file });
|
||||
domainMethods.set(domain.file, methods);
|
||||
}
|
||||
const combinedDomainSource = [...domainSources.values()].map((item) => item.source).join('\n');
|
||||
const digest = (value) =>
|
||||
crypto.createHash('sha256').update(value.replace(/\r\n/g, '\n')).digest('hex');
|
||||
const normalizeFacadeSeams = (value) => value.replaceAll('this.facade.', 'this.');
|
||||
|
||||
for (const expected of contract.methods) {
|
||||
const domain = domainSources.get(expected.file);
|
||||
const migrated = domainMethods.get(expected.file)?.get(expected.name);
|
||||
if (!migrated?.body) throw new Error(`R9 migrated method missing: ${expected.name}`);
|
||||
if (digest(normalizeFacadeSeams(migrated.body.getText(domain.file))) !== expected.bodySha256) {
|
||||
throw new Error(`R9 migrated method body changed: ${expected.name}`);
|
||||
}
|
||||
const internalFacade = submissionMethods.get(expected.name);
|
||||
if (!internalFacade?.body || !internalFacade.body.getText(submissionFile).includes(`.${expected.name}(`)) {
|
||||
throw new Error(`R9 internal submission facade does not delegate ${expected.name}`);
|
||||
}
|
||||
const facade = facadeMethods.get(expected.name);
|
||||
if (!facade?.body) throw new Error(`R9 stable facade method missing: ${expected.name}`);
|
||||
const body = facade.body.getText(serviceFile);
|
||||
if (!body.includes(`this.submission.${expected.name}(`)) {
|
||||
throw new Error(`R9 facade does not delegate ${expected.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const retained of [
|
||||
'handleSubmitSegmentResult',
|
||||
'handleSubmitResult',
|
||||
'intakeReceipt',
|
||||
'handleReceipt',
|
||||
'retryMessageIfAllowed',
|
||||
'recordCmppFailureReceipt',
|
||||
'refundMessage',
|
||||
'queueAndTryDownstreamDelivery',
|
||||
]) {
|
||||
const method = facadeMethods.get(retained);
|
||||
if (!method?.body || method.body.getText(serviceFile).includes('this.submission.')) {
|
||||
throw new Error(`R9 moved R10 behavior out of the facade: ${retained}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const invariant of [
|
||||
"new Logger('SendChainService')",
|
||||
'this.prisma.$transaction',
|
||||
'this.callbacks.releaseMessageReservation',
|
||||
'this.callbacks.recordCmppFailureReceipt',
|
||||
'GATEWAY_SUBMIT_STREAM',
|
||||
'await this.facade.publishGatewaySubmitCommand(command);',
|
||||
'new Worker<SendJob>(',
|
||||
]) {
|
||||
if (!combinedDomainSource.includes(invariant)) {
|
||||
throw new Error(`R9 submission invariant missing: ${invariant}`);
|
||||
}
|
||||
}
|
||||
|
||||
const moduleSource = fs.readFileSync(path.join(root, 'api/src/send-chain/send-chain.module.ts'), 'utf8');
|
||||
if (!moduleSource.includes('providers: [SendChainService]') || moduleSource.includes('SendSubmissionService,')) {
|
||||
throw new Error('R9 changed the public NestJS provider boundary');
|
||||
}
|
||||
|
||||
console.log(
|
||||
`R9 send submission verified: ${contract.methods.length} methods across ${contract.domains.length} domains; `
|
||||
+ `${facadeMethods.size} stable facade methods, unchanged bodies and R10 boundaries preserved.`,
|
||||
);
|
||||
Reference in New Issue
Block a user