@fiado/type-kit 3.550.0 → 3.552.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 (95) hide show
  1. package/_test_/unit/retailCatalog/singlePriceFields.test.ts +48 -0
  2. package/_test_/unit/retailCommission/commissionPayments.test.ts +168 -0
  3. package/_test_/unit/retailCommission/commissionRules.test.ts +53 -0
  4. package/_test_/unit/retailIntegration/eventPayloads.test.ts +1 -0
  5. package/_test_/unit/retailOrg/dtos/RetailUser.test.ts +47 -0
  6. package/_test_/unit/retailOrg/dtos/requests/retailUserWalletRequests.test.ts +41 -0
  7. package/_test_/unit/retailOrg/dtos/responses/retailUserWalletResponses.test.ts +30 -0
  8. package/_test_/unit/retailOrg/enums/RetailUserWalletLinkMethodEnum.test.ts +12 -0
  9. package/_test_/unit/retailPromotions/giftOptionValue.test.ts +20 -0
  10. package/_test_/unit/retailWizard/linesWithoutListPrice.test.ts +40 -0
  11. package/bin/retailCatalog/dtos/Product.d.ts +2 -1
  12. package/bin/retailCatalog/dtos/requests/BackofficeCreateSkuPriceRequest.d.ts +1 -0
  13. package/bin/retailCatalog/dtos/requests/CreateProductRequest.d.ts +1 -0
  14. package/bin/retailCatalog/dtos/requests/SyncInventoryRequest.d.ts +1 -0
  15. package/bin/retailCatalog/dtos/requests/UpdateProductRequest.d.ts +1 -0
  16. package/bin/retailCatalog/dtos/responses/InventoryLensResponse.d.ts +1 -1
  17. package/bin/retailCatalog/dtos/responses/ReserveStockResponse.d.ts +4 -1
  18. package/bin/retailCatalog/dtos/responses/SkuPriceEntry.d.ts +2 -1
  19. package/bin/retailCatalog/dtos/responses/StoreCatalogItem.d.ts +1 -1
  20. package/bin/retailCommission/dtos/CommissionApi.d.ts +80 -1
  21. package/bin/retailCommission/dtos/CommissionLedger.d.ts +96 -0
  22. package/bin/retailCommission/dtos/CommissionLedger.js +2 -0
  23. package/bin/retailCommission/dtos/CommissionRule.d.ts +14 -1
  24. package/bin/retailCommission/enums/CommissionBillingPeriodEnum.d.ts +10 -0
  25. package/bin/retailCommission/enums/CommissionBillingPeriodEnum.js +14 -0
  26. package/bin/retailCommission/enums/CommissionLedgerMovementKindEnum.d.ts +14 -0
  27. package/bin/retailCommission/enums/CommissionLedgerMovementKindEnum.js +18 -0
  28. package/bin/retailCommission/enums/CommissionPayTimingEnum.d.ts +13 -0
  29. package/bin/retailCommission/enums/CommissionPayTimingEnum.js +17 -0
  30. package/bin/retailCommission/enums/CommissionPayTriggerEnum.d.ts +18 -0
  31. package/bin/retailCommission/enums/CommissionPayTriggerEnum.js +22 -0
  32. package/bin/retailCommission/enums/CommissionPaymentAttentionEnum.d.ts +14 -0
  33. package/bin/retailCommission/enums/CommissionPaymentAttentionEnum.js +18 -0
  34. package/bin/retailCommission/enums/CommissionPaymentStatusEnum.d.ts +14 -0
  35. package/bin/retailCommission/enums/CommissionPaymentStatusEnum.js +18 -0
  36. package/bin/retailCommission/index.d.ts +7 -0
  37. package/bin/retailCommission/index.js +7 -0
  38. package/bin/retailOrg/dtos/RetailUser.d.ts +13 -0
  39. package/bin/retailOrg/dtos/RetailUser.js +39 -0
  40. package/bin/retailOrg/dtos/requests/LinkRetailUserWalletRequest.d.ts +10 -0
  41. package/bin/retailOrg/dtos/requests/LinkRetailUserWalletRequest.js +37 -0
  42. package/bin/retailOrg/dtos/requests/LookupRetailUserWalletRequest.d.ts +10 -0
  43. package/bin/retailOrg/dtos/requests/LookupRetailUserWalletRequest.js +37 -0
  44. package/bin/retailOrg/dtos/responses/LookupRetailUserWalletResponse.d.ts +10 -0
  45. package/bin/retailOrg/dtos/responses/LookupRetailUserWalletResponse.js +2 -0
  46. package/bin/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.d.ts +8 -0
  47. package/bin/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.js +2 -0
  48. package/bin/retailOrg/enums/RetailUserWalletLinkMethodEnum.d.ts +11 -0
  49. package/bin/retailOrg/enums/RetailUserWalletLinkMethodEnum.js +15 -0
  50. package/bin/retailOrg/index.d.ts +6 -0
  51. package/bin/retailOrg/index.js +7 -0
  52. package/bin/retailOrg/validators/IsSingleWalletKeyConstraint.d.ts +6 -0
  53. package/bin/retailOrg/validators/IsSingleWalletKeyConstraint.js +24 -0
  54. package/bin/retailPromotions/dtos/responses/WizardSalePromotionResponses.d.ts +4 -1
  55. package/bin/retailWizard/dtos/SaleLine.d.ts +2 -1
  56. package/bin/retailWizard/dtos/WizardGiftLine.d.ts +1 -1
  57. package/bin/retailWizard/dtos/WizardLinePromotionFields.d.ts +1 -1
  58. package/bin/retailWizard/dtos/WizardReservedLine.d.ts +2 -1
  59. package/bin/retailWizard/events/SaleCompletedV1.d.ts +1 -1
  60. package/bin/retailWizard/events/SaleCompletedV2.d.ts +1 -1
  61. package/package.json +1 -1
  62. package/src/retailCatalog/dtos/Product.ts +2 -1
  63. package/src/retailCatalog/dtos/requests/BackofficeCreateSkuPriceRequest.ts +1 -0
  64. package/src/retailCatalog/dtos/requests/CreateProductRequest.ts +1 -0
  65. package/src/retailCatalog/dtos/requests/SyncInventoryRequest.ts +1 -0
  66. package/src/retailCatalog/dtos/requests/UpdateProductRequest.ts +1 -0
  67. package/src/retailCatalog/dtos/responses/InventoryLensResponse.ts +1 -1
  68. package/src/retailCatalog/dtos/responses/ReserveStockResponse.ts +4 -1
  69. package/src/retailCatalog/dtos/responses/SkuPriceEntry.ts +2 -1
  70. package/src/retailCatalog/dtos/responses/StoreCatalogItem.ts +1 -1
  71. package/src/retailCommission/dtos/CommissionApi.ts +97 -1
  72. package/src/retailCommission/dtos/CommissionLedger.ts +101 -0
  73. package/src/retailCommission/dtos/CommissionRule.ts +15 -1
  74. package/src/retailCommission/enums/CommissionBillingPeriodEnum.ts +10 -0
  75. package/src/retailCommission/enums/CommissionLedgerMovementKindEnum.ts +14 -0
  76. package/src/retailCommission/enums/CommissionPayTimingEnum.ts +13 -0
  77. package/src/retailCommission/enums/CommissionPayTriggerEnum.ts +18 -0
  78. package/src/retailCommission/enums/CommissionPaymentAttentionEnum.ts +14 -0
  79. package/src/retailCommission/enums/CommissionPaymentStatusEnum.ts +14 -0
  80. package/src/retailCommission/index.ts +7 -0
  81. package/src/retailOrg/dtos/RetailUser.ts +39 -0
  82. package/src/retailOrg/dtos/requests/LinkRetailUserWalletRequest.ts +24 -0
  83. package/src/retailOrg/dtos/requests/LookupRetailUserWalletRequest.ts +24 -0
  84. package/src/retailOrg/dtos/responses/LookupRetailUserWalletResponse.ts +11 -0
  85. package/src/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.ts +8 -0
  86. package/src/retailOrg/enums/RetailUserWalletLinkMethodEnum.ts +11 -0
  87. package/src/retailOrg/index.ts +8 -0
  88. package/src/retailOrg/validators/IsSingleWalletKeyConstraint.ts +18 -0
  89. package/src/retailPromotions/dtos/responses/WizardSalePromotionResponses.ts +4 -1
  90. package/src/retailWizard/dtos/SaleLine.ts +2 -1
  91. package/src/retailWizard/dtos/WizardGiftLine.ts +1 -1
  92. package/src/retailWizard/dtos/WizardLinePromotionFields.ts +1 -1
  93. package/src/retailWizard/dtos/WizardReservedLine.ts +2 -1
  94. package/src/retailWizard/events/SaleCompletedV1.ts +1 -1
  95. package/src/retailWizard/events/SaleCompletedV2.ts +1 -1
@@ -0,0 +1,48 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCatalog from '../../../src/retailCatalog';
3
+
4
+ // ts-jest compila con type-check: si un campo sigue obligatorio o no existe, la suite no compila.
5
+ describe('retailCatalog — un solo precio y valor del regalo', () => {
6
+ const reservation: RetailCatalog.ReserveStockResponse = {
7
+ expiresAt: '2026-10-20T20:30:00.000Z',
8
+ basePriceCents: 0,
9
+ priceSk: '2026-10-01T00:00:00.000Z',
10
+ eligibleFinanciers: [],
11
+ sellItemCost: 1,
12
+ mdmEnabled: false,
13
+ mdmProvider: null,
14
+ };
15
+
16
+ it('ReserveStockResponse es válida sin listPriceCents ni giftValueCents', () => {
17
+ expect(reservation.giftValueCents).toBeUndefined();
18
+ expect(reservation.listPriceCents).toBeUndefined();
19
+ });
20
+
21
+ it('la reserva de regalo trae giftValueCents', () => {
22
+ const gift: RetailCatalog.ReserveStockResponse = { ...reservation, giftValueCents: 35000 };
23
+ expect(gift.giftValueCents).toBe(35000);
24
+ });
25
+
26
+ it('acepta los dos nombres con el mismo valor mientras dure la transición', () => {
27
+ const gift: RetailCatalog.ReserveStockResponse = { ...reservation, giftValueCents: 35000, listPriceCents: 35000 };
28
+ expect(gift.listPriceCents).toBe(gift.giftValueCents);
29
+ });
30
+
31
+ it('Product es válido sin listPriceCents', () => {
32
+ const product: Pick<RetailCatalog.Product, 'sku' | 'basePriceCents' | 'listPriceCents'> = {
33
+ sku: 'SKU-1',
34
+ basePriceCents: 129900,
35
+ };
36
+ expect(product.listPriceCents).toBeUndefined();
37
+ });
38
+
39
+ it('SkuPriceEntry es válido sin listPriceCents', () => {
40
+ const entry: RetailCatalog.SkuPriceEntry = {
41
+ effectiveFrom: '2026-10-20T00:00:00.000Z',
42
+ basePriceCents: 129900,
43
+ source: RetailCatalog.PriceSourceEnum.BACKOFFICE,
44
+ createdBy: 'backoffice-user-1',
45
+ };
46
+ expect(entry.listPriceCents).toBeUndefined();
47
+ });
48
+ });
@@ -0,0 +1,168 @@
1
+ import 'reflect-metadata';
2
+ import * as RetailCommission from '../../../src/retailCommission';
3
+
4
+ describe('retailCommission — enums de los pagos', () => {
5
+ it('expone los estados de un depósito', () => {
6
+ expect(Object.values(RetailCommission.CommissionPaymentStatusEnum)).toEqual([
7
+ 'PENDING', 'ACCREDITED', 'FAILED', 'UNVERIFIED',
8
+ ]);
9
+ });
10
+
11
+ it('expone por qué un saldo necesita atención', () => {
12
+ expect(Object.values(RetailCommission.CommissionPaymentAttentionEnum)).toEqual(['NO_WALLET', 'REJECTED', 'PAUSED', 'DEBT']);
13
+ });
14
+
15
+ it('expone los tipos de movimiento del libro', () => {
16
+ expect(Object.values(RetailCommission.CommissionLedgerMovementKindEnum)).toEqual([
17
+ 'EARN', 'DEDUCT', 'RELEASE', 'PAID_OUTSIDE',
18
+ ]);
19
+ });
20
+
21
+ it('expone qué pide pagarle a un vendedor', () => {
22
+ expect(Object.values(RetailCommission.CommissionPayTriggerEnum)).toEqual([
23
+ 'SALE', 'CUT', 'RELEASE', 'ADJUSTMENT', 'WALLET_LINKED', 'RETRY',
24
+ ]);
25
+ });
26
+ });
27
+
28
+ // Ana después del cierre del 12 de octubre: ganó $300, le descontaron $100 de una venta cancelada,
29
+ // se le depositaron $180 y le quedan $20 de renta retenidos hasta el 1 de noviembre.
30
+ const balance: RetailCommission.CommissionSellerBalance = {
31
+ retailerId: 'RET-1',
32
+ sellerId: 'V-01',
33
+ retainedCents: 2_000,
34
+ payableCents: 0,
35
+ inTransitCents: 0,
36
+ earnedCents: 30_000,
37
+ deductedCents: 10_000,
38
+ accreditedCents: 18_000,
39
+ paidOutsideCents: 0,
40
+ attention: null,
41
+ attentionCode: null,
42
+ attentionSince: null,
43
+ updatedAt: '2026-10-12T07:00:00.000Z',
44
+ };
45
+
46
+ const cut: RetailCommission.CommissionLedgerMovement = {
47
+ movementKey: 'MOV#2026-10-05#CUT',
48
+ kind: RetailCommission.CommissionLedgerMovementKindEnum.EARN,
49
+ periodId: '2026-10-05',
50
+ retainedDeltaCents: 2_000,
51
+ payableDeltaCents: 8_000,
52
+ byRule: [
53
+ { ruleId: 'RC-BASE', payTiming: RetailCommission.CommissionPayTimingEnum.ON_CUT, cents: 8_000 },
54
+ { ruleId: 'RC-RENTA', payTiming: RetailCommission.CommissionPayTimingEnum.MONTHLY, cents: 2_000 },
55
+ ],
56
+ saleId: null,
57
+ adjustmentId: null,
58
+ releaseMonth: null,
59
+ reason: null,
60
+ createdAt: '2026-10-12T06:00:00.000Z',
61
+ createdBy: 'system',
62
+ };
63
+
64
+ const accredited: RetailCommission.CommissionPayment = {
65
+ paymentId: '01JPAY0000000000000000001',
66
+ retailerId: 'RET-1',
67
+ sellerId: 'V-01',
68
+ amountCents: 10_000,
69
+ status: RetailCommission.CommissionPaymentStatusEnum.ACCREDITED,
70
+ directoryId: 'DIR-1',
71
+ transactionId: 'TX-1',
72
+ errorCode: null,
73
+ concept: 'Comisiones VentasLuga · 5 oct 2026',
74
+ requestedAt: '2026-10-05T18:00:00.000Z',
75
+ resolvedAt: '2026-10-05T18:00:02.000Z',
76
+ resolvedBy: null,
77
+ note: null,
78
+ };
79
+
80
+ const statement: RetailCommission.CommissionChainStatement = {
81
+ statementId: '01JSTMT00000000000000001',
82
+ retailerId: 'RET-1',
83
+ period: RetailCommission.CommissionBillingPeriodEnum.WEEKLY,
84
+ startDate: '2026-10-05',
85
+ endDate: '2026-10-11',
86
+ accreditedCents: 10_000,
87
+ paymentsCount: 1,
88
+ sellersCount: 1,
89
+ generatedAt: '2026-10-12T07:00:00.000Z',
90
+ };
91
+
92
+ describe('retailCommission — libro de pagos', () => {
93
+ it('lo que queda en las tres bolsas es lo ganado menos lo descontado, lo depositado y lo pagado por fuera', () => {
94
+ const inPockets = balance.retainedCents + balance.payableCents + balance.inTransitCents;
95
+ expect(inPockets).toBe(balance.earnedCents - balance.deductedCents - balance.accreditedCents - balance.paidOutsideCents);
96
+ });
97
+
98
+ it('el movimiento del cierre reparte por regla entre retenido y exigible', () => {
99
+ const total = cut.byRule.reduce((sum, r) => sum + r.cents, 0);
100
+ expect(total).toBe(cut.retainedDeltaCents + cut.payableDeltaCents);
101
+ });
102
+
103
+ it('un depósito confirmado trae folio y uno rechazado su código', () => {
104
+ const failed: RetailCommission.CommissionPayment = {
105
+ ...accredited,
106
+ paymentId: '01JPAY0000000000000000002',
107
+ status: RetailCommission.CommissionPaymentStatusEnum.FAILED,
108
+ transactionId: null,
109
+ errorCode: 'USER_NOT_ACTIVE',
110
+ };
111
+ expect([failed.transactionId, failed.errorCode]).toEqual([null, 'USER_NOT_ACTIVE']);
112
+ });
113
+
114
+ it('el estado de cuenta cubre un periodo de cobro', () => {
115
+ expect([statement.period, statement.startDate, statement.endDate]).toEqual(['WEEKLY', '2026-10-05', '2026-10-11']);
116
+ });
117
+ });
118
+
119
+ describe('retailCommission — contratos de los pagos', () => {
120
+ it('"Por pagar" trae pendientes, depósitos sin confirmar y deudas', () => {
121
+ const unverified: RetailCommission.CommissionPayment = {
122
+ ...accredited,
123
+ status: RetailCommission.CommissionPaymentStatusEnum.UNVERIFIED,
124
+ transactionId: null,
125
+ resolvedAt: null,
126
+ };
127
+ const debt: RetailCommission.CommissionSellerBalance = {
128
+ ...balance,
129
+ payableCents: -5_000,
130
+ attention: RetailCommission.CommissionPaymentAttentionEnum.DEBT,
131
+ attentionSince: '2026-10-09T18:00:00.000Z',
132
+ };
133
+ const report: RetailCommission.GetPaymentAttentionResponse = { pending: [], unverified: [unverified], debts: [debt] };
134
+ expect([report.unverified[0]?.status, report.debts[0]?.attention]).toEqual(['UNVERIFIED', 'DEBT']);
135
+ });
136
+
137
+ it('pagado por fuera lleva motivo; la revisión manda folio solo si sí se depositó', () => {
138
+ const outside: RetailCommission.MarkPaidOutsideRequest = { amountCents: 8_000, reason: 'Pagado en efectivo en tienda' };
139
+ const yes: RetailCommission.ResolvePaymentRequest = { accredited: true, transactionId: 'TX-9', note: 'Folio confirmado' };
140
+ const no: RetailCommission.ResolvePaymentRequest = { accredited: false, note: 'No llegó a la wallet' };
141
+ expect([outside.amountCents, yes.transactionId, no.transactionId]).toEqual([8_000, 'TX-9', undefined]);
142
+ });
143
+
144
+ it('el historial del vendedor y los estados de cuenta de la cadena', () => {
145
+ const ledger: RetailCommission.GetSellerLedgerResponse = { balance, movements: [cut], payments: [accredited] };
146
+ const list: RetailCommission.ListStatementsResponse = { statements: [statement] };
147
+ const detail: RetailCommission.GetStatementResponse = { statement, payments: [accredited] };
148
+ expect([ledger.movements.length, list.statements.length, detail.payments.length]).toEqual([1, 1, 1]);
149
+ });
150
+
151
+ it('el vendedor sin nada en el libro ve su saldo en null', () => {
152
+ const mine: RetailCommission.GetMyPaymentsResponse = { balance: null, payments: [] };
153
+ expect(mine.balance).toBeNull();
154
+ });
155
+
156
+ it('las respuestas de las acciones de "Por pagar" y del aviso privado de wallet vinculada', () => {
157
+ const retry: RetailCommission.RetrySellerPaymentView = { sellerId: 'V-01', triggerId: '01JRETRY0000000000000000001' };
158
+ const outside: RetailCommission.MarkPaidOutsideView = { balance, movement: cut };
159
+ const resolved: RetailCommission.ResolvePaymentView = { payment: accredited };
160
+ const trigger: RetailCommission.PrivateTriggerSellerPaymentResponse = { enqueued: false };
161
+ expect([retry.sellerId, outside.movement.movementKey, resolved.payment.status, trigger.enqueued]).toEqual([
162
+ 'V-01',
163
+ 'MOV#2026-10-05#CUT',
164
+ 'ACCREDITED',
165
+ false,
166
+ ]);
167
+ });
168
+ });
@@ -28,6 +28,14 @@ describe('retailCommission — enums de las reglas', () => {
28
28
  expect(Object.values(RetailCommission.CommissionCountByEnum)).toEqual(['PIECE', 'SALE']);
29
29
  });
30
30
 
31
+ it('una regla se paga al momento, al cierre de la semana o al cierre del mes', () => {
32
+ expect(Object.values(RetailCommission.CommissionPayTimingEnum)).toEqual(['ON_SALE', 'ON_CUT', 'MONTHLY']);
33
+ });
34
+
35
+ it('a la cadena se le cobra cada semana o cada mes', () => {
36
+ expect(Object.values(RetailCommission.CommissionBillingPeriodEnum)).toEqual(['WEEKLY', 'MONTHLY']);
37
+ });
38
+
31
39
  it('expone las ventanas del corte', () => {
32
40
  expect(Object.values(RetailCommission.CommissionWindowEnum)).toEqual(['SEMANA_OPERATIVA', 'ROLLING_7D', 'MES']);
33
41
  });
@@ -99,6 +107,12 @@ describe('retailCommission — regla, políticas y snapshot', () => {
99
107
  expect(npi.condition?.minSample).toBe(5);
100
108
  });
101
109
 
110
+ it('la regla dice cuándo se paga, y una sin el campo sigue siendo válida', () => {
111
+ const monthly: RetailCommission.CommissionRule = { ...base, payTiming: RetailCommission.CommissionPayTimingEnum.MONTHLY };
112
+ expect(base.payTiming).toBeUndefined();
113
+ expect(monthly.payTiming).toBe('MONTHLY');
114
+ });
115
+
102
116
  it('el snapshot congela las reglas publicadas y las políticas', () => {
103
117
  const snapshot: RetailCommission.CommissionRuleSetSnapshot = {
104
118
  retailerId: 'RET-1',
@@ -124,6 +138,17 @@ describe('retailCommission — regla, políticas y snapshot', () => {
124
138
  const policies: RetailCommission.CommissionPolicies = { clawback: null, commissions: null };
125
139
  expect(policies.clawback).toBeNull();
126
140
  });
141
+
142
+ it('la política de pagos es opcional y viaja con las demás', () => {
143
+ const payouts: RetailCommission.CommissionPayoutPolicy = {
144
+ walletDeposits: false,
145
+ chainBillingPeriod: RetailCommission.CommissionBillingPeriodEnum.MONTHLY,
146
+ };
147
+ const legacy: RetailCommission.CommissionPolicies = { clawback: null, commissions: null };
148
+ const withPayouts: RetailCommission.CommissionPolicies = { ...legacy, payouts };
149
+ expect(legacy.payouts).toBeUndefined();
150
+ expect(withPayouts.payouts?.chainBillingPeriod).toBe('MONTHLY');
151
+ });
127
152
  });
128
153
 
129
154
  describe('retailCommission — devengo con reglas', () => {
@@ -234,6 +259,23 @@ describe('retailCommission — contratos de reglas y políticas', () => {
234
259
  expect([create.name, update.expectedVersion, action.expectedVersion]).toEqual(['Bono Accesorios', 2, 3]);
235
260
  });
236
261
 
262
+ it('el editor manda cuándo se paga y el vendedor lo ve', () => {
263
+ const create: RetailCommission.CreateRuleRequest = { ...input, payTiming: RetailCommission.CommissionPayTimingEnum.ON_SALE };
264
+ const view: RetailCommission.CommissionRuleView = {
265
+ ruleId: 'RC-01',
266
+ name: create.name,
267
+ description: create.description,
268
+ levers: create.levers,
269
+ scope: create.scope,
270
+ condition: create.condition,
271
+ effectiveFrom: create.effectiveFrom,
272
+ effectiveTo: create.effectiveTo,
273
+ payTiming: create.payTiming,
274
+ };
275
+ expect(input.payTiming).toBeUndefined();
276
+ expect(view.payTiming).toBe('ON_SALE');
277
+ });
278
+
237
279
  it('las políticas se guardan juntas', () => {
238
280
  const request: RetailCommission.UpdatePoliciesRequest = {
239
281
  clawback: { cancelacionDias: 7, devolucionDias: 7, npiM30Dias: 30 },
@@ -242,6 +284,17 @@ describe('retailCommission — contratos de reglas y políticas', () => {
242
284
  expect(request.commissions.multiplicadorMaxBps).toBe(25000);
243
285
  });
244
286
 
287
+ it('la política de pagos se guarda con las demás, y un editor que no la manda sigue siendo válido', () => {
288
+ const request: RetailCommission.UpdatePoliciesRequest = {
289
+ clawback: { cancelacionDias: 7, devolucionDias: 7, npiM30Dias: 30 },
290
+ commissions: { multiplicadorMaxBps: 25000, ventanaDefault: CommissionWindowEnum.SEMANA_OPERATIVA },
291
+ payouts: { walletDeposits: true, chainBillingPeriod: RetailCommission.CommissionBillingPeriodEnum.WEEKLY },
292
+ };
293
+ const legacy: RetailCommission.UpdatePoliciesRequest = { clawback: request.clawback, commissions: request.commissions };
294
+ expect(request.payouts?.walletDeposits).toBe(true);
295
+ expect(legacy.payouts).toBeUndefined();
296
+ });
297
+
245
298
  it('el plan vigente con reglas puede no existir todavía', () => {
246
299
  const response: RetailCommission.GetRuleSetResponse = { active: null };
247
300
  const history: RetailCommission.RuleHistoryResponse = {
@@ -94,6 +94,7 @@ describe('ProductEventPayload', () => {
94
94
  ['price.effectiveFrom', { price: { ...product.price, effectiveFrom: '2026-08-17T20:32:05' } }],
95
95
  ['eligibleFinanciers', { eligibleFinanciers: ['NADIE'] }],
96
96
  ['tier', { tier: undefined }],
97
+ ['price.listPrice', { price: { basePrice: '4299.00', effectiveFrom: '2026-08-17T20:32:05Z' } }],
97
98
  ])('rechaza %s', async (path, over) => {
98
99
  expect(await errorsOf(ProductEventPayload, { ...product, ...over })).toContain(path);
99
100
  });
@@ -5,6 +5,7 @@ import { RetailUser } from '../../../../src/retailOrg/dtos/RetailUser';
5
5
  import { EmploymentTypeEnum } from '../../../../src/retailOrg/enums/EmploymentTypeEnum';
6
6
  import { RetailUserStatusEnum } from '../../../../src/retailOrg/enums/RetailUserStatusEnum';
7
7
  import { CollectorSeniorityEnum } from '../../../../src/retailOrg/enums/CollectorSeniorityEnum';
8
+ import { RetailUserWalletLinkMethodEnum } from '../../../../src/retailOrg/enums/RetailUserWalletLinkMethodEnum';
8
9
 
9
10
  const valid = {
10
11
  retailUserId: 'ru_001',
@@ -82,3 +83,49 @@ describe('RetailUser', () => {
82
83
  expect(errors.some(e => e.property === 'cognitoSub')).toBe(true);
83
84
  });
84
85
  });
86
+
87
+ describe('RetailUser — wallet PCF', () => {
88
+ const linked = {
89
+ ...valid,
90
+ fiadoDirectoryId: 'dir_001',
91
+ pcfAccountId: 'pcf_001',
92
+ walletLinkMethod: RetailUserWalletLinkMethodEnum.CLABE,
93
+ walletClabeLast4: '9012',
94
+ walletLinkedAt: '2026-10-20T10:00:00.000Z',
95
+ walletLinkedBy: 'admin_001',
96
+ };
97
+
98
+ it('valida con la wallet vinculada', async () => {
99
+ const errors = await validate(plainToInstance(RetailUser, linked));
100
+ expect(errors).toEqual([]);
101
+ });
102
+
103
+ it('valida con la wallet desvinculada (campos en null)', async () => {
104
+ const unlinked = {
105
+ ...valid,
106
+ fiadoDirectoryId: null,
107
+ pcfAccountId: null,
108
+ walletLinkMethod: null,
109
+ walletClabeLast4: null,
110
+ walletLinkedAt: null,
111
+ walletLinkedBy: null,
112
+ };
113
+ const errors = await validate(plainToInstance(RetailUser, unlinked));
114
+ expect(errors).toEqual([]);
115
+ });
116
+
117
+ it('falla si walletLinkMethod está fuera del enum', async () => {
118
+ const errors = await validate(plainToInstance(RetailUser, { ...linked, walletLinkMethod: 'EMAIL' }));
119
+ expect(errors.some(e => e.property === 'walletLinkMethod')).toBe(true);
120
+ });
121
+
122
+ it('falla si walletClabeLast4 trae la CLABE completa', async () => {
123
+ const errors = await validate(plainToInstance(RetailUser, { ...linked, walletClabeLast4: '646180123456789012' }));
124
+ expect(errors.some(e => e.property === 'walletClabeLast4')).toBe(true);
125
+ });
126
+
127
+ it('falla si walletLinkedAt no es ISO 8601', async () => {
128
+ const errors = await validate(plainToInstance(RetailUser, { ...linked, walletLinkedAt: '20/10/2026' }));
129
+ expect(errors.some(e => e.property === 'walletLinkedAt')).toBe(true);
130
+ });
131
+ });
@@ -0,0 +1,41 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance, type ClassConstructor } from 'class-transformer';
3
+ import { validateSync, type ValidationError } from 'class-validator';
4
+ import { LinkRetailUserWalletRequest, LookupRetailUserWalletRequest } from '../../../../../src/retailOrg';
5
+
6
+ const CLABE = '646180123456789012';
7
+ const PHONE = '+525512345678';
8
+
9
+ function validate(cls: ClassConstructor<object>, payload: object): ValidationError[] {
10
+ return validateSync(plainToInstance(cls, payload, { excludeExtraneousValues: true }));
11
+ }
12
+
13
+ function errorsOf(cls: ClassConstructor<object>, payload: object): string[] {
14
+ return validate(cls, payload).map((e) => e.property);
15
+ }
16
+
17
+ function clabeConstraints(cls: ClassConstructor<object>, payload: object): string[] {
18
+ return Object.keys(validate(cls, payload).find((e) => e.property === 'clabe')?.constraints ?? {});
19
+ }
20
+
21
+ const requests: Array<[string, ClassConstructor<object>]> = [
22
+ ['LookupRetailUserWalletRequest', LookupRetailUserWalletRequest],
23
+ ['LinkRetailUserWalletRequest', LinkRetailUserWalletRequest],
24
+ ];
25
+
26
+ describe.each(requests)('%s', (_name, cls) => {
27
+ it('acepta solo la CLABE', () => expect(errorsOf(cls, { clabe: CLABE })).toEqual([]));
28
+ it('acepta solo el teléfono', () => expect(errorsOf(cls, { phone: PHONE })).toEqual([]));
29
+ it('rechaza la CLABE y el teléfono juntos', () => expect(errorsOf(cls, { clabe: CLABE, phone: PHONE })).toContain('clabe'));
30
+ it('rechaza un body sin ninguno', () => expect(clabeConstraints(cls, {})).toContain('isSingleWalletKey'));
31
+ it('rechaza un teléfono en null sin CLABE', () =>
32
+ expect(clabeConstraints(cls, { phone: null })).toContain('isSingleWalletKey'));
33
+ it('acepta la CLABE en null con teléfono', () => expect(errorsOf(cls, { clabe: null, phone: '5512345678' })).toEqual([]));
34
+ it('rechaza la CLABE vacía con teléfono', () => expect(errorsOf(cls, { clabe: '', phone: '5512345678' })).toContain('clabe'));
35
+ it('rechaza una CLABE que no son 18 dígitos', () => expect(errorsOf(cls, { clabe: '64618012345678901' })).toContain('clabe'));
36
+ it('acepta el teléfono en 10 dígitos, sin +52', () => expect(errorsOf(cls, { phone: '5512345678' })).toEqual([]));
37
+ it('rechaza un teléfono que no son 10 dígitos o que trae otra lada', () => {
38
+ expect(errorsOf(cls, { phone: '551234567' })).toContain('phone');
39
+ expect(errorsOf(cls, { phone: '+15512345678' })).toContain('phone');
40
+ });
41
+ });
@@ -0,0 +1,30 @@
1
+ import 'reflect-metadata';
2
+ import {
3
+ RetailUserWalletLinkMethodEnum,
4
+ type LookupRetailUserWalletResponse,
5
+ type PrivateRetailUserWalletResponse,
6
+ } from '../../../../../src/retailOrg';
7
+
8
+ describe('respuestas de la wallet del vendedor', () => {
9
+ it('la búsqueda dice si la encontró, si está activa y a nombre de quién', () => {
10
+ const found: LookupRetailUserWalletResponse = {
11
+ found: true,
12
+ active: true,
13
+ holderName: 'Ana López',
14
+ method: RetailUserWalletLinkMethodEnum.CLABE,
15
+ };
16
+ const missing: LookupRetailUserWalletResponse = {
17
+ found: false,
18
+ active: false,
19
+ holderName: null,
20
+ method: RetailUserWalletLinkMethodEnum.PHONE,
21
+ };
22
+ expect([found.holderName, missing.holderName]).toEqual(['Ana López', null]);
23
+ });
24
+
25
+ it('el privado devuelve la wallet o null si no tiene', () => {
26
+ const linked: PrivateRetailUserWalletResponse = { directoryId: 'dir_001', pcfAccountId: 'pcf_001' };
27
+ const none: PrivateRetailUserWalletResponse = { directoryId: null, pcfAccountId: null };
28
+ expect([linked.directoryId, none.directoryId]).toEqual(['dir_001', null]);
29
+ });
30
+ });
@@ -0,0 +1,12 @@
1
+ import { RetailUserWalletLinkMethodEnum } from '../../../../src/retailOrg/enums/RetailUserWalletLinkMethodEnum';
2
+
3
+ describe('RetailUserWalletLinkMethodEnum', () => {
4
+ it('contiene exactamente los métodos del contrato con value === key', () => {
5
+ expect(RetailUserWalletLinkMethodEnum.CLABE).toBe('CLABE');
6
+ expect(RetailUserWalletLinkMethodEnum.PHONE).toBe('PHONE');
7
+ });
8
+
9
+ it('no tiene valores de más', () => {
10
+ expect(Object.values(RetailUserWalletLinkMethodEnum).sort()).toEqual(['CLABE', 'PHONE']);
11
+ });
12
+ });
@@ -0,0 +1,20 @@
1
+ import 'reflect-metadata';
2
+ import type { GiftOption } from '../../../src/retailPromotions/index';
3
+
4
+ describe('GiftOption.valueCents', () => {
5
+ it('trae el valor del regalo sin listPriceCents', () => {
6
+ const option: GiftOption = { sku: 'CASE-1', name: 'Funda', valueCents: 35000, inStock: true };
7
+ expect(option.valueCents).toBe(35000);
8
+ expect(option.listPriceCents).toBeUndefined();
9
+ });
10
+
11
+ it('acepta los dos nombres con el mismo valor mientras dure la transición', () => {
12
+ const option: GiftOption = { sku: 'CASE-1', name: 'Funda', valueCents: 35000, listPriceCents: 35000, inStock: true };
13
+ expect(option.listPriceCents).toBe(option.valueCents);
14
+ });
15
+
16
+ it('sigue aceptando solo el nombre viejo, para no romper a promotions develop', () => {
17
+ const legacy: GiftOption = { sku: 'CASE-1', name: 'Funda', listPriceCents: 35000, inStock: true };
18
+ expect(legacy.valueCents).toBeUndefined();
19
+ });
20
+ });
@@ -0,0 +1,40 @@
1
+ import 'reflect-metadata';
2
+ import {
3
+ LinePaymentTypeEnum,
4
+ WizardLineStatusEnum,
5
+ type SaleLine,
6
+ type WizardReservedLine,
7
+ } from '../../../src/retailWizard';
8
+
9
+ describe('retailWizard — líneas sin precio de lista', () => {
10
+ it('WizardReservedLine es válida sin listPriceCents', () => {
11
+ const line: WizardReservedLine = {
12
+ lineId: 'L1',
13
+ sku: 'PHONE-1',
14
+ qty: 1,
15
+ status: WizardLineStatusEnum.RESERVED,
16
+ reservationId: 'r-1',
17
+ expiresAt: '2026-10-20T20:30:00.000Z',
18
+ basePriceCents: 349900,
19
+ priceSk: 'p',
20
+ eligibleFinanciers: ['SUREKEEP'],
21
+ financeable: true,
22
+ paymentType: LinePaymentTypeEnum.CREDIT,
23
+ sellItemCost: 1,
24
+ };
25
+ expect(line.listPriceCents).toBeUndefined();
26
+ });
27
+
28
+ it('SaleLine es válida sin listPriceCents', () => {
29
+ const line: SaleLine = {
30
+ lineId: 'L1',
31
+ sku: 'PHONE-1',
32
+ qty: 1,
33
+ basePriceCents: 349900,
34
+ priceSk: 'p',
35
+ paymentType: LinePaymentTypeEnum.CREDIT,
36
+ components: [{ sku: 'PHONE-1', qty: 1, identifiers: ['356938035643809'] }],
37
+ };
38
+ expect(line.listPriceCents).toBeUndefined();
39
+ });
40
+ });
@@ -27,7 +27,8 @@ export declare class Product {
27
27
  /** `null` = Sin gama. */
28
28
  tier: ProductTierEnum | null;
29
29
  basePriceCents: number;
30
- listPriceCents: number | null;
30
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
31
+ listPriceCents?: number | null;
31
32
  eligibleFinanciers: string[];
32
33
  mdmEnabled: boolean;
33
34
  mdmProvider: MdmProviderEnum | null;
@@ -1,5 +1,6 @@
1
1
  /** Body de POST /backoffice/inventory/skus/{sku}/prices (backofficeCreateSkuPrice); crea un PRICE# nuevo con origen BACKOFFICE. */
2
2
  export declare class BackofficeCreateSkuPriceRequest {
3
3
  basePriceCents: number;
4
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
4
5
  listPriceCents?: number | null;
5
6
  }
@@ -17,6 +17,7 @@ export declare class CreateProductRequest {
17
17
  imageUrl?: string | null;
18
18
  tier: ProductTierEnum;
19
19
  basePriceCents: number;
20
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
20
21
  listPriceCents?: number | null;
21
22
  eligibleFinanciers: string[];
22
23
  mdmEnabled: boolean;
@@ -6,6 +6,7 @@ import { TrackingModeEnum } from '../../enums/TrackingModeEnum';
6
6
  import { ProductComponent } from '../ProductComponent';
7
7
  /** Precio de un producto en la puerta de entrada; crea PRICE# solo si cambió el monto. */
8
8
  export declare class SyncPrice {
9
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
9
10
  listPriceCents?: number | null;
10
11
  basePriceCents: number;
11
12
  effectiveFrom: string;
@@ -16,6 +16,7 @@ export declare class UpdateProductRequest {
16
16
  imageUrl?: string | null;
17
17
  tier?: ProductTierEnum;
18
18
  basePriceCents?: number;
19
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
19
20
  listPriceCents?: number | null;
20
21
  eligibleFinanciers?: string[];
21
22
  mdmEnabled?: boolean;
@@ -61,7 +61,7 @@ export interface InventoryLensSkuItem {
61
61
  criticalStock: boolean;
62
62
  /** Unidades de este SKU vendidas hoy en esta tienda, **financiadas por SureKeep**. */
63
63
  soldTodayUnits: number;
64
- /** Precio de contado en centavos (`listPriceCents`, o `basePriceCents` si no hay lista). */
64
+ /** Precio de contado en centavos (`basePriceCents`). */
65
65
  cashPriceCents: number;
66
66
  }
67
67
  /** Una fila del tab `transfers`: mover unidades de un SKU de una tienda con excedente a una en crítico. */
@@ -14,7 +14,10 @@ export interface ReservedComponent {
14
14
  export interface ReserveStockResponse {
15
15
  expiresAt: string;
16
16
  basePriceCents: number;
17
- listPriceCents: number | null;
17
+ /** @deprecated Usa `giftValueCents`; en reservas GIFT trae el mismo valor, en las demás `null`. */
18
+ listPriceCents?: number | null;
19
+ /** Solo en reservas `purpose: GIFT`: valor del regalo, su precio de venta. */
20
+ giftValueCents?: number;
18
21
  /** Llave del registro PRICE# del que salió el precio fijado. */
19
22
  priceSk: string;
20
23
  eligibleFinanciers: string[];
@@ -2,7 +2,8 @@ import type { PriceSourceEnum } from '../../enums/PriceSourceEnum';
2
2
  /** Un registro del historial de precios PRICE# de un SKU. */
3
3
  export interface SkuPriceEntry {
4
4
  effectiveFrom: string;
5
- listPriceCents: number | null;
5
+ /** @deprecated Ya no se usa; el único precio es `basePriceCents`. */
6
+ listPriceCents?: number | null;
6
7
  basePriceCents: number;
7
8
  source: PriceSourceEnum;
8
9
  createdBy: string;
@@ -36,7 +36,7 @@ export interface StoreCatalogItem {
36
36
  itemType?: ItemTypeEnum;
37
37
  /** Nombre visible para kits y servicios, que no tienen marca/modelo. */
38
38
  description?: string;
39
- /** Precio de lista de respaldo para un regalo: el precio vigente o, si no hay, product.basePriceCents. */
39
+ /** @deprecated Ya no se usa; el valor de un regalo es `basePriceCents`. */
40
40
  listPriceCents?: number;
41
41
  /** Prioridad de venta de 0 a 100; «Recomendados» ordena por ella. */
42
42
  salePriority?: number;