Arquitetura Limpa, SOLID e Mapa da Rubrica
Esta página documenta a engenharia do Nexo, conectando a arquitetura em 3 camadas do projeto de referência pos/App_Nasa aos 16 itens avaliados na rubrica oficial.
16 Itens da Rubrica Mapeados no Código
Filtre por competência para inspecionar os arquivos e contratos TypeScript.
Modela estados e operações financeiras com uniões de literais estritas, impedindo valores mágicos inválidos em tempo de compilação.
export type TransactionType = 'INCOME' | 'EXPENSE' | 'PIX_TRANSFER';
export type PixKeyType = 'CPF' | 'EMAIL' | 'PHONE' | 'RANDOM';
export type OpenFinanceConsentStatus = 'AUTHORIZED' | 'EXPIRED' | 'REVOKED';Aplica tipagem explícita em contratos públicos (parâmetros e retornos de métodos/UseCases) e aproveita a inferência do compilador em acumuladores locais e constantes `as const`.
public static calculateNetBalance(...items: readonly Transaction[]): number {
const total = items.reduce((acc, tx) => acc + tx.signedAmount, 0); // inferido como number
return Number(total.toFixed(2));
}Valida propriedades com `Transaction.validateProps` retornando `Either<Failure, Transaction>` e aplica sobrecarga de função em `Transaction.lookupTransaction` (por índice `number` ou por `TransactionCategory`).
public static lookupTransaction(items: readonly Transaction[], index: number): Transaction | undefined;
public static lookupTransaction(items: readonly Transaction[], category: TransactionCategory): readonly Transaction[];
public static lookupTransaction(items: readonly Transaction[], key: number | TransactionCategory) { ... }Modela meios de pagamento com União Discriminada (`PaymentMethodDetail` por `kind: "pix" | "card" | "open_finance"`), função `describePaymentMethod` e status de orçamento no PierreFinancialEngine.
export type PaymentMethodDetail = PixPaymentDetail | CreditCardPaymentDetail | OpenFinanceDebitDetail;
export function describePaymentMethod(payment: PaymentMethodDetail): string {
switch (payment.kind) { case 'pix': ... case 'card': ... case 'open_finance': ... }
}Segrega os contratos da camada Domain em interfaces específicas (`ITransactionRepository`, `IOpenFinanceRepository`, `IPixGatewayRepository` e `IUseCase<Success, Params>`).
export interface IOpenFinanceRepository {
getConnectedAccounts(): Promise<Either<Failure, readonly BankAccount[]>>;
syncInstitution(institution: InstitutionCode): Promise<Either<Failure, OpenFinanceSyncReport>>;
toggleConsent(accountId: string, authorize: boolean): Promise<Either<Failure, BankAccount>>;
}Utiliza tuplas imutáveis para coordenadas bancárias (`[banco, agencia, conta]`), tetos de orçamento (`[metaIdeal, tetoAlerta]`) e QR Code PIX, além de `...spread` em `copyWith` e `...rest` em cálculos.
export type BankCoordinatesTuple = readonly [
bankCode: string,
branchNumber: string,
accountWithDigit: string,
];
export type BudgetThresholdTuple = readonly [idealLimit: number, maxAlertCeiling: number];Protege o saldo (`private _balance`) e o consentimento (`private _consentStatus`) dentro de `BankAccount`, além de contadores e validadores `static` em `Failure` e `PixKeyValidator`.
export class BankAccount {
private _balance: number;
private _consentStatus: OpenFinanceConsentStatus;
public get balance(): number { return this._balance; }
public credit(amount: number): Either<Failure, number> { ... }
}Articula interseções de metadados (`IdentifiableContract & AuditableContract`), callbacks tipados (`FoldCallback<Input, Output>`) e herança entre `TransactionModel` e `Transaction`.
export type DomainRecordMetadata = IdentifiableContract & AuditableContract;
export type FoldCallback<Input, Output> = (value: Input) => Output;A regra `@typescript-eslint/no-explicit-any: error` proíbe `any`. Todo dado externo entra como `unknown` e passa por `isPlainRecord`, `hasStringField` e `hasNumberField`.
public static fromUnknown(raw: unknown): Either<Failure, TransactionModel> {
if (!isPlainRecord(raw) || !hasStringField(raw, 'id') || !hasNumberField(raw, 'amount')) {
return left(new ValidationFailure('Payload de transação inválido.'));
}
return right(new TransactionModel({ ... }));
}Usa `keyof` para acesso seguro a propriedades (`getPropertyByKey`, `pickObjectKeys`) e `typeof CATEGORY_METADATA` para derivar tipos a partir de constantes imutáveis.
export type CategoryMetadataMap = typeof CATEGORY_METADATA;
export function getPropertyByKey<T extends object, K extends keyof T>(target: T, key: K): T[K] {
return target[key];
}Implementa `Either<L, R>`, `IUseCase<SuccessType, Params>` e `FlavorConfig.getProfileFor<F extends AppFlavor>(flavor: F)` eliminando duplicação de contratos.
export interface IUseCase<SuccessType, Params> {
execute(params: Params): Promise<Either<Failure, SuccessType>>;
}Emprega `DeepReadonly<T>`, `EntityPatch<T>`, `FlavorGatewayContract<F>` (Conditional Type) e `assertNever(value: never): never` nos switches de Enums e chaves PIX.
export type FlavorGatewayContract<F extends AppFlavor> =
F extends AppFlavor.PROD ? ProdGatewayContract : DevGatewayContract;
export function assertNever(value: never, context = 'Valor não tratado'): never {
throw new Error(`${context}: ${String(value)}`);
}Habilita `strict`, `noImplicitAny`, `strictNullChecks`, `noImplicitOverride`, `noUnusedLocals`, `noFallthroughCasesInSwitch` e `tsc -p tsconfig.build.core.json`.
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitOverride": true,
"sourceMap": trueGerencia `@types/node`, `@types/react` e `@types/react-dom` e gera arquivos `.js.map` e `.d.ts.map` em `dist-core/` via `npm run build:core`.
"outDir": "./dist-core",
"sourceMap": true,
"declaration": true,
"declarationMap": trueDefine os enums `AppFlavor`, `TransactionCategory` e `InstitutionCode` integrados a helpers exaustivos e recursos modernos do ES2022.
export enum AppFlavor { DEV = 'DEV', PROD = 'PROD' }
export enum InstitutionCode { NUBANK = 'NUBANK', ITAU = 'ITAU', INTER = 'INTER', ASAAS = 'ASAAS' }Documenta e testa a migração do cálculo de conciliação financeira de JS para TS estrito e registra 4 revisões técnicas sobre sugestões de IA.
const result = RefactoredReconciliationService.reconcileBatch(rawExternalBatch);
// Retorna Either<Failure, ReconciliationSummary> com tupla [gross, fee, net]Seção 5 • Evidência de Evolução e Refatoração (JS Legado → TS Estrito)
Comparativo prático em src/legacy/legacy-reconciliation.ts corrigindo coerção de string, propagação de NaN e falta de exaustividade.
// [ANTES — JavaScript Legado sem tipagem estática]
// Problemas detectados durante a migração para TypeScript:
// 1. Parâmetro 'items' sem contrato (aceitava objetos malformados ou strings numéricas).
// 2. Soma 'total += item.amount' concatenava strings ("1000250.50") quando a API retornava string.
// 3. 'item.amount * item.feeOverridePercent' gerava NaN quando feeOverridePercent era undefined.
// 4. Switch de 'tier' sem default exaustivo retornava taxa undefined para novas modalidades.
function reconcilePaymentsLegacy(items) {
let totalGross = 0;
let totalFees = 0;
for (let i = 0; i < items.length; i++) {
const item = items[i];
totalGross += item.amount; // BUG 1: concatenação de string se amount for "250.50"
let rate;
if (item.feeOverridePercent !== null) {
rate = item.feeOverridePercent; // BUG 2: se for undefined, passa no !== null e gera NaN!
} else if (item.tier === 'STANDARD_PIX') {
rate = 0;
} else if (item.tier === 'PRIORITY_ASAAS') {
rate = 0.0099;
}
// BUG 3: 'OPEN_FINANCE_DIRECT' não tratado -> rate fica undefined -> totalFees vira NaN
totalFees += item.amount * rate;
}
return {
gross: totalGross,
fees: totalFees,
net: totalGross - totalFees,
};
}// [DEPOIS — TypeScript Estrito com Clean Architecture, unknown Type Guards e assertNever]
export class RefactoredReconciliationService {
public static resolveTierFeeRate(tier: SettlementTier): number {
switch (tier) {
case 'STANDARD_PIX':
return 0;
case 'PRIORITY_ASAAS':
return 0.0099;
case 'OPEN_FINANCE_DIRECT':
return 0.0045;
default:
return assertNever(tier, 'Modalidade de liquidação desconhecida');
}
}
// ... validação com Type Guards sobre unknown e retorno Either<Failure, ReconciliationSummary>
}Seção 6 • Registro e Avaliação Técnica do Uso de IA
Decisões técnicas onde sugestões padrão foram ajustadas para preservar a segurança de tipos, o encapsulamento OOP e a simplicidade da arquitetura.
Sugestão Inicial: Usar `const data = JSON.parse(raw) as any` ou `as TransactionModel[]` direto na leitura do armazenamento para economizar código.
Problema Identificado: O cast direto (`as TransactionModel[]`) oculta dados corrompidos em tempo de execução e perde os métodos da prototipagem da classe `TransactionModel`, além de violar a proibição de `any`.
Decisão Adotada: Implementamos `safeJsonParse(raw): Either<Failure, unknown>` combinado com `TransactionModel.fromUnknown(item)` usando Type Guards (`isPlainRecord`, `hasStringField`, `hasNumberField`).
Sugestão Inicial: Reutilizar integralmente o `tsconfig.json` do template base contendo `"erasableSyntaxOnly": true` e sem emissão de Source Maps.
Problema Identificado: A flag `erasableSyntaxOnly` bloqueia o uso de `enum` e propriedades de construtor (`private readonly`), recursos exigidos pelos itens 2.3 e 4.3 da rubrica, além de não gerar arquivos `.js.map`.
Decisão Adotada: Ajustamos o `tsconfig.json` com `"sourceMap": true`, `"noImplicitOverride": true` e criamos `tsconfig.build.core.json` para emitir `.js`, `.d.ts` e `.js.map` verificáveis em `dist-core/`.
Sugestão Inicial: Definir `BankAccount` e `Transaction` apenas como interfaces planas com propriedades públicas mutáveis (`balance: number`).
Problema Identificado: Interfaces planas mutáveis permitem que a camada de UI altere `account.balance -= 500` diretamente, ignorando a verificação de consentimento do Open Finance (`isAuthorized`) e regras de saldo negativo.
Decisão Adotada: Modelamos `BankAccount` e `Transaction` como classes OOP com campos encapsulados (`private _balance`, `private _consentStatus`, `protected readonly _rawAmount`) e métodos que retornam `Either<Failure, number>`.
Sugestão Inicial: Criar um tipo condicional recursivo de 4 níveis para inferir rotas, payloads e headers HTTP simultaneamente.
Problema Identificado: Tipos condicionais excessivamente aninhados prejudicam a legibilidade das mensagens de erro do compilador sem trazer ganho real ao domínio financeiro.
Decisão Adotada: Mantivemos apenas tipos utilitários com propósito claro no domínio: `FlavorGatewayContract<F>`, `UnwrapEitherSuccess<T>`, `DeepReadonly<T>` e `EntityPatch<T>`.