@fiado/type-kit 3.490.0 → 3.492.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 (90) hide show
  1. package/_test_/unit/loanCredit/customerPaymentDtos.test.ts +2 -0
  2. package/_test_/unit/loanCredit/dtos/createCustomerPaymentRequest.test.ts +18 -4
  3. package/_test_/unit/loanCredit/installmentPaidEvent.test.ts +17 -0
  4. package/_test_/unit/retailCommission/commissionPayout.test.ts +104 -0
  5. package/_test_/unit/retailCommission/commissionRent.test.ts +33 -0
  6. package/_test_/unit/retailCommission/commissionRules.test.ts +249 -0
  7. package/_test_/unit/retailCommission/commissionSimulation.test.ts +43 -0
  8. package/_test_/unit/retailCommission/myCommissions.test.ts +29 -0
  9. package/_test_/unit/retailLoan/paymentContract.test.ts +3 -1
  10. package/_test_/unit/retailWizard/ventaSagaMessages.test.ts +108 -11
  11. package/bin/loanCredit/dtos/InstallmentPaidEvent.d.ts +18 -0
  12. package/bin/loanCredit/dtos/InstallmentPaidEvent.js +5 -0
  13. package/bin/loanCredit/dtos/requests/CreateCustomerPaymentRequest.d.ts +3 -1
  14. package/bin/loanCredit/dtos/requests/CreateCustomerPaymentRequest.js +7 -0
  15. package/bin/loanCredit/dtos/responses/LoanCustomerPaymentOptions.d.ts +21 -2
  16. package/bin/loanCredit/dtos/responses/LoanCustomerPaymentResult.d.ts +2 -0
  17. package/bin/loanCredit/enums/LoanCustomerPaymentTypeEnum.d.ts +5 -1
  18. package/bin/loanCredit/enums/LoanCustomerPaymentTypeEnum.js +4 -0
  19. package/bin/loanCredit/index.d.ts +1 -0
  20. package/bin/loanCredit/index.js +1 -0
  21. package/bin/retailCommission/dtos/CommissionAccrual.d.ts +36 -0
  22. package/bin/retailCommission/dtos/CommissionApi.d.ts +241 -2
  23. package/bin/retailCommission/dtos/CommissionCalcTrace.d.ts +10 -0
  24. package/bin/retailCommission/dtos/CommissionCouponAccrual.d.ts +31 -0
  25. package/bin/retailCommission/dtos/CommissionCouponAccrual.js +2 -0
  26. package/bin/retailCommission/dtos/CommissionPayout.d.ts +84 -0
  27. package/bin/retailCommission/dtos/CommissionRule.d.ts +119 -0
  28. package/bin/retailCommission/dtos/CommissionRule.js +2 -0
  29. package/bin/retailCommission/enums/CommissionAggregateByEnum.d.ts +14 -0
  30. package/bin/retailCommission/enums/CommissionAggregateByEnum.js +18 -0
  31. package/bin/retailCommission/enums/CommissionBonusModeEnum.d.ts +10 -0
  32. package/bin/retailCommission/enums/CommissionBonusModeEnum.js +14 -0
  33. package/bin/retailCommission/enums/CommissionCalcStepEnum.d.ts +3 -1
  34. package/bin/retailCommission/enums/CommissionCalcStepEnum.js +2 -0
  35. package/bin/retailCommission/enums/CommissionMetricEnum.d.ts +14 -0
  36. package/bin/retailCommission/enums/CommissionMetricEnum.js +18 -0
  37. package/bin/retailCommission/enums/CommissionOperatorEnum.d.ts +15 -0
  38. package/bin/retailCommission/enums/CommissionOperatorEnum.js +19 -0
  39. package/bin/retailCommission/enums/CommissionRuleStatusEnum.d.ts +16 -0
  40. package/bin/retailCommission/enums/CommissionRuleStatusEnum.js +20 -0
  41. package/bin/retailCommission/enums/CommissionSeriesByEnum.d.ts +14 -0
  42. package/bin/retailCommission/enums/CommissionSeriesByEnum.js +18 -0
  43. package/bin/retailCommission/enums/CommissionSimulationSkipEnum.d.ts +14 -0
  44. package/bin/retailCommission/enums/CommissionSimulationSkipEnum.js +18 -0
  45. package/bin/retailCommission/enums/CommissionWindowEnum.d.ts +12 -0
  46. package/bin/retailCommission/enums/CommissionWindowEnum.js +16 -0
  47. package/bin/retailCommission/index.d.ts +10 -0
  48. package/bin/retailCommission/index.js +10 -0
  49. package/bin/retailLoan/dtos/CreateMyPaymentRequest.d.ts +3 -1
  50. package/bin/retailLoan/dtos/CreateMyPaymentRequest.js +7 -0
  51. package/bin/retailLoan/dtos/CreateMyPaymentResponse.d.ts +2 -0
  52. package/bin/retailLoan/dtos/MyPaymentOptions.d.ts +21 -1
  53. package/bin/retailLoan/enums/MyPaymentTypeEnum.d.ts +3 -2
  54. package/bin/retailLoan/enums/MyPaymentTypeEnum.js +2 -1
  55. package/bin/retailWizard/enums/RetailCommissionInboundMessageTypeEnum.d.ts +5 -0
  56. package/bin/retailWizard/enums/RetailCommissionInboundMessageTypeEnum.js +9 -0
  57. package/bin/retailWizard/events/RetailCommissionInboundMessage.d.ts +27 -1
  58. package/bin/retailWizard/events/RetailCommissionInboundMessage.js +70 -1
  59. package/bin/retailWizard/index.d.ts +1 -0
  60. package/bin/retailWizard/index.js +1 -0
  61. package/package.json +1 -1
  62. package/src/loanCredit/dtos/InstallmentPaidEvent.ts +20 -0
  63. package/src/loanCredit/dtos/requests/CreateCustomerPaymentRequest.ts +9 -2
  64. package/src/loanCredit/dtos/responses/LoanCustomerPaymentOptions.ts +26 -3
  65. package/src/loanCredit/dtos/responses/LoanCustomerPaymentResult.ts +2 -0
  66. package/src/loanCredit/enums/LoanCustomerPaymentTypeEnum.ts +4 -0
  67. package/src/loanCredit/index.ts +1 -0
  68. package/src/retailCommission/dtos/CommissionAccrual.ts +38 -0
  69. package/src/retailCommission/dtos/CommissionApi.ts +276 -2
  70. package/src/retailCommission/dtos/CommissionCalcTrace.ts +7 -0
  71. package/src/retailCommission/dtos/CommissionCouponAccrual.ts +33 -0
  72. package/src/retailCommission/dtos/CommissionPayout.ts +91 -0
  73. package/src/retailCommission/dtos/CommissionRule.ts +132 -0
  74. package/src/retailCommission/enums/CommissionAggregateByEnum.ts +14 -0
  75. package/src/retailCommission/enums/CommissionBonusModeEnum.ts +10 -0
  76. package/src/retailCommission/enums/CommissionCalcStepEnum.ts +2 -0
  77. package/src/retailCommission/enums/CommissionMetricEnum.ts +14 -0
  78. package/src/retailCommission/enums/CommissionOperatorEnum.ts +15 -0
  79. package/src/retailCommission/enums/CommissionRuleStatusEnum.ts +16 -0
  80. package/src/retailCommission/enums/CommissionSeriesByEnum.ts +14 -0
  81. package/src/retailCommission/enums/CommissionSimulationSkipEnum.ts +14 -0
  82. package/src/retailCommission/enums/CommissionWindowEnum.ts +12 -0
  83. package/src/retailCommission/index.ts +10 -0
  84. package/src/retailLoan/dtos/CreateMyPaymentRequest.ts +9 -2
  85. package/src/retailLoan/dtos/CreateMyPaymentResponse.ts +2 -0
  86. package/src/retailLoan/dtos/MyPaymentOptions.ts +26 -2
  87. package/src/retailLoan/enums/MyPaymentTypeEnum.ts +2 -1
  88. package/src/retailWizard/enums/RetailCommissionInboundMessageTypeEnum.ts +5 -0
  89. package/src/retailWizard/events/RetailCommissionInboundMessage.ts +75 -2
  90. package/src/retailWizard/index.ts +1 -0
@@ -22,6 +22,7 @@ describe('loanCredit — opciones y resultado del pago del cliente', () => {
22
22
  dueDate: '2026-10-01',
23
23
  },
24
24
  advance: { available: false, unavailableReason: LoanPaymentOptionUnavailableReasonEnum.NOT_AVAILABLE_YET },
25
+ prepayment: { available: false, unavailableReason: LoanPaymentOptionUnavailableReasonEnum.NOT_AVAILABLE_YET },
25
26
  settlement: {
26
27
  available: true,
27
28
  amountCents: 912345,
@@ -51,6 +52,7 @@ describe('loanCredit — opciones y resultado del pago del cliente', () => {
51
52
  pendingCharge: { kind: LoanAdHocChargeKindEnum.ADVANCE, amountCents: 105000 },
52
53
  nextInstallment: blocked,
53
54
  advance: blocked,
55
+ prepayment: blocked,
54
56
  settlement: blocked,
55
57
  };
56
58
 
@@ -24,7 +24,7 @@ const VALID = {
24
24
  };
25
25
 
26
26
  describe.each([
27
- ['LoanCustomerPaymentTypeEnum', LoanCustomerPaymentTypeEnum, ['NEXT_INSTALLMENT', 'SETTLEMENT']],
27
+ ['LoanCustomerPaymentTypeEnum', LoanCustomerPaymentTypeEnum, ['NEXT_INSTALLMENT', 'SETTLEMENT', 'ADVANCE', 'PREPAYMENT']],
28
28
  ['LoanNextInstallmentModeEnum', LoanNextInstallmentModeEnum, ['ADVANCE', 'COLLECT_DUE']],
29
29
  [
30
30
  'LoanPaymentOptionUnavailableReasonEnum',
@@ -44,7 +44,11 @@ describe.each([
44
44
  });
45
45
 
46
46
  describe('CreateCustomerPaymentRequest', () => {
47
- it.each([[LoanCustomerPaymentTypeEnum.NEXT_INSTALLMENT], [LoanCustomerPaymentTypeEnum.SETTLEMENT]])(
47
+ it.each([
48
+ [LoanCustomerPaymentTypeEnum.NEXT_INSTALLMENT],
49
+ [LoanCustomerPaymentTypeEnum.SETTLEMENT],
50
+ [LoanCustomerPaymentTypeEnum.PREPAYMENT],
51
+ ])(
48
52
  'acepta el tipo %s',
49
53
  async type => {
50
54
  expect(await errorProperties({ ...VALID, type })).toEqual([]);
@@ -57,8 +61,18 @@ describe('CreateCustomerPaymentRequest', () => {
57
61
  );
58
62
  });
59
63
 
60
- it.each([['ADVANCE'], ['PARTIAL']])('rechaza el tipo %s', async type => {
61
- expect(await errorProperties({ ...VALID, type })).toEqual(['type']);
64
+ it('acepta varias cuotas con su número de cuotas', async () => {
65
+ expect(await errorProperties({ ...VALID, type: LoanCustomerPaymentTypeEnum.ADVANCE, installmentCount: 3 })).toEqual([]);
66
+ });
67
+
68
+ it.each([[1], [0], [2.5], ['3']])('rechaza el número de cuotas %p', async installmentCount => {
69
+ expect(await errorProperties({ ...VALID, type: LoanCustomerPaymentTypeEnum.ADVANCE, installmentCount })).toEqual([
70
+ 'installmentCount',
71
+ ]);
72
+ });
73
+
74
+ it('rechaza un tipo desconocido', async () => {
75
+ expect(await errorProperties({ ...VALID, type: 'PARTIAL' })).toEqual(['type']);
62
76
  });
63
77
 
64
78
  it.each([[0], [-100], [10.5], ['912345']])('rechaza el monto %p', async expectedAmountCents => {
@@ -0,0 +1,17 @@
1
+ import 'reflect-metadata';
2
+ import * as LoanCredit from '../../../src/loanCredit';
3
+
4
+ describe('loanCredit — cuota pagada', () => {
5
+ it('el evento lleva la cuota, lo pagado y el instante del pago', () => {
6
+ const payload: LoanCredit.InstallmentPaidV1Payload = {
7
+ creditId: 'CR-1',
8
+ installmentNumber: 3,
9
+ paidCents: 10_404,
10
+ paidAt: '2026-10-05T18:00:00.000Z',
11
+ installmentStatus: LoanCredit.LoanInstallmentStatusEnum.PAID,
12
+ paymentId: '01JPAY',
13
+ };
14
+ expect(LoanCredit.INSTALLMENT_PAID_EVENT_TYPE).toBe('InstallmentPaidV1');
15
+ expect(payload.installmentNumber).toBe(3);
16
+ });
17
+ });
@@ -0,0 +1,104 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+
4
+ // Caso V-01: $915 de bonos ×1.5, umbral $200, sin ajustes → neto $1,573.
5
+ const breakdown: RetailCommission.CommissionSellerBreakdown = {
6
+ sellerId: 'V-01',
7
+ salesCount: 5,
8
+ unitsCount: 8,
9
+ placementCents: 1_000_000,
10
+ subtotalBonusCents: 91_500,
11
+ multiplierBps: 15_000,
12
+ multiplierBeforeCapBps: 15_000,
13
+ multipliers: [{ ruleId: 'RC-05', name: 'Aceleración calidad NPI', factorBps: 15_000 }],
14
+ thresholdBonusCents: 20_000,
15
+ thresholds: [{ ruleId: 'RC-07', name: 'Mix de productos', amountCents: 20_000 }],
16
+ portfolioRentCents: 0,
17
+ releasedCents: 157_250,
18
+ adjustmentsCents: 0,
19
+ clawbackCents: 0,
20
+ netCents: 157_250,
21
+ byRule: [{ ruleId: 'RC-01', name: 'Base semanal · ventas 1-5', unitsCount: 5, bonusCents: 40_000, multiplierShareCents: 20_000 }],
22
+ byStore: [{ storeId: 'ST-1', salesCount: 5, unitsCount: 8, placementCents: 1_000_000, paidCents: 137_250 }],
23
+ bySku: [{ sku: 'PHONE-A', unitsCount: 1, placementCents: 350_000, paidCents: 12_000 }],
24
+ };
25
+
26
+ describe('retailCommission — desglose del corte de un vendedor', () => {
27
+ it('liberado = subtotal × multiplicador + umbral + renta, y neto = liberado + ajustes', () => {
28
+ const liberado =
29
+ Math.round((breakdown.subtotalBonusCents * breakdown.multiplierBps) / 10_000) +
30
+ breakdown.thresholdBonusCents +
31
+ breakdown.portfolioRentCents;
32
+ expect(liberado).toBe(breakdown.releasedCents);
33
+ expect(breakdown.releasedCents + breakdown.adjustmentsCents).toBe(breakdown.netCents);
34
+ });
35
+
36
+ it('el corte de un vendedor puede traer su desglose, y uno viejo sigue siendo válido sin él', () => {
37
+ const legacy: RetailCommission.CommissionPayoutSeller = {
38
+ retailerId: 'RET-1',
39
+ periodId: '2026-09-28',
40
+ sellerId: 'V-01',
41
+ accruedCount: 5,
42
+ baseAmountCents: 91_500,
43
+ settledAmountCents: null,
44
+ quotaMet: null,
45
+ quotaFactorBps: null,
46
+ };
47
+ const withBreakdown: RetailCommission.CommissionPayoutSeller = { ...legacy, settledAmountCents: 157_250, breakdown };
48
+ expect(legacy.breakdown).toBeUndefined();
49
+ expect(withBreakdown.breakdown?.netCents).toBe(157_250);
50
+ });
51
+ });
52
+
53
+ describe('retailCommission — corte vivo, agregados y series', () => {
54
+ it('expone por qué se agrega y por qué se arma una serie', () => {
55
+ expect(Object.values(RetailCommission.CommissionAggregateByEnum)).toEqual(['RULE', 'STORE', 'SKU', 'SELLER']);
56
+ expect(Object.values(RetailCommission.CommissionSeriesByEnum)).toEqual(['SELLER', 'RULE', 'STORE', 'SKU']);
57
+ });
58
+
59
+ it('el corte vivo trae el desglose de cada vendedor y sus ventas sin datos del cliente', () => {
60
+ const live: RetailCommission.GetLivePayoutResponse = {
61
+ periodId: '2026-09-28',
62
+ periodStart: '2026-09-28',
63
+ periodEnd: '2026-10-04',
64
+ status: RetailCommission.CommissionPayoutStatusEnum.OPEN,
65
+ sellers: [breakdown],
66
+ };
67
+ const seller: RetailCommission.GetLiveSellerResponse = {
68
+ periodId: '2026-09-28',
69
+ seller: breakdown,
70
+ sales: [
71
+ {
72
+ saleId: 'S1',
73
+ soldAt: '2026-09-28T18:00:00.000Z',
74
+ storeId: 'ST-1',
75
+ status: RetailCommission.CommissionAccrualStatusEnum.ACCRUED,
76
+ planSnapshotId: 'SNAP-1',
77
+ lines: [],
78
+ appliedBonuses: [],
79
+ subtotalBonusCents: 8000,
80
+ },
81
+ ],
82
+ };
83
+ expect(live.sellers).toHaveLength(1);
84
+ expect(seller.sales[0]?.subtotalBonusCents).toBe(8000);
85
+ });
86
+
87
+ it('agregados y series salen de los cortes cerrados', () => {
88
+ const aggregates: RetailCommission.GetPayoutAggregatesResponse = {
89
+ by: RetailCommission.CommissionAggregateByEnum.RULE,
90
+ from: '2026-09-07',
91
+ to: '2026-09-28',
92
+ periodIds: ['2026-09-07', '2026-09-14', '2026-09-21', '2026-09-28'],
93
+ rows: [{ key: 'RC-01', name: 'Base semanal · ventas 1-5', sellersCount: 12, salesCount: 0, unitsCount: 140, placementCents: 0, paidCents: 1_120_000 }],
94
+ totalPaidCents: 1_120_000,
95
+ };
96
+ const series: RetailCommission.GetPayoutSeriesResponse = {
97
+ by: RetailCommission.CommissionSeriesByEnum.SELLER,
98
+ id: 'V-01',
99
+ points: [{ periodId: '2026-09-28', salesCount: 5, unitsCount: 8, placementCents: 1_000_000, paidCents: 157_250 }],
100
+ };
101
+ expect(aggregates.rows[0]?.paidCents).toBe(aggregates.totalPaidCents);
102
+ expect(series.points).toHaveLength(1);
103
+ });
104
+ });
@@ -0,0 +1,33 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+
4
+ const { CommissionAccrualStatusEnum, CommissionBonusModeEnum } = RetailCommission;
5
+
6
+ describe('retailCommission — renta de cartera', () => {
7
+ it('una cuota pagada devenga un bono por cada regla de renta que le aplica', () => {
8
+ const coupon: RetailCommission.CommissionCouponAccrual = {
9
+ sellerId: 'V-01',
10
+ retailerId: 'RET-1',
11
+ periodId: '2026-10-05',
12
+ creditId: 'CR-1',
13
+ saleId: 'S-1',
14
+ installmentNumber: 3,
15
+ couponAmountCents: 10_404,
16
+ paidAt: '2026-10-05T18:00:00.000Z',
17
+ planSnapshotId: 'SNAP-1',
18
+ bonuses: [{ ruleId: 'RC-13', name: 'Renta gama alta', mode: CommissionBonusModeEnum.PERCENT, amountCents: 104 }],
19
+ subtotalCents: 104,
20
+ status: CommissionAccrualStatusEnum.ACCRUED,
21
+ createdAt: '2026-10-05T18:00:05.000Z',
22
+ };
23
+ expect(coupon.subtotalCents).toBe(coupon.bonuses[0]?.amountCents);
24
+ });
25
+
26
+ it('el desglose puede traer la renta por regla y el NPI; uno anterior a la renta sigue siendo válido', () => {
27
+ const rent: RetailCommission.CommissionAppliedRent = { ruleId: 'RC-13', name: 'Renta gama alta', couponsCount: 2, amountCents: 208 };
28
+ const partial: Partial<RetailCommission.CommissionSellerBreakdown> = { rentByRule: [rent], npiM30Bps: 250, npiSample: 8 };
29
+ const legacy: Partial<RetailCommission.CommissionSellerBreakdown> = {};
30
+ expect(partial.rentByRule?.[0]?.couponsCount).toBe(2);
31
+ expect(legacy.npiM30Bps).toBeUndefined();
32
+ });
33
+ });
@@ -0,0 +1,249 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+ import * as RetailCatalog from '../../../src/retailCatalog';
4
+ import * as RetailWizard from '../../../src/retailWizard';
5
+
6
+ describe('retailCommission — enums de las reglas', () => {
7
+ it('expone los estados de una regla', () => {
8
+ expect(Object.values(RetailCommission.CommissionRuleStatusEnum)).toEqual([
9
+ 'DRAFT', 'SCHEDULED', 'ACTIVE', 'PAUSED', 'EXPIRED',
10
+ ]);
11
+ });
12
+
13
+ it('expone los modos del bono', () => {
14
+ expect(Object.values(RetailCommission.CommissionBonusModeEnum)).toEqual(['FIXED', 'PERCENT']);
15
+ });
16
+
17
+ it('expone las métricas de condición', () => {
18
+ expect(Object.values(RetailCommission.CommissionMetricEnum)).toEqual([
19
+ 'POSICION_VENTA_EN_PERIODO', 'COLOCACION_PERIODO', 'NPI_M30_VENDEDOR', 'SKUS_DISTINTOS_PERIODO',
20
+ ]);
21
+ });
22
+
23
+ it('expone los operadores de condición', () => {
24
+ expect(Object.values(RetailCommission.CommissionOperatorEnum)).toEqual(['GTE', 'LTE', 'LT', 'GT', 'BETWEEN', 'IN']);
25
+ });
26
+
27
+ it('expone las ventanas del corte', () => {
28
+ expect(Object.values(RetailCommission.CommissionWindowEnum)).toEqual(['SEMANA_OPERATIVA', 'ROLLING_7D', 'MES']);
29
+ });
30
+
31
+ it('agrega el paso del bono por venta a la traza sin quitar los existentes', () => {
32
+ expect(Object.values(RetailCommission.CommissionCalcStepEnum)).toEqual([
33
+ 'MECHANIC', 'EXCEPTION', 'ADDON', 'QUOTA', 'ADJUSTMENT', 'NO_CATEGORY', 'BONUS_PER_SALE',
34
+ ]);
35
+ });
36
+ });
37
+
38
+ describe('retailCommission — regla, políticas y snapshot', () => {
39
+ const { CommissionBonusModeEnum, CommissionMetricEnum, CommissionOperatorEnum, CommissionRuleStatusEnum, CommissionWindowEnum } =
40
+ RetailCommission;
41
+
42
+ // Base semanal: $80 por cada una de las primeras 5 piezas de la semana.
43
+ const base: RetailCommission.CommissionRule = {
44
+ ruleId: '01JRULE0000000000000000001',
45
+ retailerId: 'RET-1',
46
+ name: 'Base semanal · ventas 1-5',
47
+ description: null,
48
+ status: CommissionRuleStatusEnum.ACTIVE,
49
+ levers: {
50
+ bonoPorVenta: { mode: CommissionBonusModeEnum.FIXED, value: 8000 },
51
+ bonoUmbral: null,
52
+ multiplicador: null,
53
+ bonoPorCuponPagado: null,
54
+ },
55
+ scope: { tipos: [], gamas: [], skus: [], tiendas: [] },
56
+ condition: {
57
+ metric: CommissionMetricEnum.POSICION_VENTA_EN_PERIODO,
58
+ operator: CommissionOperatorEnum.BETWEEN,
59
+ value: [1, 5],
60
+ minSample: null,
61
+ },
62
+ effectiveFrom: '2026-09-28',
63
+ effectiveTo: null,
64
+ priority: 100,
65
+ replacesOthers: false,
66
+ version: 1,
67
+ publishedVersion: 1,
68
+ publishedBy: 'admin-1',
69
+ publishedAt: '2026-09-28T15:00:00.000Z',
70
+ createdBy: 'admin-1',
71
+ updatedBy: 'admin-1',
72
+ createdAt: '2026-09-28T14:00:00.000Z',
73
+ updatedAt: '2026-09-28T15:00:00.000Z',
74
+ };
75
+
76
+ it('una regla puede llevar las cuatro palancas y un alcance con los enums del catálogo', () => {
77
+ const npi: RetailCommission.CommissionRule = {
78
+ ...base,
79
+ levers: {
80
+ bonoPorVenta: null,
81
+ bonoUmbral: { amountCents: 20000 },
82
+ multiplicador: { factorBps: 15000 },
83
+ bonoPorCuponPagado: { mode: CommissionBonusModeEnum.PERCENT, value: 100 },
84
+ },
85
+ scope: {
86
+ tipos: [RetailCatalog.ProductCategoryEnum.PHONE],
87
+ gamas: [RetailCatalog.ProductTierEnum.HIGH],
88
+ skus: ['SKU-1'],
89
+ tiendas: ['ST-1'],
90
+ },
91
+ condition: { metric: CommissionMetricEnum.NPI_M30_VENDEDOR, operator: CommissionOperatorEnum.LT, value: 300, minSample: 5 },
92
+ replacesOthers: true,
93
+ };
94
+ expect(npi.levers.multiplicador?.factorBps).toBe(15000);
95
+ expect(npi.condition?.minSample).toBe(5);
96
+ });
97
+
98
+ it('el snapshot congela las reglas publicadas y las políticas', () => {
99
+ const snapshot: RetailCommission.CommissionRuleSetSnapshot = {
100
+ retailerId: 'RET-1',
101
+ snapshotId: '01JSNAP000000000000000001',
102
+ rules: [base],
103
+ policies: {
104
+ clawback: { cancelacionDias: 7, devolucionDias: 7, npiM30Dias: 30, editedBy: 'admin-1', editedAt: '2026-09-28T15:00:00.000Z' },
105
+ commissions: {
106
+ multiplicadorMaxBps: 25000,
107
+ ventanaDefault: CommissionWindowEnum.SEMANA_OPERATIVA,
108
+ editedBy: 'admin-1',
109
+ editedAt: '2026-09-28T15:00:00.000Z',
110
+ },
111
+ },
112
+ changeNote: 'Publica Base semanal · ventas 1-5',
113
+ publishedBy: 'admin-1',
114
+ publishedAt: '2026-09-28T15:00:00.000Z',
115
+ };
116
+ expect(snapshot.rules).toHaveLength(1);
117
+ });
118
+
119
+ it('las políticas pueden faltar mientras la cadena no las guarda', () => {
120
+ const policies: RetailCommission.CommissionPolicies = { clawback: null, commissions: null };
121
+ expect(policies.clawback).toBeNull();
122
+ });
123
+ });
124
+
125
+ describe('retailCommission — devengo con reglas', () => {
126
+ const { CommissionBonusModeEnum, CommissionCalcStepEnum } = RetailCommission;
127
+
128
+ const phone: RetailCommission.CommissionSaleLine = {
129
+ lineId: 'L1',
130
+ sku: 'SKU-PHONE',
131
+ qty: 1,
132
+ netCents: 450000,
133
+ category: RetailCatalog.ProductCategoryEnum.PHONE,
134
+ tier: RetailCatalog.ProductTierEnum.HIGH,
135
+ brand: 'Samsung',
136
+ paymentType: RetailWizard.LinePaymentTypeEnum.CREDIT,
137
+ };
138
+ const gift: RetailCommission.CommissionSaleLine = {
139
+ ...phone,
140
+ lineId: 'L2',
141
+ netCents: 0,
142
+ role: RetailWizard.WizardLineRoleEnum.GIFT,
143
+ };
144
+
145
+ const sale: RetailCommission.CommissionSaleInput = {
146
+ saleId: 'SALE-1',
147
+ sellerId: 'SELLER-1',
148
+ retailerId: 'RET-1',
149
+ storeId: 'ST-1',
150
+ sku: 'SKU-PHONE',
151
+ brand: 'Samsung',
152
+ equipmentPriceCents: 450000,
153
+ amountCents: 450000,
154
+ financier: RetailWizard.SaleFinancierEnum.SUREKEEP,
155
+ paymentType: 'CREDIT',
156
+ soldAt: '2026-09-30T18:00:00.000Z',
157
+ lines: [phone, gift],
158
+ };
159
+
160
+ it('la venta lleva sus líneas, y una sin líneas sigue siendo válida', () => {
161
+ const legacy: RetailCommission.CommissionSaleInput = {
162
+ saleId: sale.saleId,
163
+ sellerId: sale.sellerId,
164
+ retailerId: sale.retailerId,
165
+ storeId: sale.storeId,
166
+ sku: sale.sku,
167
+ brand: sale.brand,
168
+ equipmentPriceCents: sale.equipmentPriceCents,
169
+ amountCents: sale.amountCents,
170
+ financier: sale.financier,
171
+ paymentType: sale.paymentType,
172
+ soldAt: sale.soldAt,
173
+ };
174
+ expect(sale.lines).toHaveLength(2);
175
+ expect(legacy.lines).toBeUndefined();
176
+ });
177
+
178
+ it('el bono aplicado dice qué regla, qué pieza y en qué posición', () => {
179
+ const bonus: RetailCommission.CommissionAppliedBonus = {
180
+ ruleId: '01JRULE0000000000000000001',
181
+ name: 'Base semanal · ventas 1-5',
182
+ lineId: 'L1',
183
+ sku: 'SKU-PHONE',
184
+ position: 1,
185
+ mode: CommissionBonusModeEnum.FIXED,
186
+ amountCents: 8000,
187
+ };
188
+ const trace: RetailCommission.CommissionCalcTraceEntry = {
189
+ step: CommissionCalcStepEnum.BONUS_PER_SALE,
190
+ label: bonus.name,
191
+ outputCents: bonus.amountCents,
192
+ detail: { ruleId: bonus.ruleId, lineId: bonus.lineId, position: bonus.position },
193
+ };
194
+ expect(trace.outputCents).toBe(8000);
195
+ });
196
+
197
+ it('el devengo puede traer los bonos aplicados y su subtotal', () => {
198
+ const accrual: Pick<RetailCommission.CommissionAccrual, 'appliedBonuses' | 'subtotalBonusCents'> = {
199
+ appliedBonuses: [],
200
+ subtotalBonusCents: 0,
201
+ };
202
+ expect(accrual.subtotalBonusCents).toBe(0);
203
+ });
204
+ });
205
+
206
+ describe('retailCommission — contratos de reglas y políticas', () => {
207
+ const { CommissionBonusModeEnum, CommissionWindowEnum } = RetailCommission;
208
+
209
+ const input: RetailCommission.CommissionRuleInput = {
210
+ name: 'Bono Accesorios',
211
+ description: null,
212
+ levers: {
213
+ bonoPorVenta: { mode: CommissionBonusModeEnum.FIXED, value: 1000 },
214
+ bonoUmbral: null,
215
+ multiplicador: null,
216
+ bonoPorCuponPagado: null,
217
+ },
218
+ scope: { tipos: [RetailCatalog.ProductCategoryEnum.ACCESSORY], gamas: [], skus: [], tiendas: [] },
219
+ condition: null,
220
+ effectiveFrom: '2026-10-01',
221
+ effectiveTo: null,
222
+ priority: 100,
223
+ replacesOthers: false,
224
+ };
225
+
226
+ it('crear y editar comparten el cuerpo; editar exige la versión que se cargó', () => {
227
+ const create: RetailCommission.CreateRuleRequest = input;
228
+ const update: RetailCommission.UpdateRuleRequest = { ...input, expectedVersion: 2 };
229
+ const action: RetailCommission.RuleActionRequest = { expectedVersion: 3 };
230
+ expect([create.name, update.expectedVersion, action.expectedVersion]).toEqual(['Bono Accesorios', 2, 3]);
231
+ });
232
+
233
+ it('las políticas se guardan juntas', () => {
234
+ const request: RetailCommission.UpdatePoliciesRequest = {
235
+ clawback: { cancelacionDias: 7, devolucionDias: 7, npiM30Dias: 30 },
236
+ commissions: { multiplicadorMaxBps: 25000, ventanaDefault: CommissionWindowEnum.SEMANA_OPERATIVA },
237
+ };
238
+ expect(request.commissions.multiplicadorMaxBps).toBe(25000);
239
+ });
240
+
241
+ it('el plan vigente con reglas puede no existir todavía', () => {
242
+ const response: RetailCommission.GetRuleSetResponse = { active: null };
243
+ const history: RetailCommission.RuleHistoryResponse = {
244
+ events: [{ at: '2026-10-01T15:00:00.000Z', action: 'PUBLISH', performedBy: 'admin-1', version: 1 }],
245
+ };
246
+ expect(response.active).toBeNull();
247
+ expect(history.events[0]?.version).toBe(1);
248
+ });
249
+ });
@@ -0,0 +1,43 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+ import * as RetailCatalog from '../../../src/retailCatalog';
4
+
5
+ describe('retailCommission — simulador de reglas', () => {
6
+ it('expone por qué una regla no paga en la simulación', () => {
7
+ expect(Object.values(RetailCommission.CommissionSimulationSkipEnum)).toEqual([
8
+ 'NOT_IN_EFFECT', 'OUT_OF_SCOPE', 'CONDITION_NOT_MET', 'NO_DATA',
9
+ ]);
10
+ });
11
+
12
+ it('la simulación lleva un vendedor modelo y un producto, y devuelve venta siguiente, progresión y corte', () => {
13
+ const request: RetailCommission.SimulateRulesRequest = {
14
+ ruleIds: [],
15
+ profile: { unitsInWeek: 7, placementCents: 2_800_000, distinctSkus: 2 },
16
+ product: { sku: 'PHONE-A', category: RetailCatalog.ProductCategoryEnum.PHONE, tier: RetailCatalog.ProductTierEnum.MID, netCents: 350_000 },
17
+ storeId: null,
18
+ date: null,
19
+ };
20
+ const response: RetailCommission.SimulateRulesResponse = {
21
+ date: '2026-10-01',
22
+ nextSale: {
23
+ position: 8,
24
+ subtotalBonusCents: 15_000,
25
+ rules: [
26
+ { ruleId: 'RC-02', name: 'Extendido', amountCents: 15_000, skipped: null },
27
+ { ruleId: 'RC-24', name: 'Accesorios', amountCents: 0, skipped: RetailCommission.CommissionSimulationSkipEnum.OUT_OF_SCOPE },
28
+ ],
29
+ },
30
+ progression: [{ position: 1, subtotalBonusCents: 8_000, cumulativeBonusCents: 8_000 }],
31
+ week: {
32
+ subtotalBonusCents: 70_000,
33
+ multiplierBps: 15_000,
34
+ multiplierBeforeCapBps: 15_000,
35
+ multipliers: [],
36
+ thresholdBonusCents: 0,
37
+ thresholds: [],
38
+ releasedCents: 105_000,
39
+ },
40
+ };
41
+ expect(request.profile.unitsInWeek + 1).toBe(response.nextSale.position);
42
+ });
43
+ });
@@ -0,0 +1,29 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+
4
+ describe('retailCommission — mis comisiones', () => {
5
+ it('el vendedor ve su semana, mes, año, sus últimos cortes y las reglas vigentes', () => {
6
+ const mine: RetailCommission.GetMyCommissionsResponse = {
7
+ seller: { sellerId: 'V-01', name: 'Ana López', storeId: 'ST-1' },
8
+ period: { periodId: '2026-10-05', periodStart: '2026-10-05', periodEnd: '2026-10-11' },
9
+ week: null,
10
+ month: { periodsCount: 1, salesCount: 7, unitsCount: 9, netCents: 157_250 },
11
+ year: { periodsCount: 20, salesCount: 140, unitsCount: 180, netCents: 2_400_000 },
12
+ history: [{ periodId: '2026-09-28', salesCount: 7, unitsCount: 9, placementCents: 1_000_000, paidCents: 157_250 }],
13
+ rules: [
14
+ {
15
+ ruleId: 'RC-07',
16
+ name: 'Mix de productos',
17
+ description: null,
18
+ levers: { bonoPorVenta: null, bonoUmbral: { amountCents: 20_000 }, multiplicador: null, bonoPorCuponPagado: null },
19
+ scope: { tipos: [], gamas: [], skus: [], tiendas: [] },
20
+ condition: null,
21
+ effectiveFrom: '2026-09-01',
22
+ effectiveTo: null,
23
+ },
24
+ ],
25
+ multiplierCapBps: 25_000,
26
+ };
27
+ expect(mine.history[0]?.paidCents).toBe(mine.month.netCents);
28
+ });
29
+ });
@@ -22,7 +22,7 @@ const errorProperties = async (body: object): Promise<string[]> => {
22
22
  };
23
23
 
24
24
  describe.each([
25
- ['MyPaymentTypeEnum', MyPaymentTypeEnum, ['NEXT_INSTALLMENT', 'ADVANCE', 'SETTLEMENT']],
25
+ ['MyPaymentTypeEnum', MyPaymentTypeEnum, ['NEXT_INSTALLMENT', 'ADVANCE', 'SETTLEMENT', 'PREPAYMENT']],
26
26
  ['MyNextInstallmentModeEnum', MyNextInstallmentModeEnum, ['ADVANCE', 'COLLECT_DUE']],
27
27
  [
28
28
  'MyPaymentUnavailableReasonEnum',
@@ -85,6 +85,7 @@ describe('retailLoan — opciones y respuesta del pago', () => {
85
85
  dueDate: '2026-10-01',
86
86
  },
87
87
  advance: { available: false, unavailableReason: MyPaymentUnavailableReasonEnum.NOT_AVAILABLE_YET },
88
+ prepayment: { available: false, unavailableReason: MyPaymentUnavailableReasonEnum.NOT_AVAILABLE_YET },
88
89
  settlement: {
89
90
  available: true,
90
91
  amount: 9123.45,
@@ -104,6 +105,7 @@ describe('retailLoan — opciones y respuesta del pago', () => {
104
105
  pendingPayment: { type: MyPaymentTypeEnum.NEXT_INSTALLMENT, amount: 1050 },
105
106
  nextInstallment: blocked,
106
107
  advance: blocked,
108
+ prepayment: blocked,
107
109
  settlement: blocked,
108
110
  };
109
111