@fiado/type-kit 3.399.0 → 3.401.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 (52) hide show
  1. package/_test_/unit/loanCredit/dtos/loanDeviceContract.test.ts +405 -0
  2. package/_test_/unit/mdm/dtos/mdmDeviceContract.test.ts +83 -0
  3. package/bin/loanCredit/dtos/LoanDeviceNotifyItem.d.ts +12 -0
  4. package/bin/loanCredit/dtos/LoanDeviceNotifyItem.js +54 -0
  5. package/bin/loanCredit/dtos/LoanDeviceReleaseItem.d.ts +8 -0
  6. package/bin/loanCredit/dtos/LoanDeviceReleaseItem.js +37 -0
  7. package/bin/loanCredit/dtos/LoanDeviceUnlockItem.d.ts +6 -0
  8. package/bin/loanCredit/dtos/LoanDeviceUnlockItem.js +31 -0
  9. package/bin/loanCredit/dtos/requests/LoanDeviceManualUnlockRequest.d.ts +2 -2
  10. package/bin/loanCredit/dtos/requests/LoanDeviceManualUnlockRequest.js +2 -2
  11. package/bin/loanCredit/dtos/requests/LoanDeviceNotifyRequest.d.ts +7 -0
  12. package/bin/loanCredit/dtos/requests/LoanDeviceNotifyRequest.js +37 -0
  13. package/bin/loanCredit/dtos/requests/LoanDeviceReleaseRequest.d.ts +7 -0
  14. package/bin/loanCredit/dtos/requests/LoanDeviceReleaseRequest.js +37 -0
  15. package/bin/loanCredit/dtos/responses/LoanDeviceNotifyResponse.d.ts +5 -0
  16. package/bin/loanCredit/dtos/responses/LoanDeviceNotifyResponse.js +7 -0
  17. package/bin/loanCredit/dtos/responses/LoanDeviceNotifyResult.d.ts +8 -0
  18. package/bin/loanCredit/dtos/responses/LoanDeviceNotifyResult.js +8 -0
  19. package/bin/loanCredit/dtos/responses/LoanDeviceReleaseResponse.d.ts +5 -0
  20. package/bin/loanCredit/dtos/responses/LoanDeviceReleaseResponse.js +7 -0
  21. package/bin/loanCredit/dtos/responses/LoanDeviceReleaseResult.d.ts +8 -0
  22. package/bin/loanCredit/dtos/responses/LoanDeviceReleaseResult.js +8 -0
  23. package/bin/loanCredit/index.d.ts +12 -0
  24. package/bin/loanCredit/index.js +15 -1
  25. package/bin/loanCredit/validators/IsFutureIso8601DateTime.d.ts +18 -0
  26. package/bin/loanCredit/validators/IsFutureIso8601DateTime.js +41 -0
  27. package/bin/mdm/dtos/DeviceEnrollRequest.d.ts +3 -0
  28. package/bin/mdm/dtos/DeviceEnrollRequest.js +7 -0
  29. package/bin/mdm/dtos/DeviceUnlockRequest.d.ts +2 -0
  30. package/bin/mdm/dtos/DeviceUnlockRequest.js +6 -0
  31. package/bin/mdm/enums/MdmErrorCodeEnum.d.ts +1 -0
  32. package/bin/mdm/enums/MdmErrorCodeEnum.js +1 -0
  33. package/bin/mdm/index.d.ts +1 -0
  34. package/bin/mdm/index.js +4 -0
  35. package/package.json +1 -1
  36. package/src/loanCredit/dtos/LoanDeviceNotifyItem.ts +36 -0
  37. package/src/loanCredit/dtos/LoanDeviceReleaseItem.ts +22 -0
  38. package/src/loanCredit/dtos/LoanDeviceUnlockItem.ts +17 -0
  39. package/src/loanCredit/dtos/requests/LoanDeviceManualUnlockRequest.ts +4 -4
  40. package/src/loanCredit/dtos/requests/LoanDeviceNotifyRequest.ts +23 -0
  41. package/src/loanCredit/dtos/requests/LoanDeviceReleaseRequest.ts +23 -0
  42. package/src/loanCredit/dtos/responses/LoanDeviceNotifyResponse.ts +6 -0
  43. package/src/loanCredit/dtos/responses/LoanDeviceNotifyResult.ts +10 -0
  44. package/src/loanCredit/dtos/responses/LoanDeviceReleaseResponse.ts +6 -0
  45. package/src/loanCredit/dtos/responses/LoanDeviceReleaseResult.ts +10 -0
  46. package/src/loanCredit/index.ts +13 -0
  47. package/src/loanCredit/validators/IsFutureIso8601DateTime.ts +32 -0
  48. package/src/mdm/README.md +16 -0
  49. package/src/mdm/dtos/DeviceEnrollRequest.ts +8 -1
  50. package/src/mdm/dtos/DeviceUnlockRequest.ts +7 -1
  51. package/src/mdm/enums/MdmErrorCodeEnum.ts +1 -0
  52. package/src/mdm/index.ts +3 -0
@@ -7,7 +7,17 @@ import { LoanDeviceLastSeenRequest } from '../../../../src/loanCredit/dtos/reque
7
7
  import { LoanDeviceStatusRequest } from '../../../../src/loanCredit/dtos/requests/LoanDeviceStatusRequest';
8
8
  import { LoanDeviceManualLockRequest } from '../../../../src/loanCredit/dtos/requests/LoanDeviceManualLockRequest';
9
9
  import { LoanDeviceManualUnlockRequest } from '../../../../src/loanCredit/dtos/requests/LoanDeviceManualUnlockRequest';
10
+ import { LoanDeviceNotifyRequest } from '../../../../src/loanCredit/dtos/requests/LoanDeviceNotifyRequest';
11
+ import { LoanDeviceReleaseRequest } from '../../../../src/loanCredit/dtos/requests/LoanDeviceReleaseRequest';
12
+ import { LoanDeviceNotifyResponse } from '../../../../src/loanCredit/dtos/responses/LoanDeviceNotifyResponse';
13
+ import { LoanDeviceNotifyResult } from '../../../../src/loanCredit/dtos/responses/LoanDeviceNotifyResult';
14
+ import { LoanDeviceReleaseResponse } from '../../../../src/loanCredit/dtos/responses/LoanDeviceReleaseResponse';
15
+ import { LoanDeviceReleaseResult } from '../../../../src/loanCredit/dtos/responses/LoanDeviceReleaseResult';
10
16
  import { LoanDeviceLockReasonEnum } from '../../../../src/loanCredit/enums/LoanDeviceLockReasonEnum';
17
+ import { LoanDeviceMdmStatusEnum } from '../../../../src/loanCredit/enums/LoanDeviceMdmStatusEnum';
18
+ import { DeviceNotifyIntensityEnum } from '../../../../src/mdm/enums/DeviceNotifyIntensityEnum';
19
+ import { DeviceReleaseReasonEnum } from '../../../../src/mdm/enums/DeviceReleaseReasonEnum';
20
+ import { MdmOperationStatusEnum } from '../../../../src/mdm/enums/MdmOperationStatusEnum';
11
21
  import { LOAN_DEVICE_MAX_BATCH } from '../../../../src/loanCredit/constants/LoanDeviceBatchLimits';
12
22
 
13
23
  /** ULIDs reales del motor: así es como `LoanCreditService` genera los creditId. */
@@ -34,6 +44,9 @@ const creditIdAt = (index: number): string => {
34
44
  const hasConstraint = (errors: ValidationError[], property: string, constraint: string): boolean =>
35
45
  errors.some(e => e.property === property && Boolean(e.constraints?.[constraint]));
36
46
 
47
+ /** Instante ISO-8601 a N días de ahora; evita que el test caduque con el calendario. */
48
+ const isoInDays = (days: number): string => new Date(Date.now() + days * 86_400_000).toISOString();
49
+
37
50
  const validEnrollItem = {
38
51
  creditId: CREDIT_ID,
39
52
  imei: '351756051523999',
@@ -230,6 +243,15 @@ describe('LoanDeviceManualUnlockRequest', () => {
230
243
  expect(JSON.stringify(itemsError)).toContain('creditId');
231
244
  });
232
245
 
246
+ it(`acepta exactamente ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
247
+ const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH }, (_, i) => ({
248
+ creditId: creditIdAt(i),
249
+ }));
250
+ const dto = plainToInstance(LoanDeviceManualUnlockRequest, { ...validUnlock, items });
251
+ const errors = await validate(dto);
252
+ expect(errors).toEqual([]);
253
+ });
254
+
233
255
  it(`falla si el lote supera ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
234
256
  const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH + 1 }, (_, i) => ({
235
257
  creditId: creditIdAt(i),
@@ -294,3 +316,386 @@ describe.each([
294
316
  expect(hasConstraint(errors, 'creditIds', 'arrayUnique')).toBe(true);
295
317
  });
296
318
  });
319
+
320
+ describe('LoanDeviceNotifyRequest', () => {
321
+ const validNotifyItem = {
322
+ creditId: CREDIT_ID,
323
+ intensity: DeviceNotifyIntensityEnum.BANNER,
324
+ notificationCode: 'PAYMENT_REMINDER_D1',
325
+ title: 'Tu pago está pendiente',
326
+ content: 'Ponte al corriente para seguir usando tu equipo.',
327
+ };
328
+ const validNotify = { items: [validNotifyItem], operationReference: 'notify-2026-09-02-0001' };
329
+
330
+ const notifyWithItems = (items: unknown[]) => ({ ...validNotify, items });
331
+
332
+ /** Cota de cada campo de texto libre, para probar el borde exacto y el borde + 1. */
333
+ const NOTIFY_MAX_LENGTHS: ReadonlyArray<readonly [string, number]> = [
334
+ ['notificationCode', 64],
335
+ ['title', 128],
336
+ ['content', 1024],
337
+ ];
338
+
339
+ it('valida happy path (0 errores)', async () => {
340
+ const dto = plainToInstance(LoanDeviceNotifyRequest, validNotify);
341
+ const errors = await validate(dto);
342
+ expect(errors).toEqual([]);
343
+ });
344
+
345
+ it('valida con intensity FULLSCREEN', async () => {
346
+ const items = [{ ...validNotifyItem, intensity: DeviceNotifyIntensityEnum.FULLSCREEN }];
347
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
348
+ const errors = await validate(dto);
349
+ expect(errors).toEqual([]);
350
+ });
351
+
352
+ it('falla si intensity está fuera del enum', async () => {
353
+ const items = [{ ...validNotifyItem, intensity: 'POPUP' }];
354
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
355
+ const errors = await validate(dto);
356
+ const itemsError = errors.find(e => e.property === 'items');
357
+ expect(itemsError).toBeDefined();
358
+ expect(JSON.stringify(itemsError)).toContain('intensity');
359
+ });
360
+
361
+ it.each(['notificationCode', 'title', 'content'] as const)('falla si %s viene vacío', async field => {
362
+ const items = [{ ...validNotifyItem, [field]: '' }];
363
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
364
+ const errors = await validate(dto);
365
+ const itemsError = errors.find(e => e.property === 'items');
366
+ expect(itemsError).toBeDefined();
367
+ expect(JSON.stringify(itemsError)).toContain(field);
368
+ });
369
+
370
+ it.each(['notificationCode', 'title', 'content'] as const)('falla si %s no viene', async field => {
371
+ const { [field]: _omitted, ...rest } = validNotifyItem;
372
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems([rest]));
373
+ const errors = await validate(dto);
374
+ const itemsError = errors.find(e => e.property === 'items');
375
+ expect(itemsError).toBeDefined();
376
+ expect(JSON.stringify(itemsError)).toContain(field);
377
+ });
378
+
379
+ it.each(['notificationCode', 'title', 'content'] as const)('falla si %s no es string', async field => {
380
+ const items = [{ ...validNotifyItem, [field]: 42 }];
381
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
382
+ const errors = await validate(dto);
383
+ const itemsError = errors.find(e => e.property === 'items');
384
+ expect(itemsError).toBeDefined();
385
+ expect(JSON.stringify(itemsError)).toContain(field);
386
+ });
387
+
388
+ it('falla si intensity no viene', async () => {
389
+ const { intensity: _omitted, ...rest } = validNotifyItem;
390
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems([rest]));
391
+ const errors = await validate(dto);
392
+ const itemsError = errors.find(e => e.property === 'items');
393
+ expect(itemsError).toBeDefined();
394
+ expect(JSON.stringify(itemsError)).toContain('intensity');
395
+ });
396
+
397
+ it.each(NOTIFY_MAX_LENGTHS)('acepta %s en su largo máximo de %i', async (field, max) => {
398
+ const items = [{ ...validNotifyItem, [field]: 'x'.repeat(max) }];
399
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
400
+ const errors = await validate(dto);
401
+ expect(errors).toEqual([]);
402
+ });
403
+
404
+ it.each(NOTIFY_MAX_LENGTHS)('falla si %s supera su largo máximo de %i', async (field, max) => {
405
+ const items = [{ ...validNotifyItem, [field]: 'x'.repeat(max + 1) }];
406
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
407
+ const errors = await validate(dto);
408
+ const itemsError = errors.find(e => e.property === 'items');
409
+ expect(itemsError).toBeDefined();
410
+ expect(JSON.stringify(itemsError)).toContain('maxLength');
411
+ expect(JSON.stringify(itemsError)).toContain(field);
412
+ });
413
+
414
+ it('falla si algún creditId no es un ULID', async () => {
415
+ const items = [{ ...validNotifyItem, creditId: A_UUID }];
416
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
417
+ const errors = await validate(dto);
418
+ const itemsError = errors.find(e => e.property === 'items');
419
+ expect(itemsError).toBeDefined();
420
+ expect(JSON.stringify(itemsError)).toContain('creditId');
421
+ });
422
+
423
+ it(`acepta exactamente ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
424
+ const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH }, (_, i) => ({
425
+ ...validNotifyItem,
426
+ creditId: creditIdAt(i),
427
+ }));
428
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
429
+ const errors = await validate(dto);
430
+ expect(errors).toEqual([]);
431
+ });
432
+
433
+ it(`falla si el lote supera ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
434
+ const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH + 1 }, (_, i) => ({
435
+ ...validNotifyItem,
436
+ creditId: creditIdAt(i),
437
+ }));
438
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems(items));
439
+ const errors = await validate(dto);
440
+ expect(hasConstraint(errors, 'items', 'arrayMaxSize')).toBe(true);
441
+ });
442
+
443
+ it('falla si dos ítems traen el mismo creditId', async () => {
444
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems([validNotifyItem, { ...validNotifyItem }]));
445
+ const errors = await validate(dto);
446
+ expect(hasConstraint(errors, 'items', 'arrayUnique')).toBe(true);
447
+ });
448
+
449
+ it('falla si items viene vacío', async () => {
450
+ const dto = plainToInstance(LoanDeviceNotifyRequest, notifyWithItems([]));
451
+ const errors = await validate(dto);
452
+ expect(hasConstraint(errors, 'items', 'arrayNotEmpty')).toBe(true);
453
+ });
454
+
455
+ it('falla si operationReference supera los 64 caracteres', async () => {
456
+ const dto = plainToInstance(LoanDeviceNotifyRequest, { ...validNotify, operationReference: 'x'.repeat(200) });
457
+ const errors = await validate(dto);
458
+ expect(hasConstraint(errors, 'operationReference', 'isLength')).toBe(true);
459
+ });
460
+
461
+ it('falla si operationReference viene vacío', async () => {
462
+ const dto = plainToInstance(LoanDeviceNotifyRequest, { ...validNotify, operationReference: '' });
463
+ const errors = await validate(dto);
464
+ expect(hasConstraint(errors, 'operationReference', 'isNotEmpty')).toBe(true);
465
+ });
466
+
467
+ it('falla si operationReference no viene', async () => {
468
+ const { operationReference: _omitted, ...rest } = validNotify;
469
+ const dto = plainToInstance(LoanDeviceNotifyRequest, rest);
470
+ const errors = await validate(dto);
471
+ expect(errors.some(e => e.property === 'operationReference')).toBe(true);
472
+ });
473
+ });
474
+
475
+ describe('LoanDeviceReleaseRequest', () => {
476
+ const validReleaseItem = { creditId: CREDIT_ID, reason: DeviceReleaseReasonEnum.CREDIT_PAID_OFF };
477
+ const validRelease = { items: [validReleaseItem], operationReference: 'release-2026-09-02-0001' };
478
+
479
+ const releaseWithItems = (items: unknown[]) => ({ ...validRelease, items });
480
+
481
+ it('valida happy path sin comment (0 errores)', async () => {
482
+ const dto = plainToInstance(LoanDeviceReleaseRequest, validRelease);
483
+ const errors = await validate(dto);
484
+ expect(errors).toEqual([]);
485
+ });
486
+
487
+ it('valida con comment presente', async () => {
488
+ const items = [{ ...validReleaseItem, reason: DeviceReleaseReasonEnum.OTHER, comment: 'Equipo robado y repuesto.' }];
489
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
490
+ const errors = await validate(dto);
491
+ expect(errors).toEqual([]);
492
+ });
493
+
494
+ it('falla si comment no es string', async () => {
495
+ const items = [{ ...validReleaseItem, comment: 42 }];
496
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
497
+ const errors = await validate(dto);
498
+ const itemsError = errors.find(e => e.property === 'items');
499
+ expect(itemsError).toBeDefined();
500
+ expect(JSON.stringify(itemsError)).toContain('comment');
501
+ });
502
+
503
+ it('acepta comment en su largo máximo de 500', async () => {
504
+ const items = [{ ...validReleaseItem, comment: 'x'.repeat(500) }];
505
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
506
+ const errors = await validate(dto);
507
+ expect(errors).toEqual([]);
508
+ });
509
+
510
+ it('falla si comment supera los 500 caracteres', async () => {
511
+ const items = [{ ...validReleaseItem, comment: 'x'.repeat(501) }];
512
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
513
+ const errors = await validate(dto);
514
+ const itemsError = errors.find(e => e.property === 'items');
515
+ expect(itemsError).toBeDefined();
516
+ expect(JSON.stringify(itemsError)).toContain('maxLength');
517
+ expect(JSON.stringify(itemsError)).toContain('comment');
518
+ });
519
+
520
+ it('falla si reason está fuera del enum', async () => {
521
+ const items = [{ creditId: CREDIT_ID, reason: 'PORQUE_SI' }];
522
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
523
+ const errors = await validate(dto);
524
+ const itemsError = errors.find(e => e.property === 'items');
525
+ expect(itemsError).toBeDefined();
526
+ expect(JSON.stringify(itemsError)).toContain('reason');
527
+ });
528
+
529
+ it('falla si reason no viene', async () => {
530
+ const items = [{ creditId: CREDIT_ID }];
531
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
532
+ const errors = await validate(dto);
533
+ const itemsError = errors.find(e => e.property === 'items');
534
+ expect(itemsError).toBeDefined();
535
+ expect(JSON.stringify(itemsError)).toContain('reason');
536
+ });
537
+
538
+ it('falla si algún creditId no es un ULID', async () => {
539
+ const items = [{ ...validReleaseItem, creditId: A_UUID }];
540
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
541
+ const errors = await validate(dto);
542
+ const itemsError = errors.find(e => e.property === 'items');
543
+ expect(itemsError).toBeDefined();
544
+ expect(JSON.stringify(itemsError)).toContain('creditId');
545
+ });
546
+
547
+ it(`acepta exactamente ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
548
+ const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH }, (_, i) => ({
549
+ ...validReleaseItem,
550
+ creditId: creditIdAt(i),
551
+ }));
552
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
553
+ const errors = await validate(dto);
554
+ expect(errors).toEqual([]);
555
+ });
556
+
557
+ it(`falla si el lote supera ${LOAN_DEVICE_MAX_BATCH} ítems`, async () => {
558
+ const items = Array.from({ length: LOAN_DEVICE_MAX_BATCH + 1 }, (_, i) => ({
559
+ ...validReleaseItem,
560
+ creditId: creditIdAt(i),
561
+ }));
562
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems(items));
563
+ const errors = await validate(dto);
564
+ expect(hasConstraint(errors, 'items', 'arrayMaxSize')).toBe(true);
565
+ });
566
+
567
+ it('falla si dos ítems traen el mismo creditId', async () => {
568
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems([validReleaseItem, { ...validReleaseItem }]));
569
+ const errors = await validate(dto);
570
+ expect(hasConstraint(errors, 'items', 'arrayUnique')).toBe(true);
571
+ });
572
+
573
+ it('falla si items viene vacío', async () => {
574
+ const dto = plainToInstance(LoanDeviceReleaseRequest, releaseWithItems([]));
575
+ const errors = await validate(dto);
576
+ expect(hasConstraint(errors, 'items', 'arrayNotEmpty')).toBe(true);
577
+ });
578
+
579
+ it('falla si operationReference supera los 64 caracteres', async () => {
580
+ const dto = plainToInstance(LoanDeviceReleaseRequest, { ...validRelease, operationReference: 'x'.repeat(200) });
581
+ const errors = await validate(dto);
582
+ expect(hasConstraint(errors, 'operationReference', 'isLength')).toBe(true);
583
+ });
584
+
585
+ it('falla si operationReference viene vacío', async () => {
586
+ const dto = plainToInstance(LoanDeviceReleaseRequest, { ...validRelease, operationReference: '' });
587
+ const errors = await validate(dto);
588
+ expect(hasConstraint(errors, 'operationReference', 'isNotEmpty')).toBe(true);
589
+ });
590
+
591
+ it('falla si operationReference no viene', async () => {
592
+ const { operationReference: _omitted, ...rest } = validRelease;
593
+ const dto = plainToInstance(LoanDeviceReleaseRequest, rest);
594
+ const errors = await validate(dto);
595
+ expect(errors.some(e => e.property === 'operationReference')).toBe(true);
596
+ });
597
+ });
598
+
599
+ describe('LoanDeviceManualUnlockRequest — unlockUntil', () => {
600
+ const unlockWith = (item: Record<string, unknown>) => ({
601
+ items: [item],
602
+ operationReference: 'unlock-2026-09-02-0001',
603
+ });
604
+
605
+ /** Falla y el error apunta a `unlockUntil`. */
606
+ const expectUnlockUntilRejected = async (unlockUntil: unknown): Promise<void> => {
607
+ const dto = plainToInstance(LoanDeviceManualUnlockRequest, unlockWith({ creditId: CREDIT_ID, unlockUntil }));
608
+ const errors = await validate(dto);
609
+ const itemsError = errors.find(e => e.property === 'items');
610
+ expect(itemsError).toBeDefined();
611
+ expect(JSON.stringify(itemsError)).toContain('unlockUntil');
612
+ };
613
+
614
+ it('valida con unlockUntil datetime futuro', async () => {
615
+ const dto = plainToInstance(LoanDeviceManualUnlockRequest, unlockWith({
616
+ creditId: CREDIT_ID,
617
+ unlockUntil: isoInDays(30),
618
+ }));
619
+ const errors = await validate(dto);
620
+ expect(errors).toEqual([]);
621
+ });
622
+
623
+ it('falla si unlockUntil no es ISO-8601', async () => {
624
+ await expectUnlockUntilRejected('30/09/2026');
625
+ });
626
+
627
+ it('falla si unlockUntil es una fecha pasada', async () => {
628
+ await expectUnlockUntilRejected(isoInDays(-1));
629
+ });
630
+
631
+ it('falla si unlockUntil es solo fecha, sin hora', async () => {
632
+ await expectUnlockUntilRejected('2099-09-30');
633
+ });
634
+
635
+ it('falla si unlockUntil no trae zona horaria', async () => {
636
+ await expectUnlockUntilRejected('2099-09-30T23:59:59');
637
+ });
638
+
639
+ it('falla si unlockUntil no es string', async () => {
640
+ await expectUnlockUntilRejected(42);
641
+ });
642
+
643
+ it('sigue aceptando ítems sin unlockUntil (el campo es opcional)', async () => {
644
+ const dto = plainToInstance(LoanDeviceManualUnlockRequest, unlockWith({ creditId: CREDIT_ID }));
645
+ const errors = await validate(dto);
646
+ expect(errors).toEqual([]);
647
+ });
648
+ });
649
+
650
+ describe('responses de notify y release', () => {
651
+ it('LoanDeviceNotifyResponse arma un resultado por ítem, con la referencia del batch', () => {
652
+ const response: LoanDeviceNotifyResponse = {
653
+ results: [
654
+ {
655
+ creditId: CREDIT_ID,
656
+ status: MdmOperationStatusEnum.SUCCESS,
657
+ operationReference: 'notify-2026-09-02-0001',
658
+ mdmStatus: LoanDeviceMdmStatusEnum.LOCKED,
659
+ notifiedAt: isoInDays(0),
660
+ },
661
+ ],
662
+ };
663
+ expect(response.results[0].notifiedAt).toBeDefined();
664
+ expect(response.results[0].operationReference).toBe('notify-2026-09-02-0001');
665
+ });
666
+
667
+ it('LoanDeviceNotifyResult deja notifiedAt opcional: un ítem puede fallar sin instante', () => {
668
+ const failed: LoanDeviceNotifyResult = {
669
+ creditId: CREDIT_ID,
670
+ status: MdmOperationStatusEnum.ERROR,
671
+ operationReference: 'notify-2026-09-02-0002',
672
+ mdmStatus: LoanDeviceMdmStatusEnum.ACTIVE,
673
+ };
674
+ expect(failed.notifiedAt).toBeUndefined();
675
+ });
676
+
677
+ it('LoanDeviceReleaseResponse reporta el expediente ya liberado', () => {
678
+ const response: LoanDeviceReleaseResponse = {
679
+ results: [
680
+ {
681
+ creditId: CREDIT_ID,
682
+ status: MdmOperationStatusEnum.SUCCESS,
683
+ operationReference: 'release-2026-09-02-0001',
684
+ mdmStatus: LoanDeviceMdmStatusEnum.RELEASED,
685
+ releasedAt: isoInDays(0),
686
+ },
687
+ ],
688
+ };
689
+ expect(response.results[0].mdmStatus).toBe(LoanDeviceMdmStatusEnum.RELEASED);
690
+ });
691
+
692
+ it('LoanDeviceReleaseResult deja releasedAt opcional: un ítem puede fallar sin instante', () => {
693
+ const failed: LoanDeviceReleaseResult = {
694
+ creditId: CREDIT_ID,
695
+ status: MdmOperationStatusEnum.ERROR,
696
+ operationReference: 'release-2026-09-02-0002',
697
+ mdmStatus: LoanDeviceMdmStatusEnum.ACTIVE,
698
+ };
699
+ expect(failed.releasedAt).toBeUndefined();
700
+ });
701
+ });
@@ -0,0 +1,83 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate, ValidationError } from 'class-validator';
4
+ import { DeviceEnrollRequest } from '../../../../src/mdm/dtos/DeviceEnrollRequest';
5
+ import { DeviceUnlockRequest } from '../../../../src/mdm/dtos/DeviceUnlockRequest';
6
+ import { MdmErrorCodeEnum } from '../../../../src/mdm/enums/MdmErrorCodeEnum';
7
+ import * as MdmBarrel from '../../../../src/mdm/index';
8
+ import { MdmLockModeEnum } from '../../../../src/retailCatalog/enums/MdmLockModeEnum';
9
+
10
+ /** ULID real del motor: así genera los creditId `loan-credit-business`. */
11
+ const CREDIT_ID = '01M0B5D7FQA3T06K4REEVPRM33';
12
+
13
+ /** True si el error de `property` trae ese constraint puntual (no solo "algo falló"). */
14
+ const hasConstraint = (errors: ValidationError[], property: string, constraint: string): boolean =>
15
+ errors.some(e => e.property === property && Boolean(e.constraints?.[constraint]));
16
+
17
+ /** Payload de enrolamiento tal como lo manda hoy un consumidor ya desplegado. */
18
+ const validEnroll = {
19
+ imei: '351756051523999',
20
+ creditId: CREDIT_ID,
21
+ termMonths: 6,
22
+ operationReference: 'enroll-2026-09-01-0001',
23
+ };
24
+
25
+ const validUnlock = {
26
+ imei: '351756051523999',
27
+ operationReference: 'unlock-2026-09-01-0001',
28
+ };
29
+
30
+ describe('DeviceEnrollRequest.lockMode', () => {
31
+ it('valida sin lockMode: el consumidor de hoy no se rompe', async () => {
32
+ const dto = plainToInstance(DeviceEnrollRequest, validEnroll);
33
+ const errors = await validate(dto);
34
+ expect(errors).toEqual([]);
35
+ expect(dto.lockMode).toBeUndefined();
36
+ });
37
+
38
+ it.each([MdmLockModeEnum.ONDEMAND, MdmLockModeEnum.SCHEDULED])('valida con lockMode %s', async lockMode => {
39
+ const dto = plainToInstance(DeviceEnrollRequest, { ...validEnroll, lockMode });
40
+ const errors = await validate(dto);
41
+ expect(errors).toEqual([]);
42
+ expect(dto.lockMode).toBe(lockMode);
43
+ });
44
+
45
+ it('falla si lockMode está fuera del enum', async () => {
46
+ const dto = plainToInstance(DeviceEnrollRequest, { ...validEnroll, lockMode: 'PREPAGO' });
47
+ const errors = await validate(dto);
48
+ expect(hasConstraint(errors, 'lockMode', 'isEnum')).toBe(true);
49
+ });
50
+ });
51
+
52
+ describe('DeviceUnlockRequest.unlockUntil', () => {
53
+ it('valida sin unlockUntil: el consumidor de hoy no se rompe', async () => {
54
+ const dto = plainToInstance(DeviceUnlockRequest, validUnlock);
55
+ const errors = await validate(dto);
56
+ expect(errors).toEqual([]);
57
+ expect(dto.unlockUntil).toBeUndefined();
58
+ });
59
+
60
+ it('valida con unlockUntil en ISO-8601', async () => {
61
+ const dto = plainToInstance(DeviceUnlockRequest, { ...validUnlock, unlockUntil: '2026-10-01T00:00:00.000Z' });
62
+ const errors = await validate(dto);
63
+ expect(errors).toEqual([]);
64
+ });
65
+
66
+ it('falla si unlockUntil no es ISO-8601', async () => {
67
+ const dto = plainToInstance(DeviceUnlockRequest, { ...validUnlock, unlockUntil: '01/10/2026' });
68
+ const errors = await validate(dto);
69
+ expect(hasConstraint(errors, 'unlockUntil', 'isIso8601')).toBe(true);
70
+ });
71
+ });
72
+
73
+ describe('MdmErrorCodeEnum', () => {
74
+ it('expone el código de desbloqueo sin vigencia sobre un equipo SCHEDULED', () => {
75
+ expect(MdmErrorCodeEnum.UNLOCK_UNTIL_REQUIRED).toBe('UNLOCK_UNTIL_REQUIRED');
76
+ });
77
+ });
78
+
79
+ describe('barrel de mdm', () => {
80
+ it('reexporta MdmLockModeEnum sin duplicar el enum de retailCatalog', () => {
81
+ expect(MdmBarrel.MdmLockModeEnum).toBe(MdmLockModeEnum);
82
+ });
83
+ });
@@ -0,0 +1,12 @@
1
+ import { DeviceNotifyIntensityEnum } from '../../mdm/enums/DeviceNotifyIntensityEnum';
2
+ /**
3
+ * Ítem de notificación: a qué crédito se le avisa y con qué texto.
4
+ * Los tres campos de texto son obligatorios: Datacultr usa el código y Trustonic el texto crudo.
5
+ */
6
+ export declare class LoanDeviceNotifyItem {
7
+ creditId: string;
8
+ intensity: DeviceNotifyIntensityEnum;
9
+ notificationCode: string;
10
+ title: string;
11
+ content: string;
12
+ }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LoanDeviceNotifyItem = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const DeviceNotifyIntensityEnum_1 = require("../../mdm/enums/DeviceNotifyIntensityEnum");
16
+ const CreditIdPattern_1 = require("../constants/CreditIdPattern");
17
+ /**
18
+ * Ítem de notificación: a qué crédito se le avisa y con qué texto.
19
+ * Los tres campos de texto son obligatorios: Datacultr usa el código y Trustonic el texto crudo.
20
+ */
21
+ class LoanDeviceNotifyItem {
22
+ }
23
+ exports.LoanDeviceNotifyItem = LoanDeviceNotifyItem;
24
+ __decorate([
25
+ (0, class_transformer_1.Expose)(),
26
+ (0, class_validator_1.Matches)(CreditIdPattern_1.CREDIT_ID_PATTERN, { message: CreditIdPattern_1.CREDIT_ID_MESSAGE }),
27
+ __metadata("design:type", String)
28
+ ], LoanDeviceNotifyItem.prototype, "creditId", void 0);
29
+ __decorate([
30
+ (0, class_transformer_1.Expose)(),
31
+ (0, class_validator_1.IsEnum)(DeviceNotifyIntensityEnum_1.DeviceNotifyIntensityEnum),
32
+ __metadata("design:type", String)
33
+ ], LoanDeviceNotifyItem.prototype, "intensity", void 0);
34
+ __decorate([
35
+ (0, class_transformer_1.Expose)(),
36
+ (0, class_validator_1.IsString)(),
37
+ (0, class_validator_1.IsNotEmpty)(),
38
+ (0, class_validator_1.MaxLength)(64),
39
+ __metadata("design:type", String)
40
+ ], LoanDeviceNotifyItem.prototype, "notificationCode", void 0);
41
+ __decorate([
42
+ (0, class_transformer_1.Expose)(),
43
+ (0, class_validator_1.IsString)(),
44
+ (0, class_validator_1.IsNotEmpty)(),
45
+ (0, class_validator_1.MaxLength)(128),
46
+ __metadata("design:type", String)
47
+ ], LoanDeviceNotifyItem.prototype, "title", void 0);
48
+ __decorate([
49
+ (0, class_transformer_1.Expose)(),
50
+ (0, class_validator_1.IsString)(),
51
+ (0, class_validator_1.IsNotEmpty)(),
52
+ (0, class_validator_1.MaxLength)(1024),
53
+ __metadata("design:type", String)
54
+ ], LoanDeviceNotifyItem.prototype, "content", void 0);
@@ -0,0 +1,8 @@
1
+ import { DeviceReleaseReasonEnum } from '../../mdm/enums/DeviceReleaseReasonEnum';
2
+ /** Ítem de liberación definitiva: qué crédito se libera del MDM y por qué. */
3
+ export declare class LoanDeviceReleaseItem {
4
+ creditId: string;
5
+ reason: DeviceReleaseReasonEnum;
6
+ /** Nota libre del operador; queda en el expediente del dispositivo. */
7
+ comment?: string;
8
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LoanDeviceReleaseItem = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const DeviceReleaseReasonEnum_1 = require("../../mdm/enums/DeviceReleaseReasonEnum");
16
+ const CreditIdPattern_1 = require("../constants/CreditIdPattern");
17
+ /** Ítem de liberación definitiva: qué crédito se libera del MDM y por qué. */
18
+ class LoanDeviceReleaseItem {
19
+ }
20
+ exports.LoanDeviceReleaseItem = LoanDeviceReleaseItem;
21
+ __decorate([
22
+ (0, class_transformer_1.Expose)(),
23
+ (0, class_validator_1.Matches)(CreditIdPattern_1.CREDIT_ID_PATTERN, { message: CreditIdPattern_1.CREDIT_ID_MESSAGE }),
24
+ __metadata("design:type", String)
25
+ ], LoanDeviceReleaseItem.prototype, "creditId", void 0);
26
+ __decorate([
27
+ (0, class_transformer_1.Expose)(),
28
+ (0, class_validator_1.IsEnum)(DeviceReleaseReasonEnum_1.DeviceReleaseReasonEnum),
29
+ __metadata("design:type", String)
30
+ ], LoanDeviceReleaseItem.prototype, "reason", void 0);
31
+ __decorate([
32
+ (0, class_transformer_1.Expose)(),
33
+ (0, class_validator_1.IsString)(),
34
+ (0, class_validator_1.IsOptional)(),
35
+ (0, class_validator_1.MaxLength)(500),
36
+ __metadata("design:type", String)
37
+ ], LoanDeviceReleaseItem.prototype, "comment", void 0);
@@ -0,0 +1,6 @@
1
+ /** Ítem de desbloqueo: qué crédito se desbloquea y, opcionalmente, hasta cuándo. */
2
+ export declare class LoanDeviceUnlockItem {
3
+ creditId: string;
4
+ /** Datetime ISO-8601 futuro de hasta cuándo vale el desbloqueo; solo aplica en modo SCHEDULED. */
5
+ unlockUntil?: string;
6
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LoanDeviceUnlockItem = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ const CreditIdPattern_1 = require("../constants/CreditIdPattern");
16
+ const IsFutureIso8601DateTime_1 = require("../validators/IsFutureIso8601DateTime");
17
+ /** Ítem de desbloqueo: qué crédito se desbloquea y, opcionalmente, hasta cuándo. */
18
+ class LoanDeviceUnlockItem {
19
+ }
20
+ exports.LoanDeviceUnlockItem = LoanDeviceUnlockItem;
21
+ __decorate([
22
+ (0, class_transformer_1.Expose)(),
23
+ (0, class_validator_1.Matches)(CreditIdPattern_1.CREDIT_ID_PATTERN, { message: CreditIdPattern_1.CREDIT_ID_MESSAGE }),
24
+ __metadata("design:type", String)
25
+ ], LoanDeviceUnlockItem.prototype, "creditId", void 0);
26
+ __decorate([
27
+ (0, class_transformer_1.Expose)(),
28
+ (0, class_validator_1.Validate)(IsFutureIso8601DateTime_1.IsFutureIso8601DateTime),
29
+ (0, class_validator_1.IsOptional)(),
30
+ __metadata("design:type", String)
31
+ ], LoanDeviceUnlockItem.prototype, "unlockUntil", void 0);