@fiado/type-kit 3.552.0 → 3.554.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 (93) hide show
  1. package/_test_/unit/common/InputPatternValidators.test.ts +64 -0
  2. package/_test_/unit/common/InputPatterns.test.ts +104 -0
  3. package/_test_/unit/retailCatalog/backofficeRequests.test.ts +1 -1
  4. package/_test_/unit/retailOrg/dtos/requests/inputPatternRequests.test.ts +93 -0
  5. package/_test_/unit/retailWizard/accessorySaleRequests.test.ts +4 -3
  6. package/_test_/unit/retailWizard/fixtures/saleWithoutCustomer.strict.ts +3 -0
  7. package/_test_/unit/retailWizard/saleWithoutCustomer.test.ts +32 -0
  8. package/bin/common/constants/InputPatterns.d.ts +25 -0
  9. package/bin/common/constants/InputPatterns.js +28 -0
  10. package/bin/common/decorators/UpperCase.d.ts +2 -0
  11. package/bin/common/decorators/UpperCase.js +7 -0
  12. package/bin/common/helpers/imeiChecksum.d.ts +2 -0
  13. package/bin/common/helpers/imeiChecksum.js +20 -0
  14. package/bin/common/index.d.ts +8 -0
  15. package/bin/common/index.js +10 -0
  16. package/bin/common/validators/IsMxPhone.d.ts +3 -0
  17. package/bin/common/validators/IsMxPhone.js +8 -0
  18. package/bin/common/validators/IsMxZipCode.d.ts +3 -0
  19. package/bin/common/validators/IsMxZipCode.js +8 -0
  20. package/bin/common/validators/IsRfc.d.ts +3 -0
  21. package/bin/common/validators/IsRfc.js +8 -0
  22. package/bin/common/validators/IsSku.d.ts +3 -0
  23. package/bin/common/validators/IsSku.js +8 -0
  24. package/bin/common/validators/matchesInputPattern.d.ts +3 -0
  25. package/bin/common/validators/matchesInputPattern.js +16 -0
  26. package/bin/helpers/constans/regex.js +2 -1
  27. package/bin/loanCredit/dtos/LoanContractDetails.js +2 -2
  28. package/bin/loanCredit/dtos/LoanDeviceEnrollItem.js +4 -4
  29. package/bin/retailCatalog/constants/SkuPattern.d.ts +2 -2
  30. package/bin/retailCatalog/constants/SkuPattern.js +5 -3
  31. package/bin/retailCatalog/dtos/ProductComponent.js +1 -1
  32. package/bin/retailCatalog/dtos/requests/BackofficeCreateKitRequest.js +1 -1
  33. package/bin/retailCatalog/dtos/requests/BackofficeCreateProductRequest.js +1 -1
  34. package/bin/retailCatalog/dtos/requests/BackofficeCreateServiceRequest.js +1 -1
  35. package/bin/retailCatalog/dtos/requests/BackofficeCreateTransferRequest.js +1 -1
  36. package/bin/retailCatalog/dtos/requests/BackofficeRegisterAdjustmentRequest.js +1 -1
  37. package/bin/retailCatalog/dtos/requests/BackofficeRegisterEntryRequest.js +1 -1
  38. package/bin/retailCatalog/dtos/requests/CreateProductRequest.js +1 -1
  39. package/bin/retailCatalog/dtos/requests/ReturnPieceCondition.js +1 -1
  40. package/bin/retailCatalog/dtos/requests/SyncInventoryRequest.js +1 -1
  41. package/bin/retailIntegration/constants/retailIntegrationLimits.d.ts +1 -1
  42. package/bin/retailIntegration/constants/retailIntegrationLimits.js +2 -2
  43. package/bin/retailOrg/dtos/requests/CreateRetailUserRequest.js +2 -1
  44. package/bin/retailOrg/dtos/requests/CreateRetailerRequest.d.ts +1 -1
  45. package/bin/retailOrg/dtos/requests/CreateRetailerRequest.js +6 -2
  46. package/bin/retailOrg/dtos/requests/CreateStoreRequest.js +2 -1
  47. package/bin/retailOrg/dtos/requests/CreateSubdistributorRequest.d.ts +1 -1
  48. package/bin/retailOrg/dtos/requests/CreateSubdistributorRequest.js +6 -2
  49. package/bin/retailOrg/dtos/requests/ProvisionRetailUserRequest.js +2 -1
  50. package/bin/retailOrg/dtos/requests/RetailAddressInput.d.ts +1 -1
  51. package/bin/retailOrg/dtos/requests/RetailAddressInput.js +2 -2
  52. package/bin/retailOrg/dtos/requests/UpdateRetailUserRequest.js +2 -1
  53. package/bin/retailOrg/dtos/requests/UpdateRetailerRequest.js +2 -1
  54. package/bin/retailOrg/dtos/requests/UpdateStoreRequest.js +2 -1
  55. package/bin/retailWizard/dtos/Sale.d.ts +2 -1
  56. package/bin/retailWizard/dtos/requests/CreateAccessorySaleRequest.d.ts +1 -2
  57. package/bin/retailWizard/dtos/requests/CreateAccessorySaleRequest.js +1 -7
  58. package/package.json +1 -1
  59. package/src/common/constants/InputPatterns.ts +32 -0
  60. package/src/common/decorators/UpperCase.ts +5 -0
  61. package/src/common/helpers/imeiChecksum.ts +16 -0
  62. package/src/common/index.ts +12 -0
  63. package/src/common/validators/IsMxPhone.ts +7 -0
  64. package/src/common/validators/IsMxZipCode.ts +7 -0
  65. package/src/common/validators/IsRfc.ts +7 -0
  66. package/src/common/validators/IsSku.ts +7 -0
  67. package/src/common/validators/matchesInputPattern.ts +24 -0
  68. package/src/helpers/constans/regex.ts +2 -1
  69. package/src/loanCredit/dtos/LoanContractDetails.ts +1 -1
  70. package/src/loanCredit/dtos/LoanDeviceEnrollItem.ts +3 -3
  71. package/src/retailCatalog/constants/SkuPattern.ts +2 -2
  72. package/src/retailCatalog/dtos/ProductComponent.ts +2 -2
  73. package/src/retailCatalog/dtos/requests/BackofficeCreateKitRequest.ts +2 -2
  74. package/src/retailCatalog/dtos/requests/BackofficeCreateProductRequest.ts +2 -2
  75. package/src/retailCatalog/dtos/requests/BackofficeCreateServiceRequest.ts +2 -2
  76. package/src/retailCatalog/dtos/requests/BackofficeCreateTransferRequest.ts +2 -2
  77. package/src/retailCatalog/dtos/requests/BackofficeRegisterAdjustmentRequest.ts +2 -2
  78. package/src/retailCatalog/dtos/requests/BackofficeRegisterEntryRequest.ts +2 -2
  79. package/src/retailCatalog/dtos/requests/CreateProductRequest.ts +2 -2
  80. package/src/retailCatalog/dtos/requests/ReturnPieceCondition.ts +2 -2
  81. package/src/retailCatalog/dtos/requests/SyncInventoryRequest.ts +2 -2
  82. package/src/retailIntegration/constants/retailIntegrationLimits.ts +2 -2
  83. package/src/retailOrg/dtos/requests/CreateRetailUserRequest.ts +2 -1
  84. package/src/retailOrg/dtos/requests/CreateRetailerRequest.ts +8 -4
  85. package/src/retailOrg/dtos/requests/CreateStoreRequest.ts +2 -1
  86. package/src/retailOrg/dtos/requests/CreateSubdistributorRequest.ts +8 -4
  87. package/src/retailOrg/dtos/requests/ProvisionRetailUserRequest.ts +2 -1
  88. package/src/retailOrg/dtos/requests/RetailAddressInput.ts +3 -3
  89. package/src/retailOrg/dtos/requests/UpdateRetailUserRequest.ts +2 -1
  90. package/src/retailOrg/dtos/requests/UpdateRetailerRequest.ts +2 -1
  91. package/src/retailOrg/dtos/requests/UpdateStoreRequest.ts +2 -1
  92. package/src/retailWizard/dtos/Sale.ts +2 -1
  93. package/src/retailWizard/dtos/requests/CreateAccessorySaleRequest.ts +1 -6
@@ -0,0 +1,64 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { IsOptional, validateSync } from 'class-validator';
4
+ import { IsMxPhone, IsMxZipCode, IsRfc, IsSku, UpperCase } from '../../../src/common';
5
+
6
+ class Sample {
7
+ @IsOptional()
8
+ @IsMxPhone()
9
+ phone?: string;
10
+
11
+ @IsOptional()
12
+ @IsMxZipCode()
13
+ zipCode?: string;
14
+
15
+ @IsOptional()
16
+ @UpperCase()
17
+ @IsRfc()
18
+ rfc?: string;
19
+
20
+ @IsOptional()
21
+ @IsSku()
22
+ sku?: string;
23
+
24
+ @IsOptional()
25
+ @IsSku({ each: true })
26
+ skus?: string[];
27
+ }
28
+
29
+ const errorsOf = (plain: Record<string, unknown>): string[] =>
30
+ validateSync(plainToInstance(Sample, plain)).map((e) => e.property);
31
+
32
+ describe('Decoradores de InputPatterns', () => {
33
+ it('acepta un objeto vacío: los campos opcionales solo se validan si vienen', () => {
34
+ expect(errorsOf({})).toEqual([]);
35
+ });
36
+
37
+ it('acepta valores válidos', () => {
38
+ expect(
39
+ errorsOf({ phone: '+525512345678', zipCode: '01000', rfc: 'GODE561231GR8', sku: 'SAMSUNG A15', skus: ['A', 'B C'] }),
40
+ ).toEqual([]);
41
+ expect(errorsOf({ phone: '5512345678' })).toEqual([]);
42
+ });
43
+
44
+ it('rechaza cada campo inválido', () => {
45
+ expect(errorsOf({ phone: '55123' })).toEqual(['phone']);
46
+ expect(errorsOf({ zipCode: '6400' })).toEqual(['zipCode']);
47
+ expect(errorsOf({ rfc: 'XYZ' })).toEqual(['rfc']);
48
+ expect(errorsOf({ sku: ' A' })).toEqual(['sku']);
49
+ expect(errorsOf({ sku: 'A'.repeat(65) })).toEqual(['sku']);
50
+ expect(errorsOf({ skus: ['A', 'B '] })).toEqual(['skus']);
51
+ expect(errorsOf({ phone: 5512345678 })).toEqual(['phone']);
52
+ });
53
+
54
+ it('UpperCase normaliza el RFC antes de validarlo', () => {
55
+ const dto = plainToInstance(Sample, { rfc: 'gode561231gr8' });
56
+ expect(dto.rfc).toBe('GODE561231GR8');
57
+ expect(validateSync(dto)).toEqual([]);
58
+ });
59
+
60
+ it('mensajes en español', () => {
61
+ const [error] = validateSync(plainToInstance(Sample, { phone: '1' }));
62
+ expect(Object.values(error.constraints ?? {})[0]).toMatch(/10 dígitos/);
63
+ });
64
+ });
@@ -0,0 +1,104 @@
1
+ import 'reflect-metadata';
2
+ import {
3
+ EMAIL_MAX_LENGTH,
4
+ EMAIL_PATTERN,
5
+ HTTPS_URL_PATTERN,
6
+ IMEI_PATTERN,
7
+ PHONE_MX_ACCEPTED_PATTERN,
8
+ PHONE_MX_PATTERN,
9
+ RFC_PATTERN,
10
+ SKU_MAX_LENGTH,
11
+ SKU_PATTERN,
12
+ ZIP_CODE_MX_PATTERN,
13
+ isValidImei,
14
+ } from '../../../src/common';
15
+ import { SKU_PATTERN as CATALOG_SKU_PATTERN } from '../../../src/retailCatalog/constants/SkuPattern';
16
+ import { RETAIL_SKU_PATTERN } from '../../../src/retailIntegration/constants/retailIntegrationLimits';
17
+ import { regexPhoneMx } from '../../../src/helpers/constans/regex';
18
+
19
+ describe('InputPatterns', () => {
20
+ describe('PHONE_MX_PATTERN (UI, 10 dígitos)', () => {
21
+ it.each(['5512345678', '0000000000'])('acepta %s', (v) => expect(PHONE_MX_PATTERN.test(v)).toBe(true));
22
+ it.each(['+525512345678', '551234567', '55123456789', '55-1234-5678', '55 1234 5678', 'abcdefghij', ''])(
23
+ 'rechaza %s',
24
+ (v) => expect(PHONE_MX_PATTERN.test(v)).toBe(false),
25
+ );
26
+ it('helpers/regex reexporta el mismo patrón', () => expect(regexPhoneMx).toBe(PHONE_MX_PATTERN));
27
+ });
28
+
29
+ describe('PHONE_MX_ACCEPTED_PATTERN (backend, admite +52)', () => {
30
+ it.each(['5512345678', '+525512345678'])('acepta %s', (v) => expect(PHONE_MX_ACCEPTED_PATTERN.test(v)).toBe(true));
31
+ it.each(['525512345678', '+15512345678', '+52551234567', '+52 5512345678', '+5255123456789', ''])(
32
+ 'rechaza %s',
33
+ (v) => expect(PHONE_MX_ACCEPTED_PATTERN.test(v)).toBe(false),
34
+ );
35
+ });
36
+
37
+ describe('EMAIL_PATTERN', () => {
38
+ it.each(['ana@somosfiado.com', 'a.b+tag@sub.dominio.mx'])('acepta %s', (v) => expect(EMAIL_PATTERN.test(v)).toBe(true));
39
+ it.each(['ana', 'ana@', '@fiado.com', 'ana@fiado', 'ana @fiado.com', 'ana@@fiado.com', ''])(
40
+ 'rechaza %s',
41
+ (v) => expect(EMAIL_PATTERN.test(v)).toBe(false),
42
+ );
43
+ it('el tope es 254', () => expect(EMAIL_MAX_LENGTH).toBe(254));
44
+ });
45
+
46
+ describe('RFC_PATTERN', () => {
47
+ it.each(['ABC010203XY1', 'Ñ&A010203AB9', 'GODE561231GR8', 'ÑAAB800101A12'])('acepta %s', (v) =>
48
+ expect(RFC_PATTERN.test(v)).toBe(true),
49
+ );
50
+ it.each(['abc010203xy1', 'AB010203XY1', 'ABCDE010203XY1', 'ABC01020XY1', 'ABC010203XY', 'GODE561231GR8X', 'GODE 561231GR8', ''])(
51
+ 'rechaza %s',
52
+ (v) => expect(RFC_PATTERN.test(v)).toBe(false),
53
+ );
54
+ it('12 caracteres es persona moral y 13 persona física', () => {
55
+ expect('ABC010203XY1').toHaveLength(12);
56
+ expect('GODE561231GR8').toHaveLength(13);
57
+ });
58
+ });
59
+
60
+ describe('ZIP_CODE_MX_PATTERN', () => {
61
+ it.each(['01000', '64000'])('acepta %s', (v) => expect(ZIP_CODE_MX_PATTERN.test(v)).toBe(true));
62
+ it.each(['1000', '640000', '6400A', '64 000', ''])('rechaza %s', (v) => expect(ZIP_CODE_MX_PATTERN.test(v)).toBe(false));
63
+ });
64
+
65
+ describe('SKU_PATTERN', () => {
66
+ it.each(['IPH15-128-NEG', 'abc123', 'SAMSUNG A15 128GB', 'KIT-1 FUNDA 2', 'A B'])('acepta %p', (v) =>
67
+ expect(SKU_PATTERN.test(v)).toBe(true),
68
+ );
69
+ it.each([' SKU', 'SKU ', ' ', '', 'SKU_1', 'SKU#1', 'SKU\t1', 'SKU\n1', 'SKÚ'])('rechaza %p', (v) =>
70
+ expect(SKU_PATTERN.test(v)).toBe(false),
71
+ );
72
+ it('el tope es 64', () => expect(SKU_MAX_LENGTH).toBe(64));
73
+ it('retailCatalog reexporta el mismo patrón', () => expect(CATALOG_SKU_PATTERN).toBe(SKU_PATTERN));
74
+ });
75
+
76
+ describe('RETAIL_SKU_PATTERN (webhook, 1 a 64)', () => {
77
+ it.each(['SAMSUNG A15 128GB', 'A'.repeat(64), 'A B'])('acepta %p', (v) => expect(RETAIL_SKU_PATTERN.test(v)).toBe(true));
78
+ it.each(['A'.repeat(65), ' A', 'A ', '', 'A_B'])('rechaza %p', (v) => expect(RETAIL_SKU_PATTERN.test(v)).toBe(false));
79
+ });
80
+
81
+ describe('IMEI', () => {
82
+ it('IMEI_PATTERN exige 15 dígitos', () => {
83
+ expect(IMEI_PATTERN.test('490154203237518')).toBe(true);
84
+ expect(IMEI_PATTERN.test('49015420323751')).toBe(false);
85
+ expect(IMEI_PATTERN.test('4901542032375180')).toBe(false);
86
+ expect(IMEI_PATTERN.test('49015420323751A')).toBe(false);
87
+ });
88
+ it('isValidImei aplica Luhn', () => {
89
+ expect(isValidImei('490154203237518')).toBe(true);
90
+ expect(isValidImei('490154203237519')).toBe(false);
91
+ expect(isValidImei('49015420323751')).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe('HTTPS_URL_PATTERN', () => {
96
+ it.each(['https://cdn.fiado.com/img/1.png', 'https://fiado.com', 'https://a.b/c?d=1#e'])('acepta %s', (v) =>
97
+ expect(HTTPS_URL_PATTERN.test(v)).toBe(true),
98
+ );
99
+ it.each(['http://fiado.com', 'ftp://fiado.com', 'https://', 'https://fiado com', 'fiado.com', 'javascript:alert(1)', ''])(
100
+ 'rechaza %s',
101
+ (v) => expect(HTTPS_URL_PATTERN.test(v)).toBe(false),
102
+ );
103
+ });
104
+ });
@@ -74,7 +74,7 @@ describe('BackofficeCreateKitRequest (backofficeCreateKit)', () => {
74
74
  });
75
75
  it('rechaza un kit sin componentes', () => expect(errorsOf(BackofficeCreateKitRequest, { ...ok, components: [] })).toContain('components'));
76
76
  it('valida cada componente', () => {
77
- const errors = errorsOf(BackofficeCreateKitRequest, { ...ok, components: [{ sku: 'SIM 1', qty: 0 }] });
77
+ const errors = errorsOf(BackofficeCreateKitRequest, { ...ok, components: [{ sku: 'SIM 1 ', qty: 0 }] });
78
78
  expect(errors).toEqual(expect.arrayContaining(['components.0.sku', 'components.0.qty']));
79
79
  });
80
80
  it('rechaza descripción en blanco', () => expect(errorsOf(BackofficeCreateKitRequest, { ...ok, description: ' ' })).toContain('description'));
@@ -0,0 +1,93 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validateSync } from 'class-validator';
4
+ import { CreateRetailerRequest } from '../../../../../src/retailOrg/dtos/requests/CreateRetailerRequest';
5
+ import { CreateSubdistributorRequest } from '../../../../../src/retailOrg/dtos/requests/CreateSubdistributorRequest';
6
+ import { UpdateRetailerRequest } from '../../../../../src/retailOrg/dtos/requests/UpdateRetailerRequest';
7
+ import { CreateStoreRequest } from '../../../../../src/retailOrg/dtos/requests/CreateStoreRequest';
8
+ import { UpdateStoreRequest } from '../../../../../src/retailOrg/dtos/requests/UpdateStoreRequest';
9
+ import { CreateRetailUserRequest } from '../../../../../src/retailOrg/dtos/requests/CreateRetailUserRequest';
10
+ import { ProvisionRetailUserRequest } from '../../../../../src/retailOrg/dtos/requests/ProvisionRetailUserRequest';
11
+ import { UpdateRetailUserRequest } from '../../../../../src/retailOrg/dtos/requests/UpdateRetailUserRequest';
12
+ import { RetailAddressInput } from '../../../../../src/retailOrg/dtos/requests/RetailAddressInput';
13
+ import { RetailerTypeEnum } from '../../../../../src/retailOrg/enums/RetailerTypeEnum';
14
+ import { EmploymentTypeEnum } from '../../../../../src/retailOrg/enums/EmploymentTypeEnum';
15
+ import { PermissionScope } from '../../../../../src/platformRbac/enums/PermissionScope';
16
+
17
+ type Ctor = new () => object;
18
+ const invalidProps = (cls: Ctor, plain: object): string[] =>
19
+ validateSync(plainToInstance(cls, plain)).map((e) => e.property);
20
+
21
+ const address = { address1: 'Av. Reforma', city: 'CDMX', state: 'CDMX', zipCode: '01000' };
22
+ const retailer = {
23
+ name: 'Tienda',
24
+ legalName: 'Tienda SA',
25
+ rfc: 'VEN123456ABC',
26
+ contactEmail: 'a@b.mx',
27
+ contactPhone: '5512345678',
28
+ address,
29
+ };
30
+ const store = { zoneId: null, code: 'T1', name: 'Centro', address, location: { lat: 19.4, lng: -99.1 }, phone: '5512345678' };
31
+ const user = { cognitoSub: 'sub', homeStoreId: 's1', employmentType: Object.values(EmploymentTypeEnum)[0], commissionTier: 't', zoneId: null };
32
+ const provision = { ...user, cognitoSub: undefined, email: 'a@b.mx', roleId: 'r', scope: Object.values(PermissionScope)[0], scopeRef: 'x' };
33
+
34
+ describe('retailOrg: patrones de entrada', () => {
35
+ describe.each([
36
+ ['CreateRetailerRequest', CreateRetailerRequest, { ...retailer, type: RetailerTypeEnum.CHAIN }],
37
+ ['CreateSubdistributorRequest', CreateSubdistributorRequest, { ...retailer, type: RetailerTypeEnum.SUBDISTRIBUTOR }],
38
+ ])('%s', (_name, cls, valid) => {
39
+ it('acepta teléfono de 10 dígitos y con +52', () => {
40
+ expect(invalidProps(cls, valid)).toEqual([]);
41
+ expect(invalidProps(cls, { ...valid, contactPhone: '+525512345678' })).toEqual([]);
42
+ });
43
+ it('rechaza teléfono que no es MX de 10 dígitos', () => {
44
+ expect(invalidProps(cls, { ...valid, contactPhone: '55-1234-5678' })).toEqual(['contactPhone']);
45
+ expect(invalidProps(cls, { ...valid, contactPhone: '+15512345678' })).toEqual(['contactPhone']);
46
+ });
47
+ it('acepta RFC de 12 y 13 y normaliza minúsculas', () => {
48
+ expect(invalidProps(cls, { ...valid, rfc: 'GODE561231GR8' })).toEqual([]);
49
+ const dto = plainToInstance(cls, { ...valid, rfc: 'gode561231gr8' }) as { rfc: string };
50
+ expect(dto.rfc).toBe('GODE561231GR8');
51
+ expect(validateSync(dto)).toEqual([]);
52
+ });
53
+ it('rechaza RFC con forma inválida', () => {
54
+ expect(invalidProps(cls, { ...valid, rfc: '123456789012' })).toEqual(['rfc']);
55
+ });
56
+ });
57
+
58
+ it('UpdateRetailerRequest valida contactPhone solo si viene', () => {
59
+ expect(invalidProps(UpdateRetailerRequest, {})).toEqual([]);
60
+ expect(invalidProps(UpdateRetailerRequest, { contactPhone: '+525512345678' })).toEqual([]);
61
+ expect(invalidProps(UpdateRetailerRequest, { contactPhone: '12345' })).toEqual(['contactPhone']);
62
+ });
63
+
64
+ it('CreateStoreRequest exige teléfono MX', () => {
65
+ expect(invalidProps(CreateStoreRequest, store)).toEqual([]);
66
+ expect(invalidProps(CreateStoreRequest, { ...store, phone: '123' })).toEqual(['phone']);
67
+ });
68
+
69
+ it('UpdateStoreRequest valida phone solo si viene', () => {
70
+ expect(invalidProps(UpdateStoreRequest, {})).toEqual([]);
71
+ expect(invalidProps(UpdateStoreRequest, { phone: '+525512345678' })).toEqual([]);
72
+ expect(invalidProps(UpdateStoreRequest, { phone: '55 1234 5678' })).toEqual(['phone']);
73
+ });
74
+
75
+ describe.each([
76
+ ['CreateRetailUserRequest', CreateRetailUserRequest, user],
77
+ ['ProvisionRetailUserRequest', ProvisionRetailUserRequest, provision],
78
+ ['UpdateRetailUserRequest', UpdateRetailUserRequest, {}],
79
+ ])('%s', (_name, cls, valid) => {
80
+ it('phone opcional, MX cuando viene', () => {
81
+ expect(invalidProps(cls, valid)).toEqual([]);
82
+ expect(invalidProps(cls, { ...valid, phone: '5512345678' })).toEqual([]);
83
+ expect(invalidProps(cls, { ...valid, phone: '+525512345678' })).toEqual([]);
84
+ expect(invalidProps(cls, { ...valid, phone: '555123456789' })).toEqual(['phone']);
85
+ });
86
+ });
87
+
88
+ it('RetailAddressInput exige código postal de 5 dígitos', () => {
89
+ expect(invalidProps(RetailAddressInput, address)).toEqual([]);
90
+ expect(invalidProps(RetailAddressInput, { ...address, zipCode: '1000' })).toEqual(['zipCode']);
91
+ expect(invalidProps(RetailAddressInput, { ...address, zipCode: '0100A' })).toEqual(['zipCode']);
92
+ });
93
+ });
@@ -14,7 +14,6 @@ function errorsOf<T extends object>(cls: new () => T, payload: Record<string, un
14
14
  }
15
15
 
16
16
  const validSale = {
17
- otpChallengeId: 'chl-1',
18
17
  lines: [{ sku: 'ACC-1', qty: 2 }],
19
18
  paymentMethod: SalePaymentMethodEnum.CASH,
20
19
  amountCents: 39800,
@@ -91,8 +90,10 @@ describe('CreateAccessorySaleRequest', () => {
91
90
  expect(errorsOf(CreateAccessorySaleRequest, { ...validSale, paymentMethod: SalePaymentMethodEnum.WIRE })).toEqual([]);
92
91
  });
93
92
 
94
- it('exige el reto OTP', () => {
95
- expect(errorsOf(CreateAccessorySaleRequest, { ...validSale, otpChallengeId: '' })).toContain('otpChallengeId');
93
+ it('ya no lleva reto OTP: otpChallengeId se descarta', () => {
94
+ const dto = plainToInstance(CreateAccessorySaleRequest, { ...validSale, otpChallengeId: 'chl-1' }, { excludeExtraneousValues: true });
95
+ expect(dto).not.toHaveProperty('otpChallengeId');
96
+ expect(validateSync(dto)).toEqual([]);
96
97
  });
97
98
  });
98
99
 
@@ -0,0 +1,3 @@
1
+ import type { Sale } from '../../../../src/retailWizard/dtos/Sale';
2
+
3
+ export const counterSaleCustomerId: Sale['retailCustomerId'] = null;
@@ -0,0 +1,32 @@
1
+ import 'reflect-metadata';
2
+ import * as path from 'path';
3
+ import * as ts from 'typescript';
4
+ import { counterSaleCustomerId } from './fixtures/saleWithoutCustomer.strict';
5
+
6
+ const FIXTURE = path.join(__dirname, 'fixtures', 'saleWithoutCustomer.strict.ts');
7
+
8
+ // Sin strictNullChecks null cabe en string y la prueba no detecta nada.
9
+ function strictDiagnostics(): string[] {
10
+ const program = ts.createProgram([FIXTURE], {
11
+ strict: true,
12
+ noEmit: true,
13
+ skipLibCheck: true,
14
+ target: ts.ScriptTarget.ES2020,
15
+ module: ts.ModuleKind.CommonJS,
16
+ moduleResolution: ts.ModuleResolutionKind.Node10,
17
+ });
18
+ return ts
19
+ .getPreEmitDiagnostics(program)
20
+ .filter((d) => d.file?.fileName === FIXTURE)
21
+ .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n'));
22
+ }
23
+
24
+ describe('venta de mostrador sin cliente — retailCustomerId nullable', () => {
25
+ it('Sale acepta retailCustomerId null en strict', () => {
26
+ expect(strictDiagnostics()).toEqual([]);
27
+ });
28
+
29
+ it('el valor viaja como null', () => {
30
+ expect(counterSaleCustomerId).toBeNull();
31
+ });
32
+ });
@@ -0,0 +1,25 @@
1
+ /** Teléfono MX tal como se teclea en la UI: exactamente 10 dígitos. */
2
+ export declare const PHONE_MX_PATTERN: RegExp;
3
+ /** Teléfono MX en el backend: 10 dígitos, opcionalmente con `+52` para clientes que ya lo mandan. */
4
+ export declare const PHONE_MX_ACCEPTED_PATTERN: RegExp;
5
+ export declare const PHONE_MX_MESSAGE = "El tel\u00E9fono debe tener 10 d\u00EDgitos";
6
+ /** Correo pragmático: algo@dominio.tld, sin espacios. */
7
+ export declare const EMAIL_PATTERN: RegExp;
8
+ export declare const EMAIL_MAX_LENGTH = 254;
9
+ export declare const EMAIL_MESSAGE = "Ingresa un correo v\u00E1lido";
10
+ /** RFC en mayúsculas: 12 caracteres persona moral, 13 persona física. */
11
+ export declare const RFC_PATTERN: RegExp;
12
+ export declare const RFC_MESSAGE = "El RFC debe tener 12 o 13 caracteres con el formato del SAT";
13
+ /** Código postal MX: 5 dígitos. */
14
+ export declare const ZIP_CODE_MX_PATTERN: RegExp;
15
+ export declare const ZIP_CODE_MX_MESSAGE = "El c\u00F3digo postal debe tener 5 d\u00EDgitos";
16
+ /** SKU: letras, dígitos y guiones, con espacios solo entre palabras. */
17
+ export declare const SKU_PATTERN: RegExp;
18
+ export declare const SKU_MAX_LENGTH = 64;
19
+ export declare const SKU_MESSAGE = "El SKU admite letras, n\u00FAmeros, guiones y espacios entre palabras (m\u00E1ximo 64)";
20
+ /** IMEI: 15 dígitos; el checksum se valida con `isValidImei`. */
21
+ export declare const IMEI_PATTERN: RegExp;
22
+ export declare const IMEI_MESSAGE = "El IMEI debe tener 15 d\u00EDgitos";
23
+ /** URL https sin espacios; en DTOs de backend también sirve `@IsUrl({ protocols: ['https'], require_protocol: true })`. */
24
+ export declare const HTTPS_URL_PATTERN: RegExp;
25
+ export declare const HTTPS_URL_MESSAGE = "Ingresa una URL que empiece con https://";
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HTTPS_URL_MESSAGE = exports.HTTPS_URL_PATTERN = exports.IMEI_MESSAGE = exports.IMEI_PATTERN = exports.SKU_MESSAGE = exports.SKU_MAX_LENGTH = exports.SKU_PATTERN = exports.ZIP_CODE_MX_MESSAGE = exports.ZIP_CODE_MX_PATTERN = exports.RFC_MESSAGE = exports.RFC_PATTERN = exports.EMAIL_MESSAGE = exports.EMAIL_MAX_LENGTH = exports.EMAIL_PATTERN = exports.PHONE_MX_MESSAGE = exports.PHONE_MX_ACCEPTED_PATTERN = exports.PHONE_MX_PATTERN = void 0;
4
+ /** Teléfono MX tal como se teclea en la UI: exactamente 10 dígitos. */
5
+ exports.PHONE_MX_PATTERN = /^\d{10}$/;
6
+ /** Teléfono MX en el backend: 10 dígitos, opcionalmente con `+52` para clientes que ya lo mandan. */
7
+ exports.PHONE_MX_ACCEPTED_PATTERN = /^(?:\+52)?\d{10}$/;
8
+ exports.PHONE_MX_MESSAGE = 'El teléfono debe tener 10 dígitos';
9
+ /** Correo pragmático: algo@dominio.tld, sin espacios. */
10
+ exports.EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11
+ exports.EMAIL_MAX_LENGTH = 254;
12
+ exports.EMAIL_MESSAGE = 'Ingresa un correo válido';
13
+ /** RFC en mayúsculas: 12 caracteres persona moral, 13 persona física. */
14
+ exports.RFC_PATTERN = /^[A-ZÑ&]{3,4}\d{6}[A-Z\d]{3}$/;
15
+ exports.RFC_MESSAGE = 'El RFC debe tener 12 o 13 caracteres con el formato del SAT';
16
+ /** Código postal MX: 5 dígitos. */
17
+ exports.ZIP_CODE_MX_PATTERN = /^\d{5}$/;
18
+ exports.ZIP_CODE_MX_MESSAGE = 'El código postal debe tener 5 dígitos';
19
+ /** SKU: letras, dígitos y guiones, con espacios solo entre palabras. */
20
+ exports.SKU_PATTERN = /^[A-Za-z0-9-]+(?: +[A-Za-z0-9-]+)*$/;
21
+ exports.SKU_MAX_LENGTH = 64;
22
+ exports.SKU_MESSAGE = 'El SKU admite letras, números, guiones y espacios entre palabras (máximo 64)';
23
+ /** IMEI: 15 dígitos; el checksum se valida con `isValidImei`. */
24
+ exports.IMEI_PATTERN = /^\d{15}$/;
25
+ exports.IMEI_MESSAGE = 'El IMEI debe tener 15 dígitos';
26
+ /** URL https sin espacios; en DTOs de backend también sirve `@IsUrl({ protocols: ['https'], require_protocol: true })`. */
27
+ exports.HTTPS_URL_PATTERN = /^https:\/\/[^\s/$.?#][^\s]*$/i;
28
+ exports.HTTPS_URL_MESSAGE = 'Ingresa una URL que empiece con https://';
@@ -0,0 +1,2 @@
1
+ /** Pasa un string a mayúsculas antes de validarlo; deja pasar cualquier otro tipo sin tocarlo. */
2
+ export declare const UpperCase: () => PropertyDecorator;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UpperCase = void 0;
4
+ const class_transformer_1 = require("class-transformer");
5
+ /** Pasa un string a mayúsculas antes de validarlo; deja pasar cualquier otro tipo sin tocarlo. */
6
+ const UpperCase = () => (0, class_transformer_1.Transform)(({ value }) => (typeof value === 'string' ? value.toUpperCase() : value));
7
+ exports.UpperCase = UpperCase;
@@ -0,0 +1,2 @@
1
+ /** Valida un IMEI: 15 dígitos y checksum Luhn. */
2
+ export declare function isValidImei(imei: string): boolean;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isValidImei = isValidImei;
4
+ const InputPatterns_1 = require("../constants/InputPatterns");
5
+ /** Valida un IMEI: 15 dígitos y checksum Luhn. */
6
+ function isValidImei(imei) {
7
+ if (!InputPatterns_1.IMEI_PATTERN.test(imei))
8
+ return false;
9
+ let sum = 0;
10
+ for (let i = 0; i < 15; i++) {
11
+ let digit = Number(imei[i]);
12
+ if (i % 2 === 1) {
13
+ digit *= 2;
14
+ if (digit > 9)
15
+ digit -= 9;
16
+ }
17
+ sum += digit;
18
+ }
19
+ return sum % 10 === 0;
20
+ }
@@ -1,5 +1,13 @@
1
+ export * from './constants/InputPatterns';
2
+ export * from './helpers/imeiChecksum';
1
3
  export * from './decorators/Trim';
4
+ export * from './decorators/UpperCase';
2
5
  export * from './validators/IsStringValueRecord';
6
+ export * from './validators/matchesInputPattern';
7
+ export * from './validators/IsMxPhone';
8
+ export * from './validators/IsMxZipCode';
9
+ export * from './validators/IsRfc';
10
+ export * from './validators/IsSku';
3
11
  export * from './dtos/ProcessValidationResultDto';
4
12
  export * from './enums/ProcessValidationEnum';
5
13
  export * from './enums/ProcessResultEnum';
@@ -14,10 +14,20 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ //constants
18
+ __exportStar(require("./constants/InputPatterns"), exports);
19
+ //helpers
20
+ __exportStar(require("./helpers/imeiChecksum"), exports);
17
21
  //decorators
18
22
  __exportStar(require("./decorators/Trim"), exports);
23
+ __exportStar(require("./decorators/UpperCase"), exports);
19
24
  //validators
20
25
  __exportStar(require("./validators/IsStringValueRecord"), exports);
26
+ __exportStar(require("./validators/matchesInputPattern"), exports);
27
+ __exportStar(require("./validators/IsMxPhone"), exports);
28
+ __exportStar(require("./validators/IsMxZipCode"), exports);
29
+ __exportStar(require("./validators/IsRfc"), exports);
30
+ __exportStar(require("./validators/IsSku"), exports);
21
31
  //dtos
22
32
  __exportStar(require("./dtos/ProcessValidationResultDto"), exports);
23
33
  //Enums
@@ -0,0 +1,3 @@
1
+ import { ValidationOptions } from 'class-validator';
2
+ /** Teléfono MX de 10 dígitos; admite el prefijo `+52`. */
3
+ export declare const IsMxPhone: (validationOptions?: ValidationOptions) => PropertyDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IsMxPhone = void 0;
4
+ const InputPatterns_1 = require("../constants/InputPatterns");
5
+ const matchesInputPattern_1 = require("./matchesInputPattern");
6
+ /** Teléfono MX de 10 dígitos; admite el prefijo `+52`. */
7
+ const IsMxPhone = (validationOptions) => (0, matchesInputPattern_1.matchesInputPattern)('isMxPhone', InputPatterns_1.PHONE_MX_ACCEPTED_PATTERN, InputPatterns_1.PHONE_MX_MESSAGE, validationOptions);
8
+ exports.IsMxPhone = IsMxPhone;
@@ -0,0 +1,3 @@
1
+ import { ValidationOptions } from 'class-validator';
2
+ /** Código postal MX de 5 dígitos. */
3
+ export declare const IsMxZipCode: (validationOptions?: ValidationOptions) => PropertyDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IsMxZipCode = void 0;
4
+ const InputPatterns_1 = require("../constants/InputPatterns");
5
+ const matchesInputPattern_1 = require("./matchesInputPattern");
6
+ /** Código postal MX de 5 dígitos. */
7
+ const IsMxZipCode = (validationOptions) => (0, matchesInputPattern_1.matchesInputPattern)('isMxZipCode', InputPatterns_1.ZIP_CODE_MX_PATTERN, InputPatterns_1.ZIP_CODE_MX_MESSAGE, validationOptions);
8
+ exports.IsMxZipCode = IsMxZipCode;
@@ -0,0 +1,3 @@
1
+ import { ValidationOptions } from 'class-validator';
2
+ /** RFC en mayúsculas; combínalo con `@UpperCase()` para normalizar la entrada. */
3
+ export declare const IsRfc: (validationOptions?: ValidationOptions) => PropertyDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IsRfc = void 0;
4
+ const InputPatterns_1 = require("../constants/InputPatterns");
5
+ const matchesInputPattern_1 = require("./matchesInputPattern");
6
+ /** RFC en mayúsculas; combínalo con `@UpperCase()` para normalizar la entrada. */
7
+ const IsRfc = (validationOptions) => (0, matchesInputPattern_1.matchesInputPattern)('isRfc', InputPatterns_1.RFC_PATTERN, InputPatterns_1.RFC_MESSAGE, validationOptions);
8
+ exports.IsRfc = IsRfc;
@@ -0,0 +1,3 @@
1
+ import { ValidationOptions } from 'class-validator';
2
+ /** SKU de 1 a 64 caracteres: letras, dígitos, guiones y espacios entre palabras. */
3
+ export declare const IsSku: (validationOptions?: ValidationOptions) => PropertyDecorator;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IsSku = void 0;
4
+ const InputPatterns_1 = require("../constants/InputPatterns");
5
+ const matchesInputPattern_1 = require("./matchesInputPattern");
6
+ /** SKU de 1 a 64 caracteres: letras, dígitos, guiones y espacios entre palabras. */
7
+ const IsSku = (validationOptions) => (0, matchesInputPattern_1.matchesInputPattern)('isSku', InputPatterns_1.SKU_PATTERN, InputPatterns_1.SKU_MESSAGE, validationOptions, InputPatterns_1.SKU_MAX_LENGTH);
8
+ exports.IsSku = IsSku;
@@ -0,0 +1,3 @@
1
+ import { ValidationOptions } from 'class-validator';
2
+ /** Arma un decorador que exige un string que cumpla `pattern` y, si se da, `maxLength`. */
3
+ export declare function matchesInputPattern(name: string, pattern: RegExp, message: string, validationOptions?: ValidationOptions, maxLength?: number): PropertyDecorator;
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.matchesInputPattern = matchesInputPattern;
4
+ const class_validator_1 = require("class-validator");
5
+ /** Arma un decorador que exige un string que cumpla `pattern` y, si se da, `maxLength`. */
6
+ function matchesInputPattern(name, pattern, message, validationOptions, maxLength) {
7
+ return (0, class_validator_1.ValidateBy)({
8
+ name,
9
+ validator: {
10
+ validate: (value) => typeof value === 'string' &&
11
+ (maxLength === undefined || value.length <= maxLength) &&
12
+ pattern.test(value),
13
+ defaultMessage: () => message,
14
+ },
15
+ }, validationOptions);
16
+ }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.regexPhoneMx = exports.regexCurp = exports.regexIso8601Date = exports.regexPhoneNumber = exports.regexIso8601 = exports.regexUuidV4 = void 0;
4
+ const InputPatterns_1 = require("../../common/constants/InputPatterns");
4
5
  // Expresión regular para validar UUID v4
5
6
  exports.regexUuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
6
7
  // Expresión regular para validar fecha ISO 8601
@@ -17,4 +18,4 @@ exports.regexIso8601Date = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{1,3})?(Z|
17
18
  exports.regexCurp = /^[A-Z][AEIOUX][A-Z]{2}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[HM][A-Z]{2}[B-DF-HJ-NP-TV-Z]{3}[A-Z\d]\d$/;
18
19
  // Teléfono MX: exactamente 10 dígitos (se normalizan espacios/guiones antes de validar).
19
20
  // Usado por retailCustomer y retailWizard. SureKeep Fase 2 — pista Retail.
20
- exports.regexPhoneMx = /^\d{10}$/;
21
+ exports.regexPhoneMx = InputPatterns_1.PHONE_MX_PATTERN;
@@ -17,7 +17,7 @@ const SHORT_TEXT = 120;
17
17
  /** Largo máximo de un texto largo del contrato (nombre, domicilio, razón social). */
18
18
  const LONG_TEXT = 300;
19
19
  /** IMEI: exactamente 15 dígitos. */
20
- const IMEI_PATTERN = /^\d{15}$/;
20
+ const InputPatterns_1 = require("../../common/constants/InputPatterns");
21
21
  /** Datos del acreditado tal como se imprimen en el contrato (Sección II.1). */
22
22
  class LoanContractBorrowerDetails {
23
23
  }
@@ -135,7 +135,7 @@ __decorate([
135
135
  (0, class_transformer_1.Expose)(),
136
136
  (0, class_validator_1.IsOptional)(),
137
137
  (0, class_validator_1.IsString)(),
138
- (0, class_validator_1.Matches)(IMEI_PATTERN, { message: 'imei admite solo 15 dígitos' }),
138
+ (0, class_validator_1.Matches)(InputPatterns_1.IMEI_PATTERN, { message: 'imei admite solo 15 dígitos' }),
139
139
  __metadata("design:type", String)
140
140
  ], LoanContractDeviceDetails.prototype, "imei", void 0);
141
141
  __decorate([
@@ -15,7 +15,7 @@ const class_validator_1 = require("class-validator");
15
15
  const SkuPattern_1 = require("../../retailCatalog/constants/SkuPattern");
16
16
  const CreditIdPattern_1 = require("../constants/CreditIdPattern");
17
17
  /** Charset y largo del IMEI: exactamente 15 dígitos. */
18
- const IMEI_PATTERN = /^\d{15}$/;
18
+ const InputPatterns_1 = require("../../common/constants/InputPatterns");
19
19
  /** Plazo máximo del crédito en meses. */
20
20
  const MAX_TERM_MONTHS = 60;
21
21
  /** Ítem de enrolamiento: un crédito con el equipo que se va a enrolar en el MDM. */
@@ -31,14 +31,14 @@ __decorate([
31
31
  (0, class_transformer_1.Expose)(),
32
32
  (0, class_validator_1.IsString)(),
33
33
  (0, class_validator_1.IsNotEmpty)(),
34
- (0, class_validator_1.Matches)(IMEI_PATTERN, { message: 'imei admite solo 15 dígitos' }),
34
+ (0, class_validator_1.Matches)(InputPatterns_1.IMEI_PATTERN, { message: 'imei admite solo 15 dígitos' }),
35
35
  __metadata("design:type", String)
36
36
  ], LoanDeviceEnrollItem.prototype, "imei", void 0);
37
37
  __decorate([
38
38
  (0, class_transformer_1.Expose)(),
39
39
  (0, class_validator_1.IsOptional)(),
40
40
  (0, class_validator_1.IsString)(),
41
- (0, class_validator_1.Matches)(IMEI_PATTERN, { message: 'imei2 admite solo 15 dígitos' }),
41
+ (0, class_validator_1.Matches)(InputPatterns_1.IMEI_PATTERN, { message: 'imei2 admite solo 15 dígitos' }),
42
42
  __metadata("design:type", String)
43
43
  ], LoanDeviceEnrollItem.prototype, "imei2", void 0);
44
44
  __decorate([
@@ -46,7 +46,7 @@ __decorate([
46
46
  (0, class_validator_1.IsString)(),
47
47
  (0, class_validator_1.IsNotEmpty)(),
48
48
  (0, class_validator_1.MaxLength)(64),
49
- (0, class_validator_1.Matches)(SkuPattern_1.SKU_PATTERN, { message: 'productSku admite solo [A-Za-z0-9-]' }),
49
+ (0, class_validator_1.Matches)(SkuPattern_1.SKU_PATTERN, { message: SkuPattern_1.SKU_MESSAGE }),
50
50
  __metadata("design:type", String)
51
51
  ], LoanDeviceEnrollItem.prototype, "productSku", void 0);
52
52
  __decorate([
@@ -1,2 +1,2 @@
1
- /** Charset permitido para el SKU: alfanumérico + guion, 1-64 chars. */
2
- export declare const SKU_PATTERN: RegExp;
1
+ /** Charset del SKU: letras, dígitos, guiones y espacios entre palabras; el tope de 64 va con `@Length`. */
2
+ export { SKU_PATTERN, SKU_MESSAGE } from '../../common/constants/InputPatterns';