@fiado/type-kit 3.494.0 → 3.496.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 (205) hide show
  1. package/_test_/unit/loanCredit/dtos/createSellerPaymentRequest.test.ts +63 -0
  2. package/_test_/unit/loanCredit/dtos/originateLoanCreditRequest.test.ts +56 -0
  3. package/_test_/unit/loanCredit/inStorePaymentDtos.test.ts +141 -0
  4. package/_test_/unit/loanCredit/sellerCreditDtos.test.ts +119 -0
  5. package/_test_/unit/retailIntegration/applyRetailEventRequest.test.ts +43 -0
  6. package/_test_/unit/retailIntegration/isIsoDateTimeWithZone.test.ts +85 -0
  7. package/_test_/unit/retailIntegration/receiveRetailEventRequest.test.ts +93 -0
  8. package/_test_/unit/retailIntegration/retailOutboundRequests.test.ts +69 -0
  9. package/_test_/unit/retailIntegration/retailSystemEnums.test.ts +42 -0
  10. package/bin/index.d.ts +1 -0
  11. package/bin/index.js +3 -1
  12. package/bin/loanCredit/dtos/requests/CreateSellerPaymentRequest.d.ts +15 -0
  13. package/bin/loanCredit/dtos/requests/CreateSellerPaymentRequest.js +56 -0
  14. package/bin/loanCredit/dtos/requests/InStorePaymentDetail.d.ts +12 -0
  15. package/bin/loanCredit/dtos/requests/InStorePaymentDetail.js +40 -0
  16. package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.d.ts +7 -3
  17. package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.js +8 -1
  18. package/bin/loanCredit/dtos/responses/CreditCustomerListItem.d.ts +46 -0
  19. package/bin/loanCredit/dtos/responses/CreditCustomerListItem.js +2 -0
  20. package/bin/loanCredit/dtos/responses/CreditSaleContext.d.ts +2 -0
  21. package/bin/loanCredit/dtos/responses/InStorePaymentListItem.d.ts +19 -0
  22. package/bin/loanCredit/dtos/responses/InStorePaymentListItem.js +2 -0
  23. package/bin/loanCredit/dtos/responses/LoanPaymentListItem.d.ts +14 -1
  24. package/bin/loanCredit/dtos/responses/SellerCreditDetailResponse.d.ts +10 -0
  25. package/bin/loanCredit/dtos/responses/SellerCreditDetailResponse.js +2 -0
  26. package/bin/loanCredit/dtos/responses/SellerCreditListResponse.d.ts +7 -0
  27. package/bin/loanCredit/dtos/responses/SellerCreditListResponse.js +2 -0
  28. package/bin/loanCredit/dtos/responses/SellerCreditLookupResult.d.ts +10 -0
  29. package/bin/loanCredit/dtos/responses/SellerCreditLookupResult.js +2 -0
  30. package/bin/loanCredit/dtos/responses/SellerCreditRow.d.ts +25 -0
  31. package/bin/loanCredit/dtos/responses/SellerCreditRow.js +2 -0
  32. package/bin/loanCredit/dtos/responses/SellerPaymentOptions.d.ts +36 -0
  33. package/bin/loanCredit/dtos/responses/SellerPaymentOptions.js +2 -0
  34. package/bin/loanCredit/dtos/responses/SellerPaymentResult.d.ts +39 -0
  35. package/bin/loanCredit/dtos/responses/SellerPaymentResult.js +2 -0
  36. package/bin/loanCredit/enums/CreditCustomerStateEnum.d.ts +6 -0
  37. package/bin/loanCredit/enums/CreditCustomerStateEnum.js +10 -0
  38. package/bin/loanCredit/enums/InStorePaymentMethodEnum.d.ts +6 -0
  39. package/bin/loanCredit/enums/InStorePaymentMethodEnum.js +10 -0
  40. package/bin/loanCredit/enums/LoanPaymentOriginEnum.d.ts +5 -0
  41. package/bin/loanCredit/enums/LoanPaymentOriginEnum.js +9 -0
  42. package/bin/loanCredit/index.d.ts +13 -0
  43. package/bin/loanCredit/index.js +15 -0
  44. package/bin/retailIntegration/constants/retailIntegrationLimits.d.ts +7 -0
  45. package/bin/retailIntegration/constants/retailIntegrationLimits.js +10 -0
  46. package/bin/retailIntegration/dtos/ApplyRetailEventRequest.d.ts +29 -0
  47. package/bin/retailIntegration/dtos/ApplyRetailEventRequest.js +54 -0
  48. package/bin/retailIntegration/dtos/GetRetailInventoryRequest.d.ts +5 -0
  49. package/bin/retailIntegration/dtos/GetRetailInventoryRequest.js +23 -0
  50. package/bin/retailIntegration/dtos/ListRetailCatalogRequest.d.ts +4 -0
  51. package/bin/retailIntegration/dtos/ListRetailCatalogRequest.js +8 -0
  52. package/bin/retailIntegration/dtos/ListRetailStoresRequest.d.ts +4 -0
  53. package/bin/retailIntegration/dtos/ListRetailStoresRequest.js +8 -0
  54. package/bin/retailIntegration/dtos/ListRetailUsersRequest.d.ts +5 -0
  55. package/bin/retailIntegration/dtos/ListRetailUsersRequest.js +24 -0
  56. package/bin/retailIntegration/dtos/ReceiveInventoryEventRequest.d.ts +6 -0
  57. package/bin/retailIntegration/dtos/ReceiveInventoryEventRequest.js +23 -0
  58. package/bin/retailIntegration/dtos/ReceiveProductEventRequest.d.ts +6 -0
  59. package/bin/retailIntegration/dtos/ReceiveProductEventRequest.js +23 -0
  60. package/bin/retailIntegration/dtos/ReceiveRetailEventRequest.d.ts +12 -0
  61. package/bin/retailIntegration/dtos/ReceiveRetailEventRequest.js +2 -0
  62. package/bin/retailIntegration/dtos/ReceiveRetailUserEventRequest.d.ts +6 -0
  63. package/bin/retailIntegration/dtos/ReceiveRetailUserEventRequest.js +23 -0
  64. package/bin/retailIntegration/dtos/ReceiveStoreEventRequest.d.ts +6 -0
  65. package/bin/retailIntegration/dtos/ReceiveStoreEventRequest.js +23 -0
  66. package/bin/retailIntegration/dtos/RegisterRetailSaleRequest.d.ts +4 -0
  67. package/bin/retailIntegration/dtos/RegisterRetailSaleRequest.js +8 -0
  68. package/bin/retailIntegration/dtos/RetailEventEnvelope.d.ts +8 -0
  69. package/bin/retailIntegration/dtos/RetailEventEnvelope.js +33 -0
  70. package/bin/retailIntegration/dtos/RetailEventResult.d.ts +12 -0
  71. package/bin/retailIntegration/dtos/RetailEventResult.js +2 -0
  72. package/bin/retailIntegration/dtos/RetailOutboundResponses.d.ts +30 -0
  73. package/bin/retailIntegration/dtos/RetailOutboundResponses.js +2 -0
  74. package/bin/retailIntegration/dtos/RetailPagedQuery.d.ts +6 -0
  75. package/bin/retailIntegration/dtos/RetailPagedQuery.js +34 -0
  76. package/bin/retailIntegration/dtos/RetailTenantQuery.d.ts +4 -0
  77. package/bin/retailIntegration/dtos/RetailTenantQuery.js +23 -0
  78. package/bin/retailIntegration/enums/InventoryEventTypeEnum.d.ts +4 -0
  79. package/bin/retailIntegration/enums/InventoryEventTypeEnum.js +8 -0
  80. package/bin/retailIntegration/enums/ProductEventTypeEnum.d.ts +6 -0
  81. package/bin/retailIntegration/enums/ProductEventTypeEnum.js +10 -0
  82. package/bin/retailIntegration/enums/RetailCurrencyEnum.d.ts +5 -0
  83. package/bin/retailIntegration/enums/RetailCurrencyEnum.js +9 -0
  84. package/bin/retailIntegration/enums/RetailEventResultEnum.d.ts +6 -0
  85. package/bin/retailIntegration/enums/RetailEventResultEnum.js +10 -0
  86. package/bin/retailIntegration/enums/RetailFinancierEnum.d.ts +8 -0
  87. package/bin/retailIntegration/enums/RetailFinancierEnum.js +12 -0
  88. package/bin/retailIntegration/enums/RetailLinePaymentTypeEnum.d.ts +5 -0
  89. package/bin/retailIntegration/enums/RetailLinePaymentTypeEnum.js +9 -0
  90. package/bin/retailIntegration/enums/RetailMdmProviderEnum.d.ts +5 -0
  91. package/bin/retailIntegration/enums/RetailMdmProviderEnum.js +9 -0
  92. package/bin/retailIntegration/enums/RetailPaymentMethodEnum.d.ts +6 -0
  93. package/bin/retailIntegration/enums/RetailPaymentMethodEnum.js +10 -0
  94. package/bin/retailIntegration/enums/RetailPieceReasonEnum.d.ts +6 -0
  95. package/bin/retailIntegration/enums/RetailPieceReasonEnum.js +10 -0
  96. package/bin/retailIntegration/enums/RetailPieceStatusEnum.d.ts +7 -0
  97. package/bin/retailIntegration/enums/RetailPieceStatusEnum.js +11 -0
  98. package/bin/retailIntegration/enums/RetailProductCategoryEnum.d.ts +7 -0
  99. package/bin/retailIntegration/enums/RetailProductCategoryEnum.js +11 -0
  100. package/bin/retailIntegration/enums/RetailProductStatusEnum.d.ts +6 -0
  101. package/bin/retailIntegration/enums/RetailProductStatusEnum.js +10 -0
  102. package/bin/retailIntegration/enums/RetailProductTierEnum.d.ts +7 -0
  103. package/bin/retailIntegration/enums/RetailProductTierEnum.js +11 -0
  104. package/bin/retailIntegration/enums/RetailSaleFinancierEnum.d.ts +9 -0
  105. package/bin/retailIntegration/enums/RetailSaleFinancierEnum.js +13 -0
  106. package/bin/retailIntegration/enums/RetailSaleTypeEnum.d.ts +6 -0
  107. package/bin/retailIntegration/enums/RetailSaleTypeEnum.js +10 -0
  108. package/bin/retailIntegration/enums/RetailStoreStatusEnum.d.ts +6 -0
  109. package/bin/retailIntegration/enums/RetailStoreStatusEnum.js +10 -0
  110. package/bin/retailIntegration/enums/RetailTrackingModeEnum.d.ts +6 -0
  111. package/bin/retailIntegration/enums/RetailTrackingModeEnum.js +10 -0
  112. package/bin/retailIntegration/enums/RetailUserAccessStatusEnum.d.ts +6 -0
  113. package/bin/retailIntegration/enums/RetailUserAccessStatusEnum.js +10 -0
  114. package/bin/retailIntegration/enums/RetailUserEventTypeEnum.d.ts +7 -0
  115. package/bin/retailIntegration/enums/RetailUserEventTypeEnum.js +11 -0
  116. package/bin/retailIntegration/enums/RetailUserRoleEnum.d.ts +7 -0
  117. package/bin/retailIntegration/enums/RetailUserRoleEnum.js +11 -0
  118. package/bin/retailIntegration/enums/StoreEventTypeEnum.d.ts +6 -0
  119. package/bin/retailIntegration/enums/StoreEventTypeEnum.js +10 -0
  120. package/bin/retailIntegration/enums/index.d.ts +21 -0
  121. package/bin/retailIntegration/enums/index.js +37 -0
  122. package/bin/retailIntegration/index.d.ts +25 -0
  123. package/bin/retailIntegration/index.js +41 -0
  124. package/bin/retailIntegration/models/RetailPageMeta.d.ts +10 -0
  125. package/bin/retailIntegration/models/RetailPageMeta.js +2 -0
  126. package/bin/retailIntegration/models/RetailSystemInventory.d.ts +17 -0
  127. package/bin/retailIntegration/models/RetailSystemInventory.js +2 -0
  128. package/bin/retailIntegration/models/RetailSystemProduct.d.ts +32 -0
  129. package/bin/retailIntegration/models/RetailSystemProduct.js +2 -0
  130. package/bin/retailIntegration/models/RetailSystemSale.d.ts +46 -0
  131. package/bin/retailIntegration/models/RetailSystemSale.js +2 -0
  132. package/bin/retailIntegration/models/RetailSystemStore.d.ts +29 -0
  133. package/bin/retailIntegration/models/RetailSystemStore.js +2 -0
  134. package/bin/retailIntegration/models/RetailSystemUser.d.ts +14 -0
  135. package/bin/retailIntegration/models/RetailSystemUser.js +2 -0
  136. package/bin/retailIntegration/validators/IsIsoDateTimeWithZone.d.ts +6 -0
  137. package/bin/retailIntegration/validators/IsIsoDateTimeWithZone.js +29 -0
  138. package/bin/retailWizard/dtos/WizardSessionLookupEntry.d.ts +2 -0
  139. package/package.json +1 -1
  140. package/src/index.ts +2 -0
  141. package/src/loanCredit/dtos/requests/CreateSellerPaymentRequest.ts +40 -0
  142. package/src/loanCredit/dtos/requests/InStorePaymentDetail.ts +27 -0
  143. package/src/loanCredit/dtos/requests/OriginateLoanCreditRequest.ts +12 -3
  144. package/src/loanCredit/dtos/responses/CreditCustomerListItem.ts +51 -0
  145. package/src/loanCredit/dtos/responses/CreditSaleContext.ts +2 -0
  146. package/src/loanCredit/dtos/responses/InStorePaymentListItem.ts +21 -0
  147. package/src/loanCredit/dtos/responses/LoanPaymentListItem.ts +11 -1
  148. package/src/loanCredit/dtos/responses/SellerCreditDetailResponse.ts +11 -0
  149. package/src/loanCredit/dtos/responses/SellerCreditListResponse.ts +8 -0
  150. package/src/loanCredit/dtos/responses/SellerCreditLookupResult.ts +11 -0
  151. package/src/loanCredit/dtos/responses/SellerCreditRow.ts +26 -0
  152. package/src/loanCredit/dtos/responses/SellerPaymentOptions.ts +29 -0
  153. package/src/loanCredit/dtos/responses/SellerPaymentResult.ts +43 -0
  154. package/src/loanCredit/enums/CreditCustomerStateEnum.ts +6 -0
  155. package/src/loanCredit/enums/InStorePaymentMethodEnum.ts +6 -0
  156. package/src/loanCredit/enums/LoanPaymentOriginEnum.ts +5 -0
  157. package/src/loanCredit/index.ts +16 -0
  158. package/src/retailIntegration/constants/retailIntegrationLimits.ts +9 -0
  159. package/src/retailIntegration/dtos/ApplyRetailEventRequest.ts +49 -0
  160. package/src/retailIntegration/dtos/GetRetailInventoryRequest.ts +9 -0
  161. package/src/retailIntegration/dtos/ListRetailCatalogRequest.ts +4 -0
  162. package/src/retailIntegration/dtos/ListRetailStoresRequest.ts +4 -0
  163. package/src/retailIntegration/dtos/ListRetailUsersRequest.ts +10 -0
  164. package/src/retailIntegration/dtos/ReceiveInventoryEventRequest.ts +9 -0
  165. package/src/retailIntegration/dtos/ReceiveProductEventRequest.ts +9 -0
  166. package/src/retailIntegration/dtos/ReceiveRetailEventRequest.ts +18 -0
  167. package/src/retailIntegration/dtos/ReceiveRetailUserEventRequest.ts +9 -0
  168. package/src/retailIntegration/dtos/ReceiveStoreEventRequest.ts +9 -0
  169. package/src/retailIntegration/dtos/RegisterRetailSaleRequest.ts +4 -0
  170. package/src/retailIntegration/dtos/RetailEventEnvelope.ts +20 -0
  171. package/src/retailIntegration/dtos/RetailEventResult.ts +14 -0
  172. package/src/retailIntegration/dtos/RetailOutboundResponses.ts +35 -0
  173. package/src/retailIntegration/dtos/RetailPagedQuery.ts +19 -0
  174. package/src/retailIntegration/dtos/RetailTenantQuery.ts +9 -0
  175. package/src/retailIntegration/enums/InventoryEventTypeEnum.ts +4 -0
  176. package/src/retailIntegration/enums/ProductEventTypeEnum.ts +6 -0
  177. package/src/retailIntegration/enums/RetailCurrencyEnum.ts +5 -0
  178. package/src/retailIntegration/enums/RetailEventResultEnum.ts +6 -0
  179. package/src/retailIntegration/enums/RetailFinancierEnum.ts +8 -0
  180. package/src/retailIntegration/enums/RetailLinePaymentTypeEnum.ts +5 -0
  181. package/src/retailIntegration/enums/RetailMdmProviderEnum.ts +5 -0
  182. package/src/retailIntegration/enums/RetailPaymentMethodEnum.ts +6 -0
  183. package/src/retailIntegration/enums/RetailPieceReasonEnum.ts +6 -0
  184. package/src/retailIntegration/enums/RetailPieceStatusEnum.ts +7 -0
  185. package/src/retailIntegration/enums/RetailProductCategoryEnum.ts +7 -0
  186. package/src/retailIntegration/enums/RetailProductStatusEnum.ts +6 -0
  187. package/src/retailIntegration/enums/RetailProductTierEnum.ts +7 -0
  188. package/src/retailIntegration/enums/RetailSaleFinancierEnum.ts +9 -0
  189. package/src/retailIntegration/enums/RetailSaleTypeEnum.ts +6 -0
  190. package/src/retailIntegration/enums/RetailStoreStatusEnum.ts +6 -0
  191. package/src/retailIntegration/enums/RetailTrackingModeEnum.ts +6 -0
  192. package/src/retailIntegration/enums/RetailUserAccessStatusEnum.ts +6 -0
  193. package/src/retailIntegration/enums/RetailUserEventTypeEnum.ts +7 -0
  194. package/src/retailIntegration/enums/RetailUserRoleEnum.ts +7 -0
  195. package/src/retailIntegration/enums/StoreEventTypeEnum.ts +6 -0
  196. package/src/retailIntegration/enums/index.ts +21 -0
  197. package/src/retailIntegration/index.ts +25 -0
  198. package/src/retailIntegration/models/RetailPageMeta.ts +11 -0
  199. package/src/retailIntegration/models/RetailSystemInventory.ts +20 -0
  200. package/src/retailIntegration/models/RetailSystemProduct.ts +43 -0
  201. package/src/retailIntegration/models/RetailSystemSale.ts +51 -0
  202. package/src/retailIntegration/models/RetailSystemStore.ts +33 -0
  203. package/src/retailIntegration/models/RetailSystemUser.ts +16 -0
  204. package/src/retailIntegration/validators/IsIsoDateTimeWithZone.ts +21 -0
  205. package/src/retailWizard/dtos/WizardSessionLookupEntry.ts +2 -0
@@ -0,0 +1,63 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { CreateSellerPaymentRequest } from '../../../../src/loanCredit/index';
5
+
6
+ const errorPaths = async (body: object): Promise<string[]> => {
7
+ const dto = plainToInstance(CreateSellerPaymentRequest, body);
8
+ const errors = await validate(dto, { whitelist: true, forbidNonWhitelisted: false });
9
+ const pathsOf = (error: (typeof errors)[number], parent: string): string[] => {
10
+ const path = parent ? `${parent}.${error.property}` : error.property;
11
+ return [...(error.constraints ? [path] : []), ...(error.children ?? []).flatMap((child) => pathsOf(child, path))];
12
+ };
13
+ return errors.flatMap((error) => pathsOf(error, ''));
14
+ };
15
+
16
+ const BASE = {
17
+ type: 'NEXT_INSTALLMENT',
18
+ expectedAmountCents: 48_700,
19
+ method: 'CASH',
20
+ idempotencyKey: '7b0e3a52-6a0f-4f1f-9f43-6b4d5c1e2a10',
21
+ };
22
+
23
+ describe('CreateSellerPaymentRequest', () => {
24
+ it('acepta efectivo sin detalle', async () => {
25
+ expect(await errorPaths(BASE)).toEqual([]);
26
+ });
27
+
28
+ it('acepta tarjeta con últimos 4 y autorización', async () => {
29
+ expect(await errorPaths({ ...BASE, method: 'CARD', detail: { cardLast4: '4242', authCode: 'A1B2C3' } })).toEqual([]);
30
+ });
31
+
32
+ it('acepta SPEI con clave de rastreo de hasta 30', async () => {
33
+ expect(await errorPaths({ ...BASE, method: 'WIRE', detail: { speiTrackingKey: 'X'.repeat(30) } })).toEqual([]);
34
+ });
35
+
36
+ it('rechaza últimos 4 que no son 4 dígitos', async () => {
37
+ expect(await errorPaths({ ...BASE, method: 'CARD', detail: { cardLast4: '42a2', authCode: 'A1' } })).toEqual(['detail.cardLast4']);
38
+ });
39
+
40
+ it('rechaza clave de rastreo vacía o de más de 30', async () => {
41
+ expect(await errorPaths({ ...BASE, method: 'WIRE', detail: { speiTrackingKey: '' } })).toEqual(['detail.speiTrackingKey']);
42
+ expect(await errorPaths({ ...BASE, method: 'WIRE', detail: { speiTrackingKey: 'X'.repeat(31) } })).toEqual([
43
+ 'detail.speiTrackingKey',
44
+ ]);
45
+ });
46
+
47
+ it('rechaza forma de pago, tipo, monto o llave inválidos', async () => {
48
+ expect(await errorPaths({ ...BASE, method: 'CHEQUE' })).toEqual(['method']);
49
+ expect(await errorPaths({ ...BASE, type: 'OTHER' })).toEqual(['type']);
50
+ expect(await errorPaths({ ...BASE, expectedAmountCents: 0 })).toEqual(['expectedAmountCents']);
51
+ expect(await errorPaths({ ...BASE, idempotencyKey: 'no-uuid' })).toEqual(['idempotencyKey']);
52
+ });
53
+
54
+ it('cuotas solo de 2 en adelante', async () => {
55
+ expect(await errorPaths({ ...BASE, type: 'ADVANCE', installmentCount: 1 })).toEqual(['installmentCount']);
56
+ expect(await errorPaths({ ...BASE, type: 'ADVANCE', installmentCount: 2 })).toEqual([]);
57
+ });
58
+
59
+ it('no declara CLABE: con whitelist se descarta y nunca llega al dominio', () => {
60
+ const dto = plainToInstance(CreateSellerPaymentRequest, { ...BASE, clabe: '012345678901234567' }, { excludeExtraneousValues: true });
61
+ expect(dto).not.toHaveProperty('clabe');
62
+ });
63
+ });
@@ -0,0 +1,56 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { OriginateLoanCreditRequest } from '../../../../src/loanCredit/dtos/requests/OriginateLoanCreditRequest';
5
+
6
+ const BASE = {
7
+ wizardSessionId: 'WS-1',
8
+ borrowerId: 'B-1',
9
+ planId: 'PLAN-1',
10
+ termWeeks: 12,
11
+ equipmentPriceCents: 500000,
12
+ downPaymentCents: 100000,
13
+ storeId: 'ST-1',
14
+ };
15
+
16
+ /** Igual que `validateRequest` del motor: whitelist sin rechazar extras. */
17
+ const validateWhitelisted = (body: object) => {
18
+ const dto = plainToInstance(OriginateLoanCreditRequest, body);
19
+ return validate(dto, { whitelist: true, forbidNonWhitelisted: false }).then((errors) => ({ dto, errors }));
20
+ };
21
+
22
+ describe('OriginateLoanCreditRequest', () => {
23
+ it('acepta el request con tienda y vendedor y conserva los dos', async () => {
24
+ const { dto, errors } = await validateWhitelisted({ ...BASE, sellerId: 'U-1' });
25
+
26
+ expect(errors).toEqual([]);
27
+ expect(dto.storeId).toBe('ST-1');
28
+ expect(dto.sellerId).toBe('U-1');
29
+ });
30
+
31
+ it('rechaza el request sin tienda: sin ella el motor no resuelve la promoción ni la cartera', async () => {
32
+ const { storeId: _omitted, ...withoutStore } = BASE;
33
+ const { errors } = await validateWhitelisted(withoutStore);
34
+
35
+ expect(errors.some((error) => error.property === 'storeId')).toBe(true);
36
+ });
37
+
38
+ it('rechaza la tienda vacía', async () => {
39
+ const { errors } = await validateWhitelisted({ ...BASE, storeId: '' });
40
+
41
+ expect(errors.some((error) => error.property === 'storeId')).toBe(true);
42
+ });
43
+
44
+ it('acepta el request sin vendedor: los créditos viejos los llena el backfill', async () => {
45
+ const { dto, errors } = await validateWhitelisted(BASE);
46
+
47
+ expect(errors).toEqual([]);
48
+ expect(dto.sellerId).toBeUndefined();
49
+ });
50
+
51
+ it('rechaza el vendedor vacío: un sellerId en blanco sería una llave de GSI inválida', async () => {
52
+ const { errors } = await validateWhitelisted({ ...BASE, sellerId: '' });
53
+
54
+ expect(errors.some((error) => error.property === 'sellerId')).toBe(true);
55
+ });
56
+ });
@@ -0,0 +1,141 @@
1
+ import 'reflect-metadata';
2
+ import type {
3
+ CreditCustomerListItem,
4
+ InStorePaymentListResponse,
5
+ LoanPaymentListItem,
6
+ SellerCreditLookupResult,
7
+ SellerPaymentOptions,
8
+ SellerPaymentResult,
9
+ } from '../../../src/loanCredit/index';
10
+ import {
11
+ CreditCustomerStateEnum,
12
+ InStorePaymentMethodEnum,
13
+ LoanCustomerPaymentTypeEnum,
14
+ LoanInstallmentStatusEnum,
15
+ LoanPaymentOriginEnum,
16
+ } from '../../../src/loanCredit/index';
17
+
18
+ describe.each([
19
+ ['InStorePaymentMethodEnum', InStorePaymentMethodEnum, ['CASH', 'CARD', 'WIRE']],
20
+ ['LoanPaymentOriginEnum', LoanPaymentOriginEnum, ['COLLECTION_ENGINE', 'IN_STORE']],
21
+ ['CreditCustomerStateEnum', CreditCustomerStateEnum, ['READ', 'NO_BORROWER', 'UNAVAILABLE']],
22
+ ])('%s', (_name, enumObject, values) => {
23
+ it('tiene exactamente los valores del contrato', () => {
24
+ expect(Object.values(enumObject).sort()).toEqual([...values].sort());
25
+ });
26
+ it('cada llave es igual a su valor', () => {
27
+ for (const [key, value] of Object.entries(enumObject)) {
28
+ expect(key).toBe(value);
29
+ }
30
+ });
31
+ });
32
+
33
+ describe('loanCredit — contrato del cobro en tienda', () => {
34
+ it('el resultado lleva folio, tienda, vendedor, siguiente cuota y equipo', () => {
35
+ const result: SellerPaymentResult = {
36
+ creditId: 'CR-1',
37
+ receipt: 'SK-TD-0123456789ab',
38
+ amountCents: 48_700,
39
+ type: LoanCustomerPaymentTypeEnum.NEXT_INSTALLMENT,
40
+ method: InStorePaymentMethodEnum.CASH,
41
+ appliedAt: '2026-10-08T17:00:00.000Z',
42
+ outstandingBalanceAfterCents: 400_000,
43
+ collectedAtStore: { id: 'ST-1', name: 'Tienda Limpia' },
44
+ collectedBySeller: { id: 'RU-1', name: 'Ana' },
45
+ nextInstallment: { installmentNumber: 4, dueDate: '2026-10-15', amountCents: 48_700, status: LoanInstallmentStatusEnum.PENDING },
46
+ credit: { creditId: 'CR-1', imei: '356789012345678', productBrand: 'Samsung', productModel: 'A15' },
47
+ };
48
+ expect(result.receipt.startsWith('SK-TD-')).toBe(true);
49
+ });
50
+
51
+ it('las opciones traen el resumen y cada tipo con su monto o rango', () => {
52
+ const options: SellerPaymentOptions = {
53
+ creditId: 'CR-1',
54
+ summary: { totalCents: 1_000_000, paidCents: 600_000, remainingCents: 400_000, nextInstallment: null },
55
+ nextInstallment: { available: true, amountCents: 97_400, installmentNumbers: [3, 4], overdueCount: 1 },
56
+ advance: { available: false, unavailableReason: 'CREDIT_NOT_CURRENT' as never },
57
+ prepayment: { available: false },
58
+ settlement: { available: true, amountCents: 380_000 },
59
+ };
60
+ expect(options.nextInstallment.installmentNumbers).toEqual([3, 4]);
61
+ });
62
+
63
+ it('la búsqueda responde lo mínimo, sin el identificador que no se buscó', () => {
64
+ const found: SellerCreditLookupResult = {
65
+ creditId: 'CR-1',
66
+ customerName: 'Juan P.',
67
+ productBrand: 'Samsung',
68
+ productModel: 'A15',
69
+ nextInstallment: null,
70
+ };
71
+ expect(found).not.toHaveProperty('imei');
72
+ expect(found).not.toHaveProperty('phone');
73
+ });
74
+
75
+ it('la conciliación pagina los cobros de una tienda', () => {
76
+ const page: InStorePaymentListResponse = {
77
+ items: [
78
+ {
79
+ receipt: 'SK-TD-0123456789ab',
80
+ creditId: 'CR-1',
81
+ paymentId: '01J00000000000000000000000',
82
+ chargeKind: 'INSTALLMENT' as never,
83
+ amountCents: 48_700,
84
+ method: InStorePaymentMethodEnum.CARD,
85
+ collectedBySeller: { id: 'RU-1', name: 'Ana' },
86
+ appliedAt: '2026-10-08T17:00:00.000Z',
87
+ },
88
+ ],
89
+ };
90
+ expect(page.nextToken).toBeUndefined();
91
+ });
92
+
93
+ it('el historial dice dónde, cómo y quién; mechanism es opcional', () => {
94
+ const item: LoanPaymentListItem = {
95
+ paymentId: 'P-1',
96
+ receipt: 'SK-TD-0123456789ab',
97
+ installmentNumber: 3,
98
+ attemptId: 'IN_STORE#k',
99
+ result: 'FULL' as never,
100
+ currency: 'MXN' as never,
101
+ collectedCents: 48_700,
102
+ appliedCents: 48_700,
103
+ overpaidCents: 0,
104
+ reservedCents: 0,
105
+ needsReview: false,
106
+ appliedAt: '2026-10-08T17:00:00.000Z',
107
+ origin: LoanPaymentOriginEnum.IN_STORE,
108
+ inStoreMethod: InStorePaymentMethodEnum.CASH,
109
+ collectedAtStore: { id: 'ST-1', name: 'Tienda Limpia' },
110
+ collectedBySeller: { id: 'RU-1', name: 'Ana' },
111
+ };
112
+ expect(item.mechanism).toBeUndefined();
113
+ });
114
+
115
+ it('la fila de Clientes trae el último pago con su lugar', () => {
116
+ const row: CreditCustomerListItem = {
117
+ retailCustomerId: 'RC-1',
118
+ fullName: null,
119
+ phone: null,
120
+ originStoreId: null,
121
+ segments: [],
122
+ segmentsRead: true,
123
+ credit: {
124
+ state: CreditCustomerStateEnum.READ,
125
+ clientLevel: null,
126
+ sciScore: null,
127
+ sciSource: null,
128
+ creditsCount: 1,
129
+ activeCreditsCount: 1,
130
+ settledCreditsCount: 0,
131
+ writtenOffCreditsCount: 0,
132
+ outstandingBalanceCents: 0,
133
+ arrearsDays: 0,
134
+ lastPaymentAt: '2026-10-08T17:00:00.000Z',
135
+ lastPayment: { at: '2026-10-08T17:00:00.000Z', origin: LoanPaymentOriginEnum.IN_STORE, storeId: 'ST-1', storeName: 'Tienda Limpia' },
136
+ activeCreditId: 'CR-1',
137
+ },
138
+ };
139
+ expect(row.credit.lastPayment?.storeName).toBe('Tienda Limpia');
140
+ });
141
+ });
@@ -0,0 +1,119 @@
1
+ import 'reflect-metadata';
2
+ import type { CreditSaleContext } from '../../../src/loanCredit/index';
3
+ import type { WizardSessionLookupEntry } from '../../../src/retailWizard/index';
4
+ import { WizardSessionLookupStatusEnum } from '../../../src/retailWizard/enums/WizardSessionLookupStatusEnum';
5
+ import {
6
+ LoanCreditStatusEnum,
7
+ LoanInstallmentStatusEnum,
8
+ type SellerCreditDetailResponse,
9
+ type SellerCreditListResponse,
10
+ type SellerCreditRow,
11
+ } from '../../../src/loanCredit/index';
12
+
13
+ describe('loanCredit — contrato de la cartera del vendedor', () => {
14
+ it('el contexto de venta publica la venta que cerró la sesión', () => {
15
+ const context: CreditSaleContext = {
16
+ status: WizardSessionLookupStatusEnum.OK,
17
+ retailCustomerId: 'RC-1',
18
+ retailCustomerStatus: WizardSessionLookupStatusEnum.OK,
19
+ customerName: null,
20
+ customerNameStatus: WizardSessionLookupStatusEnum.OK,
21
+ productBrand: 'Samsung',
22
+ productModel: 'A15',
23
+ productCapacity: null,
24
+ retailerId: 'R-1',
25
+ storeId: 'ST-1',
26
+ storeName: null,
27
+ storeNameStatus: WizardSessionLookupStatusEnum.OK,
28
+ saleId: 'S-1',
29
+ };
30
+
31
+ expect(context.saleId).toBe('S-1');
32
+ });
33
+
34
+ it('la entrada del lote del wizard publica al vendedor de la sesión', () => {
35
+ const entry: WizardSessionLookupEntry = {
36
+ wizardSessionId: 'WS-1',
37
+ status: WizardSessionLookupStatusEnum.OK,
38
+ sellerRetailUserId: 'U-1',
39
+ };
40
+
41
+ expect(entry.sellerRetailUserId).toBe('U-1');
42
+ });
43
+
44
+ const saleContext: CreditSaleContext = {
45
+ status: WizardSessionLookupStatusEnum.OK,
46
+ retailCustomerId: 'RC-1',
47
+ retailCustomerStatus: WizardSessionLookupStatusEnum.OK,
48
+ customerName: 'Ana López',
49
+ customerNameStatus: WizardSessionLookupStatusEnum.OK,
50
+ productBrand: 'Samsung',
51
+ productModel: 'A15',
52
+ productCapacity: null,
53
+ retailerId: 'R-1',
54
+ storeId: 'ST-1',
55
+ storeName: null,
56
+ storeNameStatus: WizardSessionLookupStatusEnum.OK,
57
+ saleId: 'S-1',
58
+ };
59
+
60
+ it('la fila de la cartera lleva el resumen del plan y la cuota vigente', () => {
61
+ const row: SellerCreditRow = {
62
+ creditId: 'CR-1',
63
+ status: LoanCreditStatusEnum.ACTIVE,
64
+ termWeeks: 12,
65
+ equipmentPriceCents: 500000,
66
+ imei: '356789012345678',
67
+ createdAt: '2026-09-01T00:00:00.000Z',
68
+ summaryUnavailable: false,
69
+ paidInstallments: 1,
70
+ outstandingBalanceCents: 300000,
71
+ overdueDays: 0,
72
+ overdueAmountCents: 0,
73
+ currentInstallment: {
74
+ creditId: 'CR-1',
75
+ installmentNumber: 2,
76
+ status: LoanInstallmentStatusEnum.PENDING,
77
+ dueDate: '2026-10-05',
78
+ capitalCents: 8000,
79
+ interestCents: 1500,
80
+ ivaCents: 500,
81
+ totalCents: 10000,
82
+ outstandingBalanceCents: 10000,
83
+ },
84
+ saleContext,
85
+ };
86
+ const list: SellerCreditListResponse = { items: [row], count: 1, truncated: false };
87
+
88
+ expect(list.items[0]?.currentInstallment?.installmentNumber).toBe(2);
89
+ });
90
+
91
+ it('la fila sin plan legible declara summaryUnavailable y deja las cifras en null', () => {
92
+ const row: SellerCreditRow = {
93
+ creditId: 'CR-2',
94
+ status: LoanCreditStatusEnum.IN_ARREARS,
95
+ termWeeks: 12,
96
+ equipmentPriceCents: 500000,
97
+ createdAt: '2026-09-01T00:00:00.000Z',
98
+ summaryUnavailable: true,
99
+ paidInstallments: null,
100
+ outstandingBalanceCents: null,
101
+ overdueDays: null,
102
+ overdueAmountCents: null,
103
+ currentInstallment: null,
104
+ saleContext,
105
+ };
106
+
107
+ expect(row.paidInstallments).toBeNull();
108
+ });
109
+
110
+ it('el detalle es el crédito, su plan y sus últimos cobros', () => {
111
+ const detail: SellerCreditDetailResponse = {
112
+ credit: {} as SellerCreditDetailResponse['credit'],
113
+ installments: [],
114
+ recentPayments: [],
115
+ };
116
+
117
+ expect(detail.recentPayments).toEqual([]);
118
+ });
119
+ });
@@ -0,0 +1,43 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import {
5
+ ApplyInventoryEventRequest,
6
+ ApplyProductEventRequest,
7
+ ApplyRetailUserEventRequest,
8
+ ApplyStoreEventRequest,
9
+ } from '../../../src/retailIntegration/dtos/ApplyRetailEventRequest';
10
+
11
+ const base = {
12
+ tenantId: 'surekeep',
13
+ eventId: 'evt_01J8ZA1B2C3D4E5F6G7H8J9K0L',
14
+ occurredAt: '2026-08-01T15:00:00Z',
15
+ payload: { storeId: '12' },
16
+ };
17
+
18
+ async function propertiesWithErrors(cls: new () => object, plain: Record<string, unknown>): Promise<string[]> {
19
+ const errors = await validate(plainToInstance(cls, plain));
20
+ return errors.map((error) => error.property);
21
+ }
22
+
23
+ describe('DTOs del contrato connector → destino', () => {
24
+ it.each([
25
+ [ApplyStoreEventRequest, 'StoreCreatedV1', 'RetailUserCreatedV1'],
26
+ [ApplyRetailUserEventRequest, 'RetailUserTransferredV1', 'StoreCreatedV1'],
27
+ [ApplyProductEventRequest, 'ProductCreatedV1', 'InventoryMovementV1'],
28
+ [ApplyInventoryEventRequest, 'InventoryMovementV1', 'ProductUpdatedV1'],
29
+ ])('%p acepta %s y rechaza %s de otra ruta', async (cls, own, foreign) => {
30
+ expect(await propertiesWithErrors(cls, { ...base, eventType: own })).toEqual([]);
31
+ expect(await propertiesWithErrors(cls, { ...base, eventType: foreign })).toEqual(['eventType']);
32
+ });
33
+
34
+ it.each([ApplyStoreEventRequest, ApplyRetailUserEventRequest, ApplyProductEventRequest, ApplyInventoryEventRequest])(
35
+ '%p valida tenantId',
36
+ async (cls) => {
37
+ const eventType = { ApplyStoreEventRequest: 'StoreCreatedV1', ApplyRetailUserEventRequest: 'RetailUserCreatedV1', ApplyProductEventRequest: 'ProductCreatedV1', ApplyInventoryEventRequest: 'InventoryMovementV1' }[cls.name];
38
+ for (const tenantId of [undefined, '', 'sure/keep', 'a'.repeat(65)]) {
39
+ expect(await propertiesWithErrors(cls, { ...base, eventType, tenantId })).toEqual(['tenantId']);
40
+ }
41
+ },
42
+ );
43
+ });
@@ -0,0 +1,85 @@
1
+ import 'reflect-metadata';
2
+ import { IsIsoDateTimeWithZone } from '../../../src/retailIntegration/validators/IsIsoDateTimeWithZone';
3
+ import {
4
+ InventoryEventTypeEnum,
5
+ ProductEventTypeEnum,
6
+ RetailEventResultEnum,
7
+ RetailUserEventTypeEnum,
8
+ StoreEventTypeEnum,
9
+ } from '../../../src/retailIntegration/enums';
10
+ import {
11
+ RETAIL_EVENT_ID_MAX_LENGTH,
12
+ RETAIL_LIST_LIMIT_MAX,
13
+ RETAIL_LIST_LIMIT_MIN,
14
+ RETAIL_TENANT_ID_PATTERN,
15
+ } from '../../../src/retailIntegration/constants/retailIntegrationLimits';
16
+
17
+ describe('IsIsoDateTimeWithZone', () => {
18
+ const validator = new IsIsoDateTimeWithZone();
19
+
20
+ it.each([
21
+ '2026-08-17T20:32:05Z',
22
+ '2026-08-17T20:32:05.123Z',
23
+ '2026-08-17T20:32:05-06:00',
24
+ '2026-08-17T20:32:05+05:30',
25
+ ])('acepta %s', (value) => {
26
+ expect(validator.validate(value)).toBe(true);
27
+ });
28
+
29
+ it.each([
30
+ ['sin zona', '2026-08-17T20:32:05'],
31
+ ['solo fecha', '2026-08-17'],
32
+ ['sin segundos', '2026-08-17T20:32Z'],
33
+ ['mes inválido', '2026-13-01T00:00:00Z'],
34
+ ['hora inválida', '2026-08-17T25:00:00Z'],
35
+ ['31 de febrero', '2026-02-31T00:00:00Z'],
36
+ ['31 de abril', '2026-04-31T10:00:00-06:00'],
37
+ ['texto', 'ayer'],
38
+ ['vacío', ''],
39
+ ['espacios', ' '],
40
+ ])('rechaza %s', (_label, value) => {
41
+ expect(validator.validate(value)).toBe(false);
42
+ });
43
+
44
+ it.each([null, undefined, 1786998725000, {}, []])('rechaza tipos que no son string: %p', (value) => {
45
+ expect(validator.validate(value)).toBe(false);
46
+ });
47
+
48
+ it('explica el formato esperado en el mensaje', () => {
49
+ expect(validator.defaultMessage()).toContain('ISO-8601 con zona');
50
+ });
51
+ });
52
+
53
+ describe('enums de retailIntegration', () => {
54
+ it('los resultados son los de la doc', () => {
55
+ expect(Object.values(RetailEventResultEnum)).toEqual(['APPLIED', 'NOOP', 'REJECTED']);
56
+ });
57
+
58
+ it('los eventType por ruta son los de la doc', () => {
59
+ expect(Object.values(StoreEventTypeEnum)).toEqual(['StoreCreatedV1', 'StoreUpdatedV1', 'StoreStatusChangedV1']);
60
+ expect(Object.values(ProductEventTypeEnum)).toEqual(['ProductCreatedV1', 'ProductUpdatedV1', 'ProductStatusChangedV1']);
61
+ expect(Object.values(InventoryEventTypeEnum)).toEqual(['InventoryMovementV1']);
62
+ expect(Object.values(RetailUserEventTypeEnum)).toEqual([
63
+ 'RetailUserCreatedV1',
64
+ 'RetailUserUpdatedV1',
65
+ 'RetailUserTransferredV1',
66
+ 'RetailUserStatusChangedV1',
67
+ ]);
68
+ });
69
+ });
70
+
71
+ describe('límites de retailIntegration', () => {
72
+ it('fija los topes del contrato', () => {
73
+ expect(RETAIL_EVENT_ID_MAX_LENGTH).toBe(1024);
74
+ expect(RETAIL_LIST_LIMIT_MIN).toBe(1);
75
+ expect(RETAIL_LIST_LIMIT_MAX).toBe(200);
76
+ });
77
+
78
+ it('el patrón de tenantId acepta slugs y rechaza separadores de ruta', () => {
79
+ expect(RETAIL_TENANT_ID_PATTERN.test('surekeep')).toBe(true);
80
+ expect(RETAIL_TENANT_ID_PATTERN.test('tenant_2-a')).toBe(true);
81
+ expect(RETAIL_TENANT_ID_PATTERN.test('sure/keep')).toBe(false);
82
+ expect(RETAIL_TENANT_ID_PATTERN.test('')).toBe(false);
83
+ expect(RETAIL_TENANT_ID_PATTERN.test('a'.repeat(65))).toBe(false);
84
+ });
85
+ });
@@ -0,0 +1,93 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import { ReceiveStoreEventRequest } from '../../../src/retailIntegration/dtos/ReceiveStoreEventRequest';
5
+ import { ReceiveRetailUserEventRequest } from '../../../src/retailIntegration/dtos/ReceiveRetailUserEventRequest';
6
+ import { ReceiveProductEventRequest } from '../../../src/retailIntegration/dtos/ReceiveProductEventRequest';
7
+ import { ReceiveInventoryEventRequest } from '../../../src/retailIntegration/dtos/ReceiveInventoryEventRequest';
8
+
9
+ const base = {
10
+ eventId: 'evt_01J8ZA1B2C3D4E5F6G7H8J9K0L',
11
+ occurredAt: '2026-08-01T15:00:00Z',
12
+ payload: { storeId: '12', name: 'Sucursal Centro' },
13
+ };
14
+
15
+ async function propertiesWithErrors(cls: new () => object, plain: Record<string, unknown>): Promise<string[]> {
16
+ const errors = await validate(plainToInstance(cls, plain));
17
+ return errors.map((error) => error.property);
18
+ }
19
+
20
+ describe('DTOs de entrada de los webhooks', () => {
21
+ it.each([
22
+ [ReceiveStoreEventRequest, 'StoreStatusChangedV1'],
23
+ [ReceiveRetailUserEventRequest, 'RetailUserTransferredV1'],
24
+ [ReceiveProductEventRequest, 'ProductCreatedV1'],
25
+ [ReceiveInventoryEventRequest, 'InventoryMovementV1'],
26
+ ])('%p acepta un sobre válido con %s', async (cls, eventType) => {
27
+ expect(await propertiesWithErrors(cls, { ...base, eventType })).toEqual([]);
28
+ });
29
+
30
+ it.each([
31
+ [ReceiveStoreEventRequest, 'RetailUserCreatedV1'],
32
+ [ReceiveRetailUserEventRequest, 'StoreCreatedV1'],
33
+ [ReceiveProductEventRequest, 'InventoryMovementV1'],
34
+ [ReceiveInventoryEventRequest, 'ProductUpdatedV1'],
35
+ ])('%p rechaza el eventType %s de otra ruta', async (cls, eventType) => {
36
+ expect(await propertiesWithErrors(cls, { ...base, eventType })).toEqual(['eventType']);
37
+ });
38
+
39
+ describe('eventId', () => {
40
+ const valid = { ...base, eventType: 'StoreCreatedV1' };
41
+
42
+ it('acepta 1024 caracteres', async () => {
43
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, eventId: 'e'.repeat(1024) })).toEqual([]);
44
+ });
45
+
46
+ it.each([
47
+ ['ausente', undefined],
48
+ ['vacío', ''],
49
+ ['solo espacios', ' '],
50
+ ['número', 123],
51
+ ['1025 caracteres', 'e'.repeat(1025)],
52
+ ])('rechaza %s', async (_label, eventId) => {
53
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, eventId })).toEqual(['eventId']);
54
+ });
55
+ });
56
+
57
+ describe('occurredAt', () => {
58
+ const valid = { ...base, eventType: 'StoreCreatedV1' };
59
+
60
+ it.each([
61
+ ['sin zona', '2026-08-01T15:00:00'],
62
+ ['ausente', undefined],
63
+ ['texto', 'hoy'],
64
+ ])('rechaza %s', async (_label, occurredAt) => {
65
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, occurredAt })).toEqual(['occurredAt']);
66
+ });
67
+
68
+ it('acepta un offset', async () => {
69
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, occurredAt: '2026-08-01T09:00:00-06:00' })).toEqual([]);
70
+ });
71
+ });
72
+
73
+ describe('payload', () => {
74
+ const valid = { ...base, eventType: 'StoreCreatedV1' };
75
+
76
+ it.each([
77
+ ['ausente', undefined],
78
+ ['null', null],
79
+ ['arreglo', [1, 2]],
80
+ ['string', 'tienda'],
81
+ ])('rechaza %s', async (_label, payload) => {
82
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, payload })).toEqual(['payload']);
83
+ });
84
+
85
+ it('no valida el contenido del payload', async () => {
86
+ expect(await propertiesWithErrors(ReceiveStoreEventRequest, { ...valid, payload: { cualquierCosa: [1, { x: null }] } })).toEqual([]);
87
+ });
88
+ });
89
+
90
+ it('reporta todos los campos que faltan en un sobre vacío', async () => {
91
+ expect((await propertiesWithErrors(ReceiveStoreEventRequest, {})).sort()).toEqual(['eventId', 'eventType', 'occurredAt', 'payload']);
92
+ });
93
+ });
@@ -0,0 +1,69 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate } from 'class-validator';
4
+ import {
5
+ GetRetailInventoryRequest,
6
+ ListRetailCatalogRequest,
7
+ ListRetailStoresRequest,
8
+ ListRetailUsersRequest,
9
+ RegisterRetailSaleRequest,
10
+ } from '../../../src/retailIntegration';
11
+ import { RetailIntegration } from '../../../src';
12
+
13
+ async function propertiesWithErrors(cls: new () => object, plain: Record<string, unknown>): Promise<string[]> {
14
+ const errors = await validate(plainToInstance(cls, plain));
15
+ return errors.map((error) => error.property);
16
+ }
17
+
18
+ describe('DTOs de salida hacia el retailer', () => {
19
+ describe.each([ListRetailStoresRequest, ListRetailCatalogRequest, ListRetailUsersRequest])('%p (paginada)', (cls) => {
20
+ it('acepta solo tenantId', async () => {
21
+ expect(await propertiesWithErrors(cls, { tenantId: 'surekeep' })).toEqual([]);
22
+ });
23
+
24
+ it('convierte limit de string a número', async () => {
25
+ const dto = plainToInstance(cls, { tenantId: 'surekeep', limit: '50' }) as { limit?: number };
26
+ expect(dto.limit).toBe(50);
27
+ expect(await validate(dto)).toEqual([]);
28
+ });
29
+
30
+ it.each(['0', '201', 'abc', '1.5', ''])('rechaza limit=%p', async (limit) => {
31
+ expect(await propertiesWithErrors(cls, { tenantId: 'surekeep', limit })).toEqual(['limit']);
32
+ });
33
+
34
+ it('acepta nextToken opaco y rechaza nextToken vacío', async () => {
35
+ expect(await propertiesWithErrors(cls, { tenantId: 'surekeep', nextToken: 'eyJpZCI6MTQwMH0' })).toEqual([]);
36
+ expect(await propertiesWithErrors(cls, { tenantId: 'surekeep', nextToken: '' })).toEqual(['nextToken']);
37
+ });
38
+ });
39
+
40
+ it.each([
41
+ ['ausente', undefined],
42
+ ['con diagonal', 'sure/keep'],
43
+ ['vacío', ''],
44
+ ])('rechaza tenantId %s', async (_label, tenantId) => {
45
+ expect(await propertiesWithErrors(ListRetailStoresRequest, { tenantId })).toEqual(['tenantId']);
46
+ });
47
+
48
+ it('inventario exige storeId', async () => {
49
+ expect(await propertiesWithErrors(GetRetailInventoryRequest, { tenantId: 'surekeep', storeId: '12' })).toEqual([]);
50
+ expect(await propertiesWithErrors(GetRetailInventoryRequest, { tenantId: 'surekeep' })).toEqual(['storeId']);
51
+ expect(await propertiesWithErrors(GetRetailInventoryRequest, { tenantId: 'surekeep', storeId: ' ' })).toEqual(['storeId']);
52
+ });
53
+
54
+ it('usuarios acepta storeId opcional', async () => {
55
+ expect(await propertiesWithErrors(ListRetailUsersRequest, { tenantId: 'surekeep', storeId: '12' })).toEqual([]);
56
+ expect(await propertiesWithErrors(ListRetailUsersRequest, { tenantId: 'surekeep', storeId: '' })).toEqual(['storeId']);
57
+ });
58
+
59
+ it('venta solo valida tenantId', async () => {
60
+ expect(await propertiesWithErrors(RegisterRetailSaleRequest, { tenantId: 'surekeep' })).toEqual([]);
61
+ expect(await propertiesWithErrors(RegisterRetailSaleRequest, {})).toEqual(['tenantId']);
62
+ });
63
+
64
+ it('el módulo se expone en la raíz como RetailIntegration', () => {
65
+ expect(RetailIntegration.ListRetailStoresRequest).toBe(ListRetailStoresRequest);
66
+ expect(RetailIntegration.ReceiveStoreEventRequest).toBeDefined();
67
+ expect(RetailIntegration.RetailEventResultEnum.APPLIED).toBe('APPLIED');
68
+ });
69
+ });