@fiado/type-kit 3.549.0 → 3.551.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 (66) hide show
  1. package/_test_/unit/loanCredit/cancelLoanCreditRequest.test.ts +5 -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/retailOrg/dtos/RetailUser.test.ts +47 -0
  5. package/_test_/unit/retailOrg/dtos/requests/retailUserWalletRequests.test.ts +41 -0
  6. package/_test_/unit/retailOrg/dtos/responses/retailUserWalletResponses.test.ts +30 -0
  7. package/_test_/unit/retailOrg/enums/RetailUserWalletLinkMethodEnum.test.ts +12 -0
  8. package/_test_/unit/retailWizard/saleCancellation.test.ts +3 -1
  9. package/bin/loanCredit/dtos/requests/CancelLoanCreditRequest.d.ts +2 -0
  10. package/bin/loanCredit/dtos/requests/CancelLoanCreditRequest.js +9 -0
  11. package/bin/retailCommission/dtos/CommissionApi.d.ts +80 -1
  12. package/bin/retailCommission/dtos/CommissionLedger.d.ts +96 -0
  13. package/bin/retailCommission/dtos/CommissionLedger.js +2 -0
  14. package/bin/retailCommission/dtos/CommissionRule.d.ts +14 -1
  15. package/bin/retailCommission/enums/CommissionBillingPeriodEnum.d.ts +10 -0
  16. package/bin/retailCommission/enums/CommissionBillingPeriodEnum.js +14 -0
  17. package/bin/retailCommission/enums/CommissionLedgerMovementKindEnum.d.ts +14 -0
  18. package/bin/retailCommission/enums/CommissionLedgerMovementKindEnum.js +18 -0
  19. package/bin/retailCommission/enums/CommissionPayTimingEnum.d.ts +13 -0
  20. package/bin/retailCommission/enums/CommissionPayTimingEnum.js +17 -0
  21. package/bin/retailCommission/enums/CommissionPayTriggerEnum.d.ts +18 -0
  22. package/bin/retailCommission/enums/CommissionPayTriggerEnum.js +22 -0
  23. package/bin/retailCommission/enums/CommissionPaymentAttentionEnum.d.ts +14 -0
  24. package/bin/retailCommission/enums/CommissionPaymentAttentionEnum.js +18 -0
  25. package/bin/retailCommission/enums/CommissionPaymentStatusEnum.d.ts +14 -0
  26. package/bin/retailCommission/enums/CommissionPaymentStatusEnum.js +18 -0
  27. package/bin/retailCommission/index.d.ts +7 -0
  28. package/bin/retailCommission/index.js +7 -0
  29. package/bin/retailOrg/dtos/RetailUser.d.ts +13 -0
  30. package/bin/retailOrg/dtos/RetailUser.js +39 -0
  31. package/bin/retailOrg/dtos/requests/LinkRetailUserWalletRequest.d.ts +10 -0
  32. package/bin/retailOrg/dtos/requests/LinkRetailUserWalletRequest.js +37 -0
  33. package/bin/retailOrg/dtos/requests/LookupRetailUserWalletRequest.d.ts +10 -0
  34. package/bin/retailOrg/dtos/requests/LookupRetailUserWalletRequest.js +37 -0
  35. package/bin/retailOrg/dtos/responses/LookupRetailUserWalletResponse.d.ts +10 -0
  36. package/bin/retailOrg/dtos/responses/LookupRetailUserWalletResponse.js +2 -0
  37. package/bin/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.d.ts +8 -0
  38. package/bin/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.js +2 -0
  39. package/bin/retailOrg/enums/RetailUserWalletLinkMethodEnum.d.ts +11 -0
  40. package/bin/retailOrg/enums/RetailUserWalletLinkMethodEnum.js +15 -0
  41. package/bin/retailOrg/index.d.ts +6 -0
  42. package/bin/retailOrg/index.js +7 -0
  43. package/bin/retailOrg/validators/IsSingleWalletKeyConstraint.d.ts +6 -0
  44. package/bin/retailOrg/validators/IsSingleWalletKeyConstraint.js +24 -0
  45. package/bin/retailWizard/dtos/SaleCancellation.d.ts +2 -0
  46. package/package.json +1 -1
  47. package/src/loanCredit/dtos/requests/CancelLoanCreditRequest.ts +10 -1
  48. package/src/retailCommission/dtos/CommissionApi.ts +97 -1
  49. package/src/retailCommission/dtos/CommissionLedger.ts +101 -0
  50. package/src/retailCommission/dtos/CommissionRule.ts +15 -1
  51. package/src/retailCommission/enums/CommissionBillingPeriodEnum.ts +10 -0
  52. package/src/retailCommission/enums/CommissionLedgerMovementKindEnum.ts +14 -0
  53. package/src/retailCommission/enums/CommissionPayTimingEnum.ts +13 -0
  54. package/src/retailCommission/enums/CommissionPayTriggerEnum.ts +18 -0
  55. package/src/retailCommission/enums/CommissionPaymentAttentionEnum.ts +14 -0
  56. package/src/retailCommission/enums/CommissionPaymentStatusEnum.ts +14 -0
  57. package/src/retailCommission/index.ts +7 -0
  58. package/src/retailOrg/dtos/RetailUser.ts +39 -0
  59. package/src/retailOrg/dtos/requests/LinkRetailUserWalletRequest.ts +24 -0
  60. package/src/retailOrg/dtos/requests/LookupRetailUserWalletRequest.ts +24 -0
  61. package/src/retailOrg/dtos/responses/LookupRetailUserWalletResponse.ts +11 -0
  62. package/src/retailOrg/dtos/responses/PrivateRetailUserWalletResponse.ts +8 -0
  63. package/src/retailOrg/enums/RetailUserWalletLinkMethodEnum.ts +11 -0
  64. package/src/retailOrg/index.ts +8 -0
  65. package/src/retailOrg/validators/IsSingleWalletKeyConstraint.ts +18 -0
  66. package/src/retailWizard/dtos/SaleCancellation.ts +2 -0
@@ -28,6 +28,11 @@ describe('CancelLoanCreditRequest', () => {
28
28
  it('rechaza un saleId con caracteres fuera de la referencia del hecho', async () => {
29
29
  expect(await errorsOf({ ...VALID, saleId: 'SALE#123' })).toEqual(['saleId']);
30
30
  });
31
+ it('cancelledBy es opcional y comparte el charset de saleId', async () => {
32
+ expect(await errorsOf({ ...VALID, cancelledBy: 'sub-123:admin.vl' })).toEqual([]);
33
+ expect(await errorsOf({ ...VALID, cancelledBy: 'actor#1' })).toEqual(['cancelledBy']);
34
+ expect(await errorsOf({ ...VALID, cancelledBy: 'x'.repeat(121) })).toEqual(['cancelledBy']);
35
+ });
31
36
  });
32
37
 
33
38
  describe('LoanCreditResponse.hasPayments', () => {
@@ -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 = {
@@ -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
+ });
@@ -16,10 +16,12 @@ describe('SaleCancellation', () => {
16
16
  it('viaja opcional en la venta', () => {
17
17
  const cancellation: SaleCancellation = {
18
18
  allowed: false,
19
- reason: SaleCancellationReasonEnum.CREDIT_HAS_PAYMENTS,
19
+ reason: SaleCancellationReasonEnum.CANCEL_WINDOW_EXPIRED,
20
20
  windowEndsAt: '2026-10-26T00:00:00.000Z',
21
+ lateCancelExceptionId: 'EXC-1',
21
22
  };
22
23
  const sale: Pick<Sale, 'cancellation'> = { cancellation };
23
24
  expect(sale.cancellation?.allowed).toBe(false);
25
+ expect(sale.cancellation?.lateCancelExceptionId).toBe('EXC-1');
24
26
  });
25
27
  });
@@ -4,4 +4,6 @@ export declare class CancelLoanCreditRequest {
4
4
  saleId: string;
5
5
  /** Motivo de la cancelación; va a la auditoría del crédito y no se loguea. */
6
6
  reason: string;
7
+ /** Quién cancela la venta; es el actor de la auditoría del crédito. Sin él, el sistema. */
8
+ cancelledBy?: string;
7
9
  }
@@ -31,3 +31,12 @@ __decorate([
31
31
  (0, class_validator_1.MaxLength)(500),
32
32
  __metadata("design:type", String)
33
33
  ], CancelLoanCreditRequest.prototype, "reason", void 0);
34
+ __decorate([
35
+ (0, class_transformer_1.Expose)(),
36
+ (0, class_validator_1.IsOptional)(),
37
+ (0, class_validator_1.IsString)(),
38
+ (0, class_validator_1.IsNotEmpty)(),
39
+ (0, class_validator_1.MaxLength)(120),
40
+ (0, class_validator_1.Matches)(/^[A-Za-z0-9._:-]+$/),
41
+ __metadata("design:type", String)
42
+ ], CancelLoanCreditRequest.prototype, "cancelledBy", void 0);
@@ -4,8 +4,11 @@ import type { CommissionAddOn, CommissionCategory, CommissionPlanSnapshot, Commi
4
4
  import type { CommissionAccrual, CommissionAppliedBonus, CommissionSaleInput, CommissionSaleLine } from './CommissionAccrual';
5
5
  import type { CommissionAdjustment, CommissionAppliedMultiplier, CommissionAppliedThreshold, CommissionPayout, CommissionPayoutSeller, CommissionSellerBreakdown } from './CommissionPayout';
6
6
  import type { CommissionPolicies, CommissionRule, CommissionRuleCondition, CommissionRuleLevers, CommissionRuleScope, CommissionRuleSetSnapshot } from './CommissionRule';
7
+ import type { CommissionChainStatement, CommissionLedgerMovement, CommissionPayment, CommissionSellerBalance } from './CommissionLedger';
7
8
  import type { CommissionWindowEnum } from '../enums/CommissionWindowEnum';
9
+ import type { CommissionBillingPeriodEnum } from '../enums/CommissionBillingPeriodEnum';
8
10
  import type { CommissionCountByEnum } from '../enums/CommissionCountByEnum';
11
+ import type { CommissionPayTimingEnum } from '../enums/CommissionPayTimingEnum';
9
12
  import type { CommissionAggregateByEnum } from '../enums/CommissionAggregateByEnum';
10
13
  import type { CommissionSeriesByEnum } from '../enums/CommissionSeriesByEnum';
11
14
  import type { CommissionAccrualStatusEnum } from '../enums/CommissionAccrualStatusEnum';
@@ -197,6 +200,8 @@ export interface CommissionRuleInput {
197
200
  replacesOthers: boolean;
198
201
  /** Si la regla cuenta piezas o ventas. Ausente = `PIECE`. */
199
202
  countBy?: CommissionCountByEnum;
203
+ /** Cuándo se paga lo que gana. Ausente = `ON_CUT`. */
204
+ payTiming?: CommissionPayTimingEnum;
200
205
  }
201
206
  /** `POST /backoffice/commissions/rules` — la regla nace en borrador. */
202
207
  export type CreateRuleRequest = CommissionRuleInput;
@@ -231,7 +236,7 @@ export interface RuleHistoryResponse {
231
236
  export interface GetPoliciesResponse {
232
237
  policies: CommissionPolicies;
233
238
  }
234
- /** `PUT /backoffice/commissions/policies` — las dos políticas se guardan juntas. */
239
+ /** `PUT /backoffice/commissions/policies` — las políticas se guardan juntas. */
235
240
  export interface UpdatePoliciesRequest {
236
241
  clawback: {
237
242
  cancelacionDias: number;
@@ -242,6 +247,11 @@ export interface UpdatePoliciesRequest {
242
247
  multiplicadorMaxBps: number;
243
248
  ventanaDefault: CommissionWindowEnum;
244
249
  };
250
+ /** Opcional para que un editor que todavía no la manda siga funcionando. */
251
+ payouts?: {
252
+ walletDeposits: boolean;
253
+ chainBillingPeriod: CommissionBillingPeriodEnum;
254
+ };
245
255
  }
246
256
  export interface UpdatePoliciesResponse {
247
257
  policies: CommissionPolicies;
@@ -375,6 +385,8 @@ export interface CommissionRuleView {
375
385
  effectiveTo: string | null;
376
386
  /** Si la regla cuenta piezas o ventas. Ausente = `PIECE`. */
377
387
  countBy?: CommissionCountByEnum;
388
+ /** Cuándo se paga lo que gana. Ausente = `ON_CUT`. */
389
+ payTiming?: CommissionPayTimingEnum;
378
390
  }
379
391
  /** Lo pagado en varios cortes. */
380
392
  export interface MyCommissionsTotals {
@@ -411,3 +423,70 @@ export interface GetMyCommissionsResponse {
411
423
  /** Tope del multiplicador de la política; `null` sin política. */
412
424
  multiplierCapBps: number | null;
413
425
  }
426
+ /** `GET /backoffice/commissions/payments/attention` — el reporte "Por pagar" de la cadena. */
427
+ export interface GetPaymentAttentionResponse {
428
+ /** Saldos con exigible sin depositar, con su motivo. */
429
+ pending: CommissionSellerBalance[];
430
+ /** Depósitos sin confirmar: "Requiere revisión". */
431
+ unverified: CommissionPayment[];
432
+ /** Saldos con exigible negativo. */
433
+ debts: CommissionSellerBalance[];
434
+ }
435
+ /** `POST /backoffice/commissions/payments/sellers/{sellerId}/paid-outside` */
436
+ export interface MarkPaidOutsideRequest {
437
+ amountCents: number;
438
+ /** Obligatorio: dinero que sale sin depósito tiene que poder auditarse. */
439
+ reason: string;
440
+ }
441
+ /** `POST /backoffice/commissions/payments/{paymentId}/resolve` — revisión de un depósito sin confirmar. */
442
+ export interface ResolvePaymentRequest {
443
+ /** `true` = sí se depositó; `false` = no, y el monto vuelve al exigible. */
444
+ accredited: boolean;
445
+ /** El folio, cuando sí se depositó. */
446
+ transactionId?: string;
447
+ note: string;
448
+ }
449
+ /** `GET /backoffice/commissions/sellers/{sellerId}/ledger` — saldo e historial de un vendedor. */
450
+ export interface GetSellerLedgerResponse {
451
+ balance: CommissionSellerBalance;
452
+ movements: CommissionLedgerMovement[];
453
+ payments: CommissionPayment[];
454
+ }
455
+ /** `GET /backoffice/commissions/statements` */
456
+ export interface ListStatementsResponse {
457
+ statements: CommissionChainStatement[];
458
+ }
459
+ /** `GET /backoffice/commissions/statements/{statementId}` — el estado de cuenta y sus depósitos. */
460
+ export interface GetStatementResponse {
461
+ statement: CommissionChainStatement;
462
+ payments: CommissionPayment[];
463
+ }
464
+ /** `GET /commissions/me/payments` — los pagos del vendedor que llama. */
465
+ export interface GetMyPaymentsResponse {
466
+ /** `null` mientras no tiene nada en el libro. */
467
+ balance: CommissionSellerBalance | null;
468
+ payments: CommissionPayment[];
469
+ }
470
+ /** `POST /backoffice/commissions/payments/sellers/{sellerId}/retry` (202) — el pago quedó en la cola. */
471
+ export interface RetrySellerPaymentView {
472
+ sellerId: string;
473
+ /** Id del disparo `RETRY` con que se encoló. */
474
+ triggerId: string;
475
+ }
476
+ /** `POST /backoffice/commissions/payments/sellers/{sellerId}/paid-outside` (201) — el saldo nuevo y el movimiento escrito. */
477
+ export interface MarkPaidOutsideView {
478
+ balance: CommissionSellerBalance;
479
+ movement: CommissionLedgerMovement;
480
+ }
481
+ /** `POST /backoffice/commissions/payments/{paymentId}/resolve` — el depósito ya resuelto. */
482
+ export interface ResolvePaymentView {
483
+ payment: CommissionPayment;
484
+ }
485
+ /**
486
+ * `POST /private/retailers/{retailerId}/sellers/{sellerId}/payments/trigger?tenantId=` (202) — retail-org
487
+ * avisa que el vendedor vinculó su wallet. El cuerpo va vacío (`{}`).
488
+ */
489
+ export interface PrivateTriggerSellerPaymentResponse {
490
+ /** `false` si la cola no lo recibió: el saldo sigue pendiente y entra en el siguiente pago. */
491
+ enqueued: boolean;
492
+ }
@@ -0,0 +1,96 @@
1
+ import type { CommissionBillingPeriodEnum } from '../enums/CommissionBillingPeriodEnum';
2
+ import type { CommissionLedgerMovementKindEnum } from '../enums/CommissionLedgerMovementKindEnum';
3
+ import type { CommissionPayTimingEnum } from '../enums/CommissionPayTimingEnum';
4
+ import type { CommissionPaymentAttentionEnum } from '../enums/CommissionPaymentAttentionEnum';
5
+ import type { CommissionPaymentStatusEnum } from '../enums/CommissionPaymentStatusEnum';
6
+ /**
7
+ * Saldo de un vendedor en una cadena. Es parte del libro de pagos: cada peso que se le acredita, se
8
+ * le descuenta o se le deposita en su wallet. Montos en centavos; fechas en ISO.
9
+ */
10
+ export interface CommissionSellerBalance {
11
+ retailerId: string;
12
+ sellerId: string;
13
+ /** Ganado que todavía no toca pagar (reglas al cierre del mes). */
14
+ retainedCents: number;
15
+ /** Lo que se puede depositar ya. Negativo = el vendedor debe. */
16
+ payableCents: number;
17
+ /** Apartado para un depósito en curso o sin confirmar. */
18
+ inTransitCents: number;
19
+ /** Acumulado de todo lo ganado. */
20
+ earnedCents: number;
21
+ /** Acumulado de todo lo descontado. */
22
+ deductedCents: number;
23
+ /** Acumulado de lo depositado y confirmado. */
24
+ accreditedCents: number;
25
+ /** Acumulado de lo pagado por fuera. */
26
+ paidOutsideCents: number;
27
+ /** `null` si no necesita atención. */
28
+ attention: CommissionPaymentAttentionEnum | null;
29
+ /** Código del último rechazo, p. ej. `USER_NOT_ACTIVE`. */
30
+ attentionCode: string | null;
31
+ /** Desde cuándo tiene esa atención. */
32
+ attentionSince: string | null;
33
+ updatedAt: string;
34
+ }
35
+ /** Lo que una regla aporta a un movimiento. */
36
+ export interface CommissionLedgerMovementRule {
37
+ ruleId: string;
38
+ payTiming: CommissionPayTimingEnum;
39
+ cents: number;
40
+ }
41
+ /** Un movimiento del libro. Nunca cambia. */
42
+ export interface CommissionLedgerMovement {
43
+ /** Determinista por origen: una reentrega no duplica el movimiento. */
44
+ movementKey: string;
45
+ kind: CommissionLedgerMovementKindEnum;
46
+ periodId: string;
47
+ retainedDeltaCents: number;
48
+ payableDeltaCents: number;
49
+ byRule: CommissionLedgerMovementRule[];
50
+ saleId: string | null;
51
+ adjustmentId: string | null;
52
+ /** `YYYY-MM` de la liberación mensual. */
53
+ releaseMonth: string | null;
54
+ /** Obligatorio en lo pagado por fuera. */
55
+ reason: string | null;
56
+ createdAt: string;
57
+ createdBy: string;
58
+ }
59
+ /** Un depósito a la wallet del vendedor. Solo cambia su estado. */
60
+ export interface CommissionPayment {
61
+ /** También es la llave de idempotencia del depósito. */
62
+ paymentId: string;
63
+ retailerId: string;
64
+ sellerId: string;
65
+ amountCents: number;
66
+ status: CommissionPaymentStatusEnum;
67
+ /** La wallet a la que se depositó. */
68
+ directoryId: string;
69
+ /** Folio del depósito; `null` mientras no se confirma. */
70
+ transactionId: string | null;
71
+ errorCode: string | null;
72
+ /** Lo que ve el vendedor en su wallet; máximo 120 caracteres. */
73
+ concept: string;
74
+ requestedAt: string;
75
+ resolvedAt: string | null;
76
+ resolvedBy: string | null;
77
+ /** Nota de quien lo revisó a mano. */
78
+ note: string | null;
79
+ }
80
+ /**
81
+ * Estado de cuenta para cobrarle a la cadena: los depósitos confirmados del periodo. Lo pagado por
82
+ * fuera no entra. Se genera una vez y no cambia.
83
+ */
84
+ export interface CommissionChainStatement {
85
+ statementId: string;
86
+ retailerId: string;
87
+ period: CommissionBillingPeriodEnum;
88
+ /** `YYYY-MM-DD`, inclusive. */
89
+ startDate: string;
90
+ /** `YYYY-MM-DD`, inclusive. */
91
+ endDate: string;
92
+ accreditedCents: number;
93
+ paymentsCount: number;
94
+ sellersCount: number;
95
+ generatedAt: string;
96
+ }