Skip to content

CLAUDE.md — usrv-settlement

Microservicio de settlements con tres handlers:

  • settlementHandler (Lambda invoke): recibe info de un settlement, calcula force_decrease, inserta en DynamoDB.
  • changeStatusFinancesHandler (HTTP POST): recibe el payload de settlement (sin campos de cálculo interno) e invoca síncronamente la Lambda de finanzas.
  • getSettlementsHandler (HTTP GET /v1/settlements): consulta settlements desde MongoDB con filtros dinámicos y paginación.

Referencia: usrv-kushki-acq/ — consultar antes de asumir cualquier patrón.


Comandos esenciales

ComandoDescripción
npm run test:unitSolo unit tests
npm run test:coverageTests + cobertura
npm testLint + tests + cobertura (pre-push)
npm run lintFormato + duplicados + ESLint
npm run lint:fixAuto-fix ESLint
npm run test:watchRe-corre tests al guardar
npm run typesRegenerar tipos TS desde JSON Schemas
npm run deployDesplegar a AWS

Cobertura requerida: 100% en branches, lines, functions y statements. Excluidos del reporte: src/handler/**/*, src/constant/types.ts, src/constant/Tables.ts, src/constant/Lambdas.ts, src/constant/BatchResources.ts, src/utils/testSetup.ts, src/middleware/MongoConnectionMiddleware.ts.


Arquitectura

Patrón: Hexagonal + Inversify DI. Flujo: Handler → Middleware → Service → Gateway → Infra

CarpetaResponsabilidad
handler/Lambda entry points (invoke)
service/Lógica de negocio
gateway/DynamoDB, Lambda invoke
repository/Interfaces/contratos
infrastructure/Logger, Container, Enums
middleware/Middy middlewares
constant/Tipos Inversify, enums, Tables, Lambdas
schema/JSON Schemas AJV
utils/Funciones utilitarias

Inyección de dependencias

Contenedor: src/infrastructure/container.ts — Ver implementación real ahí. Símbolos: src/constant/types.ts. Singletons: DynamoDBDocumentClient, LambdaClient.

typescript
// Patrón constructor
constructor(
  @inject(DynamoGateway) private readonly _dynamo: IDynamoGateway,
  @inject(Logger) private readonly _logger: ILogger,
) {}

Handlers

HandlerTriggerDescripción
settlementHandlerLambda InvokeRecibe info de settlement por rango de tiempo
changeStatusFinancesHandlerHTTP POST /finances/change-statusInvoca Lambda de finanzas síncronamente
getSettlementsHandlerHTTP GET /v1/settlementsConsulta settlements desde MongoDB con filtros y paginación

settlementHandler sin events en serverless.yml — invocado directamente por otro microservicio. changeStatusFinancesHandler y getSettlementsHandler expuestos vía API Gateway v2 (httpApi) con custom domain.


Base de datos — DynamoDB

TablaPKGSIDescripción
SettlementTablesettlement_idSettlements procesados
  • Naming: ${service}-${stage}-settlement — definido en custom.resources.settlementTable
  • El nombre se expone como SETTLEMENT_TABLE env var y se consume en src/constant/Tables.ts
  • Atributos en camelCase · BillingMode: PAY_PER_REQUEST · DeletionPolicy: Retain
  • created_at se guarda como Unix timestamp en ms (Date.now(), tipo N) — el GSI que usaba este campo fue eliminado

Constantes de tablas, lambdas y recursos externos

Toda referencia a tablas/ARNs debe pasar por constantes con getters — la lectura de process.env ocurre en tiempo de acceso, no al importar (crítico para tests).

typescript
const TABLES: ITableList = {
  get settlementTable() { return process.env.SETTLEMENT_TABLE!; },
};
ConstanteArchivoDescripción
TABLESsrc/constant/Tables.tsNombres de tablas DynamoDB
LAMBDASsrc/constant/Lambdas.tsARNs de Lambdas externas
BATCH_RESOURCESsrc/constant/BatchResources.tsARNs de job queue y job definition de AWS Batch

BatchResources.ts también exporta isSupportedSettlementType(type) — type guard para validar si un settlement_type tiene job definition disponible. Agregar al array SUPPORTED_SETTLEMENT_TYPES cuando se soporte t1/t2.


Gateways

GatewayMétodos
DynamoGatewayputItem, getItem, query, updateItem
LambdaGatewayinvoke — síncrono (InvocationType: RequestResponse), parsea Payload, maneja FunctionError como ES008
MongoGatewayfindOne, find, insertOne, updateOne, deleteOne, aggregate — ver src/gateway/MongoGateway.ts
S3GatewaygetSignedUrl(bucket, key, expiresIn?) — genera presigned URL via @aws-sdk/s3-request-presigner
BatchGatewaysubmitJob(params) — envía job a AWS Batch via SubmitJobCommand, retorna Observable<jobId>

Ver implementación en src/gateway/.

MongoDB — autenticación y conexión

Autenticación vía STS AssumeRole → MONGODB-AWS. Singleton con validación de expiración entre invocaciones Lambda — ver src/middleware/MongoConnectionMiddleware.ts.

Variables de entorno requeridas por función con MongoDB: MONGO_CONFIG, MONGO_ROL_ARN, MONGO_DB_NAMES, MONGO_COLLECTION. Constantes con getters lazy: src/constant/MongoResources.ts. ARN del rol global en SSM: /GL/TONDER/MONGO_ROL_ARN.

IContext inyectado por MONGODB_CONNECTION_MIDDLEWARE — ver src/infrastructure/aws/ContextInterface.ts. Extiende Context de aws-lambda con mongoClient?: MongoClient.

Referencia completa: docs/specs/getsett-spec.md Part 1 y Part 2.


Schemas e interfaces

Cada handler tiene su propio JSON Schema y tipo generado. No compartir interfaces entre handlers.

SchemaInterface generadaHandler
src/schema/settlement_request.jsonISettlementRequestsettlementHandler
src/schema/change_status_finances_request.jsonIChangeStatusFinancesRequestchangeStatusFinancesHandler
src/schema/get_settlements_query_request.jsonIGetSettlementsQueryRequestgetSettlementsHandler
src/schema/get_settlements_response.jsonIGetSettlementsResponsegetSettlementsHandler

ISettlementRequest incluye: rolling_reserve_release_finances, settlement_type, s3 (todos requeridos). IChangeStatusFinancesRequest no incluye esos 3 campos — son exclusivos del flujo de settlement interno.

Al modificar un schema: editar el .json en src/schema/ → correr npm run types → actualizar specs.


Patrones de código

RxJS: Todo el código asíncrono usa Observables. No usar Promises en servicios/gateways.

Errores: throw new TonderError(ERRORS.ES001, "mensaje", metadata) — prefijo ES (Error Settlement). Logger: this._logger.info("SettlementService | process | start", { id }) — prefijo {Service} | {method} | {step}.

Diseño de servicios — un handler no implica un service nuevo

Regla: si la operación de un nuevo handler encaja semánticamente en un service ya existente, el método público se agrega ahí — no se crea un service nuevo. Los helpers son métodos privados del mismo service. Un service nuevo solo se justifica si la responsabilidad es claramente distinta.

Ejemplo: getSettlements pertenece a SettlementService (mismo dominio) — el método público se agregó ahí junto a process. En futuras HUs del mismo dominio seguir la regla.

Método público limpio: el método público solo debe contener conectores RxJS (of, pipe, mergeMap, catchError). La lógica de negocio va en métodos privados. Ejemplo en getSettlements: parsePagination, buildMatchStage, buildSettlementsPipeline, buildSettlementsResponse.

HU por spec: al completar la implementación de un spec, crear el doc docs/hu/HU-XXX-nombre.md usando docs/hu/template.md.

En research docs: definir explícitamente si el método va en un service existente o requiere uno nuevo, antes de pasar a la fase de implementación.

Filtros dinámicos y helpers MongoDB

  • parseFilterValue, parseSortParam — ver src/utils/filter.ts
  • createToDoubleFieldsStage — ver src/utils/mongoHelpers.ts. Solo campos planos top-level — dot notation para subdocumentos nunca validado en producción.
  • Campos Decimal128 en subdocumentos (settlement.*, routing.*) no se convierten en el pipeline — se normalizan post-query con normalizeDecimalFields en el service antes de retornar la respuesta.

Serialización de Decimal128 — regla obligatoria

El driver de MongoDB retorna campos Decimal128 como objetos BSON. Al pasar por JSON.stringify (middy httpResponseSerializer) se serializan como {"$numberDecimal": "1000"} en lugar de 1000 — rompiendo cualquier cliente que espere un número.

Regla: todo handler HTTP que retorne documentos MongoDB con campos Decimal128 (ya sea top-level o en subdocumentos) debe normalizar esos campos antes de retornar la respuesta. Nunca enviar $numberDecimal al cliente.

Implementación estándar — agregar normalizeDecimalFields en el service (ver src/service/SettlementService.ts):

typescript
import { Decimal128 } from "mongodb";
import traverse from "traverse";

private normalizeDecimalFields(records: Record<string, unknown>[]): Record<string, unknown>[] {
  return records.map((record) =>
    traverse(record).map(function (v: unknown) {
      if (v instanceof Decimal128) {
        this.update(Number(v.toString()));
      } else if (v !== null && typeof v === "object" && "$numberDecimal" in v) {
        this.update(Number((v as { $numberDecimal: string }).$numberDecimal));
      }
    }),
  );
}
  • instanceof Decimal128 — cubre el caso BSON raw (antes de stringify).
  • "$numberDecimal" in v — fallback si el driver ya serializó parcialmente.
  • Llamar en buildXxxResponse antes de retornar: const data = this.normalizeDecimalFields(raw);
  • No usar constructor.name === "Decimal128" — puede fallar si esbuild mangle los nombres de clase en el bundle de Lambda.
  • Paginación: $facet con metadata: [{ $count }] + data: [{ $sort }, { $skip }, { $limit }] en una sola query.
  • Extracción segura: get(results, "[0].metadata[0].total", 0) y get(results, "[0].data", []).

Referencia completa: docs/specs/getsett-spec.md Part 3.

calculateForceDecrease — lógica de negocio

diff = rolling_reserve_release - rolling_reserve_release_finances (release − finances, en ese orden).

CondiciónforceDecreaserollingReserveRelease
diff > 0 (release > finances)truediff
diff === 0 (iguales)falserolling_reserve_release original
diff < 0 (finances > release)throw ES007

Regla: la resta siempre es release - finances. Invertirla causó bug en producción (HU-003).


Convenciones de nombres

ElementoConvenciónEjemplo
Archivos clase/interfazPascalCaseSettlementService.ts
HandlerscamelCasesettlementHandler.ts
Interfacesprefijo IISettlementService
Enumssufijo EnumErrorEnum
Propiedades privadasprefijo _this._logger
Métodos privadoscamelCase sin _normalizeLambdaRequest()
ConstantesUPPER_SNAKE_CASETABLES, LAMBDAS
DynamoDB atributoscamelCasesettlementId

Tests

Framework: Mocha + Chai + Sinon + sinon-chai. Un .spec.ts por cada servicio/gateway. Ver estructura en src/service/SettlementService.spec.ts.

  • No mockear DynamoDBDocumentClient ni LambdaClient — stubear los gateways
  • En tests, setear process.env en beforeEach y limpiar en afterEach
  • Usar .calledOnce (propiedad) en lugar de .to.have.been.calledOnce para evitar unbound-method

Linting y calidad de código

  • Config: eslint.config.mjs, .prettierrc — Ver archivos reales
  • Reglas clave: sin any, T[] en lugar de Array<T>, métodos privados sin _, return types explícitos
  • Pre-commit: npm run lint (format + duplicados + ESLint)
  • Pre-push: npm run lint && npm test
  • Nunca usar --no-verify
  • Si se modifica package.json: NO hacer commit hasta que el usuario confirme que ya corrió npm install manualmente. Esperar confirmación explícita antes de stagear package-lock.json

Conventional Commits

type(scope): descripción — tipos: feat | fix | chore | refactor | test | docs | ci


serverless.yml

Ver implementación real en serverless.yml. Patrones clave:

  • Runtime: nodejs22.x, arm64
  • SSM propio: ${ssm:/${self:custom.service.name}/${self:provider.stage}/VAR}
  • SSM global: ${ssm:/GL/TONDER/${self:provider.stage}/VAR}
  • Tabla nueva: definir en custom.resources → exponer como env var → consumir en Tables.ts
  • Canary pdn: Linear10PercentEvery2Minutes
  • Log retention: dev/stage=7d, pdn=3653d
  • Plugins: serverless-plugin-resource-tagging, serverless-plugin-canary-deployments, serverless-domain-manager

HTTP API (API Gateway v2)

Handlers HTTP usan httpApi en events. Configuración global en provider.httpApi:

yaml
provider:
  httpApi:
    cors:
      allowedOrigins:
        - "*"
      allowedHeaders:
        - Content-Type
        - X-Amz-Date
        - Authorization
        - X-Api-Key
        - X-Amz-Security-Token
        - X-Amz-User-Agent
      allowCredentials: false

Body parsing: API GW v2 entrega el body como string. Usar httpBodyParserMiddleware antes de jsonSchemaValidationMiddleware en el middleware chain del handler.

Estándar unificado de error handling HTTP: todos los handlers HTTP usan httpErrorHandlerMiddleware() de @middy/http-error-handler al final de la chain. Requiere que TonderError tenga public readonly statusCode: number como propiedad de instancia (ya implementado en src/utils/TonderError.ts). No usar errorMiddleware custom.

Chain para handlers HTTP POST (body):

warmup → httpBodyParser → jsonSchemaValidation → inputOutputLogger → httpErrorHandler

Ver src/handler/changeStatusFinancesHandler.ts.

Chain para handlers HTTP GET (query params + MongoDB):

warmup → inputOutputLogger → httpEventNormalizer → httpHeaderNormalizer
→ QueryValidationMiddleware → MONGODB_CONNECTION_MIDDLEWARE
→ httpSecurityHeaders → httpCors → httpResponseSerializer → httpErrorHandler

Ver src/handler/getSettlementsHandler.ts.

QueryValidationMiddleware — valida event.queryStringParameters con AJV (no el evento completo). Lanza TonderError(ERRORS.ES009) si falla. Ver src/middleware/QueryValidationMiddleware.ts. No modificar jsonSchemaValidationMiddleware existente.

Tipo del handler GET: IApiGatewayEvent<B, Q> — ver src/infrastructure/aws/ApiGatewayEvent.ts.

Respuesta del handler HTTP: { statusCode: number, body: string } — siempre serializar body con JSON.stringify.

Custom Domain (serverless-domain-manager)

Configuración en custom.customDomain + ServerlessScripts.js:

yaml
custom:
  customDomain:
    domainName: ${file(./ServerlessScripts.js):domainName}
    basePath: settlement
    stage: $default
    certificateName: ${file(./ServerlessScripts.js):certificateName}
    createRoute53Record: false
    endpointType: REGIONAL
    apiType: http

ServerlessScripts.js lee domain.name y domain.certificate desde SSM /{service}/{stage}/SLS_BUILD.

SSM requerido por stageSLS_BUILD incluye domain + authorizer + mongo:

bash
aws ssm put-parameter --name "/usrv-settlement/{stage}/SLS_BUILD" \
  --value '{"domain":{"name":"api-{stage}.tonder.io","certificate":"*.tonder.io"},"authorizerLambda":"arn:...","mongoConfig":{"clusterName":"..."},"mongoDBNames":{"dbName":"..."},"mongoCollection":{"settlementCollection":"usrv-settlement-settlement"}}' \
  --type "SecureString" --overwrite

Endpoints por stage:

StageBase URL
devhttps://api-dev.tonder.io/settlement
stagehttps://api-stage.tonder.io/settlement
pdnhttps://api.tonder.io/settlement

Paso manual único por stage: npx serverless create_domain --stage {stage} (solo la primera vez).


Documentación de HUs (Linear)

Cada historia de usuario debe tener su doc en docs/hu/HU-XXX-nombre.md antes de iniciar desarrollo.

Estructura obligatoria: Descripción · DOR · Criterios de aceptación · DOF · Notas técnicas (SSM, decisiones, pasos manuales).

  • Template: docs/hu/template.md
  • Ejemplo real: docs/hu/HU-001-settlement-processor.md

Regla: Las notas técnicas deben incluir siempre los SSM parameters a crear manualmente con el CLI exacto.

Regla para endpoints HTTP: Las HUs con handlers HTTP deben incluir en Notas técnicas una sección "Casos de uso — curls" con ejemplos ejecutables para cada escenario posible: éxito (200), validación fallida (400), error de gateway (502), error de red (500).


Notas importantes

  • Proyecto base: ante cualquier duda revisar usrv-kushki-acq/ primero
  • ARM64: verificar compatibilidad de dependencias nativas
  • forkJoin: no serializar invoke + dynamo put — son paralelos por diseño
  • 3 stages: dev, stage, pdn — default dev
  • Workflow SDD: spec en docs/specs/ → HU en docs/hu/ → implementación por fases con commits atómicos
  • Research docs: deben definir explícitamente si el método nuevo va en un service existente o requiere uno nuevo — evita crear services innecesarios
  • getSettlements: referencia de implementación completa en docs/specs/getsett-spec.md

Vecnet — Build Spec v0.2 · Obsidian Terminal