@fiado/type-kit 3.608.0 → 3.610.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 (83) hide show
  1. package/_test_/unit/biometrics/BiometricEventTypeEnum.test.ts +35 -0
  2. package/_test_/unit/biometrics/BiometricVerificationResponse.test.ts +19 -0
  3. package/_test_/unit/kyc/KycProgressEnums.test.ts +20 -0
  4. package/_test_/unit/kyc/KycProofOfAddressCompletedV1.test.ts +24 -0
  5. package/_test_/unit/kyc/KycStepCompletedV1.test.ts +34 -0
  6. package/_test_/unit/kyc/KycVerificationChangedV1.test.ts +30 -0
  7. package/_test_/unit/kyc/KycVerificationProgressV1.test.ts +144 -0
  8. package/_test_/unit/loanCredit/dtos/originateLoanCreditRequest.test.ts +9 -0
  9. package/_test_/unit/loanOfferings/promotionCategoryTierScope.test.ts +89 -0
  10. package/_test_/unit/retailWizard/kycAttemptProgress.test.ts +131 -0
  11. package/bin/biometrics/dtos/responses/BiometricVerificationResponse.d.ts +2 -0
  12. package/bin/biometrics/dtos/responses/BiometricVerificationResponse.js +6 -0
  13. package/bin/biometrics/enums/BiometricEventTypeEnum.d.ts +7 -3
  14. package/bin/biometrics/enums/BiometricEventTypeEnum.js +6 -2
  15. package/bin/kyc/enums/KycFlowEnum.d.ts +5 -0
  16. package/bin/kyc/enums/KycFlowEnum.js +9 -0
  17. package/bin/kyc/enums/KycProgressStageEnum.d.ts +6 -0
  18. package/bin/kyc/enums/KycProgressStageEnum.js +10 -0
  19. package/bin/kyc/events/KycProofOfAddressCompletedV1.d.ts +2 -0
  20. package/bin/kyc/events/KycProofOfAddressCompletedV1.js +7 -0
  21. package/bin/kyc/events/KycStepCompletedV1.d.ts +5 -0
  22. package/bin/kyc/events/KycStepCompletedV1.js +14 -0
  23. package/bin/kyc/events/KycVerificationChangedV1.d.ts +2 -0
  24. package/bin/kyc/events/KycVerificationChangedV1.js +7 -0
  25. package/bin/kyc/events/KycVerificationProgressV1.d.ts +14 -0
  26. package/bin/kyc/events/KycVerificationProgressV1.js +63 -0
  27. package/bin/kyc/index.d.ts +3 -0
  28. package/bin/kyc/index.js +3 -0
  29. package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.d.ts +6 -0
  30. package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.js +14 -0
  31. package/bin/loanOfferings/constants/promotionSkuBatch.d.ts +2 -0
  32. package/bin/loanOfferings/constants/promotionSkuBatch.js +5 -0
  33. package/bin/loanOfferings/dtos/requests/CreatePromotionRequest.d.ts +6 -0
  34. package/bin/loanOfferings/dtos/requests/CreatePromotionRequest.js +16 -0
  35. package/bin/loanOfferings/dtos/requests/ListPromotedSkusRequest.d.ts +11 -0
  36. package/bin/loanOfferings/dtos/requests/ListPromotedSkusRequest.js +47 -0
  37. package/bin/loanOfferings/dtos/requests/ListSkuPromotionsRequest.d.ts +11 -0
  38. package/bin/loanOfferings/dtos/requests/ListSkuPromotionsRequest.js +47 -0
  39. package/bin/loanOfferings/dtos/requests/PromotionSkuInput.d.ts +8 -0
  40. package/bin/loanOfferings/dtos/requests/PromotionSkuInput.js +37 -0
  41. package/bin/loanOfferings/dtos/requests/SimulateCreditPlanRequest.d.ts +6 -0
  42. package/bin/loanOfferings/dtos/requests/SimulateCreditPlanRequest.js +14 -0
  43. package/bin/loanOfferings/dtos/requests/UpdatePromotionRequest.d.ts +6 -0
  44. package/bin/loanOfferings/dtos/requests/UpdatePromotionRequest.js +16 -0
  45. package/bin/loanOfferings/dtos/responses/PromotionResponse.d.ts +6 -0
  46. package/bin/loanOfferings/index.d.ts +4 -0
  47. package/bin/loanOfferings/index.js +5 -0
  48. package/bin/retailWizard/dtos/KycAttemptProgress.d.ts +14 -0
  49. package/bin/retailWizard/dtos/KycAttemptProgress.js +2 -0
  50. package/bin/retailWizard/dtos/KycStatusProgress.d.ts +7 -0
  51. package/bin/retailWizard/dtos/KycStatusProgress.js +2 -0
  52. package/bin/retailWizard/dtos/responses/KycPrepareResponse.d.ts +4 -0
  53. package/bin/retailWizard/dtos/responses/ProofOfAddressPrepareResponse.d.ts +2 -0
  54. package/bin/retailWizard/enums/KycAttemptStageEnum.d.ts +8 -0
  55. package/bin/retailWizard/enums/KycAttemptStageEnum.js +12 -0
  56. package/bin/retailWizard/index.d.ts +3 -0
  57. package/bin/retailWizard/index.js +3 -0
  58. package/package.json +1 -1
  59. package/src/biometrics/dtos/responses/BiometricVerificationResponse.ts +4 -0
  60. package/src/biometrics/enums/BiometricEventTypeEnum.ts +8 -2
  61. package/src/kyc/enums/KycFlowEnum.ts +5 -0
  62. package/src/kyc/enums/KycProgressStageEnum.ts +6 -0
  63. package/src/kyc/events/KycProofOfAddressCompletedV1.ts +4 -0
  64. package/src/kyc/events/KycStepCompletedV1.ts +9 -0
  65. package/src/kyc/events/KycVerificationChangedV1.ts +4 -0
  66. package/src/kyc/events/KycVerificationProgressV1.ts +33 -0
  67. package/src/kyc/index.ts +3 -0
  68. package/src/loanCredit/dtos/requests/OriginateLoanCreditRequest.ts +14 -0
  69. package/src/loanOfferings/constants/promotionSkuBatch.ts +2 -0
  70. package/src/loanOfferings/dtos/requests/CreatePromotionRequest.ts +16 -0
  71. package/src/loanOfferings/dtos/requests/ListPromotedSkusRequest.ts +32 -0
  72. package/src/loanOfferings/dtos/requests/ListSkuPromotionsRequest.ts +32 -0
  73. package/src/loanOfferings/dtos/requests/PromotionSkuInput.ts +21 -0
  74. package/src/loanOfferings/dtos/requests/SimulateCreditPlanRequest.ts +16 -1
  75. package/src/loanOfferings/dtos/requests/UpdatePromotionRequest.ts +16 -0
  76. package/src/loanOfferings/dtos/responses/PromotionResponse.ts +6 -0
  77. package/src/loanOfferings/index.ts +6 -0
  78. package/src/retailWizard/dtos/KycAttemptProgress.ts +15 -0
  79. package/src/retailWizard/dtos/KycStatusProgress.ts +8 -0
  80. package/src/retailWizard/dtos/responses/KycPrepareResponse.ts +4 -0
  81. package/src/retailWizard/dtos/responses/ProofOfAddressPrepareResponse.ts +2 -0
  82. package/src/retailWizard/enums/KycAttemptStageEnum.ts +8 -0
  83. package/src/retailWizard/index.ts +3 -0
@@ -0,0 +1,35 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { BiometricEventTypeEnum, BiometricVerificationChangedV1 } from '../../../src/biometrics/index';
5
+
6
+ const base = {
7
+ eventId: 'evt-1',
8
+ biometricVerificationId: 'bio-1',
9
+ provider: 'METAMAP',
10
+ eventType: 'VERIFICATION_STARTED',
11
+ occurredAt: '2026-10-28T10:05:00.000Z',
12
+ };
13
+
14
+ describe('BiometricEventTypeEnum', () => {
15
+ it('declara los seis eventos que traduce el webhook', () => {
16
+ expect(Object.values(BiometricEventTypeEnum)).toEqual([
17
+ 'VERIFICATION_STARTED',
18
+ 'STEP_COMPLETED',
19
+ 'VERIFICATION_COMPLETED',
20
+ 'VERIFICATION_EXPIRED',
21
+ 'INPUTS_COMPLETED',
22
+ 'UPDATED',
23
+ ]);
24
+ });
25
+
26
+ it.each([BiometricEventTypeEnum.INPUTS_COMPLETED, BiometricEventTypeEnum.UPDATED])(
27
+ 'BiometricVerificationChangedV1 acepta el eventType %s',
28
+ async (eventType) => {
29
+ const dto = plainToInstance(BiometricVerificationChangedV1, { ...base, eventType }, { excludeExtraneousValues: true });
30
+
31
+ expect(await validate(dto as object)).toHaveLength(0);
32
+ expect(dto.eventType).toBe(eventType);
33
+ },
34
+ );
35
+ });
@@ -60,4 +60,23 @@ describe('BiometricVerificationResponse', () => {
60
60
  expect(instancia(base).referenceImageSource).toBe(BiometricReferenceSourceEnum.KYC_SELFIE);
61
61
  });
62
62
  });
63
+
64
+ // Cuándo el cliente terminó de subir su información; lo usa el wizard para INPUTS_COMPLETED.
65
+ describe('inputsCompletedAt', () => {
66
+ it('es opcional', async () => {
67
+ expect(await propiedadesConError(base)).toEqual([]);
68
+ expect(instancia(base).inputsCompletedAt).toBeUndefined();
69
+ });
70
+
71
+ it('acepta ISO-8601 UTC y sobrevive al plainToInstance', async () => {
72
+ const raw = { ...base, inputsCompletedAt: '2026-10-28T10:07:00.000Z' };
73
+
74
+ expect(await propiedadesConError(raw)).toEqual([]);
75
+ expect(instancia(raw).inputsCompletedAt).toBe('2026-10-28T10:07:00.000Z');
76
+ });
77
+
78
+ it('rechaza una fecha que no es ISO-8601', async () => {
79
+ expect(await propiedadesConError({ ...base, inputsCompletedAt: '28/10/2026' })).toContain('inputsCompletedAt');
80
+ });
81
+ });
63
82
  });
@@ -0,0 +1,20 @@
1
+ import 'reflect-metadata';
2
+ import { KycFlowEnum, KycProgressStageEnum } from '../../../src/kyc/index';
3
+ import { KycMetadataStepEnum } from '../../../src/retailWizard/index';
4
+
5
+ describe('KycProgressStageEnum', () => {
6
+ it('declara solo las tres etapas que publica el webhook', () => {
7
+ expect(Object.values(KycProgressStageEnum)).toEqual(['STARTED', 'INPUTS_COMPLETED', 'EXPIRED']);
8
+ });
9
+ });
10
+
11
+ describe('KycFlowEnum', () => {
12
+ it('declara los dos flujos de Metamap que salen del wizard', () => {
13
+ expect(Object.values(KycFlowEnum)).toEqual(['IDENTITY', 'PROOF_OF_ADDRESS']);
14
+ });
15
+
16
+ // El webhook deriva flow de metadata.kycStep: si los valores divergen, el comprobante se rutea como identidad.
17
+ it('PROOF_OF_ADDRESS coincide con el kycStep de la metadata', () => {
18
+ expect(KycFlowEnum.PROOF_OF_ADDRESS).toBe(KycMetadataStepEnum.PROOF_OF_ADDRESS);
19
+ });
20
+ });
@@ -192,4 +192,28 @@ describe('KycProofOfAddressCompletedV1', () => {
192
192
  ].sort(),
193
193
  );
194
194
  });
195
+
196
+ // ── Intento (estados MetaMap/QR) ──────────────────────────────────────────
197
+
198
+ describe('attemptId', () => {
199
+ it('acepta y expone attemptId', async () => {
200
+ const dto = instancia({ ...base, attemptId: 'att-1' });
201
+
202
+ expect(await validate(dto as object)).toHaveLength(0);
203
+ expect(dto.attemptId).toBe('att-1');
204
+ });
205
+
206
+ it('valida sin attemptId (compatibilidad hacia atrás)', async () => {
207
+ const dto = instancia(base);
208
+
209
+ expect(await validate(dto as object)).toHaveLength(0);
210
+ expect(dto.attemptId).toBeUndefined();
211
+ });
212
+
213
+ it('rechaza attemptId vacío', async () => {
214
+ const errors = await validate(instancia({ ...base, attemptId: '' }) as object);
215
+
216
+ expect(errors.map((e) => e.property)).toContain('attemptId');
217
+ });
218
+ });
195
219
  });
@@ -2,6 +2,7 @@ import 'reflect-metadata';
2
2
  import { plainToInstance } from 'class-transformer';
3
3
  import { validate } from 'class-validator';
4
4
  import {
5
+ KycFlowEnum,
5
6
  KycStepCompletedV1,
6
7
  KycVerificationChangedV1,
7
8
  KycSubCheckNameEnum,
@@ -236,4 +237,37 @@ describe('KycStepCompletedV1', () => {
236
237
  );
237
238
  expect(Object.values(KycWatchlistStatusEnum)).toEqual(['CLEAR', 'HIT']);
238
239
  });
240
+
241
+ // ── Intento y flujo (estados MetaMap/QR) ──────────────────────────────────
242
+
243
+ describe('attemptId y flow', () => {
244
+ it('acepta y expone attemptId y flow', async () => {
245
+ const dto = instancia({ ...base, attemptId: 'att-1', flow: 'IDENTITY' });
246
+
247
+ expect(await validate(dto as object)).toHaveLength(0);
248
+ expect(dto.attemptId).toBe('att-1');
249
+ expect(dto.flow).toBe(KycFlowEnum.IDENTITY);
250
+ });
251
+
252
+ // Mensajes en vuelo y sesiones previas al despliegue no los traen.
253
+ it('valida sin attemptId ni flow', async () => {
254
+ const dto = instancia(base);
255
+
256
+ expect(await validate(dto as object)).toHaveLength(0);
257
+ expect(dto.attemptId).toBeUndefined();
258
+ expect(dto.flow).toBeUndefined();
259
+ });
260
+
261
+ it('rechaza attemptId vacío', async () => {
262
+ const errors = await validate(instancia({ ...base, attemptId: '' }) as object);
263
+
264
+ expect(errors.map((e) => e.property)).toContain('attemptId');
265
+ });
266
+
267
+ it.each(['FACEMATCH', 'identity'])('rechaza el flow %s', async (flow) => {
268
+ const errors = await validate(instancia({ ...base, flow }) as object);
269
+
270
+ expect(errors.map((e) => e.property)).toContain('flow');
271
+ });
272
+ });
239
273
  });
@@ -152,4 +152,34 @@ describe('KycVerificationChangedV1', () => {
152
152
 
153
153
  expect(errors.map((e) => e.property)).toContain('updateDate');
154
154
  });
155
+
156
+ // Estados MetaMap/QR: el cierre trae el intento para que el wizard descarte los ajenos.
157
+ describe('attemptId', () => {
158
+ const closing = {
159
+ verificationId: 'ver-1', directoryId: 'dir-1', peopleId: 'ppl-1',
160
+ verificationStatus: 'COMPLETED', verificationResult: 'VERIFIED',
161
+ updateDate: '2026-10-28T10:05:00.000Z',
162
+ };
163
+
164
+ it('acepta y expone attemptId', async () => {
165
+ const dto = plainToInstance(KycVerificationChangedV1, { ...closing, attemptId: 'att-1' }, { excludeExtraneousValues: true });
166
+
167
+ expect(await validate(dto as object)).toHaveLength(0);
168
+ expect(dto.attemptId).toBe('att-1');
169
+ });
170
+
171
+ it('valida sin attemptId (compatibilidad hacia atrás)', async () => {
172
+ const dto = plainToInstance(KycVerificationChangedV1, closing, { excludeExtraneousValues: true });
173
+
174
+ expect(await validate(dto as object)).toHaveLength(0);
175
+ expect(dto.attemptId).toBeUndefined();
176
+ });
177
+
178
+ it('rechaza attemptId vacío', async () => {
179
+ const dto = plainToInstance(KycVerificationChangedV1, { ...closing, attemptId: '' }, { excludeExtraneousValues: true });
180
+ const errors = await validate(dto as object);
181
+
182
+ expect(errors.map((e) => e.property)).toContain('attemptId');
183
+ });
184
+ });
155
185
  });
@@ -0,0 +1,144 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import {
5
+ KycFlowEnum,
6
+ KycProgressStageEnum,
7
+ KycProofOfAddressCompletedV1,
8
+ KycStepCompletedV1,
9
+ KycVerificationChangedV1,
10
+ KycVerificationProgressV1,
11
+ } from '../../../src/kyc/index';
12
+
13
+ /** Base válida; cada caso pisa solo el campo que prueba. */
14
+ const base = {
15
+ eventType: 'KycVerificationProgressV1',
16
+ stage: 'STARTED',
17
+ flow: 'IDENTITY',
18
+ verificationId: 'ver-1',
19
+ directoryId: 'dir-1',
20
+ attemptId: 'att-1',
21
+ occurredAt: '2026-10-28T10:05:00.000Z',
22
+ };
23
+
24
+ const toDto = (raw: Record<string, unknown>): KycVerificationProgressV1 =>
25
+ plainToInstance(KycVerificationProgressV1, raw, { excludeExtraneousValues: true });
26
+
27
+ const errorProps = async (raw: Record<string, unknown>): Promise<string[]> =>
28
+ (await validate(toDto(raw) as object)).map((e) => e.property);
29
+
30
+ describe('KycVerificationProgressV1', () => {
31
+ it('valida el shape que publica el webhook', async () => {
32
+ const dto = toDto(base);
33
+
34
+ expect(await errorProps(base)).toEqual([]);
35
+ expect(dto.stage).toBe(KycProgressStageEnum.STARTED);
36
+ expect(dto.flow).toBe(KycFlowEnum.IDENTITY);
37
+ expect(dto.attemptId).toBe('att-1');
38
+ });
39
+
40
+ it.each(Object.values(KycProgressStageEnum))('acepta la etapa %s', async (stage) => {
41
+ expect(await errorProps({ ...base, stage })).toEqual([]);
42
+ });
43
+
44
+ it.each(Object.values(KycFlowEnum))('acepta el flujo %s', async (flow) => {
45
+ expect(await errorProps({ ...base, flow })).toEqual([]);
46
+ });
47
+
48
+ // Sesiones creadas antes del despliegue: la metadata no trae attemptId.
49
+ it('valida sin attemptId', async () => {
50
+ const { attemptId: _omitted, ...withoutAttempt } = base;
51
+
52
+ expect(await errorProps(withoutAttempt)).toEqual([]);
53
+ expect(toDto(withoutAttempt).attemptId).toBeUndefined();
54
+ });
55
+
56
+ it('rechaza attemptId vacío', async () => {
57
+ expect(await errorProps({ ...base, attemptId: '' })).toContain('attemptId');
58
+ });
59
+
60
+ describe('eventType', () => {
61
+ it('rechaza el evento sin eventType', async () => {
62
+ const { eventType: _omitted, ...withoutType } = base;
63
+
64
+ expect(await errorProps(withoutType)).toContain('eventType');
65
+ });
66
+
67
+ it('rechaza el eventType de otro evento', async () => {
68
+ expect(await errorProps({ ...base, eventType: 'KycStepCompletedV1' })).toContain('eventType');
69
+ });
70
+ });
71
+
72
+ // AWAITING_START y RESULT son etapas del wizard, nunca llegan por evento.
73
+ it.each(['AWAITING_START', 'RESULT', 'COMPLETED'])('rechaza la etapa %s', async (stage) => {
74
+ expect(await errorProps({ ...base, stage })).toContain('stage');
75
+ });
76
+
77
+ // FACEMATCH no pasa por este evento: su avance sale del GET de biometrics.
78
+ it('rechaza el flujo FACEMATCH', async () => {
79
+ expect(await errorProps({ ...base, flow: 'FACEMATCH' })).toContain('flow');
80
+ });
81
+
82
+ it.each(['verificationId', 'directoryId'])('rechaza %s vacío', async (field) => {
83
+ expect(await errorProps({ ...base, [field]: '' })).toContain(field);
84
+ });
85
+
86
+ it.each(['stage', 'flow', 'verificationId', 'directoryId', 'occurredAt'])('rechaza el evento sin %s', async (field) => {
87
+ const raw: Record<string, unknown> = { ...base };
88
+ delete raw[field];
89
+
90
+ expect(await errorProps(raw)).toContain(field);
91
+ });
92
+
93
+ describe('occurredAt', () => {
94
+ it.each([
95
+ ['offset positivo en vez de Z', '2026-10-28T15:05:00+05:00'],
96
+ ['solo fecha, sin hora', '2026-10-28'],
97
+ ['sin zona horaria', '2026-10-28T10:05:00'],
98
+ ['formato local', '28/10/2026'],
99
+ ])('rechaza occurredAt %s', async (_label, value) => {
100
+ expect(await errorProps({ ...base, occurredAt: value })).toContain('occurredAt');
101
+ });
102
+
103
+ it.each([
104
+ ['con milisegundos', '2026-10-28T10:05:00.000Z'],
105
+ ['sin milisegundos', '2026-10-28T10:05:00Z'],
106
+ ])('acepta occurredAt en UTC %s', async (_label, value) => {
107
+ expect(await errorProps({ ...base, occurredAt: value })).toEqual([]);
108
+ });
109
+ });
110
+
111
+ // Comparte RetailKycInboundQueue con otros tres eventos: ninguno se puede hacer pasar por otro.
112
+ describe('convivencia en RetailKycInboundQueue', () => {
113
+ it('un avance NO valida como estación', async () => {
114
+ const errors = await validate(plainToInstance(KycStepCompletedV1, base, { excludeExtraneousValues: true }) as object);
115
+
116
+ expect(errors.map((e) => e.property)).toEqual(expect.arrayContaining(['eventType', 'step', 'status']));
117
+ });
118
+
119
+ it('un avance NO valida como cierre', async () => {
120
+ const errors = await validate(plainToInstance(KycVerificationChangedV1, base, { excludeExtraneousValues: true }) as object);
121
+
122
+ expect(errors.map((e) => e.property)).toEqual(
123
+ expect.arrayContaining(['peopleId', 'verificationStatus', 'verificationResult', 'updateDate']),
124
+ );
125
+ });
126
+
127
+ it('un avance NO valida como comprobante', async () => {
128
+ const errors = await validate(
129
+ plainToInstance(KycProofOfAddressCompletedV1, base, { excludeExtraneousValues: true }) as object,
130
+ );
131
+
132
+ expect(errors.map((e) => e.property)).toEqual(expect.arrayContaining(['eventType', 'status', 'addressPersisted']));
133
+ });
134
+
135
+ it('una estación NO valida como avance', async () => {
136
+ const station = {
137
+ eventType: 'KycStepCompletedV1', verificationId: 'ver-1', directoryId: 'dir-1',
138
+ step: 'facematch', status: 'PASS', occurredAt: '2026-10-28T10:05:00.000Z',
139
+ };
140
+
141
+ expect(await errorProps(station)).toEqual(expect.arrayContaining(['eventType', 'stage', 'flow']));
142
+ });
143
+ });
144
+ });
@@ -53,4 +53,13 @@ describe('OriginateLoanCreditRequest', () => {
53
53
 
54
54
  expect(errors.some((error) => error.property === 'sellerId')).toBe(true);
55
55
  });
56
+
57
+ it('acepta tipo y gama del equipo opcionales y rechaza valores fuera del enum', async () => {
58
+ const ok = await validateWhitelisted({ ...BASE, productCategory: 'PHONE', productTier: 'HIGH' });
59
+ expect(ok.errors).toEqual([]);
60
+ expect([ok.dto.productCategory, ok.dto.productTier]).toEqual(['PHONE', 'HIGH']);
61
+
62
+ const bad = await validateWhitelisted({ ...BASE, productCategory: 'CAR', productTier: 'ULTRA' });
63
+ expect(bad.errors.map((error) => error.property).sort()).toEqual(['productCategory', 'productTier']);
64
+ });
56
65
  });
@@ -0,0 +1,89 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import {
5
+ CreatePromotionRequest,
6
+ ListPromotedSkusRequest,
7
+ ListSkuPromotionsRequest,
8
+ PROMOTION_SKU_BATCH_MAX_ITEMS,
9
+ SimulateCreditPlanRequest,
10
+ UpdatePromotionRequest,
11
+ } from '../../../src/loanOfferings/index';
12
+ import type { PromotionResponse } from '../../../src/loanOfferings/index';
13
+ import { ProductCategoryEnum } from '../../../src/retailCatalog/enums/ProductCategoryEnum';
14
+ import { ProductTierEnum } from '../../../src/retailCatalog/enums/ProductTierEnum';
15
+
16
+ type DtoClass = new () => object;
17
+
18
+ const errorsOf = async (dtoClass: DtoClass, body: object): Promise<string[]> =>
19
+ (await validate(plainToInstance(dtoClass, body, { excludeExtraneousValues: true }) as object)).map((e) => e.property);
20
+
21
+ const createBase = {
22
+ name: 'Tasa preferente gama alta',
23
+ description: 'Baja la tasa en teléfonos de gama alta',
24
+ effect: { downPaymentDeltaPct: 0.05 },
25
+ segmentId: 'SEG_SILVER',
26
+ targetLevels: ['SILVER'],
27
+ };
28
+
29
+ describe('alcance por tipo y gama en promociones de crédito', () => {
30
+ it('Create acepta tipos y gamas del catálogo, y null como «todos»', async () => {
31
+ const dto = plainToInstance(CreatePromotionRequest, { ...createBase, productCategories: ['PHONE'], tiers: ['HIGH', 'PREMIUM'] }, { excludeExtraneousValues: true });
32
+ expect(await validate(dto)).toHaveLength(0);
33
+ expect(dto.productCategories).toEqual([ProductCategoryEnum.PHONE]);
34
+ expect(dto.tiers).toEqual([ProductTierEnum.HIGH, ProductTierEnum.PREMIUM]);
35
+ expect(await errorsOf(CreatePromotionRequest, { ...createBase, productCategories: null, tiers: null })).toEqual([]);
36
+ });
37
+
38
+ it.each([CreatePromotionRequest, UpdatePromotionRequest])('%p rechaza tipo o gama fuera del enum', async (dtoClass) => {
39
+ const body = dtoClass === CreatePromotionRequest ? createBase : {};
40
+ expect(await errorsOf(dtoClass, { ...body, productCategories: ['CAR'] })).toContain('productCategories');
41
+ expect(await errorsOf(dtoClass, { ...body, tiers: ['ULTRA'] })).toContain('tiers');
42
+ expect(await errorsOf(dtoClass, { ...body, tiers: 'HIGH' })).toContain('tiers');
43
+ });
44
+
45
+ it('Update acepta el parche solo con tipo y gama', async () => {
46
+ expect(await errorsOf(UpdatePromotionRequest, { productCategories: ['SIM', 'MODEM'], tiers: null })).toEqual([]);
47
+ });
48
+
49
+ it('la respuesta expone tipos y gamas con null como «todos»', () => {
50
+ const scope: Pick<PromotionResponse, 'productCategories' | 'tiers'> = { productCategories: null, tiers: [ProductTierEnum.LOW] };
51
+ expect(scope.tiers).toEqual(['LOW']);
52
+ });
53
+
54
+ it('el simulador recibe tipo y gama opcionales y valida el enum', async () => {
55
+ const base = { productPriceCents: 500000, downPaymentPct: 0.2, termWeeks: 26 };
56
+ expect(await errorsOf(SimulateCreditPlanRequest, base)).toEqual([]);
57
+ expect(await errorsOf(SimulateCreditPlanRequest, { ...base, productCategory: 'PHONE', productTier: 'MID' })).toEqual([]);
58
+ expect(await errorsOf(SimulateCreditPlanRequest, { ...base, productCategory: 'CAR' })).toContain('productCategory');
59
+ expect(await errorsOf(SimulateCreditPlanRequest, { ...base, productTier: 'ULTRA' })).toContain('productTier');
60
+ });
61
+ });
62
+
63
+ describe.each([ListPromotedSkusRequest, ListSkuPromotionsRequest])('%p (lote por SKU)', (dtoClass) => {
64
+ const valid = { storeId: 'st-1', items: [{ sku: 'SKU-1', category: 'PHONE', tier: 'MID' }] };
65
+
66
+ it('acepta tienda, nivel y segmento opcionales, y SKUs con tipo y gama opcionales', async () => {
67
+ expect(await errorsOf(dtoClass, valid)).toEqual([]);
68
+ expect(await errorsOf(dtoClass, { ...valid, level: 'GOLD', segmentId: 'SEG-1', items: [{ sku: 'SKU-2' }] })).toEqual([]);
69
+ });
70
+
71
+ it.each([
72
+ ['sin tienda', { ...valid, storeId: undefined }, 'storeId'],
73
+ ['tienda vacía', { ...valid, storeId: '' }, 'storeId'],
74
+ ['nivel fuera del enum', { ...valid, level: 'PLATINUM' }, 'level'],
75
+ ['sin items', { ...valid, items: undefined }, 'items'],
76
+ ['tipo fuera del enum', { ...valid, items: [{ sku: 'A', category: 'CAR' }] }, 'items'],
77
+ ['gama fuera del enum', { ...valid, items: [{ sku: 'A', tier: 'ULTRA' }] }, 'items'],
78
+ ['SKU no string', { ...valid, items: [{ sku: 7 }] }, 'items'],
79
+ ])('rechaza %s', async (_label, body, property) => {
80
+ expect(await errorsOf(dtoClass, body)).toContain(property);
81
+ });
82
+
83
+ it(`rechaza más de ${PROMOTION_SKU_BATCH_MAX_ITEMS} SKUs y acepta exactamente el tope`, async () => {
84
+ const items = (n: number) => Array.from({ length: n }, (_v, i) => ({ sku: `S${i}` }));
85
+ expect(PROMOTION_SKU_BATCH_MAX_ITEMS).toBe(100);
86
+ expect(await errorsOf(dtoClass, { ...valid, items: items(PROMOTION_SKU_BATCH_MAX_ITEMS) })).toEqual([]);
87
+ expect(await errorsOf(dtoClass, { ...valid, items: items(PROMOTION_SKU_BATCH_MAX_ITEMS + 1) })).toContain('items');
88
+ });
89
+ });
@@ -0,0 +1,131 @@
1
+ import 'reflect-metadata';
2
+ import { KycProgressStageEnum } from '../../../src/kyc/index';
3
+ import { KycModalityEnum } from '../../../src/retailCustomer/enums/KycModalityEnum';
4
+ import {
5
+ KycAttemptStageEnum,
6
+ KycMetadataSourceEnum,
7
+ KycMetadataStepEnum,
8
+ type KycAttemptProgress,
9
+ type KycPrepareResponse,
10
+ type KycSdkMetadata,
11
+ type KycStatusProgress,
12
+ type ProofOfAddressPrepareResponse,
13
+ } from '../../../src/retailWizard/index';
14
+
15
+ const qrAttempt: KycAttemptProgress = {
16
+ attemptId: 'att-1',
17
+ modality: KycModalityEnum.CUSTOMER_LINK,
18
+ link: 'https://sk.fiado.test/abc',
19
+ stage: KycAttemptStageEnum.STARTED,
20
+ stageAt: '2026-10-28T10:05:00.000Z',
21
+ attemptStartedAt: '2026-10-28T10:00:00.000Z',
22
+ };
23
+
24
+ const tabletAttempt: KycAttemptProgress = {
25
+ attemptId: 'att-2',
26
+ modality: KycModalityEnum.STORE_DEVICE,
27
+ stage: KycAttemptStageEnum.AWAITING_START,
28
+ stageAt: '2026-10-28T10:00:00.000Z',
29
+ attemptStartedAt: '2026-10-28T10:00:00.000Z',
30
+ };
31
+
32
+ describe('KycAttemptStageEnum', () => {
33
+ it('declara las cinco etapas en orden de rango', () => {
34
+ expect(Object.values(KycAttemptStageEnum)).toEqual([
35
+ 'AWAITING_START', 'STARTED', 'INPUTS_COMPLETED', 'EXPIRED', 'RESULT',
36
+ ]);
37
+ });
38
+
39
+ // El wizard copia la etapa del evento al intento por valor.
40
+ it.each(Object.values(KycProgressStageEnum))('incluye la etapa de evento %s', (stage) => {
41
+ expect(Object.values(KycAttemptStageEnum)).toContain(stage);
42
+ });
43
+ });
44
+
45
+ describe('KycAttemptProgress', () => {
46
+ it('lleva link en QR y lo omite en tablet', () => {
47
+ expect(qrAttempt.link).toBe('https://sk.fiado.test/abc');
48
+ expect(tabletAttempt.link).toBeUndefined();
49
+ });
50
+
51
+ it('solo admite las modalidades de prepare', () => {
52
+ const manual: KycAttemptProgress = {
53
+ ...tabletAttempt,
54
+ // @ts-expect-error MANUAL no es una modalidad de prepare
55
+ modality: KycModalityEnum.MANUAL,
56
+ };
57
+
58
+ expect(manual.modality).toBe('MANUAL');
59
+ });
60
+
61
+ // El tope de 25 min del front se mide contra la fecha del prepare, no contra el montaje.
62
+ it('exige attemptStartedAt', () => {
63
+ const { attemptStartedAt: _omitted, ...rest } = qrAttempt;
64
+ // @ts-expect-error attemptStartedAt es obligatorio
65
+ const withoutStart: KycAttemptProgress = rest;
66
+
67
+ expect(qrAttempt.attemptStartedAt).toBe('2026-10-28T10:00:00.000Z');
68
+ expect(withoutStart.attemptStartedAt).toBeUndefined();
69
+ });
70
+ });
71
+
72
+ describe('KycStatusProgress', () => {
73
+ it('trae un intento o null por cada flujo', () => {
74
+ const progress: KycStatusProgress = { identity: qrAttempt, facematch: null, proofOfAddress: tabletAttempt };
75
+
76
+ expect(Object.keys(progress).sort()).toEqual(['facematch', 'identity', 'proofOfAddress']);
77
+ expect(progress.facematch).toBeNull();
78
+ });
79
+ });
80
+
81
+ describe('KycSdkMetadata', () => {
82
+ it('acepta attemptId junto a source y kycStep', () => {
83
+ const metadata: KycSdkMetadata = {
84
+ directoryId: 'dir-1',
85
+ typeOfDirectoryId: 'PHONE',
86
+ peopleId: 'ppl-1',
87
+ countryId: 'MX',
88
+ phoneNumber: '5512345678',
89
+ source: KycMetadataSourceEnum.SUREKEEP_WIZARD,
90
+ kycStep: KycMetadataStepEnum.PROOF_OF_ADDRESS,
91
+ attemptId: 'att-1',
92
+ };
93
+
94
+ expect(metadata.attemptId).toBe('att-1');
95
+ });
96
+ });
97
+
98
+ // El front liga el SDK y los callbacks al attemptId que devuelve el prepare.
99
+ describe('attemptId en las respuestas de prepare', () => {
100
+ // Sin intento creado (alreadyVerified o bypass de dev) no hay attemptId.
101
+ it('KycPrepareResponse lleva attemptId al preparar y lo omite sin intento', () => {
102
+ const prepared: KycPrepareResponse = {
103
+ modality: KycModalityEnum.CUSTOMER_LINK,
104
+ shortUrl: 'https://sk.fiado.test/abc',
105
+ attemptId: 'att-1',
106
+ };
107
+ const alreadyVerified: KycPrepareResponse = {
108
+ modality: KycModalityEnum.CUSTOMER_LINK,
109
+ alreadyVerified: true,
110
+ };
111
+
112
+ expect(prepared.attemptId).toBe('att-1');
113
+ expect(alreadyVerified.attemptId).toBeUndefined();
114
+ });
115
+
116
+ // Con domicilio ya registrado no se crea intento, por eso no hay attemptId.
117
+ it('ProofOfAddressPrepareResponse lleva attemptId al preparar y lo omite con alreadyHasAddress', () => {
118
+ const prepared: ProofOfAddressPrepareResponse = {
119
+ modality: KycModalityEnum.STORE_DEVICE,
120
+ clientId: 'client-1',
121
+ attemptId: 'att-3',
122
+ };
123
+ const alreadyHasAddress: ProofOfAddressPrepareResponse = {
124
+ modality: KycModalityEnum.STORE_DEVICE,
125
+ alreadyHasAddress: true,
126
+ };
127
+
128
+ expect(prepared.attemptId).toBe('att-3');
129
+ expect(alreadyHasAddress.attemptId).toBeUndefined();
130
+ });
131
+ });
@@ -91,6 +91,8 @@ export declare class BiometricVerificationResponse {
91
91
  updatedAt: string;
92
92
  /** Cuándo vence la entrega. Es lo que el `GET` compara para persistir `EXPIRED` (DEC-015). */
93
93
  expiresAt: string;
94
+ /** Cuándo el usuario terminó de subir su información. ISO-8601 UTC con `Z`; ausente si aún no. */
95
+ inputsCompletedAt?: string;
94
96
  /** Cuándo alcanzó un estado terminal. Ausente mientras siga viva. */
95
97
  completedAt?: string;
96
98
  }
@@ -129,6 +129,12 @@ __decorate([
129
129
  (0, class_validator_1.IsISO8601)({ strict: true }),
130
130
  __metadata("design:type", String)
131
131
  ], BiometricVerificationResponse.prototype, "expiresAt", void 0);
132
+ __decorate([
133
+ (0, class_transformer_1.Expose)(),
134
+ (0, class_validator_1.IsOptional)(),
135
+ (0, class_validator_1.IsISO8601)({ strict: true }),
136
+ __metadata("design:type", String)
137
+ ], BiometricVerificationResponse.prototype, "inputsCompletedAt", void 0);
132
138
  __decorate([
133
139
  (0, class_transformer_1.Expose)(),
134
140
  (0, class_validator_1.IsOptional)(),
@@ -3,8 +3,8 @@
3
3
  * lo que el consumer usa para decidir la transición de estado.
4
4
  *
5
5
  * Es el vocabulario NUESTRO, no el de Metamap: `kyc-metamap-webhook` traduce sus `eventName` a
6
- * estos cuatro antes de publicar. Así el consumer no aprende el catálogo de un proveedor externo, y
7
- * el día que entre otro proveedor traduce a los mismos cuatro.
6
+ * estos valores antes de publicar. Así el consumer no aprende el catálogo de un proveedor externo, y
7
+ * el día que entre otro proveedor traduce a los mismos valores.
8
8
  *
9
9
  * `biometrics-business` — Entrega 1.
10
10
  */
@@ -24,5 +24,9 @@ export declare enum BiometricEventTypeEnum {
24
24
  * ⚠️ **No se depende de este evento para expirar.** El `GET` persiste `EXPIRED` comparando
25
25
  * contra `expiresAt` (DEC-015), así que una verificación vence aunque este evento nunca llegue.
26
26
  */
27
- VERIFICATION_EXPIRED = "VERIFICATION_EXPIRED"
27
+ VERIFICATION_EXPIRED = "VERIFICATION_EXPIRED",
28
+ /** El usuario terminó de subir su información. Sigue en `IN_PROGRESS` y graba `inputsCompletedAt`. */
29
+ INPUTS_COMPLETED = "INPUTS_COMPLETED",
30
+ /** El proveedor cambió el veredicto (revisión manual). Se trata igual que `VERIFICATION_COMPLETED`. */
31
+ UPDATED = "UPDATED"
28
32
  }
@@ -6,8 +6,8 @@ exports.BiometricEventTypeEnum = void 0;
6
6
  * lo que el consumer usa para decidir la transición de estado.
7
7
  *
8
8
  * Es el vocabulario NUESTRO, no el de Metamap: `kyc-metamap-webhook` traduce sus `eventName` a
9
- * estos cuatro antes de publicar. Así el consumer no aprende el catálogo de un proveedor externo, y
10
- * el día que entre otro proveedor traduce a los mismos cuatro.
9
+ * estos valores antes de publicar. Así el consumer no aprende el catálogo de un proveedor externo, y
10
+ * el día que entre otro proveedor traduce a los mismos valores.
11
11
  *
12
12
  * `biometrics-business` — Entrega 1.
13
13
  */
@@ -29,4 +29,8 @@ var BiometricEventTypeEnum;
29
29
  * contra `expiresAt` (DEC-015), así que una verificación vence aunque este evento nunca llegue.
30
30
  */
31
31
  BiometricEventTypeEnum["VERIFICATION_EXPIRED"] = "VERIFICATION_EXPIRED";
32
+ /** El usuario terminó de subir su información. Sigue en `IN_PROGRESS` y graba `inputsCompletedAt`. */
33
+ BiometricEventTypeEnum["INPUTS_COMPLETED"] = "INPUTS_COMPLETED";
34
+ /** El proveedor cambió el veredicto (revisión manual). Se trata igual que `VERIFICATION_COMPLETED`. */
35
+ BiometricEventTypeEnum["UPDATED"] = "UPDATED";
32
36
  })(BiometricEventTypeEnum || (exports.BiometricEventTypeEnum = BiometricEventTypeEnum = {}));
@@ -0,0 +1,5 @@
1
+ /** Flujo de Metamap al que pertenece un evento; el webhook lo deriva de `metadata.kycStep`. */
2
+ export declare enum KycFlowEnum {
3
+ IDENTITY = "IDENTITY",
4
+ PROOF_OF_ADDRESS = "PROOF_OF_ADDRESS"
5
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.KycFlowEnum = void 0;
4
+ /** Flujo de Metamap al que pertenece un evento; el webhook lo deriva de `metadata.kycStep`. */
5
+ var KycFlowEnum;
6
+ (function (KycFlowEnum) {
7
+ KycFlowEnum["IDENTITY"] = "IDENTITY";
8
+ KycFlowEnum["PROOF_OF_ADDRESS"] = "PROOF_OF_ADDRESS";
9
+ })(KycFlowEnum || (exports.KycFlowEnum = KycFlowEnum = {}));
@@ -0,0 +1,6 @@
1
+ /** Etapa de avance de una verificación de Metamap, tal como la publica `kyc-metamap-webhook`. */
2
+ export declare enum KycProgressStageEnum {
3
+ STARTED = "STARTED",
4
+ INPUTS_COMPLETED = "INPUTS_COMPLETED",
5
+ EXPIRED = "EXPIRED"
6
+ }