@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
@@ -18,6 +18,7 @@ __exportStar(require("./constants/retailIntegrationLimits"), exports);
18
18
  __exportStar(require("./constants/retailIntegrationErrorHttpStatus"), exports);
19
19
  __exportStar(require("./enums"), exports);
20
20
  __exportStar(require("./validators/IsIsoDateTimeWithZone"), exports);
21
+ __exportStar(require("./validators/IsMovementsRangeValid"), exports);
21
22
  __exportStar(require("./dtos/RetailEventEnvelope"), exports);
22
23
  __exportStar(require("./dtos/ReceiveStoreEventRequest"), exports);
23
24
  __exportStar(require("./dtos/ReceiveRetailUserEventRequest"), exports);
@@ -37,11 +38,13 @@ __exportStar(require("./dtos/RetailPagedQuery"), exports);
37
38
  __exportStar(require("./dtos/ListRetailStoresRequest"), exports);
38
39
  __exportStar(require("./dtos/ListRetailCatalogRequest"), exports);
39
40
  __exportStar(require("./dtos/GetRetailInventoryRequest"), exports);
41
+ __exportStar(require("./dtos/ListRetailInventoryMovementsRequest"), exports);
40
42
  __exportStar(require("./dtos/ListRetailUsersRequest"), exports);
41
43
  __exportStar(require("./dtos/RegisterRetailSaleRequest"), exports);
42
44
  __exportStar(require("./dtos/CancelRetailSaleRequest"), exports);
43
45
  __exportStar(require("./dtos/CheckRetailInventoryRequest"), exports);
44
46
  __exportStar(require("./dtos/RetailOutboundResponses"), exports);
47
+ __exportStar(require("./dtos/RetailSystemResponses"), exports);
45
48
  __exportStar(require("./models/RetailPageMeta"), exports);
46
49
  __exportStar(require("./models/RetailSystemStore"), exports);
47
50
  __exportStar(require("./models/RetailSystemProduct"), exports);
@@ -3,8 +3,10 @@ export interface RetailPageMeta {
3
3
  count: number;
4
4
  nextToken: string | null;
5
5
  }
6
- /** `meta` del inventario de una tienda. */
6
+ /** `meta` del inventario de una tienda; `count` y `nextToken` llegan cuando la lectura pagina. */
7
7
  export interface RetailInventoryMeta {
8
8
  storeId: string;
9
9
  generatedAt: string;
10
+ count?: number;
11
+ nextToken?: string | null;
10
12
  }
@@ -1,4 +1,5 @@
1
- import { RetailPieceReasonEnum, RetailPieceStatusEnum } from '../enums';
1
+ import type { InventoryEventPayload } from '../dtos/InventoryEventPayload';
2
+ import { InventoryEventTypeEnum, RetailPieceReasonEnum, RetailPieceStatusEnum } from '../enums';
2
3
  export type RetailPieceStatus = `${RetailPieceStatusEnum}`;
3
4
  export type RetailPieceReason = `${RetailPieceReasonEnum}`;
4
5
  /** Pieza con IMEI o número de serie. */
@@ -15,3 +16,10 @@ export interface RetailSystemStock {
15
16
  storeId: string;
16
17
  qty: number;
17
18
  }
19
+ /** Movimiento de inventario de Zentral: el mismo sobre que llega a `POST /webhooks/inventory`. */
20
+ export interface RetailSystemInventoryMovement {
21
+ eventId: string;
22
+ eventType: `${InventoryEventTypeEnum}`;
23
+ occurredAt: string;
24
+ payload: InventoryEventPayload;
25
+ }
@@ -0,0 +1,6 @@
1
+ import { ValidationArguments, ValidatorConstraintInterface } from 'class-validator';
2
+ /** `to`: mayor que `from` y a lo más 7 días después; sin `from` legible lo rechaza su propio validador. */
3
+ export declare class IsMovementsRangeValid implements ValidatorConstraintInterface {
4
+ validate(value: unknown, args: ValidationArguments): boolean;
5
+ defaultMessage(): string;
6
+ }
@@ -0,0 +1,28 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.IsMovementsRangeValid = void 0;
10
+ const class_validator_1 = require("class-validator");
11
+ const retailIntegrationLimits_1 = require("../constants/retailIntegrationLimits");
12
+ /** `to`: mayor que `from` y a lo más 7 días después; sin `from` legible lo rechaza su propio validador. */
13
+ let IsMovementsRangeValid = class IsMovementsRangeValid {
14
+ validate(value, args) {
15
+ const from = Date.parse(String(args.object.from));
16
+ const to = Date.parse(String(value));
17
+ if (Number.isNaN(from) || Number.isNaN(to))
18
+ return true;
19
+ return to > from && to - from <= retailIntegrationLimits_1.RETAIL_MOVEMENTS_RANGE_MAX_MS;
20
+ }
21
+ defaultMessage() {
22
+ return 'to debe ser mayor que from y a lo más 7 días después';
23
+ }
24
+ };
25
+ exports.IsMovementsRangeValid = IsMovementsRangeValid;
26
+ exports.IsMovementsRangeValid = IsMovementsRangeValid = __decorate([
27
+ (0, class_validator_1.ValidatorConstraint)({ name: 'isMovementsRangeValid', async: false })
28
+ ], IsMovementsRangeValid);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/type-kit",
3
- "version": "3.583.0",
3
+ "version": "3.585.0",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "types": "bin/index.d.ts",
@@ -14,7 +14,8 @@ const hasAnyGrantField = (o: CreateTenantUserRequest): boolean =>
14
14
  * Consumido por el alta de usuario orquestada de SureKeep F1 (retail-org → RBAC → RetailUser) vía api-invoker.
15
15
  * - Usuario provisionado en estado INVITED (mitiga orphan si el paso siguiente en retail-org falla).
16
16
  * - Idempotente por identidad Cognito.
17
- * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol no hay invitación.
17
+ * - `roleId`/`scope`/`scopeRef`: todos o ninguno; sin rol exige `suppressInvitation: true`
18
+ * y la invitación queda pendiente (`invitationPending`) hasta asignar el primer rol.
18
19
  * - `createdBy` = cognitoSub del admin real (audit + grantedBy del assignment).
19
20
  * - `customFields` = valores de los campos personalizados del tenant; si el correo ya tiene usuario, se fusionan.
20
21
  */
@@ -0,0 +1,12 @@
1
+ import { Expose, Transform } from 'class-transformer';
2
+ import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
3
+
4
+ /** Body M2M que cambia el correo de un usuario del tenant; el correo se guarda en minúsculas. */
5
+ export class UpdateTenantUserEmailRequest {
6
+ @Expose()
7
+ @Transform(({ value }) => (typeof value === 'string' ? value.trim().toLowerCase() : value))
8
+ @IsEmail()
9
+ email!: string;
10
+
11
+ @Expose() @IsString() @IsNotEmpty() actedBy!: string;
12
+ }
@@ -165,6 +165,8 @@ export enum Permission {
165
165
  TENANT_INTEGRATION_DIAGNOSTIC = 'tenant.integration.diagnostic',
166
166
  // Alta, rotación de secreto y edición de IPs de usuarios de API; se exige además de tenant.user.create.
167
167
  TENANT_API_USER_MANAGE = 'tenant.api_user.manage',
168
+ // Activa la integración de un retailer externo (Zentral); scope PLATFORM.
169
+ PLATFORM_RETAIL_INTEGRATION_ACTIVATE = 'platform.retail_integration.activate',
168
170
  }
169
171
 
170
172
  /**
@@ -286,6 +288,7 @@ export const PERMISSION_BIT_ORDER: readonly Permission[] = [
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
  ] as const;
290
293
 
291
294
  function djb2(input: string): number {
@@ -167,6 +167,7 @@ export type { ListTenantUsersResponse } from './dtos/ListTenantUsersResponse';
167
167
  export type { TenantUserListItemResponse } from './dtos/TenantUserListItemResponse';
168
168
  export type { GetTenantUserResponse } from './dtos/GetTenantUserResponse';
169
169
  export * from './dtos/UpdateTenantUserRequest';
170
+ export * from './dtos/UpdateTenantUserEmailRequest';
170
171
  export * from './dtos/DeleteTenantUserRequest';
171
172
  export * from './dtos/DisableTenantUserRequest';
172
173
  export * from './dtos/EnableTenantUserRequest';
@@ -29,3 +29,40 @@ export const RETAIL_DECIMAL_PATTERN = /^\d+\.\d{2}$/;
29
29
 
30
30
  /** Tope de identificadores por consulta de inventario en vivo. */
31
31
  export const RETAIL_LIVE_CHECK_IDENTIFIERS_MAX = 50;
32
+
33
+ /** Tope de piezas por evento `InventoryMovementV1`; más se rechaza con INVALID_PAYLOAD. */
34
+ export const RETAIL_EVENT_PIECES_MAX = 30;
35
+
36
+ /** Tope de ids y códigos libres del retailer, como `externalId`. */
37
+ export const RETAIL_ID_TEXT_MAX_LENGTH = 64;
38
+
39
+ /** Tope de textos cortos de los payloads: nombres, dirección, teléfono, marca, modelo, `identifier`. */
40
+ export const RETAIL_SHORT_TEXT_MAX_LENGTH = 255;
41
+
42
+ /** Tope de textos largos de los payloads: descripción y referencia. */
43
+ export const RETAIL_LONG_TEXT_MAX_LENGTH = 1000;
44
+
45
+ /** Tope de existencias por evento `InventoryMovementV1`. */
46
+ export const RETAIL_EVENT_STOCK_MAX = 200;
47
+
48
+ /** Tope de financieras elegibles por producto. */
49
+ export const RETAIL_ELIGIBLE_FINANCIERS_MAX = 50;
50
+
51
+ /** Decimal de los eventos del retailer: hasta 12 enteros y dos decimales, nunca negativo. */
52
+ export const RETAIL_EVENT_DECIMAL_PATTERN = /^\d{1,12}\.\d{2}$/;
53
+
54
+ /** Tope del `nextToken` opaco que devuelve Zentral. */
55
+ export const RETAIL_NEXT_TOKEN_MAX_LENGTH = 2048;
56
+
57
+ /** Id que asigna Zentral, como `externalSaleId`: no vacío, sin espacios y de a lo más 64. */
58
+ export const RETAIL_EXTERNAL_ID_PATTERN = /^\S{1,64}$/;
59
+
60
+ /** Tope del `code` en el cuerpo de error de Zentral. */
61
+ export const RETAIL_ERROR_CODE_MAX_LENGTH = 64;
62
+
63
+ /** Rango de `lineIndex` en un error de línea de venta. */
64
+ export const RETAIL_LINE_INDEX_MIN = 0;
65
+ export const RETAIL_LINE_INDEX_MAX = 999;
66
+
67
+ /** Rango máximo de `GET /inventory/movements`: 7 días en milisegundos. */
68
+ export const RETAIL_MOVEMENTS_RANGE_MAX_MS = 7 * 24 * 60 * 60 * 1000;
@@ -8,7 +8,10 @@ function csvToArray({ value }: { value: unknown }): unknown {
8
8
  return typeof value === 'string' ? value.split(',').map((item) => item.trim()) : value;
9
9
  }
10
10
 
11
- /** Query de `GET /private/retail/inventory/live-check`; `identifiers` llega como csv. */
11
+ /**
12
+ * Query de `GET /private/retail/inventory/live-check`; `identifiers` llega como csv.
13
+ * @deprecated el live-check se va a quitar; el inventario se toma de movimientos y de la lectura diaria.
14
+ */
12
15
  export class CheckRetailInventoryRequest extends RetailTenantQuery {
13
16
  @IsString() @Matches(/\S/) storeId!: string;
14
17
 
@@ -1,8 +1,8 @@
1
1
  import { IsString, Matches } from 'class-validator';
2
- import { RetailListQuery } from './RetailListQuery';
2
+ import { RetailPagedQuery } from './RetailPagedQuery';
3
3
 
4
- /** Query de `GET /private/retail/inventory`; el inventario es por tienda y no pagina. */
5
- export class GetRetailInventoryRequest extends RetailListQuery {
4
+ /** Query de `GET /private/retail/inventory`; el inventario es por tienda y pagina con `limit`/`nextToken`. */
5
+ export class GetRetailInventoryRequest extends RetailPagedQuery {
6
6
  @IsString()
7
7
  @Matches(/\S/)
8
8
  storeId!: string;
@@ -1,6 +1,12 @@
1
1
  import { Type } from 'class-transformer';
2
- import { IsArray, IsIn, IsInt, IsString, Matches, Min, Validate, ValidateNested } from 'class-validator';
3
- import { RETAIL_ENTITY_ID_PATTERN, RETAIL_SKU_PATTERN } from '../constants/retailIntegrationLimits';
2
+ import { ArrayMaxSize, IsArray, IsIn, IsInt, IsString, Matches, MaxLength, Min, Validate, ValidateNested } from 'class-validator';
3
+ import {
4
+ RETAIL_EVENT_PIECES_MAX,
5
+ RETAIL_ENTITY_ID_PATTERN,
6
+ RETAIL_EVENT_STOCK_MAX,
7
+ RETAIL_SHORT_TEXT_MAX_LENGTH,
8
+ RETAIL_SKU_PATTERN,
9
+ } from '../constants/retailIntegrationLimits';
4
10
  import { RetailMovementTypeEnum, RetailPieceStatusEnum } from '../enums';
5
11
  import type { RetailPieceReason, RetailPieceStatus, RetailSystemPiece, RetailSystemStock } from '../models/RetailSystemInventory';
6
12
  import { HasUniqueKeys, ReasonMatchesStatus, TargetStoreMatchesMovement } from '../validators/payloadValidators';
@@ -10,7 +16,7 @@ export type RetailMovementType = `${RetailMovementTypeEnum}`;
10
16
  /** Pieza con IMEI o serie, como quedó después del movimiento. */
11
17
  export class PieceEventPayload implements RetailSystemPiece {
12
18
  @IsString() @Matches(RETAIL_SKU_PATTERN) sku!: string;
13
- @IsString() @Matches(/\S/) identifier!: string;
19
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) identifier!: string;
14
20
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) storeId!: string;
15
21
  @IsIn(Object.values(RetailPieceStatusEnum)) status!: RetailPieceStatus;
16
22
  @Validate(ReasonMatchesStatus) reason!: RetailPieceReason | null;
@@ -28,6 +34,6 @@ export class InventoryEventPayload {
28
34
  @IsIn(Object.values(RetailMovementTypeEnum)) movementType!: RetailMovementType;
29
35
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) storeId!: string;
30
36
  @Validate(TargetStoreMatchesMovement) targetStoreId!: string | null;
31
- @IsArray() @ValidateNested({ each: true }) @Type(() => PieceEventPayload) @Validate(HasUniqueKeys, ['identifier']) pieces!: PieceEventPayload[];
32
- @IsArray() @ValidateNested({ each: true }) @Type(() => StockEventPayload) @Validate(HasUniqueKeys, ['storeId', 'sku']) stock!: StockEventPayload[];
37
+ @IsArray() @ArrayMaxSize(RETAIL_EVENT_PIECES_MAX) @ValidateNested({ each: true }) @Type(() => PieceEventPayload) @Validate(HasUniqueKeys, ['identifier']) pieces!: PieceEventPayload[];
38
+ @IsArray() @ArrayMaxSize(RETAIL_EVENT_STOCK_MAX) @ValidateNested({ each: true }) @Type(() => StockEventPayload) @Validate(HasUniqueKeys, ['storeId', 'sku']) stock!: StockEventPayload[];
33
39
  }
@@ -0,0 +1,14 @@
1
+ import { Validate } from 'class-validator';
2
+ import { IsIsoDateTimeWithZone } from '../validators/IsIsoDateTimeWithZone';
3
+ import { IsMovementsRangeValid } from '../validators/IsMovementsRangeValid';
4
+ import { RetailPagedQuery } from './RetailPagedQuery';
5
+
6
+ /** Query de `GET /private/retail/inventory/movements`: rango `[from, to)` de a lo más 7 días. */
7
+ export class ListRetailInventoryMovementsRequest extends RetailPagedQuery {
8
+ @Validate(IsIsoDateTimeWithZone)
9
+ from!: string;
10
+
11
+ @Validate(IsIsoDateTimeWithZone)
12
+ @Validate(IsMovementsRangeValid)
13
+ to!: string;
14
+ }
@@ -1,6 +1,13 @@
1
1
  import { Type } from 'class-transformer';
2
- import { IsArray, IsDefined, IsIn, IsString, Matches, Validate, ValidateIf, ValidateNested } from 'class-validator';
3
- import { RETAIL_DECIMAL_PATTERN, RETAIL_SKU_PATTERN } from '../constants/retailIntegrationLimits';
2
+ import { ArrayMaxSize, IsArray, IsDefined, IsIn, IsObject, IsString, Matches, MaxLength, Validate, ValidateIf, ValidateNested } from 'class-validator';
3
+ import {
4
+ RETAIL_ELIGIBLE_FINANCIERS_MAX,
5
+ RETAIL_EVENT_DECIMAL_PATTERN,
6
+ RETAIL_ID_TEXT_MAX_LENGTH,
7
+ RETAIL_LONG_TEXT_MAX_LENGTH,
8
+ RETAIL_SHORT_TEXT_MAX_LENGTH,
9
+ RETAIL_SKU_PATTERN,
10
+ } from '../constants/retailIntegrationLimits';
4
11
  import {
5
12
  RetailCurrencyEnum,
6
13
  RetailFinancierEnum,
@@ -26,26 +33,26 @@ import { isNullOrValidated } from '../validators/payloadValidators';
26
33
 
27
34
  /** Precio del producto en decimales string. */
28
35
  export class ProductEventPricePayload implements RetailSystemProductPrice {
29
- @IsString() @Matches(RETAIL_DECIMAL_PATTERN) basePrice!: string;
30
- @ValidateIf(isNullOrValidated) @IsString() @Matches(RETAIL_DECIMAL_PATTERN) listPrice!: string | null;
36
+ @IsString() @Matches(RETAIL_EVENT_DECIMAL_PATTERN) basePrice!: string;
37
+ @ValidateIf(isNullOrValidated) @IsString() @Matches(RETAIL_EVENT_DECIMAL_PATTERN) listPrice!: string | null;
31
38
  @Validate(IsIsoDateTimeWithZone) effectiveFrom!: string;
32
39
  }
33
40
 
34
41
  /** Payload de los eventos de producto: el producto completo. */
35
42
  export class ProductEventPayload implements RetailSystemProduct {
36
43
  @IsString() @Matches(RETAIL_SKU_PATTERN) sku!: string;
37
- @IsString() @Matches(/\S/) externalId!: string;
38
- @IsString() @Matches(/\S/) brand!: string;
39
- @IsString() @Matches(/\S/) model!: string;
40
- @ValidateIf(isNullOrValidated) @IsString() color!: string | null;
41
- @ValidateIf(isNullOrValidated) @IsString() capacity!: string | null;
42
- @ValidateIf(isNullOrValidated) @IsString() description!: string | null;
44
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_ID_TEXT_MAX_LENGTH) externalId!: string;
45
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) brand!: string;
46
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) model!: string;
47
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) color!: string | null;
48
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) capacity!: string | null;
49
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_LONG_TEXT_MAX_LENGTH) description!: string | null;
43
50
  @IsIn(Object.values(RetailProductCategoryEnum)) category!: RetailProductCategory;
44
51
  @IsIn(Object.values(RetailTrackingModeEnum)) trackingMode!: RetailTrackingMode;
45
52
  @ValidateIf(isNullOrValidated) @IsIn(Object.values(RetailProductTierEnum)) tier!: RetailProductTier | null;
46
53
  @IsIn(Object.values(RetailCurrencyEnum)) currencyId!: RetailCurrency;
47
- @IsDefined() @ValidateNested() @Type(() => ProductEventPricePayload) price!: ProductEventPricePayload;
48
- @IsArray() @IsIn(Object.values(RetailFinancierEnum), { each: true }) eligibleFinanciers!: RetailFinancier[];
54
+ @IsDefined() @IsObject() @ValidateNested() @Type(() => ProductEventPricePayload) price!: ProductEventPricePayload;
55
+ @IsArray() @ArrayMaxSize(RETAIL_ELIGIBLE_FINANCIERS_MAX) @IsIn(Object.values(RetailFinancierEnum), { each: true }) eligibleFinanciers!: RetailFinancier[];
49
56
  @ValidateIf(isNullOrValidated) @IsIn(Object.values(RetailMdmProviderEnum)) mdmProvider!: RetailMdmProvider | null;
50
57
  @IsIn(Object.values(RetailProductStatusEnum)) status!: RetailProductStatus;
51
58
  }
@@ -1,6 +1,7 @@
1
1
  import { Expose } from 'class-transformer';
2
- import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Min } from 'class-validator';
2
+ import { IsEnum, IsInt, IsNotEmpty, IsOptional, IsString, Matches, Min, Validate } from 'class-validator';
3
3
  import { ReconciliationEntityEnum } from '../enums/ReconciliationEntityEnum';
4
+ import { IsIsoDateTimeWithZone } from '../validators/IsIsoDateTimeWithZone';
4
5
 
5
6
  /** Mensaje de la cola de revisión: una página, o una tienda completa en inventario. */
6
7
  export class ReconciliationPageMessage {
@@ -43,4 +44,23 @@ export class ReconciliationPageMessage {
43
44
  @IsInt()
44
45
  @Min(0)
45
46
  requeueCount?: number;
47
+
48
+ /** Cursor de Zentral para la siguiente página del inventario de la tienda. */
49
+ @Expose()
50
+ @IsOptional()
51
+ @IsString()
52
+ @Matches(/\S/)
53
+ nextToken?: string;
54
+
55
+ /** Inicio del rango de movimientos, incluido. */
56
+ @Expose()
57
+ @IsOptional()
58
+ @Validate(IsIsoDateTimeWithZone)
59
+ from?: string;
60
+
61
+ /** Fin del rango de movimientos, excluido. */
62
+ @Expose()
63
+ @IsOptional()
64
+ @Validate(IsIsoDateTimeWithZone)
65
+ to?: string;
46
66
  }
@@ -1,5 +1,5 @@
1
1
  import { RetailInventoryMeta, RetailPageMeta } from '../models/RetailPageMeta';
2
- import { RetailSystemPiece, RetailSystemStock } from '../models/RetailSystemInventory';
2
+ import { RetailSystemInventoryMovement, RetailSystemPiece, RetailSystemStock } from '../models/RetailSystemInventory';
3
3
  import { RetailSystemProduct } from '../models/RetailSystemProduct';
4
4
  import { RetailSystemStore } from '../models/RetailSystemStore';
5
5
  import { RetailSystemUser } from '../models/RetailSystemUser';
@@ -23,6 +23,12 @@ export interface GetRetailInventoryResponse {
23
23
  meta: RetailInventoryMeta;
24
24
  }
25
25
 
26
+ /** `data` de `GET /private/retail/inventory/movements`. */
27
+ export interface ListRetailInventoryMovementsResponse {
28
+ items: RetailSystemInventoryMovement[];
29
+ meta: RetailPageMeta;
30
+ }
31
+
26
32
  /** `data` de `GET /private/retail/users`. */
27
33
  export interface ListRetailUsersResponse {
28
34
  items: RetailSystemUser[];
@@ -39,7 +45,10 @@ export interface CancelRetailSaleResponse {
39
45
  saleId: string;
40
46
  }
41
47
 
42
- /** `data` de `GET /private/retail/inventory/live-check`: los identificadores que el retailer lista en la tienda. */
48
+ /**
49
+ * `data` de `GET /private/retail/inventory/live-check`: los identificadores que el retailer lista en la tienda.
50
+ * @deprecated el live-check se va a quitar; el inventario se toma de movimientos y de la lectura diaria.
51
+ */
43
52
  export interface CheckRetailInventoryResponse {
44
53
  listed: string[];
45
54
  generatedAt: string;
@@ -0,0 +1,75 @@
1
+ import { Type } from 'class-transformer';
2
+ import {
3
+ IsArray,
4
+ IsInt,
5
+ IsObject,
6
+ IsOptional,
7
+ IsString,
8
+ Matches,
9
+ Max,
10
+ MaxLength,
11
+ Min,
12
+ Validate,
13
+ ValidateIf,
14
+ ValidateNested,
15
+ } from 'class-validator';
16
+ import {
17
+ RETAIL_ENTITY_ID_PATTERN,
18
+ RETAIL_ERROR_CODE_MAX_LENGTH,
19
+ RETAIL_ID_TEXT_MAX_LENGTH,
20
+ RETAIL_LINE_INDEX_MAX,
21
+ RETAIL_LINE_INDEX_MIN,
22
+ RETAIL_NEXT_TOKEN_MAX_LENGTH,
23
+ RETAIL_EXTERNAL_ID_PATTERN,
24
+ } from '../constants/retailIntegrationLimits';
25
+ import { IsIsoDateTimeWithZone } from '../validators/IsIsoDateTimeWithZone';
26
+ import { isNullOrValidated } from '../validators/payloadValidators';
27
+ import { ReceiveInventoryEventRequest } from './ReceiveInventoryEventRequest';
28
+
29
+ /** `meta` de una página de Zentral; `nextToken` en `null` cierra la lectura y nunca se omite. */
30
+ export class RetailSystemPageMeta {
31
+ @IsInt() @Min(0) count!: number;
32
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_NEXT_TOKEN_MAX_LENGTH) nextToken!: string | null;
33
+ }
34
+
35
+ /** Página de una lista de Zentral; cada consumidor valida sus renglones de `data`. */
36
+ export class RetailSystemPageResponse {
37
+ @IsArray() data!: unknown[];
38
+ @IsObject() @ValidateNested() @Type(() => RetailSystemPageMeta) meta!: RetailSystemPageMeta;
39
+ }
40
+
41
+ /** `data` de una página de `GET /inventory`: piezas y existencias crudas. */
42
+ export class RetailSystemInventoryData {
43
+ @IsArray() pieces!: unknown[];
44
+ @IsArray() stock!: unknown[];
45
+ }
46
+
47
+ /** `meta` de una página de `GET /inventory`; `generatedAt` es igual en toda la lectura. */
48
+ export class RetailSystemInventoryMeta extends RetailSystemPageMeta {
49
+ @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) storeId!: string;
50
+ @Validate(IsIsoDateTimeWithZone) generatedAt!: string;
51
+ }
52
+
53
+ /** Respuesta de `GET /inventory` de Zentral. */
54
+ export class RetailSystemInventoryResponse {
55
+ @IsObject() @ValidateNested() @Type(() => RetailSystemInventoryData) data!: RetailSystemInventoryData;
56
+ @IsObject() @ValidateNested() @Type(() => RetailSystemInventoryMeta) meta!: RetailSystemInventoryMeta;
57
+ }
58
+
59
+ /** `data` de `POST /sales` de Zentral. */
60
+ export class RetailSystemSaleResponse {
61
+ @IsString() @MaxLength(RETAIL_ID_TEXT_MAX_LENGTH) @Matches(RETAIL_EXTERNAL_ID_PATTERN) externalSaleId!: string;
62
+ }
63
+
64
+ /** Cuerpo de error de Zentral; `lineIndex` solo viene en errores de una línea de venta. */
65
+ export class RetailSystemErrorBody {
66
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_ERROR_CODE_MAX_LENGTH) code!: string;
67
+ @IsOptional() @IsString() msg?: string;
68
+ @IsOptional() @IsInt() @Min(RETAIL_LINE_INDEX_MIN) @Max(RETAIL_LINE_INDEX_MAX) lineIndex?: number;
69
+ }
70
+
71
+ /** Respuesta de `GET /inventory/movements`: cada elemento es el mismo sobre del webhook de inventario. */
72
+ export class RetailSystemMovementsResponse {
73
+ @IsArray() @ValidateNested({ each: true }) @Type(() => ReceiveInventoryEventRequest) data!: ReceiveInventoryEventRequest[];
74
+ @IsObject() @ValidateNested() @Type(() => RetailSystemPageMeta) meta!: RetailSystemPageMeta;
75
+ }
@@ -1,5 +1,5 @@
1
- import { IsEmail, IsIn, IsString, Matches, ValidateIf } from 'class-validator';
2
- import { RETAIL_ENTITY_ID_PATTERN, RETAIL_ZONE_ID_PATTERN } from '../constants/retailIntegrationLimits';
1
+ import { IsEmail, IsIn, IsString, Matches, MaxLength, ValidateIf } from 'class-validator';
2
+ import { RETAIL_ENTITY_ID_PATTERN, RETAIL_SHORT_TEXT_MAX_LENGTH, RETAIL_ZONE_ID_PATTERN } from '../constants/retailIntegrationLimits';
3
3
  import { RetailUserAccessStatusEnum, RetailUserRoleEnum } from '../enums';
4
4
  import type { RetailSystemUser, RetailUserAccessStatus, RetailUserRole } from '../models/RetailSystemUser';
5
5
  import { isNullOrValidated } from '../validators/payloadValidators';
@@ -7,9 +7,9 @@ import { isNullOrValidated } from '../validators/payloadValidators';
7
7
  /** Payload de los cuatro eventos de usuario: el usuario completo. */
8
8
  export class RetailUserEventPayload implements RetailSystemUser {
9
9
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) retailUserId!: string;
10
- @IsString() @Matches(/\S/) displayName!: string;
11
- @IsEmail() email!: string;
12
- @ValidateIf(isNullOrValidated) @IsString() phone!: string | null;
10
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) displayName!: string;
11
+ @IsEmail() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) email!: string;
12
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) phone!: string | null;
13
13
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) homeStoreId!: string;
14
14
  @ValidateIf(isNullOrValidated) @IsString() @Matches(RETAIL_ZONE_ID_PATTERN) zoneId!: string | null;
15
15
  @IsIn(Object.values(RetailUserRoleEnum)) role!: RetailUserRole;
@@ -1,7 +1,9 @@
1
1
  import { Type } from 'class-transformer';
2
- import { IsDefined, IsIn, IsNumber, IsString, Length, Matches, Max, Min, ValidateIf, ValidateNested } from 'class-validator';
2
+ import { IsDefined, IsIn, IsNumber, IsObject, IsString, Length, Matches, Max, MaxLength, Min, ValidateIf, ValidateNested } from 'class-validator';
3
3
  import {
4
4
  RETAIL_ENTITY_ID_PATTERN,
5
+ RETAIL_LONG_TEXT_MAX_LENGTH,
6
+ RETAIL_SHORT_TEXT_MAX_LENGTH,
5
7
  RETAIL_STORE_CODE_MAX_LENGTH,
6
8
  RETAIL_ZIP_CODE_MAX_LENGTH,
7
9
  RETAIL_ZIP_CODE_MIN_LENGTH,
@@ -16,15 +18,15 @@ const LNG_MAX = 180;
16
18
 
17
19
  /** Dirección de la tienda; los opcionales llegan como `null`, nunca se omiten. */
18
20
  export class StoreEventAddressPayload implements RetailSystemStoreAddress {
19
- @IsString() @Matches(/\S/) address1!: string;
20
- @ValidateIf(isNullOrValidated) @IsString() externalNumber!: string | null;
21
- @ValidateIf(isNullOrValidated) @IsString() internalNumber!: string | null;
22
- @ValidateIf(isNullOrValidated) @IsString() neighborhood!: string | null;
23
- @IsString() @Matches(/\S/) city!: string;
24
- @IsString() @Matches(/\S/) state!: string;
21
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) address1!: string;
22
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) externalNumber!: string | null;
23
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) internalNumber!: string | null;
24
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) neighborhood!: string | null;
25
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) city!: string;
26
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) state!: string;
25
27
  @IsString() @Length(RETAIL_ZIP_CODE_MIN_LENGTH, RETAIL_ZIP_CODE_MAX_LENGTH) zipCode!: string;
26
- @ValidateIf(isNullOrValidated) @IsString() country!: string | null;
27
- @ValidateIf(isNullOrValidated) @IsString() reference!: string | null;
28
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) country!: string | null;
29
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_LONG_TEXT_MAX_LENGTH) reference!: string | null;
28
30
  }
29
31
 
30
32
  /** Coordenada de la tienda; `null` si no se informa. */
@@ -37,11 +39,11 @@ export class StoreEventLocationPayload implements RetailSystemStoreLocation {
37
39
  export class StoreEventPayload implements RetailSystemStore {
38
40
  @IsString() @Matches(RETAIL_ENTITY_ID_PATTERN) storeId!: string;
39
41
  @IsString() @Length(1, RETAIL_STORE_CODE_MAX_LENGTH) code!: string;
40
- @IsString() @Matches(/\S/) name!: string;
42
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) name!: string;
41
43
  @ValidateIf(isNullOrValidated) @IsString() @Matches(RETAIL_ZONE_ID_PATTERN) zoneId!: string | null;
42
- @IsDefined() @ValidateNested() @Type(() => StoreEventAddressPayload) address!: StoreEventAddressPayload;
43
- @IsDefined() @ValidateNested() @Type(() => StoreEventLocationPayload) location!: StoreEventLocationPayload;
44
- @IsString() @Matches(/\S/) phone!: string;
45
- @ValidateIf(isNullOrValidated) @IsString() schedule!: string | null;
44
+ @IsDefined() @IsObject() @ValidateNested() @Type(() => StoreEventAddressPayload) address!: StoreEventAddressPayload;
45
+ @IsDefined() @IsObject() @ValidateNested() @Type(() => StoreEventLocationPayload) location!: StoreEventLocationPayload;
46
+ @IsString() @Matches(/\S/) @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) phone!: string;
47
+ @ValidateIf(isNullOrValidated) @IsString() @MaxLength(RETAIL_SHORT_TEXT_MAX_LENGTH) schedule!: string | null;
46
48
  @IsIn(Object.values(RetailStoreStatusEnum)) status!: RetailStoreStatus;
47
49
  }
@@ -4,4 +4,5 @@ export enum ReconciliationEntityEnum {
4
4
  USERS = 'USERS',
5
5
  CATALOG = 'CATALOG',
6
6
  INVENTORY = 'INVENTORY',
7
+ MOVEMENTS = 'MOVEMENTS',
7
8
  }
@@ -1,6 +1,8 @@
1
- /** Quién abrió la corrida: el reloj horario, el botón «Actualizar» o la activación del retailer. */
1
+ /** Quién abrió la corrida: el reloj horario o diario, el botón «Actualizar», la activación o una discrepancia. */
2
2
  export enum ReconciliationTriggerEnum {
3
3
  SCHEDULED = 'SCHEDULED',
4
4
  MANUAL = 'MANUAL',
5
5
  ACTIVATION = 'ACTIVATION',
6
+ DISCREPANCY = 'DISCREPANCY',
7
+ DAILY = 'DAILY',
6
8
  }
@@ -2,6 +2,7 @@ export * from './constants/retailIntegrationLimits';
2
2
  export * from './constants/retailIntegrationErrorHttpStatus';
3
3
  export * from './enums';
4
4
  export * from './validators/IsIsoDateTimeWithZone';
5
+ export * from './validators/IsMovementsRangeValid';
5
6
  export * from './dtos/RetailEventEnvelope';
6
7
  export * from './dtos/ReceiveStoreEventRequest';
7
8
  export * from './dtos/ReceiveRetailUserEventRequest';
@@ -21,11 +22,13 @@ export * from './dtos/RetailPagedQuery';
21
22
  export * from './dtos/ListRetailStoresRequest';
22
23
  export * from './dtos/ListRetailCatalogRequest';
23
24
  export * from './dtos/GetRetailInventoryRequest';
25
+ export * from './dtos/ListRetailInventoryMovementsRequest';
24
26
  export * from './dtos/ListRetailUsersRequest';
25
27
  export * from './dtos/RegisterRetailSaleRequest';
26
28
  export * from './dtos/CancelRetailSaleRequest';
27
29
  export * from './dtos/CheckRetailInventoryRequest';
28
30
  export * from './dtos/RetailOutboundResponses';
31
+ export * from './dtos/RetailSystemResponses';
29
32
  export * from './models/RetailPageMeta';
30
33
  export * from './models/RetailSystemStore';
31
34
  export * from './models/RetailSystemProduct';
@@ -4,8 +4,10 @@ export interface RetailPageMeta {
4
4
  nextToken: string | null;
5
5
  }
6
6
 
7
- /** `meta` del inventario de una tienda. */
7
+ /** `meta` del inventario de una tienda; `count` y `nextToken` llegan cuando la lectura pagina. */
8
8
  export interface RetailInventoryMeta {
9
9
  storeId: string;
10
10
  generatedAt: string;
11
+ count?: number;
12
+ nextToken?: string | null;
11
13
  }
@@ -1,4 +1,5 @@
1
- import { RetailPieceReasonEnum, RetailPieceStatusEnum } from '../enums';
1
+ import type { InventoryEventPayload } from '../dtos/InventoryEventPayload';
2
+ import { InventoryEventTypeEnum, RetailPieceReasonEnum, RetailPieceStatusEnum } from '../enums';
2
3
 
3
4
  export type RetailPieceStatus = `${RetailPieceStatusEnum}`;
4
5
  export type RetailPieceReason = `${RetailPieceReasonEnum}`;
@@ -18,3 +19,11 @@ export interface RetailSystemStock {
18
19
  storeId: string;
19
20
  qty: number;
20
21
  }
22
+
23
+ /** Movimiento de inventario de Zentral: el mismo sobre que llega a `POST /webhooks/inventory`. */
24
+ export interface RetailSystemInventoryMovement {
25
+ eventId: string;
26
+ eventType: `${InventoryEventTypeEnum}`;
27
+ occurredAt: string;
28
+ payload: InventoryEventPayload;
29
+ }
@@ -0,0 +1,17 @@
1
+ import { ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator';
2
+ import { RETAIL_MOVEMENTS_RANGE_MAX_MS } from '../constants/retailIntegrationLimits';
3
+
4
+ /** `to`: mayor que `from` y a lo más 7 días después; sin `from` legible lo rechaza su propio validador. */
5
+ @ValidatorConstraint({ name: 'isMovementsRangeValid', async: false })
6
+ export class IsMovementsRangeValid implements ValidatorConstraintInterface {
7
+ validate(value: unknown, args: ValidationArguments): boolean {
8
+ const from = Date.parse(String((args.object as { from?: unknown }).from));
9
+ const to = Date.parse(String(value));
10
+ if (Number.isNaN(from) || Number.isNaN(to)) return true;
11
+ return to > from && to - from <= RETAIL_MOVEMENTS_RANGE_MAX_MS;
12
+ }
13
+
14
+ defaultMessage(): string {
15
+ return 'to debe ser mayor que from y a lo más 7 días después';
16
+ }
17
+ }