@fiado/type-kit 3.602.0 → 3.603.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 (35) hide show
  1. package/_test_/unit/retailIntegration/retailSaleContracts.test.ts +162 -10
  2. package/_test_/unit/retailIntegration/retailSystemEnums.test.ts +3 -1
  3. package/bin/retailIntegration/constants/retailIntegrationLimits.d.ts +15 -0
  4. package/bin/retailIntegration/constants/retailIntegrationLimits.js +16 -1
  5. package/bin/retailIntegration/dtos/CancelRetailSaleRequest.d.ts +7 -0
  6. package/bin/retailIntegration/dtos/CancelRetailSaleRequest.js +34 -1
  7. package/bin/retailIntegration/enums/RetailPaymentFrequencyEnum.d.ts +6 -0
  8. package/bin/retailIntegration/enums/RetailPaymentFrequencyEnum.js +10 -0
  9. package/bin/retailIntegration/enums/RetailPaymentMethodEnum.d.ts +3 -2
  10. package/bin/retailIntegration/enums/RetailPaymentMethodEnum.js +3 -2
  11. package/bin/retailIntegration/enums/index.d.ts +1 -0
  12. package/bin/retailIntegration/enums/index.js +1 -0
  13. package/bin/retailIntegration/index.d.ts +2 -0
  14. package/bin/retailIntegration/index.js +2 -0
  15. package/bin/retailIntegration/models/RetailSystemSale.d.ts +23 -2
  16. package/bin/retailIntegration/models/RetailSystemSale.js +89 -11
  17. package/bin/retailIntegration/validators/IsCalendarDate.d.ts +6 -0
  18. package/bin/retailIntegration/validators/IsCalendarDate.js +25 -0
  19. package/bin/retailIntegration/validators/IsIsoDateTimeWithZone.js +2 -6
  20. package/bin/retailIntegration/validators/calendarDate.d.ts +2 -0
  21. package/bin/retailIntegration/validators/calendarDate.js +8 -0
  22. package/bin/retailIntegration/validators/saleValidators.d.ts +26 -0
  23. package/bin/retailIntegration/validators/saleValidators.js +100 -0
  24. package/package.json +1 -1
  25. package/src/retailIntegration/constants/retailIntegrationLimits.ts +22 -0
  26. package/src/retailIntegration/dtos/CancelRetailSaleRequest.ts +14 -2
  27. package/src/retailIntegration/enums/RetailPaymentFrequencyEnum.ts +6 -0
  28. package/src/retailIntegration/enums/RetailPaymentMethodEnum.ts +3 -2
  29. package/src/retailIntegration/enums/index.ts +1 -0
  30. package/src/retailIntegration/index.ts +2 -0
  31. package/src/retailIntegration/models/RetailSystemSale.ts +57 -10
  32. package/src/retailIntegration/validators/IsCalendarDate.ts +16 -0
  33. package/src/retailIntegration/validators/IsIsoDateTimeWithZone.ts +1 -6
  34. package/src/retailIntegration/validators/calendarDate.ts +5 -0
  35. package/src/retailIntegration/validators/saleValidators.ts +80 -0
@@ -23,6 +23,12 @@ async function errorsOf(cls: new () => object, plain: object): Promise<string[]>
23
23
  return paths(await validate(plainToInstance(cls, plain)));
24
24
  }
25
25
 
26
+ const cashPayment = { method: 'CASH', amount: '400.00', cardLast4: null, authCode: null, speiTrackingKey: null, senderClabe: null } as const;
27
+ const cardPayment = { method: 'CARD', amount: '200.00', cardLast4: '4242', authCode: 'A1B2C3', speiTrackingKey: null, senderClabe: null } as const;
28
+ const wirePayment = {
29
+ method: 'WIRE', amount: '200.00', cardLast4: null, authCode: null, speiTrackingKey: '123456789012345678', senderClabe: '012180001234567897',
30
+ } as const;
31
+
26
32
  // Literal tipado: prueba en compilación que la clase sigue aceptando objetos planos.
27
33
  const sale: RetailSystemSale = {
28
34
  saleId: '7f0c2a52-1b7e-4f0e-9d7a-2a1d3c4b5e6f',
@@ -33,30 +39,39 @@ const sale: RetailSystemSale = {
33
39
  saleType: 'CREDIT',
34
40
  financier: 'SUREKEEP',
35
41
  creditId: 'CR-991',
42
+ credit: {
43
+ paymentFrequency: 'WEEKLY', installments: 16, installmentAmount: '260.67', downPayment: '600.00', totalToPay: '4170.72', firstPaymentDate: '2026-10-30',
44
+ },
36
45
  currencyId: 'MXN',
37
46
  customer: { customerId: 'C-1', names: 'Ana', paternalLastName: 'López', maternalLastName: null, phone: '7221234567' },
38
47
  equipmentPrice: '5999.00',
39
48
  amountPaid: '600.00',
40
- paymentMethod: 'CASH',
49
+ payments: [cashPayment, cardPayment],
41
50
  lines: [
42
51
  {
43
52
  lineId: 'L1', sku: 'IPH-15', qty: 1, basePrice: '5999.00', discount: '0.00', paymentType: 'CREDIT',
44
- financedAmount: '5399.00', role: null, components: [{ sku: 'IPH-15', qty: 1, identifiers: ['356789012345678'] }],
53
+ financedAmount: '5399.00', role: null, promotionId: null, components: [{ sku: 'IPH-15', qty: 1, identifiers: ['356789012345678'] }],
45
54
  },
46
55
  {
47
56
  lineId: 'L2', sku: 'FUNDA-1', qty: 1, basePrice: '0.00', discount: '0.00', paymentType: 'CASH',
48
- financedAmount: '0.00', role: 'GIFT', components: [],
57
+ financedAmount: '0.00', role: 'GIFT', promotionId: 'PRM-0012', components: [],
49
58
  },
50
59
  ],
51
60
  };
52
61
 
62
+ const cashSale = { ...sale, saleType: 'CASH', financier: 'NONE', creditId: null, credit: null };
63
+
53
64
  describe('RetailSystemSale (body de privateRegisterRetailSale)', () => {
54
- it('acepta una venta completa', async () => {
65
+ it('acepta una venta a crédito SureKeep completa', async () => {
55
66
  expect(await errorsOf(RetailSystemSale, sale)).toEqual([]);
56
67
  });
57
68
 
58
- it('acepta los nullables en null', async () => {
59
- expect(await errorsOf(RetailSystemSale, { ...sale, creditId: null, paymentMethod: null })).toEqual([]);
69
+ it('acepta una venta de contado con creditId y credit en null', async () => {
70
+ expect(await errorsOf(RetailSystemSale, cashSale)).toEqual([]);
71
+ });
72
+
73
+ it('acepta una referida sin creditId ni credit', async () => {
74
+ expect(await errorsOf(RetailSystemSale, { ...sale, financier: 'PAYJOY', creditId: null, credit: null })).toEqual([]);
60
75
  });
61
76
 
62
77
  it.each([
@@ -67,14 +82,19 @@ describe('RetailSystemSale (body de privateRegisterRetailSale)', () => {
67
82
  ['financier', { financier: 'OTRO' }],
68
83
  ['currencyId', { currencyId: 'EUR' }],
69
84
  ['equipmentPrice', { equipmentPrice: '5999' }],
70
- ['amountPaid', { amountPaid: '-1.00' }],
71
- ['paymentMethod', { paymentMethod: 'BITCOIN' }],
85
+ ['equipmentPrice', { equipmentPrice: '1234567890123.00' }],
72
86
  ['creditId', { creditId: undefined }],
87
+ ['credit', { credit: undefined }],
88
+ ['payments', { payments: undefined }],
73
89
  ['lines', { lines: [] }],
74
90
  ])('rechaza %s inválido', async (field, patch) => {
75
91
  expect(await errorsOf(RetailSystemSale, { ...sale, ...patch })).toEqual([field]);
76
92
  });
77
93
 
94
+ it.each(['722123456', '72212345678', '+527221234567', '722-123-4567'])('rechaza customer.phone %s', async (phone) => {
95
+ expect(await errorsOf(RetailSystemSale, { ...sale, customer: { ...sale.customer, phone } })).toEqual(['customer.phone']);
96
+ });
97
+
78
98
  it('valida el cliente anidado', async () => {
79
99
  expect(await errorsOf(RetailSystemSale, { ...sale, customer: { ...sale.customer, names: '' } })).toEqual(['customer.names']);
80
100
  });
@@ -90,11 +110,130 @@ describe('RetailSystemSale (body de privateRegisterRetailSale)', () => {
90
110
  });
91
111
  });
92
112
 
113
+ describe('RetailSystemSale.credit', () => {
114
+ it('SUREKEEP exige credit y creditId', async () => {
115
+ expect(await errorsOf(RetailSystemSale, { ...sale, credit: null })).toEqual(['financier']);
116
+ expect(await errorsOf(RetailSystemSale, { ...sale, creditId: null })).toEqual(['financier']);
117
+ });
118
+
119
+ it('contado exige credit y creditId en null', async () => {
120
+ expect(await errorsOf(RetailSystemSale, { ...cashSale, credit: sale.credit })).toEqual(['financier']);
121
+ expect(await errorsOf(RetailSystemSale, { ...cashSale, creditId: 'CR-1' })).toEqual(['financier']);
122
+ });
123
+
124
+ it.each([
125
+ ['paymentFrequency', { paymentFrequency: 'DAILY' }],
126
+ ['installments', { installments: 0 }],
127
+ ['installments', { installments: 1.5 }],
128
+ ['installmentAmount', { installmentAmount: '260.6' }],
129
+ ['downPayment', { downPayment: undefined }],
130
+ ['totalToPay', { totalToPay: '1234567890123.00' }],
131
+ ['firstPaymentDate', { firstPaymentDate: '2026-02-30' }],
132
+ ['firstPaymentDate', { firstPaymentDate: '2026-10-30T00:00:00Z' }],
133
+ ])('rechaza credit.%s inválido', async (field, patch) => {
134
+ expect(await errorsOf(RetailSystemSale, { ...sale, credit: { ...sale.credit, ...patch } })).toEqual([`credit.${field}`]);
135
+ });
136
+ });
137
+
138
+ describe('RetailSystemSale.payments', () => {
139
+ it('acepta [] cuando amountPaid es 0.00', async () => {
140
+ expect(await errorsOf(RetailSystemSale, { ...sale, amountPaid: '0.00', payments: [] })).toEqual([]);
141
+ });
142
+
143
+ it('acepta WIRE con y sin senderClabe', async () => {
144
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments: [cashPayment, wirePayment] })).toEqual([]);
145
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments: [cashPayment, { ...wirePayment, senderClabe: null }] })).toEqual([]);
146
+ });
147
+
148
+ it('acepta FIADO sin detalle', async () => {
149
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments: [{ ...cashPayment, method: 'FIADO', amount: '600.00' }] })).toEqual([]);
150
+ });
151
+
152
+ it('rechaza que la suma no cuadre con amountPaid', async () => {
153
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments: [cashPayment] })).toEqual(['payments']);
154
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments: [] })).toEqual(['payments']);
155
+ });
156
+
157
+ it.each([
158
+ ['method', { method: 'SPEI' }],
159
+ ['amount', { amount: '0.00' }],
160
+ ['amount', { amount: '400' }],
161
+ ['cardLast4', { cardLast4: '4242' }],
162
+ ['cardLast4', { cardLast4: undefined }],
163
+ ['authCode', { authCode: 'X' }],
164
+ ['speiTrackingKey', { speiTrackingKey: undefined }],
165
+ ['senderClabe', { senderClabe: undefined }],
166
+ ])('rechaza %s fuera de regla en CASH', async (field, patch) => {
167
+ const payments = [{ ...cashPayment, ...patch }, cardPayment];
168
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments })).toEqual([`payments.0.${field}`]);
169
+ });
170
+
171
+ it.each([
172
+ ['cardLast4', { cardLast4: null }],
173
+ ['cardLast4', { cardLast4: '424' }],
174
+ ['authCode', { authCode: null }],
175
+ ['authCode', { authCode: ' ' }],
176
+ ['authCode', { authCode: 'A'.repeat(65) }],
177
+ ['speiTrackingKey', { speiTrackingKey: '123456789012345678' }],
178
+ ])('rechaza %s fuera de regla en CARD', async (field, patch) => {
179
+ const payments = [cashPayment, { ...cardPayment, ...patch }];
180
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments })).toEqual([`payments.1.${field}`]);
181
+ });
182
+
183
+ it.each([
184
+ ['speiTrackingKey', { speiTrackingKey: null }],
185
+ ['speiTrackingKey', { speiTrackingKey: '12345' }],
186
+ ['senderClabe', { senderClabe: '0121' }],
187
+ ['cardLast4', { cardLast4: '4242' }],
188
+ ])('rechaza %s fuera de regla en WIRE', async (field, patch) => {
189
+ const payments = [cashPayment, { ...wirePayment, ...patch }];
190
+ expect(await errorsOf(RetailSystemSale, { ...sale, payments })).toEqual([`payments.1.${field}`]);
191
+ });
192
+ });
193
+
194
+ describe('RetailSystemSale.lines[].promotionId', () => {
195
+ it('la llave es obligatoria aunque sea null', async () => {
196
+ const lines = [{ ...sale.lines[0], promotionId: undefined }, sale.lines[1]];
197
+ expect(await errorsOf(RetailSystemSale, { ...sale, lines })).toEqual(['lines.0.promotionId']);
198
+ });
199
+
200
+ it('acepta una promoción en una línea con descuento', async () => {
201
+ const lines = [{ ...sale.lines[0], discount: '500.00', promotionId: 'PRM-1' }, sale.lines[1]];
202
+ expect(await errorsOf(RetailSystemSale, { ...sale, lines })).toEqual([]);
203
+ });
204
+
205
+ it.each([
206
+ ['vacío', ' '],
207
+ ['más de 64', 'P'.repeat(65)],
208
+ ])('rechaza promotionId %s', async (_case, promotionId) => {
209
+ const lines = [{ ...sale.lines[0], promotionId }, sale.lines[1]];
210
+ expect(await errorsOf(RetailSystemSale, { ...sale, lines })).toEqual(['lines.0.promotionId']);
211
+ });
212
+
213
+ it('un regalo exige promotionId', async () => {
214
+ const lines = [sale.lines[0], { ...sale.lines[1], promotionId: null }];
215
+ expect(await errorsOf(RetailSystemSale, { ...sale, lines })).toEqual(['lines.1.promotionId']);
216
+ });
217
+ });
218
+
93
219
  describe('CancelRetailSaleRequest', () => {
94
- const cancel = { saleId: sale.saleId, cancelledAt: '2026-10-25T19:00:00-06:00', reason: 'Cliente desistió' };
220
+ const cancel: CancelRetailSaleRequest = {
221
+ saleId: sale.saleId,
222
+ cancelledAt: '2026-10-25T19:00:00-06:00',
223
+ reason: 'Cliente desistió',
224
+ damaged: [
225
+ { sku: 'IPH-15', identifier: '356789012345678', qty: 1 },
226
+ { sku: 'FUNDA-1', identifier: null, qty: 3 },
227
+ ],
228
+ };
95
229
 
96
- it('acepta la cancelación', async () => {
230
+ it('acepta la cancelación con y sin dañados', async () => {
97
231
  expect(await errorsOf(CancelRetailSaleRequest, cancel)).toEqual([]);
232
+ expect(await errorsOf(CancelRetailSaleRequest, { ...cancel, damaged: [] })).toEqual([]);
233
+ });
234
+
235
+ it('acepta reason de 200 caracteres', async () => {
236
+ expect(await errorsOf(CancelRetailSaleRequest, { ...cancel, reason: 'r'.repeat(200) })).toEqual([]);
98
237
  });
99
238
 
100
239
  it.each([
@@ -102,10 +241,23 @@ describe('CancelRetailSaleRequest', () => {
102
241
  ['cancelledAt', { cancelledAt: '2026-02-31T10:00:00Z' }],
103
242
  ['reason', { reason: '' }],
104
243
  ['reason', { reason: undefined }],
244
+ ['reason', { reason: 'r'.repeat(201) }],
245
+ ['damaged', { damaged: undefined }],
105
246
  ])('rechaza %s inválido', async (field, patch) => {
106
247
  expect(await errorsOf(CancelRetailSaleRequest, { ...cancel, ...patch })).toEqual([field]);
107
248
  });
108
249
 
250
+ it.each([
251
+ ['sku', { sku: 'a/b' }],
252
+ ['identifier', { identifier: undefined }],
253
+ ['identifier', { identifier: ' ' }],
254
+ ['qty', { qty: 0 }],
255
+ ['qty', { qty: 2 }],
256
+ ])('rechaza damaged[].%s inválido', async (field, patch) => {
257
+ const damaged = [{ ...cancel.damaged[0], ...patch }];
258
+ expect(await errorsOf(CancelRetailSaleRequest, { ...cancel, damaged })).toEqual([`damaged.0.${field}`]);
259
+ });
260
+
109
261
  it('la respuesta trae el saleId', () => {
110
262
  const response: CancelRetailSaleResponse = { saleId: cancel.saleId };
111
263
  expect(response.saleId).toBe(cancel.saleId);
@@ -5,6 +5,7 @@ import {
5
5
  RetailFinancierEnum,
6
6
  RetailLinePaymentTypeEnum,
7
7
  RetailMdmProviderEnum,
8
+ RetailPaymentFrequencyEnum,
8
9
  RetailPaymentMethodEnum,
9
10
  RetailPieceReasonEnum,
10
11
  RetailPieceStatusEnum,
@@ -35,7 +36,8 @@ describe('enums de los objetos del retailer', () => {
35
36
  [RetailUserAccessStatusEnum, ['ACTIVE', 'SUSPENDED', 'INACTIVE']],
36
37
  [RetailSaleTypeEnum, ['CASH', 'CREDIT', 'EXTERNAL_REFERRAL']],
37
38
  [RetailSaleFinancierEnum, ['NONE', 'SUREKEEP', 'PAYJOY', 'CREDIYA', 'LESPAGO', 'INNOVA']],
38
- [RetailPaymentMethodEnum, ['CASH', 'CARD', 'SPEI']],
39
+ [RetailPaymentMethodEnum, ['CASH', 'CARD', 'WIRE', 'FIADO']],
40
+ [RetailPaymentFrequencyEnum, ['WEEKLY', 'BIWEEKLY', 'MONTHLY']],
39
41
  [RetailLinePaymentTypeEnum, ['CASH', 'CREDIT']],
40
42
  ])('%p trae exactamente los valores del contrato', (enumObject, values) => {
41
43
  expect(Object.values(enumObject)).toEqual(values);
@@ -45,3 +45,18 @@ export declare const RETAIL_LINE_INDEX_MIN = 0;
45
45
  export declare const RETAIL_LINE_INDEX_MAX = 999;
46
46
  /** Rango máximo de `GET /inventory/movements`: 7 días en milisegundos. */
47
47
  export declare const RETAIL_MOVEMENTS_RANGE_MAX_MS: number;
48
+ /** Decimal de la venta y la cancelación: hasta 12 enteros y dos decimales, nunca negativo. */
49
+ export declare const RETAIL_SALE_DECIMAL_PATTERN: RegExp;
50
+ /** Decimal de la venta mayor que cero, como `payments[].amount`. */
51
+ export declare const RETAIL_SALE_POSITIVE_DECIMAL_PATTERN: RegExp;
52
+ /** Texto libre corto de la venta, como `authCode` y `promotionId`: no vacío y de a lo más 64. */
53
+ export declare const RETAIL_SALE_ID_TEXT_PATTERN: RegExp;
54
+ /** Últimos 4 dígitos de la tarjeta en un pago `CARD`. */
55
+ export declare const RETAIL_CARD_LAST4_PATTERN: RegExp;
56
+ /** Clave de rastreo SPEI y CLABE de un pago `WIRE`: 18 dígitos. */
57
+ export declare const RETAIL_SPEI_TRACKING_KEY_PATTERN: RegExp;
58
+ export declare const RETAIL_CLABE_PATTERN: RegExp;
59
+ /** Tope del motivo de cancelación de una venta. */
60
+ export declare const RETAIL_CANCEL_REASON_MAX_LENGTH = 200;
61
+ /** Teléfono del cliente en la venta: 10 dígitos. */
62
+ export declare const RETAIL_CUSTOMER_PHONE_PATTERN: RegExp;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RETAIL_MOVEMENTS_RANGE_MAX_MS = exports.RETAIL_LINE_INDEX_MAX = exports.RETAIL_LINE_INDEX_MIN = exports.RETAIL_ERROR_CODE_MAX_LENGTH = exports.RETAIL_EXTERNAL_ID_PATTERN = exports.RETAIL_NEXT_TOKEN_MAX_LENGTH = exports.RETAIL_EVENT_DECIMAL_PATTERN = exports.RETAIL_ELIGIBLE_FINANCIERS_MAX = exports.RETAIL_EVENT_STOCK_MAX = exports.RETAIL_LONG_TEXT_MAX_LENGTH = exports.RETAIL_SHORT_TEXT_MAX_LENGTH = exports.RETAIL_ID_TEXT_MAX_LENGTH = exports.RETAIL_EVENT_PIECES_MAX = exports.RETAIL_LIVE_CHECK_IDENTIFIERS_MAX = exports.RETAIL_DECIMAL_PATTERN = exports.RETAIL_SKU_PATTERN = exports.RETAIL_ZIP_CODE_MAX_LENGTH = exports.RETAIL_ZIP_CODE_MIN_LENGTH = exports.RETAIL_STORE_CODE_MAX_LENGTH = exports.RETAIL_ZONE_ID_PATTERN = exports.RETAIL_ENTITY_ID_PATTERN = exports.RETAIL_TENANT_ID_PATTERN = exports.RETAIL_LIST_LIMIT_MAX = exports.RETAIL_LIST_LIMIT_MIN = exports.RETAIL_EVENT_ID_MAX_LENGTH = void 0;
3
+ exports.RETAIL_CUSTOMER_PHONE_PATTERN = exports.RETAIL_CANCEL_REASON_MAX_LENGTH = exports.RETAIL_CLABE_PATTERN = exports.RETAIL_SPEI_TRACKING_KEY_PATTERN = exports.RETAIL_CARD_LAST4_PATTERN = exports.RETAIL_SALE_ID_TEXT_PATTERN = exports.RETAIL_SALE_POSITIVE_DECIMAL_PATTERN = exports.RETAIL_SALE_DECIMAL_PATTERN = exports.RETAIL_MOVEMENTS_RANGE_MAX_MS = exports.RETAIL_LINE_INDEX_MAX = exports.RETAIL_LINE_INDEX_MIN = exports.RETAIL_ERROR_CODE_MAX_LENGTH = exports.RETAIL_EXTERNAL_ID_PATTERN = exports.RETAIL_NEXT_TOKEN_MAX_LENGTH = exports.RETAIL_EVENT_DECIMAL_PATTERN = exports.RETAIL_ELIGIBLE_FINANCIERS_MAX = exports.RETAIL_EVENT_STOCK_MAX = exports.RETAIL_LONG_TEXT_MAX_LENGTH = exports.RETAIL_SHORT_TEXT_MAX_LENGTH = exports.RETAIL_ID_TEXT_MAX_LENGTH = exports.RETAIL_EVENT_PIECES_MAX = exports.RETAIL_LIVE_CHECK_IDENTIFIERS_MAX = exports.RETAIL_DECIMAL_PATTERN = exports.RETAIL_SKU_PATTERN = exports.RETAIL_ZIP_CODE_MAX_LENGTH = exports.RETAIL_ZIP_CODE_MIN_LENGTH = exports.RETAIL_STORE_CODE_MAX_LENGTH = exports.RETAIL_ZONE_ID_PATTERN = exports.RETAIL_ENTITY_ID_PATTERN = exports.RETAIL_TENANT_ID_PATTERN = exports.RETAIL_LIST_LIMIT_MAX = exports.RETAIL_LIST_LIMIT_MIN = exports.RETAIL_EVENT_ID_MAX_LENGTH = void 0;
4
4
  /** Tope de `eventId`: el límite seguro de una llave de DynamoDB. */
5
5
  exports.RETAIL_EVENT_ID_MAX_LENGTH = 1024;
6
6
  /** Rango de `limit` en las listas paginadas del retailer. */
@@ -48,3 +48,18 @@ exports.RETAIL_LINE_INDEX_MIN = 0;
48
48
  exports.RETAIL_LINE_INDEX_MAX = 999;
49
49
  /** Rango máximo de `GET /inventory/movements`: 7 días en milisegundos. */
50
50
  exports.RETAIL_MOVEMENTS_RANGE_MAX_MS = 7 * 24 * 60 * 60 * 1000;
51
+ /** Decimal de la venta y la cancelación: hasta 12 enteros y dos decimales, nunca negativo. */
52
+ exports.RETAIL_SALE_DECIMAL_PATTERN = /^\d{1,12}\.\d{2}$/;
53
+ /** Decimal de la venta mayor que cero, como `payments[].amount`. */
54
+ exports.RETAIL_SALE_POSITIVE_DECIMAL_PATTERN = /^(?=.*[1-9])\d{1,12}\.\d{2}$/;
55
+ /** Texto libre corto de la venta, como `authCode` y `promotionId`: no vacío y de a lo más 64. */
56
+ exports.RETAIL_SALE_ID_TEXT_PATTERN = /^(?=.*\S).{1,64}$/;
57
+ /** Últimos 4 dígitos de la tarjeta en un pago `CARD`. */
58
+ exports.RETAIL_CARD_LAST4_PATTERN = /^\d{4}$/;
59
+ /** Clave de rastreo SPEI y CLABE de un pago `WIRE`: 18 dígitos. */
60
+ exports.RETAIL_SPEI_TRACKING_KEY_PATTERN = /^\d{18}$/;
61
+ exports.RETAIL_CLABE_PATTERN = /^\d{18}$/;
62
+ /** Tope del motivo de cancelación de una venta. */
63
+ exports.RETAIL_CANCEL_REASON_MAX_LENGTH = 200;
64
+ /** Teléfono del cliente en la venta: 10 dígitos. */
65
+ exports.RETAIL_CUSTOMER_PHONE_PATTERN = /^\d{10}$/;
@@ -1,6 +1,13 @@
1
+ /** Lo que regresa dañado al cancelar; `identifier` en `null` si el producto es `QUANTITY`. */
2
+ export declare class RetailSystemDamagedItem {
3
+ sku: string;
4
+ identifier: string | null;
5
+ qty: number;
6
+ }
1
7
  /** Cuerpo de `POST /private/retail/sales/cancel`; el `saleId` es el mismo del registro. */
2
8
  export declare class CancelRetailSaleRequest {
3
9
  saleId: string;
4
10
  cancelledAt: string;
5
11
  reason: string;
12
+ damaged: RetailSystemDamagedItem[];
6
13
  }
@@ -9,9 +9,35 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.CancelRetailSaleRequest = void 0;
12
+ exports.CancelRetailSaleRequest = exports.RetailSystemDamagedItem = void 0;
13
+ const class_transformer_1 = require("class-transformer");
13
14
  const class_validator_1 = require("class-validator");
15
+ const retailIntegrationLimits_1 = require("../constants/retailIntegrationLimits");
14
16
  const IsIsoDateTimeWithZone_1 = require("../validators/IsIsoDateTimeWithZone");
17
+ const payloadValidators_1 = require("../validators/payloadValidators");
18
+ const saleValidators_1 = require("../validators/saleValidators");
19
+ /** Lo que regresa dañado al cancelar; `identifier` en `null` si el producto es `QUANTITY`. */
20
+ class RetailSystemDamagedItem {
21
+ }
22
+ exports.RetailSystemDamagedItem = RetailSystemDamagedItem;
23
+ __decorate([
24
+ (0, class_validator_1.IsString)(),
25
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SKU_PATTERN),
26
+ __metadata("design:type", String)
27
+ ], RetailSystemDamagedItem.prototype, "sku", void 0);
28
+ __decorate([
29
+ (0, class_validator_1.ValidateIf)(payloadValidators_1.isNullOrValidated),
30
+ (0, class_validator_1.IsString)(),
31
+ (0, class_validator_1.Matches)(/\S/),
32
+ (0, class_validator_1.MaxLength)(retailIntegrationLimits_1.RETAIL_SHORT_TEXT_MAX_LENGTH),
33
+ __metadata("design:type", String)
34
+ ], RetailSystemDamagedItem.prototype, "identifier", void 0);
35
+ __decorate([
36
+ (0, class_validator_1.IsInt)(),
37
+ (0, class_validator_1.Min)(1),
38
+ (0, class_validator_1.Validate)(saleValidators_1.QtyMatchesIdentifier),
39
+ __metadata("design:type", Number)
40
+ ], RetailSystemDamagedItem.prototype, "qty", void 0);
15
41
  /** Cuerpo de `POST /private/retail/sales/cancel`; el `saleId` es el mismo del registro. */
16
42
  class CancelRetailSaleRequest {
17
43
  }
@@ -28,5 +54,12 @@ __decorate([
28
54
  __decorate([
29
55
  (0, class_validator_1.IsString)(),
30
56
  (0, class_validator_1.Matches)(/\S/),
57
+ (0, class_validator_1.MaxLength)(retailIntegrationLimits_1.RETAIL_CANCEL_REASON_MAX_LENGTH),
31
58
  __metadata("design:type", String)
32
59
  ], CancelRetailSaleRequest.prototype, "reason", void 0);
60
+ __decorate([
61
+ (0, class_validator_1.IsArray)(),
62
+ (0, class_validator_1.ValidateNested)({ each: true }),
63
+ (0, class_transformer_1.Type)(() => RetailSystemDamagedItem),
64
+ __metadata("design:type", Array)
65
+ ], CancelRetailSaleRequest.prototype, "damaged", void 0);
@@ -0,0 +1,6 @@
1
+ /** Cada cuánto paga el cliente en `credit.paymentFrequency`. */
2
+ export declare enum RetailPaymentFrequencyEnum {
3
+ WEEKLY = "WEEKLY",
4
+ BIWEEKLY = "BIWEEKLY",
5
+ MONTHLY = "MONTHLY"
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetailPaymentFrequencyEnum = void 0;
4
+ /** Cada cuánto paga el cliente en `credit.paymentFrequency`. */
5
+ var RetailPaymentFrequencyEnum;
6
+ (function (RetailPaymentFrequencyEnum) {
7
+ RetailPaymentFrequencyEnum["WEEKLY"] = "WEEKLY";
8
+ RetailPaymentFrequencyEnum["BIWEEKLY"] = "BIWEEKLY";
9
+ RetailPaymentFrequencyEnum["MONTHLY"] = "MONTHLY";
10
+ })(RetailPaymentFrequencyEnum || (exports.RetailPaymentFrequencyEnum = RetailPaymentFrequencyEnum = {}));
@@ -1,6 +1,7 @@
1
- /** Método de pago de la venta. */
1
+ /** Medio de pago de `payments[].method` en la venta. */
2
2
  export declare enum RetailPaymentMethodEnum {
3
3
  CASH = "CASH",
4
4
  CARD = "CARD",
5
- SPEI = "SPEI"
5
+ WIRE = "WIRE",
6
+ FIADO = "FIADO"
6
7
  }
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RetailPaymentMethodEnum = void 0;
4
- /** Método de pago de la venta. */
4
+ /** Medio de pago de `payments[].method` en la venta. */
5
5
  var RetailPaymentMethodEnum;
6
6
  (function (RetailPaymentMethodEnum) {
7
7
  RetailPaymentMethodEnum["CASH"] = "CASH";
8
8
  RetailPaymentMethodEnum["CARD"] = "CARD";
9
- RetailPaymentMethodEnum["SPEI"] = "SPEI";
9
+ RetailPaymentMethodEnum["WIRE"] = "WIRE";
10
+ RetailPaymentMethodEnum["FIADO"] = "FIADO";
10
11
  })(RetailPaymentMethodEnum || (exports.RetailPaymentMethodEnum = RetailPaymentMethodEnum = {}));
@@ -18,6 +18,7 @@ export * from './RetailUserAccessStatusEnum';
18
18
  export * from './RetailSaleTypeEnum';
19
19
  export * from './RetailSaleFinancierEnum';
20
20
  export * from './RetailPaymentMethodEnum';
21
+ export * from './RetailPaymentFrequencyEnum';
21
22
  export * from './RetailLinePaymentTypeEnum';
22
23
  export * from './RetailSaleLineRoleEnum';
23
24
  export * from './RetailEventRejectCodeEnum';
@@ -34,6 +34,7 @@ __exportStar(require("./RetailUserAccessStatusEnum"), exports);
34
34
  __exportStar(require("./RetailSaleTypeEnum"), exports);
35
35
  __exportStar(require("./RetailSaleFinancierEnum"), exports);
36
36
  __exportStar(require("./RetailPaymentMethodEnum"), exports);
37
+ __exportStar(require("./RetailPaymentFrequencyEnum"), exports);
37
38
  __exportStar(require("./RetailLinePaymentTypeEnum"), exports);
38
39
  __exportStar(require("./RetailSaleLineRoleEnum"), exports);
39
40
  __exportStar(require("./RetailEventRejectCodeEnum"), exports);
@@ -3,6 +3,8 @@ export * from './constants/retailIntegrationErrorHttpStatus';
3
3
  export * from './enums';
4
4
  export * from './validators/IsIsoDateTimeWithZone';
5
5
  export * from './validators/IsMovementsRangeValid';
6
+ export * from './validators/IsCalendarDate';
7
+ export * from './validators/saleValidators';
6
8
  export * from './dtos/RetailEventEnvelope';
7
9
  export * from './dtos/ReceiveStoreEventRequest';
8
10
  export * from './dtos/ReceiveRetailUserEventRequest';
@@ -19,6 +19,8 @@ __exportStar(require("./constants/retailIntegrationErrorHttpStatus"), exports);
19
19
  __exportStar(require("./enums"), exports);
20
20
  __exportStar(require("./validators/IsIsoDateTimeWithZone"), exports);
21
21
  __exportStar(require("./validators/IsMovementsRangeValid"), exports);
22
+ __exportStar(require("./validators/IsCalendarDate"), exports);
23
+ __exportStar(require("./validators/saleValidators"), exports);
22
24
  __exportStar(require("./dtos/RetailEventEnvelope"), exports);
23
25
  __exportStar(require("./dtos/ReceiveStoreEventRequest"), exports);
24
26
  __exportStar(require("./dtos/ReceiveRetailUserEventRequest"), exports);
@@ -1,8 +1,9 @@
1
- import { RetailLinePaymentTypeEnum, RetailPaymentMethodEnum, RetailSaleFinancierEnum, RetailSaleLineRoleEnum, RetailSaleTypeEnum } from '../enums';
1
+ import { RetailLinePaymentTypeEnum, RetailPaymentFrequencyEnum, RetailPaymentMethodEnum, RetailSaleFinancierEnum, RetailSaleLineRoleEnum, RetailSaleTypeEnum } from '../enums';
2
2
  import { RetailCurrency } from './RetailSystemProduct';
3
3
  export type RetailSaleType = `${RetailSaleTypeEnum}`;
4
4
  export type RetailSaleFinancier = `${RetailSaleFinancierEnum}`;
5
5
  export type RetailPaymentMethod = `${RetailPaymentMethodEnum}`;
6
+ export type RetailPaymentFrequency = `${RetailPaymentFrequencyEnum}`;
6
7
  export type RetailLinePaymentType = `${RetailLinePaymentTypeEnum}`;
7
8
  export type RetailSaleLineRole = `${RetailSaleLineRoleEnum}`;
8
9
  export declare class RetailSystemSaleCustomer {
@@ -12,6 +13,24 @@ export declare class RetailSystemSaleCustomer {
12
13
  maternalLastName: string | null;
13
14
  phone: string;
14
15
  }
16
+ /** Condiciones del crédito; todas obligatorias cuando `credit` no es `null`. */
17
+ export declare class RetailSystemSaleCredit {
18
+ paymentFrequency: RetailPaymentFrequency;
19
+ installments: number;
20
+ installmentAmount: string;
21
+ downPayment: string;
22
+ totalToPay: string;
23
+ firstPaymentDate: string;
24
+ }
25
+ /** Un medio de pago de `amountPaid`; las seis llaves van siempre, en `null` las que no aplican. */
26
+ export declare class RetailSystemSalePayment {
27
+ method: RetailPaymentMethod;
28
+ amount: string;
29
+ cardLast4: string | null;
30
+ authCode: string | null;
31
+ speiTrackingKey: string | null;
32
+ senderClabe: string | null;
33
+ }
15
34
  export declare class RetailSystemSaleComponent {
16
35
  sku: string;
17
36
  qty: number;
@@ -26,6 +45,7 @@ export declare class RetailSystemSaleLine {
26
45
  paymentType: RetailLinePaymentType;
27
46
  financedAmount: string;
28
47
  role: RetailSaleLineRole | null;
48
+ promotionId: string | null;
29
49
  components: RetailSystemSaleComponent[];
30
50
  }
31
51
  /** Cuerpo de `POST /sales` de la doc y de `privateRegisterRetailSale`; `saleId` es la llave de idempotencia. */
@@ -38,10 +58,11 @@ export declare class RetailSystemSale {
38
58
  saleType: RetailSaleType;
39
59
  financier: RetailSaleFinancier;
40
60
  creditId: string | null;
61
+ credit: RetailSystemSaleCredit | null;
41
62
  currencyId: RetailCurrency;
42
63
  customer: RetailSystemSaleCustomer;
43
64
  equipmentPrice: string;
44
65
  amountPaid: string;
45
- paymentMethod: RetailPaymentMethod | null;
66
+ payments: RetailSystemSalePayment[];
46
67
  lines: RetailSystemSaleLine[];
47
68
  }
@@ -9,13 +9,16 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.RetailSystemSale = exports.RetailSystemSaleLine = exports.RetailSystemSaleComponent = exports.RetailSystemSaleCustomer = void 0;
12
+ exports.RetailSystemSale = exports.RetailSystemSaleLine = exports.RetailSystemSaleComponent = exports.RetailSystemSalePayment = exports.RetailSystemSaleCredit = exports.RetailSystemSaleCustomer = void 0;
13
13
  const class_transformer_1 = require("class-transformer");
14
14
  const class_validator_1 = require("class-validator");
15
15
  const retailIntegrationLimits_1 = require("../constants/retailIntegrationLimits");
16
16
  const enums_1 = require("../enums");
17
+ const IsCalendarDate_1 = require("../validators/IsCalendarDate");
17
18
  const IsIsoDateTimeWithZone_1 = require("../validators/IsIsoDateTimeWithZone");
18
19
  const payloadValidators_1 = require("../validators/payloadValidators");
20
+ const saleValidators_1 = require("../validators/saleValidators");
21
+ const { CARD, WIRE } = enums_1.RetailPaymentMethodEnum;
19
22
  class RetailSystemSaleCustomer {
20
23
  }
21
24
  exports.RetailSystemSaleCustomer = RetailSystemSaleCustomer;
@@ -41,9 +44,70 @@ __decorate([
41
44
  ], RetailSystemSaleCustomer.prototype, "maternalLastName", void 0);
42
45
  __decorate([
43
46
  (0, class_validator_1.IsString)(),
44
- (0, class_validator_1.Matches)(/\S/),
47
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_CUSTOMER_PHONE_PATTERN),
45
48
  __metadata("design:type", String)
46
49
  ], RetailSystemSaleCustomer.prototype, "phone", void 0);
50
+ /** Condiciones del crédito; todas obligatorias cuando `credit` no es `null`. */
51
+ class RetailSystemSaleCredit {
52
+ }
53
+ exports.RetailSystemSaleCredit = RetailSystemSaleCredit;
54
+ __decorate([
55
+ (0, class_validator_1.IsIn)(Object.values(enums_1.RetailPaymentFrequencyEnum)),
56
+ __metadata("design:type", String)
57
+ ], RetailSystemSaleCredit.prototype, "paymentFrequency", void 0);
58
+ __decorate([
59
+ (0, class_validator_1.IsInt)(),
60
+ (0, class_validator_1.Min)(1),
61
+ __metadata("design:type", Number)
62
+ ], RetailSystemSaleCredit.prototype, "installments", void 0);
63
+ __decorate([
64
+ (0, class_validator_1.IsString)(),
65
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
66
+ __metadata("design:type", String)
67
+ ], RetailSystemSaleCredit.prototype, "installmentAmount", void 0);
68
+ __decorate([
69
+ (0, class_validator_1.IsString)(),
70
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
71
+ __metadata("design:type", String)
72
+ ], RetailSystemSaleCredit.prototype, "downPayment", void 0);
73
+ __decorate([
74
+ (0, class_validator_1.IsString)(),
75
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
76
+ __metadata("design:type", String)
77
+ ], RetailSystemSaleCredit.prototype, "totalToPay", void 0);
78
+ __decorate([
79
+ (0, class_validator_1.Validate)(IsCalendarDate_1.IsCalendarDate),
80
+ __metadata("design:type", String)
81
+ ], RetailSystemSaleCredit.prototype, "firstPaymentDate", void 0);
82
+ /** Un medio de pago de `amountPaid`; las seis llaves van siempre, en `null` las que no aplican. */
83
+ class RetailSystemSalePayment {
84
+ }
85
+ exports.RetailSystemSalePayment = RetailSystemSalePayment;
86
+ __decorate([
87
+ (0, class_validator_1.IsIn)(Object.values(enums_1.RetailPaymentMethodEnum)),
88
+ __metadata("design:type", String)
89
+ ], RetailSystemSalePayment.prototype, "method", void 0);
90
+ __decorate([
91
+ (0, class_validator_1.IsString)(),
92
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_POSITIVE_DECIMAL_PATTERN),
93
+ __metadata("design:type", String)
94
+ ], RetailSystemSalePayment.prototype, "amount", void 0);
95
+ __decorate([
96
+ (0, class_validator_1.Validate)(saleValidators_1.MatchesForPaymentMethod, [CARD, retailIntegrationLimits_1.RETAIL_CARD_LAST4_PATTERN]),
97
+ __metadata("design:type", String)
98
+ ], RetailSystemSalePayment.prototype, "cardLast4", void 0);
99
+ __decorate([
100
+ (0, class_validator_1.Validate)(saleValidators_1.MatchesForPaymentMethod, [CARD, retailIntegrationLimits_1.RETAIL_SALE_ID_TEXT_PATTERN]),
101
+ __metadata("design:type", String)
102
+ ], RetailSystemSalePayment.prototype, "authCode", void 0);
103
+ __decorate([
104
+ (0, class_validator_1.Validate)(saleValidators_1.MatchesForPaymentMethod, [WIRE, retailIntegrationLimits_1.RETAIL_SPEI_TRACKING_KEY_PATTERN]),
105
+ __metadata("design:type", String)
106
+ ], RetailSystemSalePayment.prototype, "speiTrackingKey", void 0);
107
+ __decorate([
108
+ (0, class_validator_1.Validate)(saleValidators_1.MatchesForPaymentMethod, [WIRE, retailIntegrationLimits_1.RETAIL_CLABE_PATTERN, true]),
109
+ __metadata("design:type", String)
110
+ ], RetailSystemSalePayment.prototype, "senderClabe", void 0);
47
111
  class RetailSystemSaleComponent {
48
112
  }
49
113
  exports.RetailSystemSaleComponent = RetailSystemSaleComponent;
@@ -83,12 +147,12 @@ __decorate([
83
147
  ], RetailSystemSaleLine.prototype, "qty", void 0);
84
148
  __decorate([
85
149
  (0, class_validator_1.IsString)(),
86
- (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_DECIMAL_PATTERN),
150
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
87
151
  __metadata("design:type", String)
88
152
  ], RetailSystemSaleLine.prototype, "basePrice", void 0);
89
153
  __decorate([
90
154
  (0, class_validator_1.IsString)(),
91
- (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_DECIMAL_PATTERN),
155
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
92
156
  __metadata("design:type", String)
93
157
  ], RetailSystemSaleLine.prototype, "discount", void 0);
94
158
  __decorate([
@@ -97,7 +161,7 @@ __decorate([
97
161
  ], RetailSystemSaleLine.prototype, "paymentType", void 0);
98
162
  __decorate([
99
163
  (0, class_validator_1.IsString)(),
100
- (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_DECIMAL_PATTERN),
164
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
101
165
  __metadata("design:type", String)
102
166
  ], RetailSystemSaleLine.prototype, "financedAmount", void 0);
103
167
  __decorate([
@@ -105,6 +169,10 @@ __decorate([
105
169
  (0, class_validator_1.IsIn)(Object.values(enums_1.RetailSaleLineRoleEnum)),
106
170
  __metadata("design:type", String)
107
171
  ], RetailSystemSaleLine.prototype, "role", void 0);
172
+ __decorate([
173
+ (0, class_validator_1.Validate)(saleValidators_1.PromotionIdMatchesRole),
174
+ __metadata("design:type", String)
175
+ ], RetailSystemSaleLine.prototype, "promotionId", void 0);
108
176
  __decorate([
109
177
  (0, class_validator_1.IsArray)(),
110
178
  (0, class_validator_1.ValidateNested)({ each: true }),
@@ -145,6 +213,7 @@ __decorate([
145
213
  ], RetailSystemSale.prototype, "saleType", void 0);
146
214
  __decorate([
147
215
  (0, class_validator_1.IsIn)(Object.values(enums_1.RetailSaleFinancierEnum)),
216
+ (0, class_validator_1.Validate)(saleValidators_1.CreditMatchesFinancier),
148
217
  __metadata("design:type", String)
149
218
  ], RetailSystemSale.prototype, "financier", void 0);
150
219
  __decorate([
@@ -153,6 +222,13 @@ __decorate([
153
222
  (0, class_validator_1.Matches)(/\S/),
154
223
  __metadata("design:type", String)
155
224
  ], RetailSystemSale.prototype, "creditId", void 0);
225
+ __decorate([
226
+ (0, class_validator_1.ValidateIf)(payloadValidators_1.isNullOrValidated),
227
+ (0, class_validator_1.IsObject)(),
228
+ (0, class_validator_1.ValidateNested)(),
229
+ (0, class_transformer_1.Type)(() => RetailSystemSaleCredit),
230
+ __metadata("design:type", RetailSystemSaleCredit)
231
+ ], RetailSystemSale.prototype, "credit", void 0);
156
232
  __decorate([
157
233
  (0, class_validator_1.IsIn)(Object.values(enums_1.RetailCurrencyEnum)),
158
234
  __metadata("design:type", String)
@@ -165,19 +241,21 @@ __decorate([
165
241
  ], RetailSystemSale.prototype, "customer", void 0);
166
242
  __decorate([
167
243
  (0, class_validator_1.IsString)(),
168
- (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_DECIMAL_PATTERN),
244
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
169
245
  __metadata("design:type", String)
170
246
  ], RetailSystemSale.prototype, "equipmentPrice", void 0);
171
247
  __decorate([
172
248
  (0, class_validator_1.IsString)(),
173
- (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_DECIMAL_PATTERN),
249
+ (0, class_validator_1.Matches)(retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN),
174
250
  __metadata("design:type", String)
175
251
  ], RetailSystemSale.prototype, "amountPaid", void 0);
176
252
  __decorate([
177
- (0, class_validator_1.ValidateIf)(payloadValidators_1.isNullOrValidated),
178
- (0, class_validator_1.IsIn)(Object.values(enums_1.RetailPaymentMethodEnum)),
179
- __metadata("design:type", String)
180
- ], RetailSystemSale.prototype, "paymentMethod", void 0);
253
+ (0, class_validator_1.IsArray)(),
254
+ (0, class_validator_1.ValidateNested)({ each: true }),
255
+ (0, class_transformer_1.Type)(() => RetailSystemSalePayment),
256
+ (0, class_validator_1.Validate)(saleValidators_1.PaymentsMatchAmountPaid),
257
+ __metadata("design:type", Array)
258
+ ], RetailSystemSale.prototype, "payments", void 0);
181
259
  __decorate([
182
260
  (0, class_validator_1.IsArray)(),
183
261
  (0, class_validator_1.ArrayNotEmpty)(),
@@ -0,0 +1,6 @@
1
+ import { ValidatorConstraintInterface } from 'class-validator';
2
+ /** Acepta una fecha `YYYY-MM-DD` que exista en el calendario. */
3
+ export declare class IsCalendarDate implements ValidatorConstraintInterface {
4
+ validate(value: unknown): boolean;
5
+ defaultMessage(): string;
6
+ }
@@ -0,0 +1,25 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IsCalendarDate = void 0;
10
+ const class_validator_1 = require("class-validator");
11
+ const calendarDate_1 = require("./calendarDate");
12
+ const CALENDAR_DATE = /^\d{4}-\d{2}-\d{2}$/;
13
+ /** Acepta una fecha `YYYY-MM-DD` que exista en el calendario. */
14
+ let IsCalendarDate = class IsCalendarDate {
15
+ validate(value) {
16
+ return typeof value === 'string' && CALENDAR_DATE.test(value) && (0, calendarDate_1.isRealCalendarDate)(value);
17
+ }
18
+ defaultMessage() {
19
+ return 'debe ser una fecha YYYY-MM-DD, por ejemplo 2026-08-21';
20
+ }
21
+ };
22
+ exports.IsCalendarDate = IsCalendarDate;
23
+ exports.IsCalendarDate = IsCalendarDate = __decorate([
24
+ (0, class_validator_1.ValidatorConstraint)({ name: 'isCalendarDate', async: false })
25
+ ], IsCalendarDate);
@@ -8,16 +8,12 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.IsIsoDateTimeWithZone = void 0;
10
10
  const class_validator_1 = require("class-validator");
11
+ const calendarDate_1 = require("./calendarDate");
11
12
  const ISO_DATE_TIME_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
12
- /** Descarta fechas que `Date` recorre en silencio, como el 31 de febrero. */
13
- function isRealCalendarDate(value) {
14
- const [year, month, day] = value.slice(0, 10).split('-').map(Number);
15
- return !Number.isNaN(Date.parse(value)) && new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;
16
- }
17
13
  /** Acepta fecha y hora ISO-8601 con segundos y zona; sin zona se rechaza, como dice la doc. */
18
14
  let IsIsoDateTimeWithZone = class IsIsoDateTimeWithZone {
19
15
  validate(value) {
20
- return typeof value === 'string' && ISO_DATE_TIME_WITH_ZONE.test(value) && isRealCalendarDate(value);
16
+ return typeof value === 'string' && ISO_DATE_TIME_WITH_ZONE.test(value) && (0, calendarDate_1.isRealCalendarDate)(value);
21
17
  }
22
18
  defaultMessage() {
23
19
  return 'debe ser fecha y hora ISO-8601 con zona, por ejemplo 2026-08-17T20:32:05Z';
@@ -0,0 +1,2 @@
1
+ /** Descarta fechas que `Date` recorre en silencio, como el 31 de febrero; lee los primeros 10 caracteres. */
2
+ export declare function isRealCalendarDate(value: string): boolean;
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRealCalendarDate = isRealCalendarDate;
4
+ /** Descarta fechas que `Date` recorre en silencio, como el 31 de febrero; lee los primeros 10 caracteres. */
5
+ function isRealCalendarDate(value) {
6
+ const [year, month, day] = value.slice(0, 10).split('-').map(Number);
7
+ return !Number.isNaN(Date.parse(value)) && new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;
8
+ }
@@ -0,0 +1,26 @@
1
+ import { ValidationArguments, ValidatorConstraintInterface } from 'class-validator';
2
+ /** Campo de un pago que solo aplica a un medio: con ese medio cumple el patrón; con los demás es `null`. */
3
+ export declare class MatchesForPaymentMethod implements ValidatorConstraintInterface {
4
+ validate(value: unknown, args: ValidationArguments): boolean;
5
+ defaultMessage(args: ValidationArguments): string;
6
+ }
7
+ /** La suma de `payments[].amount` es `amountPaid`; con `amountPaid` en `"0.00"` el arreglo va vacío. */
8
+ export declare class PaymentsMatchAmountPaid implements ValidatorConstraintInterface {
9
+ validate(value: unknown, args: ValidationArguments): boolean;
10
+ defaultMessage(): string;
11
+ }
12
+ /** `credit` y `creditId`: obligatorios con `SUREKEEP` y `null` en contado. */
13
+ export declare class CreditMatchesFinancier implements ValidatorConstraintInterface {
14
+ validate(value: unknown, args: ValidationArguments): boolean;
15
+ defaultMessage(): string;
16
+ }
17
+ /** `promotionId`: texto o `null`, nunca omitido; un regalo siempre lo trae. */
18
+ export declare class PromotionIdMatchesRole implements ValidatorConstraintInterface {
19
+ validate(value: unknown, args: ValidationArguments): boolean;
20
+ defaultMessage(): string;
21
+ }
22
+ /** Un dañado con `identifier` es una sola pieza: `qty` debe ser 1. */
23
+ export declare class QtyMatchesIdentifier implements ValidatorConstraintInterface {
24
+ validate(value: unknown, args: ValidationArguments): boolean;
25
+ defaultMessage(): string;
26
+ }
@@ -0,0 +1,100 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.QtyMatchesIdentifier = exports.PromotionIdMatchesRole = exports.CreditMatchesFinancier = exports.PaymentsMatchAmountPaid = exports.MatchesForPaymentMethod = void 0;
10
+ const class_validator_1 = require("class-validator");
11
+ const retailIntegrationLimits_1 = require("../constants/retailIntegrationLimits");
12
+ const enums_1 = require("../enums");
13
+ function decimalToCents(value) {
14
+ return Number(value.replace('.', ''));
15
+ }
16
+ /** Campo de un pago que solo aplica a un medio: con ese medio cumple el patrón; con los demás es `null`. */
17
+ let MatchesForPaymentMethod = class MatchesForPaymentMethod {
18
+ validate(value, args) {
19
+ const [method, pattern, nullable] = args.constraints;
20
+ if (args.object.method !== method)
21
+ return value === null;
22
+ if (value === null)
23
+ return nullable === true;
24
+ return typeof value === 'string' && pattern.test(value);
25
+ }
26
+ defaultMessage(args) {
27
+ return `${args.property} es obligatorio y válido en ${args.constraints[0]} y null en los demás medios`;
28
+ }
29
+ };
30
+ exports.MatchesForPaymentMethod = MatchesForPaymentMethod;
31
+ exports.MatchesForPaymentMethod = MatchesForPaymentMethod = __decorate([
32
+ (0, class_validator_1.ValidatorConstraint)({ name: 'matchesForPaymentMethod', async: false })
33
+ ], MatchesForPaymentMethod);
34
+ /** La suma de `payments[].amount` es `amountPaid`; con `amountPaid` en `"0.00"` el arreglo va vacío. */
35
+ let PaymentsMatchAmountPaid = class PaymentsMatchAmountPaid {
36
+ validate(value, args) {
37
+ const amountPaid = args.object.amountPaid;
38
+ if (!Array.isArray(value) || typeof amountPaid !== 'string' || !retailIntegrationLimits_1.RETAIL_SALE_DECIMAL_PATTERN.test(amountPaid))
39
+ return true;
40
+ const amounts = value.map((p) => p?.amount);
41
+ if (!amounts.every((a) => typeof a === 'string' && retailIntegrationLimits_1.RETAIL_SALE_POSITIVE_DECIMAL_PATTERN.test(a)))
42
+ return true;
43
+ const total = amounts.reduce((sum, a) => sum + decimalToCents(a), 0);
44
+ return total === decimalToCents(amountPaid);
45
+ }
46
+ defaultMessage() {
47
+ return 'la suma de payments[].amount debe ser amountPaid; [] solo si amountPaid es 0.00';
48
+ }
49
+ };
50
+ exports.PaymentsMatchAmountPaid = PaymentsMatchAmountPaid;
51
+ exports.PaymentsMatchAmountPaid = PaymentsMatchAmountPaid = __decorate([
52
+ (0, class_validator_1.ValidatorConstraint)({ name: 'paymentsMatchAmountPaid', async: false })
53
+ ], PaymentsMatchAmountPaid);
54
+ /** `credit` y `creditId`: obligatorios con `SUREKEEP` y `null` en contado. */
55
+ let CreditMatchesFinancier = class CreditMatchesFinancier {
56
+ validate(value, args) {
57
+ const { saleType, credit, creditId } = args.object;
58
+ if (value === enums_1.RetailSaleFinancierEnum.SUREKEEP)
59
+ return credit !== null && creditId !== null;
60
+ if (value === enums_1.RetailSaleFinancierEnum.NONE || saleType === enums_1.RetailSaleTypeEnum.CASH)
61
+ return credit == null && creditId == null;
62
+ return true;
63
+ }
64
+ defaultMessage() {
65
+ return 'credit y creditId son obligatorios con SUREKEEP y null en contado';
66
+ }
67
+ };
68
+ exports.CreditMatchesFinancier = CreditMatchesFinancier;
69
+ exports.CreditMatchesFinancier = CreditMatchesFinancier = __decorate([
70
+ (0, class_validator_1.ValidatorConstraint)({ name: 'creditMatchesFinancier', async: false })
71
+ ], CreditMatchesFinancier);
72
+ /** `promotionId`: texto o `null`, nunca omitido; un regalo siempre lo trae. */
73
+ let PromotionIdMatchesRole = class PromotionIdMatchesRole {
74
+ validate(value, args) {
75
+ if (value === null)
76
+ return args.object.role !== enums_1.RetailSaleLineRoleEnum.GIFT;
77
+ return typeof value === 'string' && retailIntegrationLimits_1.RETAIL_SALE_ID_TEXT_PATTERN.test(value);
78
+ }
79
+ defaultMessage() {
80
+ return 'promotionId es texto de 1 a 64 o null, y obligatorio con role GIFT';
81
+ }
82
+ };
83
+ exports.PromotionIdMatchesRole = PromotionIdMatchesRole;
84
+ exports.PromotionIdMatchesRole = PromotionIdMatchesRole = __decorate([
85
+ (0, class_validator_1.ValidatorConstraint)({ name: 'promotionIdMatchesRole', async: false })
86
+ ], PromotionIdMatchesRole);
87
+ /** Un dañado con `identifier` es una sola pieza: `qty` debe ser 1. */
88
+ let QtyMatchesIdentifier = class QtyMatchesIdentifier {
89
+ validate(value, args) {
90
+ const identifier = args.object.identifier;
91
+ return typeof identifier !== 'string' || value === 1;
92
+ }
93
+ defaultMessage() {
94
+ return 'qty debe ser 1 cuando viene identifier';
95
+ }
96
+ };
97
+ exports.QtyMatchesIdentifier = QtyMatchesIdentifier;
98
+ exports.QtyMatchesIdentifier = QtyMatchesIdentifier = __decorate([
99
+ (0, class_validator_1.ValidatorConstraint)({ name: 'qtyMatchesIdentifier', async: false })
100
+ ], QtyMatchesIdentifier);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/type-kit",
3
- "version": "3.602.0",
3
+ "version": "3.603.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",
@@ -66,3 +66,25 @@ export const RETAIL_LINE_INDEX_MAX = 999;
66
66
 
67
67
  /** Rango máximo de `GET /inventory/movements`: 7 días en milisegundos. */
68
68
  export const RETAIL_MOVEMENTS_RANGE_MAX_MS = 7 * 24 * 60 * 60 * 1000;
69
+
70
+ /** Decimal de la venta y la cancelación: hasta 12 enteros y dos decimales, nunca negativo. */
71
+ export const RETAIL_SALE_DECIMAL_PATTERN = /^\d{1,12}\.\d{2}$/;
72
+
73
+ /** Decimal de la venta mayor que cero, como `payments[].amount`. */
74
+ export const RETAIL_SALE_POSITIVE_DECIMAL_PATTERN = /^(?=.*[1-9])\d{1,12}\.\d{2}$/;
75
+
76
+ /** Texto libre corto de la venta, como `authCode` y `promotionId`: no vacío y de a lo más 64. */
77
+ export const RETAIL_SALE_ID_TEXT_PATTERN = /^(?=.*\S).{1,64}$/;
78
+
79
+ /** Últimos 4 dígitos de la tarjeta en un pago `CARD`. */
80
+ export const RETAIL_CARD_LAST4_PATTERN = /^\d{4}$/;
81
+
82
+ /** Clave de rastreo SPEI y CLABE de un pago `WIRE`: 18 dígitos. */
83
+ export const RETAIL_SPEI_TRACKING_KEY_PATTERN = /^\d{18}$/;
84
+ export const RETAIL_CLABE_PATTERN = /^\d{18}$/;
85
+
86
+ /** Tope del motivo de cancelación de una venta. */
87
+ export const RETAIL_CANCEL_REASON_MAX_LENGTH = 200;
88
+
89
+ /** Teléfono del cliente en la venta: 10 dígitos. */
90
+ export const RETAIL_CUSTOMER_PHONE_PATTERN = /^\d{10}$/;
@@ -1,9 +1,21 @@
1
- import { IsString, Matches, Validate } from 'class-validator';
1
+ import { Type } from 'class-transformer';
2
+ import { IsArray, IsInt, IsString, Matches, MaxLength, Min, Validate, ValidateIf, ValidateNested } from 'class-validator';
3
+ import { RETAIL_CANCEL_REASON_MAX_LENGTH, RETAIL_SHORT_TEXT_MAX_LENGTH, RETAIL_SKU_PATTERN } from '../constants/retailIntegrationLimits';
2
4
  import { IsIsoDateTimeWithZone } from '../validators/IsIsoDateTimeWithZone';
5
+ import { isNullOrValidated } from '../validators/payloadValidators';
6
+ import { QtyMatchesIdentifier } from '../validators/saleValidators';
7
+
8
+ /** Lo que regresa dañado al cancelar; `identifier` en `null` si el producto es `QUANTITY`. */
9
+ export class RetailSystemDamagedItem {
10
+ @IsString() @Matches(RETAIL_SKU_PATTERN) sku!: string;
11
+ @ValidateIf(isNullOrValidated) @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) identifier!: string | null;
12
+ @IsInt() @Min(1) @Validate(QtyMatchesIdentifier) qty!: number;
13
+ }
3
14
 
4
15
  /** Cuerpo de `POST /private/retail/sales/cancel`; el `saleId` es el mismo del registro. */
5
16
  export class CancelRetailSaleRequest {
6
17
  @IsString() @Matches(/\S/) saleId!: string;
7
18
  @Validate(IsIsoDateTimeWithZone) cancelledAt!: string;
8
- @IsString() @Matches(/\S/) reason!: string;
19
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_CANCEL_REASON_MAX_LENGTH) reason!: string;
20
+ @IsArray() @ValidateNested({ each: true }) @Type(() => RetailSystemDamagedItem) damaged!: RetailSystemDamagedItem[];
9
21
  }
@@ -0,0 +1,6 @@
1
+ /** Cada cuánto paga el cliente en `credit.paymentFrequency`. */
2
+ export enum RetailPaymentFrequencyEnum {
3
+ WEEKLY = 'WEEKLY',
4
+ BIWEEKLY = 'BIWEEKLY',
5
+ MONTHLY = 'MONTHLY',
6
+ }
@@ -1,6 +1,7 @@
1
- /** Método de pago de la venta. */
1
+ /** Medio de pago de `payments[].method` en la venta. */
2
2
  export enum RetailPaymentMethodEnum {
3
3
  CASH = 'CASH',
4
4
  CARD = 'CARD',
5
- SPEI = 'SPEI',
5
+ WIRE = 'WIRE',
6
+ FIADO = 'FIADO',
6
7
  }
@@ -18,6 +18,7 @@ export * from './RetailUserAccessStatusEnum';
18
18
  export * from './RetailSaleTypeEnum';
19
19
  export * from './RetailSaleFinancierEnum';
20
20
  export * from './RetailPaymentMethodEnum';
21
+ export * from './RetailPaymentFrequencyEnum';
21
22
  export * from './RetailLinePaymentTypeEnum';
22
23
  export * from './RetailSaleLineRoleEnum';
23
24
  export * from './RetailEventRejectCodeEnum';
@@ -3,6 +3,8 @@ export * from './constants/retailIntegrationErrorHttpStatus';
3
3
  export * from './enums';
4
4
  export * from './validators/IsIsoDateTimeWithZone';
5
5
  export * from './validators/IsMovementsRangeValid';
6
+ export * from './validators/IsCalendarDate';
7
+ export * from './validators/saleValidators';
6
8
  export * from './dtos/RetailEventEnvelope';
7
9
  export * from './dtos/ReceiveStoreEventRequest';
8
10
  export * from './dtos/ReceiveRetailUserEventRequest';
@@ -1,23 +1,66 @@
1
1
  import { Type } from 'class-transformer';
2
2
  import { ArrayNotEmpty, IsArray, IsIn, IsInt, IsObject, IsString, Matches, Min, Validate, ValidateIf, ValidateNested } from 'class-validator';
3
- import { RETAIL_DECIMAL_PATTERN, RETAIL_ENTITY_ID_PATTERN, RETAIL_SKU_PATTERN } from '../constants/retailIntegrationLimits';
4
- import { RetailCurrencyEnum, RetailLinePaymentTypeEnum, RetailPaymentMethodEnum, RetailSaleFinancierEnum, RetailSaleLineRoleEnum, RetailSaleTypeEnum } from '../enums';
3
+ import {
4
+ RETAIL_CARD_LAST4_PATTERN,
5
+ RETAIL_CLABE_PATTERN,
6
+ RETAIL_CUSTOMER_PHONE_PATTERN,
7
+ RETAIL_ENTITY_ID_PATTERN,
8
+ RETAIL_SALE_DECIMAL_PATTERN,
9
+ RETAIL_SALE_ID_TEXT_PATTERN,
10
+ RETAIL_SALE_POSITIVE_DECIMAL_PATTERN,
11
+ RETAIL_SKU_PATTERN,
12
+ RETAIL_SPEI_TRACKING_KEY_PATTERN,
13
+ } from '../constants/retailIntegrationLimits';
14
+ import {
15
+ RetailCurrencyEnum,
16
+ RetailLinePaymentTypeEnum,
17
+ RetailPaymentFrequencyEnum,
18
+ RetailPaymentMethodEnum,
19
+ RetailSaleFinancierEnum,
20
+ RetailSaleLineRoleEnum,
21
+ RetailSaleTypeEnum,
22
+ } from '../enums';
23
+ import { IsCalendarDate } from '../validators/IsCalendarDate';
5
24
  import { IsIsoDateTimeWithZone } from '../validators/IsIsoDateTimeWithZone';
6
25
  import { HasUniqueKeys, isNullOrValidated } from '../validators/payloadValidators';
26
+ import { CreditMatchesFinancier, MatchesForPaymentMethod, PaymentsMatchAmountPaid, PromotionIdMatchesRole } from '../validators/saleValidators';
7
27
  import { RetailCurrency } from './RetailSystemProduct';
8
28
 
9
29
  export type RetailSaleType = `${RetailSaleTypeEnum}`;
10
30
  export type RetailSaleFinancier = `${RetailSaleFinancierEnum}`;
11
31
  export type RetailPaymentMethod = `${RetailPaymentMethodEnum}`;
32
+ export type RetailPaymentFrequency = `${RetailPaymentFrequencyEnum}`;
12
33
  export type RetailLinePaymentType = `${RetailLinePaymentTypeEnum}`;
13
34
  export type RetailSaleLineRole = `${RetailSaleLineRoleEnum}`;
14
35
 
36
+ const { CARD, WIRE } = RetailPaymentMethodEnum;
37
+
15
38
  export class RetailSystemSaleCustomer {
16
39
  @IsString() @Matches(/\S/) customerId!: string;
17
40
  @IsString() @Matches(/\S/) names!: string;
18
41
  @IsString() @Matches(/\S/) paternalLastName!: string;
19
42
  @ValidateIf(isNullOrValidated) @IsString() maternalLastName!: string | null;
20
- @IsString() @Matches(/\S/) phone!: string;
43
+ @IsString() @Matches(RETAIL_CUSTOMER_PHONE_PATTERN) phone!: string;
44
+ }
45
+
46
+ /** Condiciones del crédito; todas obligatorias cuando `credit` no es `null`. */
47
+ export class RetailSystemSaleCredit {
48
+ @IsIn(Object.values(RetailPaymentFrequencyEnum)) paymentFrequency!: RetailPaymentFrequency;
49
+ @IsInt() @Min(1) installments!: number;
50
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) installmentAmount!: string;
51
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) downPayment!: string;
52
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) totalToPay!: string;
53
+ @Validate(IsCalendarDate) firstPaymentDate!: string;
54
+ }
55
+
56
+ /** Un medio de pago de `amountPaid`; las seis llaves van siempre, en `null` las que no aplican. */
57
+ export class RetailSystemSalePayment {
58
+ @IsIn(Object.values(RetailPaymentMethodEnum)) method!: RetailPaymentMethod;
59
+ @IsString() @Matches(RETAIL_SALE_POSITIVE_DECIMAL_PATTERN) amount!: string;
60
+ @Validate(MatchesForPaymentMethod, [CARD, RETAIL_CARD_LAST4_PATTERN]) cardLast4!: string | null;
61
+ @Validate(MatchesForPaymentMethod, [CARD, RETAIL_SALE_ID_TEXT_PATTERN]) authCode!: string | null;
62
+ @Validate(MatchesForPaymentMethod, [WIRE, RETAIL_SPEI_TRACKING_KEY_PATTERN]) speiTrackingKey!: string | null;
63
+ @Validate(MatchesForPaymentMethod, [WIRE, RETAIL_CLABE_PATTERN, true]) senderClabe!: string | null;
21
64
  }
22
65
 
23
66
  export class RetailSystemSaleComponent {
@@ -30,11 +73,12 @@ export class RetailSystemSaleLine {
30
73
  @IsString() @Matches(/\S/) lineId!: string;
31
74
  @IsString() @Matches(RETAIL_SKU_PATTERN) sku!: string;
32
75
  @IsInt() @Min(1) qty!: number;
33
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) basePrice!: string;
34
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) discount!: string;
76
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) basePrice!: string;
77
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) discount!: string;
35
78
  @IsIn(Object.values(RetailLinePaymentTypeEnum)) paymentType!: RetailLinePaymentType;
36
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) financedAmount!: string;
79
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) financedAmount!: string;
37
80
  @ValidateIf(isNullOrValidated) @IsIn(Object.values(RetailSaleLineRoleEnum)) role!: RetailSaleLineRole | null;
81
+ @Validate(PromotionIdMatchesRole) promotionId!: string | null;
38
82
  @IsArray() @ValidateNested({ each: true }) @Type(() => RetailSystemSaleComponent) components!: RetailSystemSaleComponent[];
39
83
  }
40
84
 
@@ -46,13 +90,16 @@ export class RetailSystemSale {
46
90
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) sellerRetailUserId!: string;
47
91
  @Validate(IsIsoDateTimeWithZone) soldAt!: string;
48
92
  @IsIn(Object.values(RetailSaleTypeEnum)) saleType!: RetailSaleType;
49
- @IsIn(Object.values(RetailSaleFinancierEnum)) financier!: RetailSaleFinancier;
93
+ @IsIn(Object.values(RetailSaleFinancierEnum)) @Validate(CreditMatchesFinancier) financier!: RetailSaleFinancier;
50
94
  @ValidateIf(isNullOrValidated) @IsString() @Matches(/\S/) creditId!: string | null;
95
+ @ValidateIf(isNullOrValidated) @IsObject() @ValidateNested() @Type(() => RetailSystemSaleCredit) credit!: RetailSystemSaleCredit | null;
51
96
  @IsIn(Object.values(RetailCurrencyEnum)) currencyId!: RetailCurrency;
52
97
  @IsObject() @ValidateNested() @Type(() => RetailSystemSaleCustomer) customer!: RetailSystemSaleCustomer;
53
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) equipmentPrice!: string;
54
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) amountPaid!: string;
55
- @ValidateIf(isNullOrValidated) @IsIn(Object.values(RetailPaymentMethodEnum)) paymentMethod!: RetailPaymentMethod | null;
98
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) equipmentPrice!: string;
99
+ @IsString() @Matches(RETAIL_SALE_DECIMAL_PATTERN) amountPaid!: string;
100
+
101
+ @IsArray() @ValidateNested({ each: true }) @Type(() => RetailSystemSalePayment) @Validate(PaymentsMatchAmountPaid)
102
+ payments!: RetailSystemSalePayment[];
56
103
 
57
104
  @IsArray() @ArrayNotEmpty() @ValidateNested({ each: true }) @Type(() => RetailSystemSaleLine) @Validate(HasUniqueKeys, ['lineId'])
58
105
  lines!: RetailSystemSaleLine[];
@@ -0,0 +1,16 @@
1
+ import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
2
+ import { isRealCalendarDate } from './calendarDate';
3
+
4
+ const CALENDAR_DATE = /^\d{4}-\d{2}-\d{2}$/;
5
+
6
+ /** Acepta una fecha `YYYY-MM-DD` que exista en el calendario. */
7
+ @ValidatorConstraint({ name: 'isCalendarDate', async: false })
8
+ export class IsCalendarDate implements ValidatorConstraintInterface {
9
+ validate(value: unknown): boolean {
10
+ return typeof value === 'string' && CALENDAR_DATE.test(value) && isRealCalendarDate(value);
11
+ }
12
+
13
+ defaultMessage(): string {
14
+ return 'debe ser una fecha YYYY-MM-DD, por ejemplo 2026-08-21';
15
+ }
16
+ }
@@ -1,13 +1,8 @@
1
1
  import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
2
+ import { isRealCalendarDate } from './calendarDate';
2
3
 
3
4
  const ISO_DATE_TIME_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
4
5
 
5
- /** Descarta fechas que `Date` recorre en silencio, como el 31 de febrero. */
6
- function isRealCalendarDate(value: string): boolean {
7
- const [year, month, day] = value.slice(0, 10).split('-').map(Number);
8
- return !Number.isNaN(Date.parse(value)) && new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;
9
- }
10
-
11
6
  /** Acepta fecha y hora ISO-8601 con segundos y zona; sin zona se rechaza, como dice la doc. */
12
7
  @ValidatorConstraint({ name: 'isIsoDateTimeWithZone', async: false })
13
8
  export class IsIsoDateTimeWithZone implements ValidatorConstraintInterface {
@@ -0,0 +1,5 @@
1
+ /** Descarta fechas que `Date` recorre en silencio, como el 31 de febrero; lee los primeros 10 caracteres. */
2
+ export function isRealCalendarDate(value: string): boolean {
3
+ const [year, month, day] = value.slice(0, 10).split('-').map(Number);
4
+ return !Number.isNaN(Date.parse(value)) && new Date(Date.UTC(year, month - 1, day)).getUTCDate() === day;
5
+ }
@@ -0,0 +1,80 @@
1
+ import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
2
+ import { RETAIL_SALE_DECIMAL_PATTERN, RETAIL_SALE_ID_TEXT_PATTERN, RETAIL_SALE_POSITIVE_DECIMAL_PATTERN } from '../constants/retailIntegrationLimits';
3
+ import { RetailSaleFinancierEnum, RetailSaleLineRoleEnum, RetailSaleTypeEnum } from '../enums';
4
+
5
+ function decimalToCents(value: string): number {
6
+ return Number(value.replace('.', ''));
7
+ }
8
+
9
+ /** Campo de un pago que solo aplica a un medio: con ese medio cumple el patrón; con los demás es `null`. */
10
+ @ValidatorConstraint({ name: 'matchesForPaymentMethod', async: false })
11
+ export class MatchesForPaymentMethod implements ValidatorConstraintInterface {
12
+ validate(value: unknown, args: ValidationArguments): boolean {
13
+ const [method, pattern, nullable] = args.constraints as [string, RegExp, boolean?];
14
+ if ((args.object as { method?: string }).method !== method) return value === null;
15
+ if (value === null) return nullable === true;
16
+ return typeof value === 'string' && pattern.test(value);
17
+ }
18
+
19
+ defaultMessage(args: ValidationArguments): string {
20
+ return `${args.property} es obligatorio y válido en ${args.constraints[0]} y null en los demás medios`;
21
+ }
22
+ }
23
+
24
+ /** La suma de `payments[].amount` es `amountPaid`; con `amountPaid` en `"0.00"` el arreglo va vacío. */
25
+ @ValidatorConstraint({ name: 'paymentsMatchAmountPaid', async: false })
26
+ export class PaymentsMatchAmountPaid implements ValidatorConstraintInterface {
27
+ validate(value: unknown, args: ValidationArguments): boolean {
28
+ const amountPaid = (args.object as { amountPaid?: unknown }).amountPaid;
29
+ if (!Array.isArray(value) || typeof amountPaid !== 'string' || !RETAIL_SALE_DECIMAL_PATTERN.test(amountPaid)) return true;
30
+ const amounts = value.map((p) => (p as { amount?: unknown })?.amount);
31
+ if (!amounts.every((a) => typeof a === 'string' && RETAIL_SALE_POSITIVE_DECIMAL_PATTERN.test(a))) return true;
32
+ const total = (amounts as string[]).reduce((sum, a) => sum + decimalToCents(a), 0);
33
+ return total === decimalToCents(amountPaid);
34
+ }
35
+
36
+ defaultMessage(): string {
37
+ return 'la suma de payments[].amount debe ser amountPaid; [] solo si amountPaid es 0.00';
38
+ }
39
+ }
40
+
41
+ /** `credit` y `creditId`: obligatorios con `SUREKEEP` y `null` en contado. */
42
+ @ValidatorConstraint({ name: 'creditMatchesFinancier', async: false })
43
+ export class CreditMatchesFinancier implements ValidatorConstraintInterface {
44
+ validate(value: unknown, args: ValidationArguments): boolean {
45
+ const { saleType, credit, creditId } = args.object as { saleType?: unknown; credit?: unknown; creditId?: unknown };
46
+ if (value === RetailSaleFinancierEnum.SUREKEEP) return credit !== null && creditId !== null;
47
+ if (value === RetailSaleFinancierEnum.NONE || saleType === RetailSaleTypeEnum.CASH) return credit == null && creditId == null;
48
+ return true;
49
+ }
50
+
51
+ defaultMessage(): string {
52
+ return 'credit y creditId son obligatorios con SUREKEEP y null en contado';
53
+ }
54
+ }
55
+
56
+ /** `promotionId`: texto o `null`, nunca omitido; un regalo siempre lo trae. */
57
+ @ValidatorConstraint({ name: 'promotionIdMatchesRole', async: false })
58
+ export class PromotionIdMatchesRole implements ValidatorConstraintInterface {
59
+ validate(value: unknown, args: ValidationArguments): boolean {
60
+ if (value === null) return (args.object as { role?: unknown }).role !== RetailSaleLineRoleEnum.GIFT;
61
+ return typeof value === 'string' && RETAIL_SALE_ID_TEXT_PATTERN.test(value);
62
+ }
63
+
64
+ defaultMessage(): string {
65
+ return 'promotionId es texto de 1 a 64 o null, y obligatorio con role GIFT';
66
+ }
67
+ }
68
+
69
+ /** Un dañado con `identifier` es una sola pieza: `qty` debe ser 1. */
70
+ @ValidatorConstraint({ name: 'qtyMatchesIdentifier', async: false })
71
+ export class QtyMatchesIdentifier implements ValidatorConstraintInterface {
72
+ validate(value: unknown, args: ValidationArguments): boolean {
73
+ const identifier = (args.object as { identifier?: unknown }).identifier;
74
+ return typeof identifier !== 'string' || value === 1;
75
+ }
76
+
77
+ defaultMessage(): string {
78
+ return 'qty debe ser 1 cuando viene identifier';
79
+ }
80
+ }