Appearance
CLAUDE.md — usrv-settlement
Microservicio de settlements con tres handlers:
settlementHandler(Lambda invoke): recibe info de un settlement, calculaforce_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
| Comando | Descripción |
|---|---|
npm run test:unit | Solo unit tests |
npm run test:coverage | Tests + cobertura |
npm test | Lint + tests + cobertura (pre-push) |
npm run lint | Formato + duplicados + ESLint |
npm run lint:fix | Auto-fix ESLint |
npm run test:watch | Re-corre tests al guardar |
npm run types | Regenerar tipos TS desde JSON Schemas |
npm run deploy | Desplegar 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
| Carpeta | Responsabilidad |
|---|---|
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
| Handler | Trigger | Descripción |
|---|---|---|
settlementHandler | Lambda Invoke | Recibe info de settlement por rango de tiempo |
changeStatusFinancesHandler | HTTP POST /finances/change-status | Invoca Lambda de finanzas síncronamente |
getSettlementsHandler | HTTP GET /v1/settlements | Consulta 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
| Tabla | PK | GSI | Descripción |
|---|---|---|---|
SettlementTable | settlement_id | — | Settlements procesados |
- Naming:
${service}-${stage}-settlement— definido encustom.resources.settlementTable - El nombre se expone como
SETTLEMENT_TABLEenv var y se consume ensrc/constant/Tables.ts - Atributos en
camelCase·BillingMode: PAY_PER_REQUEST·DeletionPolicy: Retain created_atse guarda como Unix timestamp en ms (Date.now(), tipoN) — 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!; },
};| Constante | Archivo | Descripción |
|---|---|---|
TABLES | src/constant/Tables.ts | Nombres de tablas DynamoDB |
LAMBDAS | src/constant/Lambdas.ts | ARNs de Lambdas externas |
BATCH_RESOURCES | src/constant/BatchResources.ts | ARNs 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
| Gateway | Métodos |
|---|---|
DynamoGateway | putItem, getItem, query, updateItem |
LambdaGateway | invoke — síncrono (InvocationType: RequestResponse), parsea Payload, maneja FunctionError como ES008 |
MongoGateway | findOne, find, insertOne, updateOne, deleteOne, aggregate — ver src/gateway/MongoGateway.ts |
S3Gateway | getSignedUrl(bucket, key, expiresIn?) — genera presigned URL via @aws-sdk/s3-request-presigner |
BatchGateway | submitJob(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.
| Schema | Interface generada | Handler |
|---|---|---|
src/schema/settlement_request.json | ISettlementRequest | settlementHandler |
src/schema/change_status_finances_request.json | IChangeStatusFinancesRequest | changeStatusFinancesHandler |
src/schema/get_settlements_query_request.json | IGetSettlementsQueryRequest | getSettlementsHandler |
src/schema/get_settlements_response.json | IGetSettlementsResponse | getSettlementsHandler |
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— versrc/utils/filter.tscreateToDoubleFieldsStage— versrc/utils/mongoHelpers.ts. Solo campos planos top-level — dot notation para subdocumentos nunca validado en producción.- Campos
Decimal128en subdocumentos (settlement.*,routing.*) no se convierten en el pipeline — se normalizan post-query connormalizeDecimalFieldsen 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
buildXxxResponseantes 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:
$facetconmetadata: [{ $count }]+data: [{ $sort }, { $skip }, { $limit }]en una sola query. - Extracción segura:
get(results, "[0].metadata[0].total", 0)yget(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ón | forceDecrease | rollingReserveRelease |
|---|---|---|
diff > 0 (release > finances) | true | diff |
diff === 0 (iguales) | false | rolling_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
| Elemento | Convención | Ejemplo |
|---|---|---|
| Archivos clase/interfaz | PascalCase | SettlementService.ts |
| Handlers | camelCase | settlementHandler.ts |
| Interfaces | prefijo I | ISettlementService |
| Enums | sufijo Enum | ErrorEnum |
| Propiedades privadas | prefijo _ | this._logger |
| Métodos privados | camelCase sin _ | normalizeLambdaRequest() |
| Constantes | UPPER_SNAKE_CASE | TABLES, LAMBDAS |
| DynamoDB atributos | camelCase | settlementId |
Tests
Framework: Mocha + Chai + Sinon + sinon-chai. Un .spec.ts por cada servicio/gateway. Ver estructura en src/service/SettlementService.spec.ts.
- No mockear
DynamoDBDocumentClientniLambdaClient— stubear los gateways - En tests, setear
process.envenbeforeEachy limpiar enafterEach - Usar
.calledOnce(propiedad) en lugar de.to.have.been.calledOncepara evitarunbound-method
Linting y calidad de código
- Config:
eslint.config.mjs,.prettierrc— Ver archivos reales - Reglas clave: sin
any,T[]en lugar deArray<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 installmanualmente. Esperar confirmación explícita antes de stagearpackage-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 enTables.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: falseBody 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 → httpErrorHandlerVer src/handler/changeStatusFinancesHandler.ts.
Chain para handlers HTTP GET (query params + MongoDB):
warmup → inputOutputLogger → httpEventNormalizer → httpHeaderNormalizer
→ QueryValidationMiddleware → MONGODB_CONNECTION_MIDDLEWARE
→ httpSecurityHeaders → httpCors → httpResponseSerializer → httpErrorHandlerVer 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: httpServerlessScripts.js lee domain.name y domain.certificate desde SSM /{service}/{stage}/SLS_BUILD.
SSM requerido por stage — SLS_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" --overwriteEndpoints por stage:
| Stage | Base URL |
|---|---|
dev | https://api-dev.tonder.io/settlement |
stage | https://api-stage.tonder.io/settlement |
pdn | https://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— defaultdev - Workflow SDD: spec en
docs/specs/→ HU endocs/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