@fiado/type-kit 3.583.0 → 3.585.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/platformRbac/dtos/UpdateTenantUserEmailRequest.test.ts +24 -0
  2. package/_test_/unit/platformRbac/enums/__snapshots__/permissionBits.test.ts.snap +2 -1
  3. package/_test_/unit/platformRbac/enums/apiUserPermission.test.ts +3 -2
  4. package/_test_/unit/platformRbac/enums/retailIntegrationPermission.test.ts +13 -0
  5. package/_test_/unit/retailIntegration/eventPayloadBounds.test.ts +147 -0
  6. package/_test_/unit/retailIntegration/eventPayloads.test.ts +8 -0
  7. package/_test_/unit/retailIntegration/reconciliationContracts.test.ts +3 -3
  8. package/_test_/unit/retailIntegration/retailMovementsContracts.test.ts +109 -0
  9. package/_test_/unit/retailIntegration/retailSystemResponses.test.ts +120 -0
  10. package/bin/platformRbac/dtos/CreateTenantUserRequest.d.ts +2 -1
  11. package/bin/platformRbac/dtos/CreateTenantUserRequest.js +2 -1
  12. package/bin/platformRbac/dtos/UpdateTenantUserEmailRequest.d.ts +5 -0
  13. package/bin/platformRbac/dtos/UpdateTenantUserEmailRequest.js +30 -0
  14. package/bin/platformRbac/enums/Permission.d.ts +2 -1
  15. package/bin/platformRbac/enums/Permission.js +3 -0
  16. package/bin/platformRbac/index.d.ts +1 -0
  17. package/bin/platformRbac/index.js +1 -0
  18. package/bin/retailIntegration/constants/retailIntegrationLimits.d.ts +25 -0
  19. package/bin/retailIntegration/constants/retailIntegrationLimits.js +26 -1
  20. package/bin/retailIntegration/dtos/CheckRetailInventoryRequest.d.ts +4 -1
  21. package/bin/retailIntegration/dtos/CheckRetailInventoryRequest.js +4 -1
  22. package/bin/retailIntegration/dtos/GetRetailInventoryRequest.d.ts +3 -3
  23. package/bin/retailIntegration/dtos/GetRetailInventoryRequest.js +3 -3
  24. package/bin/retailIntegration/dtos/InventoryEventPayload.js +3 -0
  25. package/bin/retailIntegration/dtos/ListRetailInventoryMovementsRequest.d.ts +6 -0
  26. package/bin/retailIntegration/dtos/ListRetailInventoryMovementsRequest.js +29 -0
  27. package/bin/retailIntegration/dtos/ProductEventPayload.js +10 -2
  28. package/bin/retailIntegration/dtos/ReconciliationPageMessage.d.ts +6 -0
  29. package/bin/retailIntegration/dtos/ReconciliationPageMessage.js +20 -0
  30. package/bin/retailIntegration/dtos/RetailOutboundResponses.d.ts +10 -2
  31. package/bin/retailIntegration/dtos/RetailSystemResponses.d.ts +41 -0
  32. package/bin/retailIntegration/dtos/RetailSystemResponses.js +136 -0
  33. package/bin/retailIntegration/dtos/RetailUserEventPayload.js +3 -0
  34. package/bin/retailIntegration/dtos/StoreEventPayload.js +13 -0
  35. package/bin/retailIntegration/enums/ReconciliationEntityEnum.d.ts +2 -1
  36. package/bin/retailIntegration/enums/ReconciliationEntityEnum.js +1 -0
  37. package/bin/retailIntegration/enums/ReconciliationTriggerEnum.d.ts +4 -2
  38. package/bin/retailIntegration/enums/ReconciliationTriggerEnum.js +3 -1
  39. package/bin/retailIntegration/index.d.ts +3 -0
  40. package/bin/retailIntegration/index.js +3 -0
  41. package/bin/retailIntegration/models/RetailPageMeta.d.ts +3 -1
  42. package/bin/retailIntegration/models/RetailSystemInventory.d.ts +9 -1
  43. package/bin/retailIntegration/validators/IsMovementsRangeValid.d.ts +6 -0
  44. package/bin/retailIntegration/validators/IsMovementsRangeValid.js +28 -0
  45. package/package.json +1 -1
  46. package/src/platformRbac/dtos/CreateTenantUserRequest.ts +2 -1
  47. package/src/platformRbac/dtos/UpdateTenantUserEmailRequest.ts +12 -0
  48. package/src/platformRbac/enums/Permission.ts +3 -0
  49. package/src/platformRbac/index.ts +1 -0
  50. package/src/retailIntegration/constants/retailIntegrationLimits.ts +37 -0
  51. package/src/retailIntegration/dtos/CheckRetailInventoryRequest.ts +4 -1
  52. package/src/retailIntegration/dtos/GetRetailInventoryRequest.ts +3 -3
  53. package/src/retailIntegration/dtos/InventoryEventPayload.ts +11 -5
  54. package/src/retailIntegration/dtos/ListRetailInventoryMovementsRequest.ts +14 -0
  55. package/src/retailIntegration/dtos/ProductEventPayload.ts +19 -12
  56. package/src/retailIntegration/dtos/ReconciliationPageMessage.ts +21 -1
  57. package/src/retailIntegration/dtos/RetailOutboundResponses.ts +11 -2
  58. package/src/retailIntegration/dtos/RetailSystemResponses.ts +75 -0
  59. package/src/retailIntegration/dtos/RetailUserEventPayload.ts +5 -5
  60. package/src/retailIntegration/dtos/StoreEventPayload.ts +16 -14
  61. package/src/retailIntegration/enums/ReconciliationEntityEnum.ts +1 -0
  62. package/src/retailIntegration/enums/ReconciliationTriggerEnum.ts +3 -1
  63. package/src/retailIntegration/index.ts +3 -0
  64. package/src/retailIntegration/models/RetailPageMeta.ts +3 -1
  65. package/src/retailIntegration/models/RetailSystemInventory.ts +10 -1
  66. package/src/retailIntegration/validators/IsMovementsRangeValid.ts +17 -0
@@ -0,0 +1,24 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validateSync } from 'class-validator';
4
+ import { UpdateTenantUserEmailRequest } from '../../../../src/platformRbac';
5
+
6
+ function propsWithErrors(payload: object): string[] {
7
+ return validateSync(plainToInstance(UpdateTenantUserEmailRequest, payload)).map((e) => e.property);
8
+ }
9
+
10
+ describe('UpdateTenantUserEmailRequest', () => {
11
+ it('normaliza el correo a minúsculas sin espacios', () => {
12
+ const dto = plainToInstance(UpdateTenantUserEmailRequest, { email: ' Ana.Perez@Example.COM ', actedBy: 'retail-org-business' });
13
+ expect(dto.email).toBe('ana.perez@example.com');
14
+ expect(validateSync(dto)).toEqual([]);
15
+ });
16
+
17
+ it.each([undefined, '', 'no-es-correo', 42])('rechaza email=%p', (email) => {
18
+ expect(propsWithErrors({ email, actedBy: 'retail-org-business' })).toEqual(['email']);
19
+ });
20
+
21
+ it.each([undefined, '', 7])('rechaza actedBy=%p', (actedBy) => {
22
+ expect(propsWithErrors({ email: 'ana@example.com', actedBy })).toEqual(['actedBy']);
23
+ });
24
+ });
@@ -1,6 +1,6 @@
1
1
  // Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
2
2
 
3
- exports[`PERMISSION_BIT_ORDER PERMS_VERSION es número estable 1`] = `2686516275`;
3
+ exports[`PERMISSION_BIT_ORDER PERMS_VERSION es número estable 1`] = `1071810597`;
4
4
 
5
5
  exports[`PERMISSION_BIT_ORDER append-only: snapshot del ORDEN COMPLETO (rompe ante cualquier reorden/inserción) 1`] = `
6
6
  [
@@ -96,5 +96,6 @@ exports[`PERMISSION_BIT_ORDER append-only: snapshot del ORDEN COMPLETO (rompe an
96
96
  "platform.integration.key.revoke",
97
97
  "tenant.integration.diagnostic",
98
98
  "tenant.api_user.manage",
99
+ "platform.retail_integration.activate",
99
100
  ]
100
101
  `;
@@ -5,7 +5,8 @@ describe('Permiso de administración de usuarios de API', () => {
5
5
  expect(Permission.TENANT_API_USER_MANAGE).toBe('tenant.api_user.manage');
6
6
  });
7
7
 
8
- it('va al FINAL del orden de bits (append-only)', () => {
9
- expect(PERMISSION_BIT_ORDER[PERMISSION_BIT_ORDER.length - 1]).toBe(Permission.TENANT_API_USER_MANAGE);
8
+ it('conserva su bit después de las integraciones M2M (append-only)', () => {
9
+ const index = PERMISSION_BIT_ORDER.indexOf(Permission.TENANT_API_USER_MANAGE);
10
+ expect(PERMISSION_BIT_ORDER[index - 1]).toBe(Permission.TENANT_INTEGRATION_DIAGNOSTIC);
10
11
  });
11
12
  });
@@ -0,0 +1,13 @@
1
+ import { Permission, PERMISSION_BIT_ORDER } from '../../../../src/platformRbac/enums/Permission';
2
+
3
+ describe('Permiso de activación de la integración retail', () => {
4
+ it('existe con la llave platform.retail_integration.activate (scope PLATFORM)', () => {
5
+ expect(Permission.PLATFORM_RETAIL_INTEGRATION_ACTIVATE).toBe('platform.retail_integration.activate');
6
+ });
7
+
8
+ it('va al FINAL del orden de bits, justo después de tenant.api_user.manage', () => {
9
+ const last = PERMISSION_BIT_ORDER.length - 1;
10
+ expect(PERMISSION_BIT_ORDER[last]).toBe(Permission.PLATFORM_RETAIL_INTEGRATION_ACTIVATE);
11
+ expect(PERMISSION_BIT_ORDER[last - 1]).toBe(Permission.TENANT_API_USER_MANAGE);
12
+ });
13
+ });
@@ -0,0 +1,147 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance } from 'class-transformer';
3
+ import { validate, ValidationError } from 'class-validator';
4
+ import { StoreEventPayload } from '../../../src/retailIntegration/dtos/StoreEventPayload';
5
+ import { RetailUserEventPayload } from '../../../src/retailIntegration/dtos/RetailUserEventPayload';
6
+ import { ProductEventPayload } from '../../../src/retailIntegration/dtos/ProductEventPayload';
7
+ import { InventoryEventPayload } from '../../../src/retailIntegration/dtos/InventoryEventPayload';
8
+ import {
9
+ RETAIL_ELIGIBLE_FINANCIERS_MAX,
10
+ RETAIL_EVENT_STOCK_MAX,
11
+ RETAIL_ID_TEXT_MAX_LENGTH,
12
+ RETAIL_LONG_TEXT_MAX_LENGTH,
13
+ RETAIL_SHORT_TEXT_MAX_LENGTH,
14
+ } from '../../../src/retailIntegration/constants/retailIntegrationLimits';
15
+
16
+ function paths(errors: ValidationError[], prefix = ''): string[] {
17
+ return errors.flatMap((e) => {
18
+ const p = prefix ? `${prefix}.${e.property}` : e.property;
19
+ return e.children && e.children.length > 0 ? paths(e.children, p) : [p];
20
+ });
21
+ }
22
+
23
+ async function errorsOf(cls: new () => object, plain: Record<string, unknown>): Promise<string[]> {
24
+ return paths(await validate(plainToInstance(cls, plain)));
25
+ }
26
+
27
+ const text = (n: number): string => 'a'.repeat(n);
28
+ const SHORT = RETAIL_SHORT_TEXT_MAX_LENGTH;
29
+ const LONG = RETAIL_LONG_TEXT_MAX_LENGTH;
30
+ const ID = RETAIL_ID_TEXT_MAX_LENGTH;
31
+
32
+ const address = { address1: 'Av. Juárez', externalNumber: '120', internalNumber: null, neighborhood: 'Centro', city: 'Toluca', state: 'Estado de México', zipCode: '50000', country: 'MX', reference: null };
33
+ const store = {
34
+ storeId: '12', code: 'T12', name: 'Sucursal Centro', zoneId: 'SUR', address,
35
+ location: { lat: 19.2826, lng: -99.6557 }, phone: '7221234567', schedule: 'Lun a Dom 10:00 a 20:00', status: 'ACTIVE',
36
+ };
37
+ const user = {
38
+ retailUserId: '431', displayName: 'Grecia Cruz Mendoza', email: 'gcruz@ejemplo.mx', phone: null,
39
+ homeStoreId: '12', zoneId: null, role: 'SELLER', status: 'ACTIVE',
40
+ };
41
+ const price = { basePrice: '4299.00', listPrice: null, effectiveFrom: '2026-08-17T20:32:05Z' };
42
+ const product = {
43
+ sku: 'SAM-A17-128-NEG', externalId: '9001', brand: 'Samsung', model: 'A17', color: 'Negro', capacity: '128 GB', description: null,
44
+ category: 'PHONE', trackingMode: 'IMEI', tier: null, currencyId: 'MXN', price,
45
+ eligibleFinanciers: [], mdmProvider: null, status: 'ACTIVE',
46
+ };
47
+ const piece = { sku: 'SAM-A17-128-NEG', identifier: '356938035643809', storeId: '12', status: 'SOLD', reason: null };
48
+ const movement = { movementType: 'SALE', storeId: '12', targetStoreId: null, pieces: [piece], stock: [] };
49
+
50
+ describe('StoreEventPayload: topes', () => {
51
+ it('acepta textos justo en su tope', async () => {
52
+ const atMax = {
53
+ ...store, name: text(SHORT), phone: text(SHORT), schedule: text(SHORT),
54
+ address: { ...address, address1: text(SHORT), externalNumber: text(SHORT), internalNumber: text(SHORT), neighborhood: text(SHORT), city: text(SHORT), state: text(SHORT), country: text(SHORT), reference: text(LONG) },
55
+ };
56
+ expect(await errorsOf(StoreEventPayload, atMax)).toEqual([]);
57
+ });
58
+
59
+ it.each([
60
+ ['name', { name: text(SHORT + 1) }],
61
+ ['phone', { phone: text(SHORT + 1) }],
62
+ ['schedule', { schedule: text(SHORT + 1) }],
63
+ ['address.address1', { address: { ...address, address1: text(SHORT + 1) } }],
64
+ ['address.externalNumber', { address: { ...address, externalNumber: text(SHORT + 1) } }],
65
+ ['address.internalNumber', { address: { ...address, internalNumber: text(SHORT + 1) } }],
66
+ ['address.neighborhood', { address: { ...address, neighborhood: text(SHORT + 1) } }],
67
+ ['address.city', { address: { ...address, city: text(SHORT + 1) } }],
68
+ ['address.state', { address: { ...address, state: text(SHORT + 1) } }],
69
+ ['address.country', { address: { ...address, country: text(SHORT + 1) } }],
70
+ ['address.reference', { address: { ...address, reference: text(LONG + 1) } }],
71
+ ])('rechaza %s sobre su tope', async (path, over) => {
72
+ expect(await errorsOf(StoreEventPayload, { ...store, ...over })).toContain(path);
73
+ });
74
+
75
+ it.each([
76
+ ['address', 'Av. Juárez 120'],
77
+ ['address', [address]],
78
+ ['location', 'lat,lng'],
79
+ ['location', [19.2, -99.6]],
80
+ ])('rechaza %s que no es objeto', async (path, value) => {
81
+ const errors = await validate(plainToInstance(StoreEventPayload, { ...store, [path]: value }));
82
+ expect(errors.find((e) => e.property === path)?.constraints).toHaveProperty('isObject');
83
+ });
84
+ });
85
+
86
+ describe('RetailUserEventPayload: topes', () => {
87
+ it('acepta textos justo en su tope', async () => {
88
+ // IsEmail ya topa en 254, uno abajo de RETAIL_SHORT_TEXT_MAX_LENGTH.
89
+ const email = `${text(64)}@${'b'.repeat(63)}.${'c'.repeat(63)}.${'d'.repeat(58)}.mx`;
90
+ expect(email).toHaveLength(SHORT - 1);
91
+ expect(await errorsOf(RetailUserEventPayload, { ...user, displayName: text(SHORT), phone: text(SHORT), email })).toEqual([]);
92
+ });
93
+
94
+ it.each([
95
+ ['displayName', { displayName: text(SHORT + 1) }],
96
+ ['phone', { phone: text(SHORT + 1) }],
97
+ ['email', { email: `${text(SHORT)}@ejemplo.mx` }],
98
+ ])('rechaza %s sobre su tope', async (path, over) => {
99
+ expect(await errorsOf(RetailUserEventPayload, { ...user, ...over })).toContain(path);
100
+ });
101
+ });
102
+
103
+ describe('ProductEventPayload: topes', () => {
104
+ it('acepta textos, financieras y montos justo en su tope', async () => {
105
+ const atMax = {
106
+ ...product, externalId: text(ID), brand: text(SHORT), model: text(SHORT), color: text(SHORT), capacity: text(SHORT), description: text(LONG),
107
+ price: { ...price, basePrice: '999999999999.99', listPrice: '999999999999.99' },
108
+ eligibleFinanciers: Array.from({ length: RETAIL_ELIGIBLE_FINANCIERS_MAX }, () => 'SUREKEEP'),
109
+ };
110
+ expect(await errorsOf(ProductEventPayload, atMax)).toEqual([]);
111
+ });
112
+
113
+ it.each([
114
+ ['externalId', { externalId: text(ID + 1) }],
115
+ ['brand', { brand: text(SHORT + 1) }],
116
+ ['model', { model: text(SHORT + 1) }],
117
+ ['color', { color: text(SHORT + 1) }],
118
+ ['capacity', { capacity: text(SHORT + 1) }],
119
+ ['description', { description: text(LONG + 1) }],
120
+ ['price.basePrice', { price: { ...price, basePrice: '1000000000000.00' } }],
121
+ ['price.listPrice', { price: { ...price, listPrice: '1000000000000.00' } }],
122
+ ['eligibleFinanciers', { eligibleFinanciers: Array.from({ length: RETAIL_ELIGIBLE_FINANCIERS_MAX + 1 }, () => 'SUREKEEP') }],
123
+ ])('rechaza %s', async (path, over) => {
124
+ expect(await errorsOf(ProductEventPayload, { ...product, ...over })).toContain(path);
125
+ });
126
+
127
+ it.each([['4299.00'], [[price]]])('rechaza price que no es objeto: %j', async (value) => {
128
+ const errors = await validate(plainToInstance(ProductEventPayload, { ...product, price: value }));
129
+ expect(errors.find((e) => e.property === 'price')?.constraints).toHaveProperty('isObject');
130
+ });
131
+ });
132
+
133
+ describe('InventoryEventPayload: topes', () => {
134
+ const stockOf = (n: number) => Array.from({ length: n }, (_, i) => ({ sku: `MICA-${String(i)}`, storeId: '12', qty: 1 }));
135
+
136
+ it('acepta identifier y stock justo en su tope', async () => {
137
+ expect(await errorsOf(InventoryEventPayload, { ...movement, pieces: [{ ...piece, identifier: text(SHORT) }], stock: stockOf(RETAIL_EVENT_STOCK_MAX) })).toEqual([]);
138
+ });
139
+
140
+ it('rechaza identifier sobre su tope', async () => {
141
+ expect(await errorsOf(InventoryEventPayload, { ...movement, pieces: [{ ...piece, identifier: text(SHORT + 1) }] })).toContain('pieces.0.identifier');
142
+ });
143
+
144
+ it('rechaza stock sobre su tope', async () => {
145
+ expect(await errorsOf(InventoryEventPayload, { ...movement, stock: stockOf(RETAIL_EVENT_STOCK_MAX + 1) })).toContain('stock');
146
+ });
147
+ });
@@ -5,6 +5,7 @@ import { StoreEventPayload } from '../../../src/retailIntegration/dtos/StoreEven
5
5
  import { RetailUserEventPayload } from '../../../src/retailIntegration/dtos/RetailUserEventPayload';
6
6
  import { ProductEventPayload } from '../../../src/retailIntegration/dtos/ProductEventPayload';
7
7
  import { InventoryEventPayload } from '../../../src/retailIntegration/dtos/InventoryEventPayload';
8
+ import { RETAIL_EVENT_PIECES_MAX } from '../../../src/retailIntegration/constants/retailIntegrationLimits';
8
9
 
9
10
  function paths(errors: ValidationError[], prefix = ''): string[] {
10
11
  return errors.flatMap((e) => {
@@ -130,4 +131,11 @@ describe('InventoryEventPayload', () => {
130
131
  expect(await errorsOf(InventoryEventPayload, { ...movement, stock: [s, s] })).toContain('stock');
131
132
  expect(await errorsOf(InventoryEventPayload, { ...movement, stock: [{ ...s, qty: -1 }] })).toContain('stock.0.qty');
132
133
  });
134
+
135
+ it('acepta hasta RETAIL_EVENT_PIECES_MAX piezas y rechaza una más', async () => {
136
+ const piecesOf = (n: number) => Array.from({ length: n }, (_, i) => ({ ...piece, identifier: `35693803564${String(i).padStart(4, '0')}` }));
137
+ expect(RETAIL_EVENT_PIECES_MAX).toBe(30);
138
+ expect(await errorsOf(InventoryEventPayload, { ...movement, pieces: piecesOf(RETAIL_EVENT_PIECES_MAX) })).toEqual([]);
139
+ expect(await errorsOf(InventoryEventPayload, { ...movement, pieces: piecesOf(RETAIL_EVENT_PIECES_MAX + 1) })).toContain('pieces');
140
+ });
133
141
  });
@@ -20,8 +20,8 @@ function propsWithErrors<T extends object>(cls: ClassConstructor<T>, payload: ob
20
20
  }
21
21
 
22
22
  describe('enums de la revisión', () => {
23
- it('ReconciliationEntityEnum trae las cuatro entidades', () => {
24
- expect(Object.values(ReconciliationEntityEnum)).toEqual(['STORES', 'USERS', 'CATALOG', 'INVENTORY']);
23
+ it('ReconciliationEntityEnum trae las cinco entidades', () => {
24
+ expect(Object.values(ReconciliationEntityEnum)).toEqual(['STORES', 'USERS', 'CATALOG', 'INVENTORY', 'MOVEMENTS']);
25
25
  });
26
26
 
27
27
  it('ReconciliationRunStatusEnum trae los cuatro estados de la corrida', () => {
@@ -29,7 +29,7 @@ describe('enums de la revisión', () => {
29
29
  });
30
30
 
31
31
  it('ReconciliationTriggerEnum distingue el reloj del botón', () => {
32
- expect(Object.values(ReconciliationTriggerEnum)).toEqual(['SCHEDULED', 'MANUAL', 'ACTIVATION']);
32
+ expect(Object.values(ReconciliationTriggerEnum)).toEqual(['SCHEDULED', 'MANUAL', 'ACTIVATION', 'DISCREPANCY', 'DAILY']);
33
33
  });
34
34
 
35
35
  it('InactiveReasonEnum distingue la baja de la revisión de la del evento', () => {
@@ -0,0 +1,109 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance, type ClassConstructor } from 'class-transformer';
3
+ import { validateSync } from 'class-validator';
4
+ import {
5
+ GetRetailInventoryRequest,
6
+ type GetRetailInventoryResponse,
7
+ ListRetailInventoryMovementsRequest,
8
+ type ListRetailInventoryMovementsResponse,
9
+ ReconciliationEntityEnum,
10
+ ReconciliationPageMessage,
11
+ ReconciliationTriggerEnum,
12
+ } from '../../../src/retailIntegration';
13
+
14
+ function propsWithErrors<T extends object>(cls: ClassConstructor<T>, payload: object): string[] {
15
+ return validateSync(plainToInstance(cls, payload)).map((e) => e.property);
16
+ }
17
+
18
+ const tenant = { tenantId: 'surekeep', retailerId: 'ret-1' };
19
+
20
+ describe('ListRetailInventoryMovementsRequest', () => {
21
+ const valid = { ...tenant, from: '2026-08-17T19:55:00Z', to: '2026-08-17T21:00:00Z' };
22
+
23
+ it('acepta el rango con y sin paginación', () => {
24
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, valid)).toEqual([]);
25
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, { ...valid, limit: '200', nextToken: 'abc', runId: 'run-1' })).toEqual([]);
26
+ });
27
+
28
+ it('acepta un rango de 7 días exactos', () => {
29
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, { ...tenant, from: '2026-08-10T00:00:00Z', to: '2026-08-17T00:00:00Z' })).toEqual([]);
30
+ });
31
+
32
+ it.each([
33
+ ['to igual a from', '2026-08-17T20:00:00Z', '2026-08-17T20:00:00Z'],
34
+ ['to antes de from', '2026-08-17T20:00:00Z', '2026-08-17T19:00:00Z'],
35
+ ['más de 7 días', '2026-08-10T00:00:00Z', '2026-08-17T00:00:01Z'],
36
+ ])('rechaza %s', (_label, from, to) => {
37
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, { ...tenant, from, to })).toEqual(['to']);
38
+ });
39
+
40
+ it.each(['from', 'to'])('rechaza sin %s o con fecha sin zona', (field) => {
41
+ const payload: Record<string, unknown> = { ...valid };
42
+ delete payload[field];
43
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, payload)).toContain(field);
44
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, { ...valid, [field]: '2026-08-17T20:00:00' })).toContain(field);
45
+ });
46
+
47
+ it.each(['0', '201'])('rechaza limit=%p', (limit) => {
48
+ expect(propsWithErrors(ListRetailInventoryMovementsRequest, { ...valid, limit })).toEqual(['limit']);
49
+ });
50
+
51
+ it('la respuesta trae items y meta de página', () => {
52
+ const response: ListRetailInventoryMovementsResponse = {
53
+ items: [
54
+ {
55
+ eventId: 'evt-1',
56
+ eventType: 'InventoryMovementV1',
57
+ occurredAt: '2026-08-17T20:32:07Z',
58
+ payload: { movementType: 'SALE', storeId: '12', targetStoreId: null, pieces: [], stock: [] },
59
+ },
60
+ ],
61
+ meta: { count: 1, nextToken: null },
62
+ };
63
+ expect(response.meta.nextToken).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe('GetRetailInventoryRequest paginado', () => {
68
+ it('acepta limit y nextToken opcionales', () => {
69
+ expect(propsWithErrors(GetRetailInventoryRequest, { ...tenant, storeId: '12', limit: '50', nextToken: 'abc' })).toEqual([]);
70
+ expect(propsWithErrors(GetRetailInventoryRequest, { ...tenant, storeId: '12', limit: '0' })).toEqual(['limit']);
71
+ });
72
+
73
+ it('la respuesta admite meta.count y meta.nextToken', () => {
74
+ const response: GetRetailInventoryResponse = {
75
+ pieces: [],
76
+ stock: [],
77
+ meta: { storeId: '12', generatedAt: '2026-08-17T15:00:00Z', count: 0, nextToken: null },
78
+ };
79
+ expect(response.meta.count).toBe(0);
80
+ });
81
+ });
82
+
83
+ describe('enums de revisión para movimientos', () => {
84
+ it('ReconciliationEntityEnum suma MOVEMENTS', () => {
85
+ expect(ReconciliationEntityEnum.MOVEMENTS).toBe('MOVEMENTS');
86
+ });
87
+
88
+ it('ReconciliationTriggerEnum suma DISCREPANCY y DAILY', () => {
89
+ expect(ReconciliationTriggerEnum.DISCREPANCY).toBe('DISCREPANCY');
90
+ expect(ReconciliationTriggerEnum.DAILY).toBe('DAILY');
91
+ });
92
+ });
93
+
94
+ describe('ReconciliationPageMessage con cursor y rango', () => {
95
+ const base = { ...tenant, runId: 'run-1', pageIndex: 0 };
96
+
97
+ it('acepta nextToken en inventario y from/to en movimientos', () => {
98
+ expect(propsWithErrors(ReconciliationPageMessage, { ...base, entity: 'INVENTORY', storeId: '12', nextToken: 'abc' })).toEqual([]);
99
+ expect(
100
+ propsWithErrors(ReconciliationPageMessage, { ...base, entity: 'MOVEMENTS', from: '2026-08-17T19:55:00Z', to: '2026-08-17T21:00:00Z' }),
101
+ ).toEqual([]);
102
+ });
103
+
104
+ it('rechaza nextToken vacío y fechas sin zona', () => {
105
+ expect(propsWithErrors(ReconciliationPageMessage, { ...base, entity: 'INVENTORY', nextToken: '' })).toEqual(['nextToken']);
106
+ expect(propsWithErrors(ReconciliationPageMessage, { ...base, entity: 'MOVEMENTS', from: '2026-08-17' })).toEqual(['from']);
107
+ expect(propsWithErrors(ReconciliationPageMessage, { ...base, entity: 'MOVEMENTS', to: 'ayer' })).toEqual(['to']);
108
+ });
109
+ });
@@ -0,0 +1,120 @@
1
+ import 'reflect-metadata';
2
+ import { plainToInstance, type ClassConstructor } from 'class-transformer';
3
+ import { validateSync, type ValidationError } from 'class-validator';
4
+ import {
5
+ RetailSystemErrorBody,
6
+ RetailSystemInventoryResponse,
7
+ RetailSystemMovementsResponse,
8
+ RetailSystemPageMeta,
9
+ RetailSystemPageResponse,
10
+ RetailSystemSaleResponse,
11
+ } from '../../../src/retailIntegration';
12
+
13
+ function errorPaths(errors: ValidationError[], prefix = ''): string[] {
14
+ return errors.flatMap((error) => {
15
+ const path = prefix ? `${prefix}.${error.property}` : error.property;
16
+ return error.children?.length ? errorPaths(error.children, path) : [path];
17
+ });
18
+ }
19
+
20
+ function pathsWithErrors<T extends object>(cls: ClassConstructor<T>, payload: object): string[] {
21
+ return errorPaths(validateSync(plainToInstance(cls, payload)));
22
+ }
23
+
24
+ const movement = {
25
+ eventId: 'evt_01J8ZE5M8Y2Q4R7T1V3W6X8Z1C',
26
+ eventType: 'InventoryMovementV1',
27
+ occurredAt: '2026-08-17T20:32:07Z',
28
+ payload: { movementType: 'SALE', storeId: '12', targetStoreId: null, pieces: [], stock: [] },
29
+ };
30
+
31
+ describe('RetailSystemPageMeta', () => {
32
+ it('acepta la última página y una intermedia', () => {
33
+ expect(pathsWithErrors(RetailSystemPageMeta, { count: 0, nextToken: null })).toEqual([]);
34
+ expect(pathsWithErrors(RetailSystemPageMeta, { count: 200, nextToken: 'eyJpZCI6MTQwMH0' })).toEqual([]);
35
+ });
36
+
37
+ it.each([-1, 1.5, '3', undefined])('rechaza count=%p', (count) => {
38
+ expect(pathsWithErrors(RetailSystemPageMeta, { count, nextToken: null })).toEqual(['count']);
39
+ });
40
+
41
+ it.each([undefined, 7, 'x'.repeat(2049)])('rechaza nextToken=%p', (nextToken) => {
42
+ expect(pathsWithErrors(RetailSystemPageMeta, { count: 1, nextToken })).toEqual(['nextToken']);
43
+ });
44
+ });
45
+
46
+ describe('RetailSystemPageResponse', () => {
47
+ it('acepta data con renglones crudos', () => {
48
+ expect(pathsWithErrors(RetailSystemPageResponse, { data: [{ any: 'row' }], meta: { count: 1, nextToken: null } })).toEqual([]);
49
+ });
50
+
51
+ it('rechaza data que no es arreglo y meta inválida', () => {
52
+ expect(pathsWithErrors(RetailSystemPageResponse, { data: {}, meta: { count: 1, nextToken: null } })).toEqual(['data']);
53
+ expect(pathsWithErrors(RetailSystemPageResponse, { data: [], meta: { count: -1, nextToken: null } })).toEqual(['meta.count']);
54
+ expect(pathsWithErrors(RetailSystemPageResponse, { data: [] })).toEqual(['meta']);
55
+ });
56
+ });
57
+
58
+ describe('RetailSystemInventoryResponse', () => {
59
+ const valid = {
60
+ data: { pieces: [{ sku: 'SAM-A17' }], stock: [] },
61
+ meta: { storeId: '12', generatedAt: '2026-08-17T15:00:00Z', count: 1, nextToken: null },
62
+ };
63
+
64
+ it('acepta una página del inventario', () => {
65
+ expect(pathsWithErrors(RetailSystemInventoryResponse, valid)).toEqual([]);
66
+ });
67
+
68
+ it('rechaza pieces o stock que no son arreglo', () => {
69
+ expect(pathsWithErrors(RetailSystemInventoryResponse, { ...valid, data: { pieces: null, stock: [] } })).toEqual(['data.pieces']);
70
+ expect(pathsWithErrors(RetailSystemInventoryResponse, { ...valid, data: { pieces: [] } })).toEqual(['data.stock']);
71
+ });
72
+
73
+ it.each([
74
+ ['storeId', 'tienda/12'],
75
+ ['generatedAt', '2026-08-17 15:00'],
76
+ ['count', -2],
77
+ ['nextToken', ''.padEnd(2049, 'a')],
78
+ ])('rechaza meta.%s inválido', (field, value) => {
79
+ expect(pathsWithErrors(RetailSystemInventoryResponse, { ...valid, meta: { ...valid.meta, [field]: value } })).toEqual([`meta.${field}`]);
80
+ });
81
+ });
82
+
83
+ describe('RetailSystemSaleResponse', () => {
84
+ it.each(['58812', 'SALE-12_a.b:c', 'a/b#ñ'])('acepta externalSaleId=%p', (externalSaleId) => {
85
+ expect(pathsWithErrors(RetailSystemSaleResponse, { externalSaleId })).toEqual([]);
86
+ });
87
+
88
+ it.each(['', 'con espacio', ' 58812', 'x'.repeat(65), 58812, undefined])('rechaza externalSaleId=%p', (externalSaleId) => {
89
+ expect(pathsWithErrors(RetailSystemSaleResponse, { externalSaleId })).toEqual(['externalSaleId']);
90
+ });
91
+ });
92
+
93
+ describe('RetailSystemErrorBody', () => {
94
+ it('acepta el error con y sin lineIndex', () => {
95
+ expect(pathsWithErrors(RetailSystemErrorBody, { code: 'IDENTIFIER_NOT_AVAILABLE', msg: 'texto', lineIndex: 1 })).toEqual([]);
96
+ expect(pathsWithErrors(RetailSystemErrorBody, { code: 'VALIDATION_ERROR' })).toEqual([]);
97
+ });
98
+
99
+ it('rechaza code ausente o largo y lineIndex fuera de rango', () => {
100
+ expect(pathsWithErrors(RetailSystemErrorBody, {})).toEqual(['code']);
101
+ expect(pathsWithErrors(RetailSystemErrorBody, { code: 'X'.repeat(65) })).toEqual(['code']);
102
+ expect(pathsWithErrors(RetailSystemErrorBody, { code: 'X', lineIndex: 1000 })).toEqual(['lineIndex']);
103
+ expect(pathsWithErrors(RetailSystemErrorBody, { code: 'X', lineIndex: -1 })).toEqual(['lineIndex']);
104
+ });
105
+ });
106
+
107
+ describe('RetailSystemMovementsResponse', () => {
108
+ it('acepta movimientos con el sobre del webhook', () => {
109
+ expect(pathsWithErrors(RetailSystemMovementsResponse, { data: [movement], meta: { count: 1, nextToken: null } })).toEqual([]);
110
+ expect(pathsWithErrors(RetailSystemMovementsResponse, { data: [], meta: { count: 0, nextToken: null } })).toEqual([]);
111
+ });
112
+
113
+ it('rechaza un movimiento con sobre inválido', () => {
114
+ const bad = { ...movement, eventType: 'StoreUpsertedV1', occurredAt: '2026-08-17' };
115
+ expect(pathsWithErrors(RetailSystemMovementsResponse, { data: [bad], meta: { count: 1, nextToken: null } }).sort()).toEqual([
116
+ 'data.0.eventType',
117
+ 'data.0.occurredAt',
118
+ ]);
119
+ });
120
+ });
@@ -6,7 +6,8 @@ import { PermissionScope } from '../enums/PermissionScope';
6
6
  * Consumido por el alta de usuario orquestada de SureKeep F1 (retail-org → RBAC → RetailUser) vía api-invoker.
7
7
  * - Usuario provisionado en estado INVITED (mitiga orphan si el paso siguiente en retail-org falla).
8
8
  * - Idempotente por identidad Cognito.
9
- * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol no hay invitación.
9
+ * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol exige `suppressInvitation: true`
10
+ * y la invitación queda pendiente (`invitationPending`) hasta asignar el primer rol.
10
11
  * - `createdBy` = cognitoSub del admin real (audit + grantedBy del assignment).
11
12
  * - `customFields` = valores de los campos personalizados del tenant; si el correo ya tiene usuario, se fusionan.
12
13
  */
@@ -23,7 +23,8 @@ const hasAnyGrantField = (o) => o.roleId !== undefined || o.scope !== undefined
23
23
  * Consumido por el alta de usuario orquestada de SureKeep F1 (retail-org → RBAC → RetailUser) vía api-invoker.
24
24
  * - Usuario provisionado en estado INVITED (mitiga orphan si el paso siguiente en retail-org falla).
25
25
  * - Idempotente por identidad Cognito.
26
- * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol no hay invitación.
26
+ * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol exige `suppressInvitation: true`
27
+ * y la invitación queda pendiente (`invitationPending`) hasta asignar el primer rol.
27
28
  * - `createdBy` = cognitoSub del admin real (audit + grantedBy del assignment).
28
29
  * - `customFields` = valores de los campos personalizados del tenant; si el correo ya tiene usuario, se fusionan.
29
30
  */
@@ -0,0 +1,5 @@
1
+ /** Body M2M que cambia el correo de un usuario del tenant; el correo se guarda en minúsculas. */
2
+ export declare class UpdateTenantUserEmailRequest {
3
+ email: string;
4
+ actedBy: string;
5
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.UpdateTenantUserEmailRequest = void 0;
13
+ const class_transformer_1 = require("class-transformer");
14
+ const class_validator_1 = require("class-validator");
15
+ /** Body M2M que cambia el correo de un usuario del tenant; el correo se guarda en minúsculas. */
16
+ class UpdateTenantUserEmailRequest {
17
+ }
18
+ exports.UpdateTenantUserEmailRequest = UpdateTenantUserEmailRequest;
19
+ __decorate([
20
+ (0, class_transformer_1.Expose)(),
21
+ (0, class_transformer_1.Transform)(({ value }) => (typeof value === 'string' ? value.trim().toLowerCase() : value)),
22
+ (0, class_validator_1.IsEmail)(),
23
+ __metadata("design:type", String)
24
+ ], UpdateTenantUserEmailRequest.prototype, "email", void 0);
25
+ __decorate([
26
+ (0, class_transformer_1.Expose)(),
27
+ (0, class_validator_1.IsString)(),
28
+ (0, class_validator_1.IsNotEmpty)(),
29
+ __metadata("design:type", String)
30
+ ], UpdateTenantUserEmailRequest.prototype, "actedBy", void 0);
@@ -120,7 +120,8 @@ export declare enum Permission {
120
120
  PLATFORM_INTEGRATION_KEY_ISSUE = "platform.integration.key.issue",
121
121
  PLATFORM_INTEGRATION_KEY_REVOKE = "platform.integration.key.revoke",
122
122
  TENANT_INTEGRATION_DIAGNOSTIC = "tenant.integration.diagnostic",
123
- TENANT_API_USER_MANAGE = "tenant.api_user.manage"
123
+ TENANT_API_USER_MANAGE = "tenant.api_user.manage",
124
+ PLATFORM_RETAIL_INTEGRATION_ACTIVATE = "platform.retail_integration.activate"
124
125
  }
125
126
  /**
126
127
  * Orden CANÓNICO y APPEND-ONLY de los permisos para el bitset del token.
@@ -166,6 +166,8 @@ var Permission;
166
166
  Permission["TENANT_INTEGRATION_DIAGNOSTIC"] = "tenant.integration.diagnostic";
167
167
  // Alta, rotación de secreto y edición de IPs de usuarios de API; se exige además de tenant.user.create.
168
168
  Permission["TENANT_API_USER_MANAGE"] = "tenant.api_user.manage";
169
+ // Activa la integración de un retailer externo (Zentral); scope PLATFORM.
170
+ Permission["PLATFORM_RETAIL_INTEGRATION_ACTIVATE"] = "platform.retail_integration.activate";
169
171
  })(Permission || (exports.Permission = Permission = {}));
170
172
  /**
171
173
  * Orden CANÓNICO y APPEND-ONLY de los permisos para el bitset del token.
@@ -286,6 +288,7 @@ exports.PERMISSION_BIT_ORDER = [
286
288
  Permission.PLATFORM_INTEGRATION_KEY_REVOKE,
287
289
  Permission.TENANT_INTEGRATION_DIAGNOSTIC,
288
290
  Permission.TENANT_API_USER_MANAGE,
291
+ Permission.PLATFORM_RETAIL_INTEGRATION_ACTIVATE,
289
292
  ];
290
293
  function djb2(input) {
291
294
  let h = 5381;
@@ -73,6 +73,7 @@ export type { ListTenantUsersResponse } from './dtos/ListTenantUsersResponse';
73
73
  export type { TenantUserListItemResponse } from './dtos/TenantUserListItemResponse';
74
74
  export type { GetTenantUserResponse } from './dtos/GetTenantUserResponse';
75
75
  export * from './dtos/UpdateTenantUserRequest';
76
+ export * from './dtos/UpdateTenantUserEmailRequest';
76
77
  export * from './dtos/DeleteTenantUserRequest';
77
78
  export * from './dtos/DisableTenantUserRequest';
78
79
  export * from './dtos/EnableTenantUserRequest';
@@ -120,6 +120,7 @@ __exportStar(require("./dtos/UpdateIntegrationRequest"), exports);
120
120
  __exportStar(require("./dtos/UpdateLevelEntityRequest"), exports);
121
121
  __exportStar(require("./dtos/DeleteLevelEntityRequest"), exports);
122
122
  __exportStar(require("./dtos/UpdateTenantUserRequest"), exports);
123
+ __exportStar(require("./dtos/UpdateTenantUserEmailRequest"), exports);
123
124
  __exportStar(require("./dtos/DeleteTenantUserRequest"), exports);
124
125
  __exportStar(require("./dtos/DisableTenantUserRequest"), exports);
125
126
  __exportStar(require("./dtos/EnableTenantUserRequest"), exports);
@@ -20,3 +20,28 @@ export declare const RETAIL_SKU_PATTERN: RegExp;
20
20
  export declare const RETAIL_DECIMAL_PATTERN: RegExp;
21
21
  /** Tope de identificadores por consulta de inventario en vivo. */
22
22
  export declare const RETAIL_LIVE_CHECK_IDENTIFIERS_MAX = 50;
23
+ /** Tope de piezas por evento `InventoryMovementV1`; más se rechaza con INVALID_PAYLOAD. */
24
+ export declare const RETAIL_EVENT_PIECES_MAX = 30;
25
+ /** Tope de ids y códigos libres del retailer, como `externalId`. */
26
+ export declare const RETAIL_ID_TEXT_MAX_LENGTH = 64;
27
+ /** Tope de textos cortos de los payloads: nombres, dirección, teléfono, marca, modelo, `identifier`. */
28
+ export declare const RETAIL_SHORT_TEXT_MAX_LENGTH = 255;
29
+ /** Tope de textos largos de los payloads: descripción y referencia. */
30
+ export declare const RETAIL_LONG_TEXT_MAX_LENGTH = 1000;
31
+ /** Tope de existencias por evento `InventoryMovementV1`. */
32
+ export declare const RETAIL_EVENT_STOCK_MAX = 200;
33
+ /** Tope de financieras elegibles por producto. */
34
+ export declare const RETAIL_ELIGIBLE_FINANCIERS_MAX = 50;
35
+ /** Decimal de los eventos del retailer: hasta 12 enteros y dos decimales, nunca negativo. */
36
+ export declare const RETAIL_EVENT_DECIMAL_PATTERN: RegExp;
37
+ /** Tope del `nextToken` opaco que devuelve Zentral. */
38
+ export declare const RETAIL_NEXT_TOKEN_MAX_LENGTH = 2048;
39
+ /** Id que asigna Zentral, como `externalSaleId`: no vacío, sin espacios y de a lo más 64. */
40
+ export declare const RETAIL_EXTERNAL_ID_PATTERN: RegExp;
41
+ /** Tope del `code` en el cuerpo de error de Zentral. */
42
+ export declare const RETAIL_ERROR_CODE_MAX_LENGTH = 64;
43
+ /** Rango de `lineIndex` en un error de línea de venta. */
44
+ export declare const RETAIL_LINE_INDEX_MIN = 0;
45
+ export declare const RETAIL_LINE_INDEX_MAX = 999;
46
+ /** Rango máximo de `GET /inventory/movements`: 7 días en milisegundos. */
47
+ export declare const RETAIL_MOVEMENTS_RANGE_MAX_MS: number;