@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.
- package/_test_/unit/loanCredit/dtos/createSellerPaymentRequest.test.ts +63 -0
- package/_test_/unit/loanCredit/dtos/originateLoanCreditRequest.test.ts +56 -0
- package/_test_/unit/loanCredit/inStorePaymentDtos.test.ts +141 -0
- package/_test_/unit/loanCredit/sellerCreditDtos.test.ts +119 -0
- package/_test_/unit/retailIntegration/applyRetailEventRequest.test.ts +43 -0
- package/_test_/unit/retailIntegration/isIsoDateTimeWithZone.test.ts +85 -0
- package/_test_/unit/retailIntegration/receiveRetailEventRequest.test.ts +93 -0
- package/_test_/unit/retailIntegration/retailOutboundRequests.test.ts +69 -0
- package/_test_/unit/retailIntegration/retailSystemEnums.test.ts +42 -0
- package/bin/index.d.ts +1 -0
- package/bin/index.js +3 -1
- package/bin/loanCredit/dtos/requests/CreateSellerPaymentRequest.d.ts +15 -0
- package/bin/loanCredit/dtos/requests/CreateSellerPaymentRequest.js +56 -0
- package/bin/loanCredit/dtos/requests/InStorePaymentDetail.d.ts +12 -0
- package/bin/loanCredit/dtos/requests/InStorePaymentDetail.js +40 -0
- package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.d.ts +7 -3
- package/bin/loanCredit/dtos/requests/OriginateLoanCreditRequest.js +8 -1
- package/bin/loanCredit/dtos/responses/CreditCustomerListItem.d.ts +46 -0
- package/bin/loanCredit/dtos/responses/CreditCustomerListItem.js +2 -0
- package/bin/loanCredit/dtos/responses/CreditSaleContext.d.ts +2 -0
- package/bin/loanCredit/dtos/responses/InStorePaymentListItem.d.ts +19 -0
- package/bin/loanCredit/dtos/responses/InStorePaymentListItem.js +2 -0
- package/bin/loanCredit/dtos/responses/LoanPaymentListItem.d.ts +14 -1
- package/bin/loanCredit/dtos/responses/SellerCreditDetailResponse.d.ts +10 -0
- package/bin/loanCredit/dtos/responses/SellerCreditDetailResponse.js +2 -0
- package/bin/loanCredit/dtos/responses/SellerCreditListResponse.d.ts +7 -0
- package/bin/loanCredit/dtos/responses/SellerCreditListResponse.js +2 -0
- package/bin/loanCredit/dtos/responses/SellerCreditLookupResult.d.ts +10 -0
- package/bin/loanCredit/dtos/responses/SellerCreditLookupResult.js +2 -0
- package/bin/loanCredit/dtos/responses/SellerCreditRow.d.ts +25 -0
- package/bin/loanCredit/dtos/responses/SellerCreditRow.js +2 -0
- package/bin/loanCredit/dtos/responses/SellerPaymentOptions.d.ts +36 -0
- package/bin/loanCredit/dtos/responses/SellerPaymentOptions.js +2 -0
- package/bin/loanCredit/dtos/responses/SellerPaymentResult.d.ts +39 -0
- package/bin/loanCredit/dtos/responses/SellerPaymentResult.js +2 -0
- package/bin/loanCredit/enums/CreditCustomerStateEnum.d.ts +6 -0
- package/bin/loanCredit/enums/CreditCustomerStateEnum.js +10 -0
- package/bin/loanCredit/enums/InStorePaymentMethodEnum.d.ts +6 -0
- package/bin/loanCredit/enums/InStorePaymentMethodEnum.js +10 -0
- package/bin/loanCredit/enums/LoanPaymentOriginEnum.d.ts +5 -0
- package/bin/loanCredit/enums/LoanPaymentOriginEnum.js +9 -0
- package/bin/loanCredit/index.d.ts +13 -0
- package/bin/loanCredit/index.js +15 -0
- package/bin/retailIntegration/constants/retailIntegrationLimits.d.ts +7 -0
- package/bin/retailIntegration/constants/retailIntegrationLimits.js +10 -0
- package/bin/retailIntegration/dtos/ApplyRetailEventRequest.d.ts +29 -0
- package/bin/retailIntegration/dtos/ApplyRetailEventRequest.js +54 -0
- package/bin/retailIntegration/dtos/GetRetailInventoryRequest.d.ts +5 -0
- package/bin/retailIntegration/dtos/GetRetailInventoryRequest.js +23 -0
- package/bin/retailIntegration/dtos/ListRetailCatalogRequest.d.ts +4 -0
- package/bin/retailIntegration/dtos/ListRetailCatalogRequest.js +8 -0
- package/bin/retailIntegration/dtos/ListRetailStoresRequest.d.ts +4 -0
- package/bin/retailIntegration/dtos/ListRetailStoresRequest.js +8 -0
- package/bin/retailIntegration/dtos/ListRetailUsersRequest.d.ts +5 -0
- package/bin/retailIntegration/dtos/ListRetailUsersRequest.js +24 -0
- package/bin/retailIntegration/dtos/ReceiveInventoryEventRequest.d.ts +6 -0
- package/bin/retailIntegration/dtos/ReceiveInventoryEventRequest.js +23 -0
- package/bin/retailIntegration/dtos/ReceiveProductEventRequest.d.ts +6 -0
- package/bin/retailIntegration/dtos/ReceiveProductEventRequest.js +23 -0
- package/bin/retailIntegration/dtos/ReceiveRetailEventRequest.d.ts +12 -0
- package/bin/retailIntegration/dtos/ReceiveRetailEventRequest.js +2 -0
- package/bin/retailIntegration/dtos/ReceiveRetailUserEventRequest.d.ts +6 -0
- package/bin/retailIntegration/dtos/ReceiveRetailUserEventRequest.js +23 -0
- package/bin/retailIntegration/dtos/ReceiveStoreEventRequest.d.ts +6 -0
- package/bin/retailIntegration/dtos/ReceiveStoreEventRequest.js +23 -0
- package/bin/retailIntegration/dtos/RegisterRetailSaleRequest.d.ts +4 -0
- package/bin/retailIntegration/dtos/RegisterRetailSaleRequest.js +8 -0
- package/bin/retailIntegration/dtos/RetailEventEnvelope.d.ts +8 -0
- package/bin/retailIntegration/dtos/RetailEventEnvelope.js +33 -0
- package/bin/retailIntegration/dtos/RetailEventResult.d.ts +12 -0
- package/bin/retailIntegration/dtos/RetailEventResult.js +2 -0
- package/bin/retailIntegration/dtos/RetailOutboundResponses.d.ts +30 -0
- package/bin/retailIntegration/dtos/RetailOutboundResponses.js +2 -0
- package/bin/retailIntegration/dtos/RetailPagedQuery.d.ts +6 -0
- package/bin/retailIntegration/dtos/RetailPagedQuery.js +34 -0
- package/bin/retailIntegration/dtos/RetailTenantQuery.d.ts +4 -0
- package/bin/retailIntegration/dtos/RetailTenantQuery.js +23 -0
- package/bin/retailIntegration/enums/InventoryEventTypeEnum.d.ts +4 -0
- package/bin/retailIntegration/enums/InventoryEventTypeEnum.js +8 -0
- package/bin/retailIntegration/enums/ProductEventTypeEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/ProductEventTypeEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailCurrencyEnum.d.ts +5 -0
- package/bin/retailIntegration/enums/RetailCurrencyEnum.js +9 -0
- package/bin/retailIntegration/enums/RetailEventResultEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailEventResultEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailFinancierEnum.d.ts +8 -0
- package/bin/retailIntegration/enums/RetailFinancierEnum.js +12 -0
- package/bin/retailIntegration/enums/RetailLinePaymentTypeEnum.d.ts +5 -0
- package/bin/retailIntegration/enums/RetailLinePaymentTypeEnum.js +9 -0
- package/bin/retailIntegration/enums/RetailMdmProviderEnum.d.ts +5 -0
- package/bin/retailIntegration/enums/RetailMdmProviderEnum.js +9 -0
- package/bin/retailIntegration/enums/RetailPaymentMethodEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailPaymentMethodEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailPieceReasonEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailPieceReasonEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailPieceStatusEnum.d.ts +7 -0
- package/bin/retailIntegration/enums/RetailPieceStatusEnum.js +11 -0
- package/bin/retailIntegration/enums/RetailProductCategoryEnum.d.ts +7 -0
- package/bin/retailIntegration/enums/RetailProductCategoryEnum.js +11 -0
- package/bin/retailIntegration/enums/RetailProductStatusEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailProductStatusEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailProductTierEnum.d.ts +7 -0
- package/bin/retailIntegration/enums/RetailProductTierEnum.js +11 -0
- package/bin/retailIntegration/enums/RetailSaleFinancierEnum.d.ts +9 -0
- package/bin/retailIntegration/enums/RetailSaleFinancierEnum.js +13 -0
- package/bin/retailIntegration/enums/RetailSaleTypeEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailSaleTypeEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailStoreStatusEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailStoreStatusEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailTrackingModeEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailTrackingModeEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailUserAccessStatusEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/RetailUserAccessStatusEnum.js +10 -0
- package/bin/retailIntegration/enums/RetailUserEventTypeEnum.d.ts +7 -0
- package/bin/retailIntegration/enums/RetailUserEventTypeEnum.js +11 -0
- package/bin/retailIntegration/enums/RetailUserRoleEnum.d.ts +7 -0
- package/bin/retailIntegration/enums/RetailUserRoleEnum.js +11 -0
- package/bin/retailIntegration/enums/StoreEventTypeEnum.d.ts +6 -0
- package/bin/retailIntegration/enums/StoreEventTypeEnum.js +10 -0
- package/bin/retailIntegration/enums/index.d.ts +21 -0
- package/bin/retailIntegration/enums/index.js +37 -0
- package/bin/retailIntegration/index.d.ts +25 -0
- package/bin/retailIntegration/index.js +41 -0
- package/bin/retailIntegration/models/RetailPageMeta.d.ts +10 -0
- package/bin/retailIntegration/models/RetailPageMeta.js +2 -0
- package/bin/retailIntegration/models/RetailSystemInventory.d.ts +17 -0
- package/bin/retailIntegration/models/RetailSystemInventory.js +2 -0
- package/bin/retailIntegration/models/RetailSystemProduct.d.ts +32 -0
- package/bin/retailIntegration/models/RetailSystemProduct.js +2 -0
- package/bin/retailIntegration/models/RetailSystemSale.d.ts +46 -0
- package/bin/retailIntegration/models/RetailSystemSale.js +2 -0
- package/bin/retailIntegration/models/RetailSystemStore.d.ts +29 -0
- package/bin/retailIntegration/models/RetailSystemStore.js +2 -0
- package/bin/retailIntegration/models/RetailSystemUser.d.ts +14 -0
- package/bin/retailIntegration/models/RetailSystemUser.js +2 -0
- package/bin/retailIntegration/validators/IsIsoDateTimeWithZone.d.ts +6 -0
- package/bin/retailIntegration/validators/IsIsoDateTimeWithZone.js +29 -0
- package/bin/retailWizard/dtos/WizardSessionLookupEntry.d.ts +2 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/loanCredit/dtos/requests/CreateSellerPaymentRequest.ts +40 -0
- package/src/loanCredit/dtos/requests/InStorePaymentDetail.ts +27 -0
- package/src/loanCredit/dtos/requests/OriginateLoanCreditRequest.ts +12 -3
- package/src/loanCredit/dtos/responses/CreditCustomerListItem.ts +51 -0
- package/src/loanCredit/dtos/responses/CreditSaleContext.ts +2 -0
- package/src/loanCredit/dtos/responses/InStorePaymentListItem.ts +21 -0
- package/src/loanCredit/dtos/responses/LoanPaymentListItem.ts +11 -1
- package/src/loanCredit/dtos/responses/SellerCreditDetailResponse.ts +11 -0
- package/src/loanCredit/dtos/responses/SellerCreditListResponse.ts +8 -0
- package/src/loanCredit/dtos/responses/SellerCreditLookupResult.ts +11 -0
- package/src/loanCredit/dtos/responses/SellerCreditRow.ts +26 -0
- package/src/loanCredit/dtos/responses/SellerPaymentOptions.ts +29 -0
- package/src/loanCredit/dtos/responses/SellerPaymentResult.ts +43 -0
- package/src/loanCredit/enums/CreditCustomerStateEnum.ts +6 -0
- package/src/loanCredit/enums/InStorePaymentMethodEnum.ts +6 -0
- package/src/loanCredit/enums/LoanPaymentOriginEnum.ts +5 -0
- package/src/loanCredit/index.ts +16 -0
- package/src/retailIntegration/constants/retailIntegrationLimits.ts +9 -0
- package/src/retailIntegration/dtos/ApplyRetailEventRequest.ts +49 -0
- package/src/retailIntegration/dtos/GetRetailInventoryRequest.ts +9 -0
- package/src/retailIntegration/dtos/ListRetailCatalogRequest.ts +4 -0
- package/src/retailIntegration/dtos/ListRetailStoresRequest.ts +4 -0
- package/src/retailIntegration/dtos/ListRetailUsersRequest.ts +10 -0
- package/src/retailIntegration/dtos/ReceiveInventoryEventRequest.ts +9 -0
- package/src/retailIntegration/dtos/ReceiveProductEventRequest.ts +9 -0
- package/src/retailIntegration/dtos/ReceiveRetailEventRequest.ts +18 -0
- package/src/retailIntegration/dtos/ReceiveRetailUserEventRequest.ts +9 -0
- package/src/retailIntegration/dtos/ReceiveStoreEventRequest.ts +9 -0
- package/src/retailIntegration/dtos/RegisterRetailSaleRequest.ts +4 -0
- package/src/retailIntegration/dtos/RetailEventEnvelope.ts +20 -0
- package/src/retailIntegration/dtos/RetailEventResult.ts +14 -0
- package/src/retailIntegration/dtos/RetailOutboundResponses.ts +35 -0
- package/src/retailIntegration/dtos/RetailPagedQuery.ts +19 -0
- package/src/retailIntegration/dtos/RetailTenantQuery.ts +9 -0
- package/src/retailIntegration/enums/InventoryEventTypeEnum.ts +4 -0
- package/src/retailIntegration/enums/ProductEventTypeEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailCurrencyEnum.ts +5 -0
- package/src/retailIntegration/enums/RetailEventResultEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailFinancierEnum.ts +8 -0
- package/src/retailIntegration/enums/RetailLinePaymentTypeEnum.ts +5 -0
- package/src/retailIntegration/enums/RetailMdmProviderEnum.ts +5 -0
- package/src/retailIntegration/enums/RetailPaymentMethodEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailPieceReasonEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailPieceStatusEnum.ts +7 -0
- package/src/retailIntegration/enums/RetailProductCategoryEnum.ts +7 -0
- package/src/retailIntegration/enums/RetailProductStatusEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailProductTierEnum.ts +7 -0
- package/src/retailIntegration/enums/RetailSaleFinancierEnum.ts +9 -0
- package/src/retailIntegration/enums/RetailSaleTypeEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailStoreStatusEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailTrackingModeEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailUserAccessStatusEnum.ts +6 -0
- package/src/retailIntegration/enums/RetailUserEventTypeEnum.ts +7 -0
- package/src/retailIntegration/enums/RetailUserRoleEnum.ts +7 -0
- package/src/retailIntegration/enums/StoreEventTypeEnum.ts +6 -0
- package/src/retailIntegration/enums/index.ts +21 -0
- package/src/retailIntegration/index.ts +25 -0
- package/src/retailIntegration/models/RetailPageMeta.ts +11 -0
- package/src/retailIntegration/models/RetailSystemInventory.ts +20 -0
- package/src/retailIntegration/models/RetailSystemProduct.ts +43 -0
- package/src/retailIntegration/models/RetailSystemSale.ts +51 -0
- package/src/retailIntegration/models/RetailSystemStore.ts +33 -0
- package/src/retailIntegration/models/RetailSystemUser.ts +16 -0
- package/src/retailIntegration/validators/IsIsoDateTimeWithZone.ts +21 -0
- 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
|
+
});
|