@fiado/type-kit 3.218.0 → 3.220.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/_test_/unit/kyc/IsKycStepStatusByStep.test.ts +86 -0
  2. package/_test_/unit/kyc/KycStepCompletedV1.test.ts +239 -0
  3. package/_test_/unit/kyc/applyKycStep.test.ts +75 -0
  4. package/bin/benefitCenter/enums/BenefitFlowEnum.d.ts +11 -0
  5. package/bin/benefitCenter/enums/BenefitFlowEnum.js +15 -0
  6. package/bin/kyc/enums/KycSubCheckNameEnum.d.ts +32 -0
  7. package/bin/kyc/enums/KycSubCheckNameEnum.js +39 -0
  8. package/bin/kyc/events/KycStepCompletedV1.d.ts +85 -0
  9. package/bin/kyc/events/KycStepCompletedV1.js +93 -0
  10. package/bin/kyc/helpers/applyKycStep.d.ts +26 -0
  11. package/bin/kyc/helpers/applyKycStep.js +30 -0
  12. package/bin/kyc/index.d.ts +4 -0
  13. package/bin/kyc/index.js +6 -0
  14. package/bin/kyc/validators/IsKycStepStatusByStep.d.ts +26 -0
  15. package/bin/kyc/validators/IsKycStepStatusByStep.js +64 -0
  16. package/bin/loanConfig/enums/ModifiableByRoleEnum.d.ts +11 -0
  17. package/bin/loanConfig/enums/ModifiableByRoleEnum.js +15 -0
  18. package/bin/places/dtos/CashInFeeDto.d.ts +17 -0
  19. package/bin/places/dtos/CashInFeeDto.js +12 -0
  20. package/bin/platformRbac/dtos/ResendOtpRequest.d.ts +22 -0
  21. package/bin/platformRbac/dtos/ResendOtpRequest.js +36 -0
  22. package/bin/platformRbac/dtos/ResendSelfRegisterOtpRequest.d.ts +11 -0
  23. package/bin/platformRbac/dtos/ResendSelfRegisterOtpRequest.js +36 -0
  24. package/bin/retailWizard/dtos/responses/KycPrepareResponse.d.ts +6 -0
  25. package/package.json +1 -1
  26. package/src/kyc/enums/KycSubCheckNameEnum.ts +36 -0
  27. package/src/kyc/events/KycStepCompletedV1.ts +105 -0
  28. package/src/kyc/helpers/applyKycStep.ts +36 -0
  29. package/src/kyc/index.ts +8 -0
  30. package/src/kyc/validators/IsKycStepStatusByStep.ts +57 -0
  31. package/src/retailWizard/dtos/responses/KycPrepareResponse.ts +6 -0
@@ -0,0 +1,86 @@
1
+ import 'reflect-metadata';
2
+ import { ValidationArguments } from 'class-validator';
3
+ import {
4
+ IsKycStepStatusByStep,
5
+ KycSubCheckNameEnum,
6
+ } from '../../../src/kyc/index';
7
+
8
+ /**
9
+ * Tests del validador AISLADO — sin pasar por el DTO. `KycStepCompletedV1.test.ts` ya prueba el
10
+ * cruce estación ↔ vocabulario a través del boundary; acá se ejercen las entradas degeneradas y el
11
+ * `defaultMessage()`, que desde el DTO no se alcanzan.
12
+ */
13
+ const validador = new IsKycStepStatusByStep();
14
+
15
+ const args = (step: unknown): ValidationArguments =>
16
+ ({ object: { step }, value: undefined, targetName: '', property: 'status', constraints: [] });
17
+
18
+ describe('IsKycStepStatusByStep', () => {
19
+ describe('validate — entradas que no son un status', () => {
20
+ // Un `''` o un `' '` no es un status: sin este candado, `includes` los rechaza hoy por
21
+ // casualidad, pero nada impide que alguien agregue un `.trim()` permisivo mañana.
22
+ it.each([
23
+ ['string vacío', ''],
24
+ ['whitespace puro', ' '],
25
+ ['número', 123],
26
+ ['null', null],
27
+ ['undefined', undefined],
28
+ ['objeto', { status: 'PASS' }],
29
+ ['array', ['PASS']],
30
+ ])('rechaza %s', (_etiqueta, valor) => {
31
+ expect(validador.validate(valor, args(KycSubCheckNameEnum.FACEMATCH))).toBe(false);
32
+ });
33
+
34
+ // Los enums viajan en MAYÚSCULAS. Aceptar 'pass' abriría la puerta a que dos repos escriban
35
+ // el mismo estado de dos formas y el consumidor tenga que normalizar.
36
+ it.each(['pass', 'Pass', 'fail', 'clear', 'Hit'])('rechaza %s (el casing importa)', (valor) => {
37
+ const step = valor.toLowerCase() === 'clear' || valor.toLowerCase() === 'hit'
38
+ ? KycSubCheckNameEnum.WATCHLISTS
39
+ : KycSubCheckNameEnum.FACEMATCH;
40
+
41
+ expect(validador.validate(valor, args(step))).toBe(false);
42
+ });
43
+ });
44
+
45
+ describe('validate — sin estación reconocida no hay vocabulario', () => {
46
+ it.each([
47
+ ['step ausente', undefined],
48
+ ['step inventado', 'inventado'],
49
+ ['step que Metamap emite pero no está en el contrato', 'selfie'],
50
+ ['step no-string', 42],
51
+ ])('rechaza cualquier status con %s', (_etiqueta, step) => {
52
+ expect(validador.validate('PASS', args(step))).toBe(false);
53
+ expect(validador.validate('CLEAR', args(step))).toBe(false);
54
+ });
55
+ });
56
+
57
+ describe('defaultMessage — las tres ramas', () => {
58
+ it('en una estación de lista nombra CLEAR/HIT y explica que un HIT no es un fallo', () => {
59
+ const msg = validador.defaultMessage(args(KycSubCheckNameEnum.WATCHLISTS));
60
+
61
+ expect(msg).toContain('CLEAR');
62
+ expect(msg).toContain('HIT');
63
+ expect(msg).toContain('watchlists');
64
+ expect(msg).not.toContain('PASS');
65
+ });
66
+
67
+ it('en una estación binaria nombra PASS/FAIL', () => {
68
+ const msg = validador.defaultMessage(args(KycSubCheckNameEnum.LIVENESS));
69
+
70
+ expect(msg).toContain('PASS');
71
+ expect(msg).toContain('FAIL');
72
+ expect(msg).toContain('liveness');
73
+ expect(msg).not.toContain('CLEAR');
74
+ });
75
+
76
+ // La rama que ningún test del DTO alcanza: sin step reconocido el mensaje no puede nombrar
77
+ // un vocabulario, y decir "debe ser PASS o FAIL" ahí sería mentir.
78
+ it('sin estación reconocida dice que no se puede validar, sin inventar un vocabulario', () => {
79
+ const msg = validador.defaultMessage(args('inventado'));
80
+
81
+ expect(msg).toContain('step');
82
+ expect(msg).not.toContain('PASS');
83
+ expect(msg).not.toContain('CLEAR');
84
+ });
85
+ });
86
+ });
@@ -0,0 +1,239 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import {
5
+ KycStepCompletedV1,
6
+ KycVerificationChangedV1,
7
+ KycSubCheckNameEnum,
8
+ KycSubCheckStatusEnum,
9
+ KycWatchlistStatusEnum,
10
+ KYC_WATCHLIST_STEPS,
11
+ } from '../../../src/kyc/index';
12
+
13
+ /** Base válida de una estación binaria; cada caso pisa solo el campo que está probando. */
14
+ const base = {
15
+ verificationId: 'ver-1',
16
+ directoryId: 'dir-1',
17
+ step: 'facematch',
18
+ status: 'PASS',
19
+ occurredAt: '2026-07-26T10:05:00.000Z',
20
+ };
21
+
22
+ const instancia = (raw: Record<string, unknown>): KycStepCompletedV1 =>
23
+ plainToInstance(KycStepCompletedV1, raw, { excludeExtraneousValues: true });
24
+
25
+ describe('KycStepCompletedV1', () => {
26
+ it('valida el shape que publica el webhook desde un step_completed', async () => {
27
+ const dto = instancia({ ...base, metamapFlowId: 'flow-1' });
28
+
29
+ expect(await validate(dto as object)).toHaveLength(0);
30
+ expect(dto.step).toBe(KycSubCheckNameEnum.FACEMATCH);
31
+ expect(dto.status).toBe(KycSubCheckStatusEnum.PASS);
32
+ });
33
+
34
+ it('valida sin metamapFlowId (es opcional, solo diagnóstico)', async () => {
35
+ const dto = instancia(base);
36
+
37
+ expect(await validate(dto as object)).toHaveLength(0);
38
+ expect(dto.metamapFlowId).toBeUndefined();
39
+ });
40
+
41
+ // Sin directoryId el consumidor no puede encontrar la sesión: el evento no sirve para nada.
42
+ it('rechaza un evento sin directoryId (sin él no hay gate posible)', async () => {
43
+ const { directoryId: _omitido, ...sinDirectory } = base;
44
+ const errors = await validate(instancia(sinDirectory) as object);
45
+
46
+ expect(errors.map((e) => e.property)).toContain('directoryId');
47
+ });
48
+
49
+ // DEC-KYC-015: un '' pasa @IsString() en silencio y falla tres pasos río abajo.
50
+ it.each(['verificationId', 'directoryId'])('rechaza %s vacío', async (campo) => {
51
+ const errors = await validate(instancia({ ...base, [campo]: '' }) as object);
52
+
53
+ expect(errors.map((e) => e.property)).toContain(campo);
54
+ });
55
+
56
+ // Candado del contrato: solo viajan las 8 estaciones de KycSubChecks. Metamap emite 13 —
57
+ // 'selfie', 'voice', 'ipValidation' y compañía NO forman parte del contrato. Sin esto, el
58
+ // campo se podría degradar a @IsString() y una estación desconocida llegaría al progreso.
59
+ it('rechaza una estación que Metamap emite pero NO está en el contrato', async () => {
60
+ const errors = await validate(instancia({ ...base, step: 'selfie' }) as object);
61
+
62
+ expect(errors.map((e) => e.property)).toContain('step');
63
+ });
64
+
65
+ // ── El cruce step ↔ status: la razón de ser del validador custom ───────────
66
+
67
+ it.each(KYC_WATCHLIST_STEPS)('acepta CLEAR y HIT en la estación de lista %s', async (step) => {
68
+ const clear = await validate(instancia({ ...base, step, status: 'CLEAR' }) as object);
69
+ const hit = await validate(instancia({ ...base, step, status: 'HIT' }) as object);
70
+
71
+ expect(clear).toHaveLength(0);
72
+ expect(hit).toHaveLength(0);
73
+ });
74
+
75
+ /**
76
+ * El candado que justifica el validador. Un `@IsIn(['PASS','FAIL','CLEAR','HIT'])` dejaría pasar
77
+ * esto y el matiz se perdería: un HIT es "que lo mire cumplimiento", no una falla. Con este evento
78
+ * colado, la pantalla del vendedor marcaría como fallida una estación que solo pide revisión.
79
+ */
80
+ it.each(KYC_WATCHLIST_STEPS)('rechaza FAIL en la estación de lista %s (un HIT no es un fallo)', async (step) => {
81
+ const errors = await validate(instancia({ ...base, step, status: 'FAIL' }) as object);
82
+
83
+ expect(errors.map((e) => e.property)).toContain('status');
84
+ });
85
+
86
+ const BINARIOS = Object.values(KycSubCheckNameEnum).filter((s) => !KYC_WATCHLIST_STEPS.includes(s));
87
+
88
+ it.each(BINARIOS)('acepta PASS y FAIL en la estación binaria %s', async (step) => {
89
+ const pass = await validate(instancia({ ...base, step, status: 'PASS' }) as object);
90
+ const fail = await validate(instancia({ ...base, step, status: 'FAIL' }) as object);
91
+
92
+ expect(pass).toHaveLength(0);
93
+ expect(fail).toHaveLength(0);
94
+ });
95
+
96
+ it.each(BINARIOS)('rechaza HIT en la estación binaria %s', async (step) => {
97
+ const errors = await validate(instancia({ ...base, step, status: 'HIT' }) as object);
98
+
99
+ expect(errors.map((e) => e.property)).toContain('status');
100
+ });
101
+
102
+ it('rechaza un status inventado', async () => {
103
+ const errors = await validate(instancia({ ...base, status: 'MAS_O_MENOS' }) as object);
104
+
105
+ expect(errors.map((e) => e.property)).toContain('status');
106
+ });
107
+
108
+ // Con un step inválido no hay vocabulario contra el cual medir el status: fallan los dos.
109
+ it('rechaza el status cuando el step no es reconocido', async () => {
110
+ const errors = await validate(instancia({ ...base, step: 'inventado' }) as object);
111
+
112
+ expect(errors.map((e) => e.property)).toEqual(expect.arrayContaining(['step', 'status']));
113
+ });
114
+
115
+ // ── Orden ─────────────────────────────────────────────────────────────────
116
+
117
+ /**
118
+ * SQS estándar no garantiza orden. El consumidor descarta las estaciones que llegan más viejas
119
+ * que lo que ya escribió, y para eso `occurredAt` tiene que ser léxicamente ordenable.
120
+ */
121
+ it('rechaza occurredAt con formato no ISO-8601', async () => {
122
+ const errors = await validate(instancia({ ...base, occurredAt: '26/07/2026' }) as object);
123
+
124
+ expect(errors.map((e) => e.property)).toContain('occurredAt');
125
+ });
126
+
127
+ it('rechaza un evento sin occurredAt (sin marca de tiempo no hay cómo ordenar)', async () => {
128
+ const { occurredAt: _omitido, ...sinFecha } = base;
129
+ const errors = await validate(instancia(sinFecha) as object);
130
+
131
+ expect(errors.map((e) => e.property)).toContain('occurredAt');
132
+ });
133
+
134
+ /**
135
+ * `@IsISO8601()` a secas acepta TODOS estos, y cada uno rompe la comparación léxica de distinta
136
+ * forma. El offset es el peligroso: `T15:05:00+05:00` son las 10:05 UTC, pero léxicamente es
137
+ * "mayor" que un `T10:06:00Z` posterior — el consumidor lo lee como más nuevo, descarta la
138
+ * estación real y esa casilla nunca se pinta. Sin este candado el `@Matches` se puede quitar y
139
+ * la suite sigue verde.
140
+ */
141
+ it.each([
142
+ ['offset positivo en vez de Z', '2026-07-26T15:05:00+05:00'],
143
+ ['offset negativo en vez de Z', '2026-07-26T05:05:00-05:00'],
144
+ ['solo fecha, sin hora', '2026-07-26'],
145
+ ['formato básico sin guiones', '20260726T100500Z'],
146
+ ['fecha de semana ISO', '2026-W30-1'],
147
+ ['fecha imposible', '2026-02-30T10:05:00.000Z'],
148
+ ['sin zona horaria', '2026-07-26T10:05:00'],
149
+ ])('rechaza occurredAt %s', async (_etiqueta, valor) => {
150
+ const errors = await validate(instancia({ ...base, occurredAt: valor }) as object);
151
+
152
+ expect(errors.map((e) => e.property)).toContain('occurredAt');
153
+ });
154
+
155
+ it.each([
156
+ ['con milisegundos', '2026-07-26T10:05:00.000Z'],
157
+ ['sin milisegundos', '2026-07-26T10:05:00Z'],
158
+ ])('acepta occurredAt en UTC %s', async (_etiqueta, valor) => {
159
+ expect(await validate(instancia({ ...base, occurredAt: valor }) as object)).toHaveLength(0);
160
+ });
161
+
162
+ // ── eventType: el discriminador explícito ─────────────────────────────────
163
+
164
+ it('acepta el eventType explícito', async () => {
165
+ const dto = instancia({ ...base, eventType: 'KycStepCompletedV1' });
166
+
167
+ expect(await validate(dto as object)).toHaveLength(0);
168
+ expect(dto.eventType).toBe('KycStepCompletedV1');
169
+ });
170
+
171
+ // Opcional a propósito: agregarlo ahora es gratis, hacerlo obligatorio después del primer
172
+ // deploy sería breaking. Este candado prueba que hoy NO lo es.
173
+ it('valida sin eventType (es opcional para no romper al webhook antes de que lo mande)', async () => {
174
+ const dto = instancia(base);
175
+
176
+ expect(await validate(dto as object)).toHaveLength(0);
177
+ expect(dto.eventType).toBeUndefined();
178
+ });
179
+
180
+ // Si el día de mañana llega un `KycStepStartedV1`, tiene que caer como "no soportado" en el
181
+ // ruteo, no colarse por esta rama y morir en el DLQ como "corrupto".
182
+ it('rechaza un eventType de otro evento', async () => {
183
+ const errors = await validate(instancia({ ...base, eventType: 'KycStepStartedV1' }) as object);
184
+
185
+ expect(errors.map((e) => e.property)).toContain('eventType');
186
+ });
187
+
188
+ // ── Convivencia en la misma cola ──────────────────────────────────────────
189
+
190
+ /**
191
+ * Los dos eventos comparten `RetailKycInboundQueue` y el discriminador es la presencia de `step`.
192
+ * Estos dos candados prueban que un evento NO se puede hacer pasar por el otro: si alguien
193
+ * agregara campos que los volvieran mutuamente válidos, el consumidor tomaría la rama equivocada
194
+ * y el cierre de la verificación se procesaría como una estación suelta (o al revés).
195
+ */
196
+ it('un cierre de verificación NO valida como evento de estación', async () => {
197
+ const cierre = {
198
+ verificationId: 'ver-1', directoryId: 'dir-1', peopleId: 'ppl-1',
199
+ verificationStatus: 'COMPLETED', verificationResult: 'VERIFIED',
200
+ updateDate: '2026-07-26T10:05:00.000Z',
201
+ };
202
+
203
+ const errors = await validate(instancia(cierre) as object);
204
+
205
+ expect(errors.map((e) => e.property)).toEqual(expect.arrayContaining(['step', 'status', 'occurredAt']));
206
+ });
207
+
208
+ it('un evento de estación NO valida como cierre de verificación', async () => {
209
+ const dto = plainToInstance(KycVerificationChangedV1, base, { excludeExtraneousValues: true });
210
+ const errors = await validate(dto as object);
211
+
212
+ expect(errors.map((e) => e.property)).toEqual(
213
+ expect.arrayContaining(['peopleId', 'verificationStatus', 'verificationResult', 'updateDate']),
214
+ );
215
+ });
216
+
217
+ // ── El contrato con KycSubChecks ──────────────────────────────────────────
218
+
219
+ /**
220
+ * El consumidor arma el progreso con `progreso[evento.step] = evento.status`. Si un valor del enum
221
+ * dejara de coincidir con su llave en `KycSubChecks`, escribiría un campo huérfano y esa estación
222
+ * nunca se marcaría — sin que nada truene. Este test es el único lugar donde eso se detecta.
223
+ */
224
+ it('cada valor del enum es una llave real de KycSubChecks', () => {
225
+ const llavesDeSubChecks = [
226
+ 'documentReading', 'templateMatching', 'alterationDetection', 'facematch',
227
+ 'liveness', 'ageCheck', 'watchlists', 'premiumAmlWatchlists',
228
+ ];
229
+
230
+ expect(Object.values(KycSubCheckNameEnum).sort()).toEqual(llavesDeSubChecks.sort());
231
+ });
232
+
233
+ it('las estaciones de lista son exactamente las dos que usan CLEAR/HIT', () => {
234
+ expect([...KYC_WATCHLIST_STEPS].sort()).toEqual(
235
+ [KycSubCheckNameEnum.PREMIUM_AML_WATCHLISTS, KycSubCheckNameEnum.WATCHLISTS].sort(),
236
+ );
237
+ expect(Object.values(KycWatchlistStatusEnum)).toEqual(['CLEAR', 'HIT']);
238
+ });
239
+ });
@@ -0,0 +1,75 @@
1
+ import 'reflect-metadata';
2
+ import {
3
+ applyKycStep,
4
+ KycSubChecks,
5
+ KycSubCheckNameEnum,
6
+ KycSubCheckStatusEnum,
7
+ KycWatchlistStatusEnum,
8
+ } from '../../../src/kyc/index';
9
+
10
+ describe('applyKycStep', () => {
11
+ it('asienta la primera estación sobre un progreso inexistente', () => {
12
+ const progreso = applyKycStep(undefined, KycSubCheckNameEnum.FACEMATCH, KycSubCheckStatusEnum.PASS);
13
+
14
+ expect(progreso.facematch).toBe(KycSubCheckStatusEnum.PASS);
15
+ });
16
+
17
+ it('acumula estaciones sin borrar las anteriores', () => {
18
+ let progreso = applyKycStep(undefined, KycSubCheckNameEnum.DOCUMENT_READING, KycSubCheckStatusEnum.PASS);
19
+ progreso = applyKycStep(progreso, KycSubCheckNameEnum.LIVENESS, KycSubCheckStatusEnum.FAIL);
20
+ progreso = applyKycStep(progreso, KycSubCheckNameEnum.WATCHLISTS, KycWatchlistStatusEnum.HIT);
21
+
22
+ expect(progreso).toEqual({
23
+ documentReading: KycSubCheckStatusEnum.PASS,
24
+ liveness: KycSubCheckStatusEnum.FAIL,
25
+ watchlists: KycWatchlistStatusEnum.HIT,
26
+ });
27
+ });
28
+
29
+ /**
30
+ * No muta el objeto recibido. Importa porque el consumidor lee el progreso de la fila de sesión y
31
+ * lo vuelve a escribir: si el helper mutara, una comparación "¿cambió algo?" contra el objeto
32
+ * original siempre daría que no, y la escritura se saltaría.
33
+ */
34
+ it('devuelve un objeto nuevo y deja intacto el que recibe', () => {
35
+ const original: KycSubChecks = { documentReading: KycSubCheckStatusEnum.PASS };
36
+
37
+ const siguiente = applyKycStep(original, KycSubCheckNameEnum.LIVENESS, KycSubCheckStatusEnum.PASS);
38
+
39
+ expect(siguiente).not.toBe(original);
40
+ expect(original).toEqual({ documentReading: KycSubCheckStatusEnum.PASS });
41
+ expect(original.liveness).toBeUndefined();
42
+ });
43
+
44
+ // Una reentrega de SQS del mismo evento tiene que dejar el progreso igual: esa es toda la razón
45
+ // por la que el evento no necesita `eventId`.
46
+ it('es idempotente — reprocesar la misma estación no cambia nada', () => {
47
+ const una = applyKycStep(undefined, KycSubCheckNameEnum.AGE_CHECK, KycSubCheckStatusEnum.PASS);
48
+ const dos = applyKycStep(una, KycSubCheckNameEnum.AGE_CHECK, KycSubCheckStatusEnum.PASS);
49
+
50
+ expect(dos).toEqual(una);
51
+ });
52
+
53
+ it('un reintento del cliente pisa el resultado anterior de esa estación', () => {
54
+ const fallido = applyKycStep(undefined, KycSubCheckNameEnum.DOCUMENT_READING, KycSubCheckStatusEnum.FAIL);
55
+ const reintento = applyKycStep(fallido, KycSubCheckNameEnum.DOCUMENT_READING, KycSubCheckStatusEnum.PASS);
56
+
57
+ expect(reintento.documentReading).toBe(KycSubCheckStatusEnum.PASS);
58
+ });
59
+
60
+ /**
61
+ * El candado que justifica que el helper exista: la llave que escribe tiene que ser una propiedad
62
+ * REAL de `KycSubChecks`, para las 8. Si un valor del enum dejara de coincidir con su llave, el
63
+ * helper escribiría un campo huérfano y la estación nunca se marcaría, sin que nada truene.
64
+ */
65
+ it.each(Object.values(KycSubCheckNameEnum))('la estación %s cae en su llave real de KycSubChecks', (step) => {
66
+ const status = step === KycSubCheckNameEnum.WATCHLISTS || step === KycSubCheckNameEnum.PREMIUM_AML_WATCHLISTS
67
+ ? KycWatchlistStatusEnum.CLEAR
68
+ : KycSubCheckStatusEnum.PASS;
69
+
70
+ const progreso = applyKycStep(undefined, step, status);
71
+
72
+ expect(Object.keys(progreso)).toEqual([step]);
73
+ expect(progreso[step as keyof KycSubChecks]).toBe(status);
74
+ });
75
+ });
@@ -0,0 +1,11 @@
1
+ export declare enum BenefitFlowEnum {
2
+ TOPUPS = "TOPUPS",
3
+ BILL_PAYMENT = "BILL_PAYMENT",
4
+ CREDIT = "CREDIT",
5
+ INSURANCE = "INSURANCE",
6
+ DONATION = "DONATION",
7
+ PHARMACY = "PHARMACY",
8
+ REMITTANCE = "REMITTANCE",
9
+ /** Fondeo de wallet PCF con efectivo via provider externo (Equality/Passport, OpenPay, …) — spec 13. */
10
+ WALLET_FUNDING = "WALLET_FUNDING"
11
+ }
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BenefitFlowEnum = void 0;
4
+ var BenefitFlowEnum;
5
+ (function (BenefitFlowEnum) {
6
+ BenefitFlowEnum["TOPUPS"] = "TOPUPS";
7
+ BenefitFlowEnum["BILL_PAYMENT"] = "BILL_PAYMENT";
8
+ BenefitFlowEnum["CREDIT"] = "CREDIT";
9
+ BenefitFlowEnum["INSURANCE"] = "INSURANCE";
10
+ BenefitFlowEnum["DONATION"] = "DONATION";
11
+ BenefitFlowEnum["PHARMACY"] = "PHARMACY";
12
+ BenefitFlowEnum["REMITTANCE"] = "REMITTANCE";
13
+ /** Fondeo de wallet PCF con efectivo via provider externo (Equality/Passport, OpenPay, …) — spec 13. */
14
+ BenefitFlowEnum["WALLET_FUNDING"] = "WALLET_FUNDING";
15
+ })(BenefitFlowEnum || (exports.BenefitFlowEnum = BenefitFlowEnum = {}));
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Nombre de la estación del KYC que reporta un `KycStepCompletedV1`.
3
+ *
4
+ * ⚠️ Los VALORES son exactamente las llaves de `KycSubChecks`. No es coincidencia ni estética: el
5
+ * consumidor arma el progreso de la verificación asentando cada estación sobre el acumulado, sin
6
+ * tabla de traducción. Si un valor dejara de coincidir con su llave, el progreso quedaría con un
7
+ * campo huérfano y la estación real nunca se marcaría — una falla muda. Ese acumulado se arma con
8
+ * `applyKycStep`, NO indexando a mano: la asignación directa no compila (los campos de `KycSubChecks`
9
+ * son opcionales) y el cast que la destraba desactiva el candado del vocabulario.
10
+ *
11
+ * Son las 8 del contrato, no las 13 que Metamap puede emitir: las otras (ip-validation, selfie,
12
+ * voice, duplicate-user-detection, curp/ine validation) no forman parte de `KycSubChecks` y por lo
13
+ * tanto no viajan. Ampliar la lista es un cambio de contrato en los dos lados, no un valor más aquí.
14
+ *
15
+ * SureKeep Fase 2 — progreso del KYC estación por estación.
16
+ */
17
+ export declare enum KycSubCheckNameEnum {
18
+ DOCUMENT_READING = "documentReading",
19
+ TEMPLATE_MATCHING = "templateMatching",
20
+ ALTERATION_DETECTION = "alterationDetection",
21
+ FACEMATCH = "facematch",
22
+ LIVENESS = "liveness",
23
+ AGE_CHECK = "ageCheck",
24
+ WATCHLISTS = "watchlists",
25
+ PREMIUM_AML_WATCHLISTS = "premiumAmlWatchlists"
26
+ }
27
+ /**
28
+ * Las estaciones de LISTA — las únicas cuyo resultado se expresa en `CLEAR`/`HIT`. El resto son
29
+ * binarias (`PASS`/`FAIL`). Se exporta porque el validador del evento y los consumidores necesitan
30
+ * la MISMA partición: duplicarla en cada repo es drift garantizado.
31
+ */
32
+ export declare const KYC_WATCHLIST_STEPS: readonly KycSubCheckNameEnum[];
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KYC_WATCHLIST_STEPS = exports.KycSubCheckNameEnum = void 0;
4
+ /**
5
+ * Nombre de la estación del KYC que reporta un `KycStepCompletedV1`.
6
+ *
7
+ * ⚠️ Los VALORES son exactamente las llaves de `KycSubChecks`. No es coincidencia ni estética: el
8
+ * consumidor arma el progreso de la verificación asentando cada estación sobre el acumulado, sin
9
+ * tabla de traducción. Si un valor dejara de coincidir con su llave, el progreso quedaría con un
10
+ * campo huérfano y la estación real nunca se marcaría — una falla muda. Ese acumulado se arma con
11
+ * `applyKycStep`, NO indexando a mano: la asignación directa no compila (los campos de `KycSubChecks`
12
+ * son opcionales) y el cast que la destraba desactiva el candado del vocabulario.
13
+ *
14
+ * Son las 8 del contrato, no las 13 que Metamap puede emitir: las otras (ip-validation, selfie,
15
+ * voice, duplicate-user-detection, curp/ine validation) no forman parte de `KycSubChecks` y por lo
16
+ * tanto no viajan. Ampliar la lista es un cambio de contrato en los dos lados, no un valor más aquí.
17
+ *
18
+ * SureKeep Fase 2 — progreso del KYC estación por estación.
19
+ */
20
+ var KycSubCheckNameEnum;
21
+ (function (KycSubCheckNameEnum) {
22
+ KycSubCheckNameEnum["DOCUMENT_READING"] = "documentReading";
23
+ KycSubCheckNameEnum["TEMPLATE_MATCHING"] = "templateMatching";
24
+ KycSubCheckNameEnum["ALTERATION_DETECTION"] = "alterationDetection";
25
+ KycSubCheckNameEnum["FACEMATCH"] = "facematch";
26
+ KycSubCheckNameEnum["LIVENESS"] = "liveness";
27
+ KycSubCheckNameEnum["AGE_CHECK"] = "ageCheck";
28
+ KycSubCheckNameEnum["WATCHLISTS"] = "watchlists";
29
+ KycSubCheckNameEnum["PREMIUM_AML_WATCHLISTS"] = "premiumAmlWatchlists";
30
+ })(KycSubCheckNameEnum || (exports.KycSubCheckNameEnum = KycSubCheckNameEnum = {}));
31
+ /**
32
+ * Las estaciones de LISTA — las únicas cuyo resultado se expresa en `CLEAR`/`HIT`. El resto son
33
+ * binarias (`PASS`/`FAIL`). Se exporta porque el validador del evento y los consumidores necesitan
34
+ * la MISMA partición: duplicarla en cada repo es drift garantizado.
35
+ */
36
+ exports.KYC_WATCHLIST_STEPS = [
37
+ KycSubCheckNameEnum.WATCHLISTS,
38
+ KycSubCheckNameEnum.PREMIUM_AML_WATCHLISTS,
39
+ ];
@@ -0,0 +1,85 @@
1
+ import { KycSubCheckNameEnum } from '../enums/KycSubCheckNameEnum';
2
+ import { KycSubCheckStatusEnum } from '../enums/KycSubCheckStatusEnum';
3
+ import { KycWatchlistStatusEnum } from '../enums/KycWatchlistStatusEnum';
4
+ /**
5
+ * El resultado de UNA estación del KYC, publicado por `kyc-metamap-webhook` en cuanto Metamap la
6
+ * cierra (`step_completed`) — no al final de la verificación.
7
+ *
8
+ * **Por qué existe:** `KycVerificationChangedV1` llega UNA vez, cuando todo terminó. Con eso la
9
+ * pantalla del vendedor solo puede mostrar una ruedita hasta el veredicto. Metamap emite un webhook
10
+ * por estación y el webhook ya los recibía; lo único que faltaba era reenviarlos. Este evento es ese
11
+ * reenvío: deja que SureKeep pinte el avance estación por estación mientras el cliente espera.
12
+ *
13
+ * **Convive con `KycVerificationChangedV1` en la MISMA cola** (`RetailKycInboundQueue`). El
14
+ * discriminador es la presencia de `step`: si lo trae, es una estación; si no, es el cierre. Por eso
15
+ * `KycVerificationChangedV1` no se tocó — un consumidor viejo que no conoce este evento lo descarta
16
+ * en su propio parseo sin romperse.
17
+ *
18
+ * 🔴 **NO es un veredicto.** Un `FAIL` acá no rechaza nada ni frena la venta: el cliente reintenta
19
+ * dentro del mismo SDK, en el mismo dispositivo, sin salir de la sesión. La decisión sigue viniendo
20
+ * del estado consolidado (`RetailKycStatusEnum`) que llega en el cierre. Esto es progreso visible,
21
+ * no política.
22
+ *
23
+ * 🔴 **Sin PII, a propósito.** No lleva `person`, ni `address`, ni `peopleId`: una estación solo
24
+ * necesita decir cuál es y cómo salió. Lo que sí lleva son identificadores operacionales, que es lo
25
+ * único que se puede loguear.
26
+ *
27
+ * **Idempotencia:** la clave es `(verificationId, step)` — NO viaja `eventId` a propósito. Una
28
+ * estación cierra una sola vez por verificación, y asentar su resultado sobre el progreso
29
+ * (`applyKycStep`) es idempotente por diseño: reprocesar el mismo evento escribe el mismo valor. Que
30
+ * quede dicho acá y no en la cabeza de cada consumidor: deducirlo mal lleva a deduplicar por el
31
+ * `MessageId` de SQS, que cambia en cada reintento de entrega y por lo tanto no deduplica nada.
32
+ *
33
+ * SureKeep Fase 2 — progreso del KYC estación por estación.
34
+ */
35
+ export declare class KycStepCompletedV1 {
36
+ /**
37
+ * Discriminador EXPLÍCITO del tipo de evento. Opcional hoy, y esa es toda la gracia: agregarlo
38
+ * ahora es gratis; agregarlo después de que el webhook publique en Dev ya sería breaking.
39
+ *
40
+ * Sin él, el ruteo del consumidor depende de la presencia de `step`. Eso funciona con los dos
41
+ * eventos de hoy, pero Metamap también emite `step_started`: el día que alguien publique un
42
+ * `KycStepStartedV1` traería `step` sin `status`, el consumidor lo rutearía a esta rama y el
43
+ * mensaje caería al DLQ etiquetado como "corrupto" en vez de "no soportado" — se diagnostica
44
+ * como bug del webhook cuando en realidad es un evento nuevo legítimo.
45
+ *
46
+ * Regla de ruteo hacia adelante: si `eventType` viene y matchea → esta rama; si no viene
47
+ * ninguno → el cierre legacy (`KycVerificationChangedV1`).
48
+ */
49
+ eventType?: string;
50
+ /** Correlaciona la estación con su verificación. */
51
+ verificationId: string;
52
+ /**
53
+ * Con esto el consumidor encuentra la sesión (el gate de pertenencia). DEC-KYC-015: `@IsNotEmpty()`
54
+ * y no solo `@IsString()` — un `''` colado dejaría el gate sin nada contra qué buscar y el evento
55
+ * fallaría tres pasos río abajo en vez de en el boundary.
56
+ */
57
+ directoryId: string;
58
+ /** Cuál estación cerró. El valor coincide con su llave en `KycSubChecks`. */
59
+ step: KycSubCheckNameEnum;
60
+ /**
61
+ * Cómo salió, en el vocabulario de SU estación: `CLEAR`/`HIT` en las de lista, `PASS`/`FAIL` en
62
+ * las binarias. El cruce lo valida `IsKycStepStatusByStep` — un `@IsIn` con los cuatro valores
63
+ * aceptaría `watchlists: FAIL` y perdería el matiz de que un HIT no es un fallo.
64
+ */
65
+ status: KycSubCheckStatusEnum | KycWatchlistStatusEnum;
66
+ /**
67
+ * Cuándo cerró la estación, según Metamap. **Obligatorio, y no es decorativo:** la cola es SQS
68
+ * estándar, que no garantiza orden. Sin una marca de tiempo por evento, una estación reordenada
69
+ * puede pisar el cierre de la verificación y "des-cerrar" la lista ya completa en pantalla. El
70
+ * consumidor descarta las que llegan más viejas que lo que ya escribió.
71
+ *
72
+ * ⚠️ **UTC estricto, con `Z` y sin offset** — por eso el `@Matches` además del `@IsISO8601()`.
73
+ * `@IsISO8601()` solo NO alcanza: acepta `2026-07-26` (sin hora), `2026-W30-1` (fecha de semana)
74
+ * y, sobre todo, `2026-07-26T15:05:00+05:00`. Ese último es el que hace daño. Si una estación
75
+ * llega con offset `+05:00` y la siguiente en `Z`, la comparación léxica que hace el consumidor
76
+ * concluye que la más nueva es más vieja y la DESCARTA: la estación nunca se pinta y nada truena.
77
+ *
78
+ * Normalizar a UTC es trabajo del webhook — es el traductor del proveedor, y este candado existe
79
+ * para que si algún día deja de hacerlo, el evento caiga al DLQ en el boundary en vez de
80
+ * corromper el orden en silencio.
81
+ */
82
+ occurredAt: string;
83
+ /** El flujo de Metamap que corrió. Solo para diagnóstico — nadie ramifica por él. */
84
+ metamapFlowId?: string;
85
+ }