@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.
- package/_test_/unit/kyc/IsKycStepStatusByStep.test.ts +86 -0
- package/_test_/unit/kyc/KycStepCompletedV1.test.ts +239 -0
- package/_test_/unit/kyc/applyKycStep.test.ts +75 -0
- package/bin/benefitCenter/enums/BenefitFlowEnum.d.ts +11 -0
- package/bin/benefitCenter/enums/BenefitFlowEnum.js +15 -0
- package/bin/kyc/enums/KycSubCheckNameEnum.d.ts +32 -0
- package/bin/kyc/enums/KycSubCheckNameEnum.js +39 -0
- package/bin/kyc/events/KycStepCompletedV1.d.ts +85 -0
- package/bin/kyc/events/KycStepCompletedV1.js +93 -0
- package/bin/kyc/helpers/applyKycStep.d.ts +26 -0
- package/bin/kyc/helpers/applyKycStep.js +30 -0
- package/bin/kyc/index.d.ts +4 -0
- package/bin/kyc/index.js +6 -0
- package/bin/kyc/validators/IsKycStepStatusByStep.d.ts +26 -0
- package/bin/kyc/validators/IsKycStepStatusByStep.js +64 -0
- package/bin/loanConfig/enums/ModifiableByRoleEnum.d.ts +11 -0
- package/bin/loanConfig/enums/ModifiableByRoleEnum.js +15 -0
- package/bin/places/dtos/CashInFeeDto.d.ts +17 -0
- package/bin/places/dtos/CashInFeeDto.js +12 -0
- package/bin/platformRbac/dtos/ResendOtpRequest.d.ts +22 -0
- package/bin/platformRbac/dtos/ResendOtpRequest.js +36 -0
- package/bin/platformRbac/dtos/ResendSelfRegisterOtpRequest.d.ts +11 -0
- package/bin/platformRbac/dtos/ResendSelfRegisterOtpRequest.js +36 -0
- package/bin/retailWizard/dtos/responses/KycPrepareResponse.d.ts +6 -0
- package/package.json +1 -1
- package/src/kyc/enums/KycSubCheckNameEnum.ts +36 -0
- package/src/kyc/events/KycStepCompletedV1.ts +105 -0
- package/src/kyc/helpers/applyKycStep.ts +36 -0
- package/src/kyc/index.ts +8 -0
- package/src/kyc/validators/IsKycStepStatusByStep.ts +57 -0
- 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
|
+
}
|