@fiado/api-invoker 5.22.0 → 5.24.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.
@@ -1,6 +1,6 @@
1
1
  import { ApiGatewayResponse } from "@fiado/gateway-adapter";
2
- import { IHttpRequest } from "@fiado/http-client";
3
- import { CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
2
+ import type { IHttpRequest } from "@fiado/http-client";
3
+ import { CardApplicationRequest, CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
4
4
  import { CardsByStatusResponse, ICardApi } from "./interfaces/ICardApi.js";
5
5
  import { Provider } from "@fiado/type-kit/bin/provider/index.js";
6
6
  import { InfoSelfVerifiedStatus } from "@fiado/type-kit/bin/identity/index.js";
@@ -19,4 +19,5 @@ export declare class CardApi implements ICardApi {
19
19
  updateIssuanceObservation(directoryId: string, input: CardIssuanceObservationUpdateRequest): Promise<ApiGatewayResponse<void>>;
20
20
  issuance(directoryId: string, USA_InfoSelfVerified: InfoSelfVerifiedStatus): Promise<void>;
21
21
  updateShipping(provider: Provider, shippingId: string, input: UpdateBankAccountCardRequest): Promise<ApiGatewayResponse<void>>;
22
+ cardApplication(directoryId: string, provider: Provider, input: CardApplicationRequest): Promise<void>;
22
23
  }
@@ -70,6 +70,13 @@ let CardApi = class CardApi {
70
70
  const url = `${this.baseUrl}${provider}/shipping/${shippingId}`;
71
71
  return await this.httpRequest.put(url, input);
72
72
  }
73
+ async cardApplication(directoryId, provider, input) {
74
+ const url = `${this.baseUrl}cards/virtual`;
75
+ // `directoryId` y `provider` van DESPUES del spread a proposito: son los
76
+ // dos campos que el endpoint exige, y un `input` que los trajera con
77
+ // otro valor no debe poder pisarlos.
78
+ await this.httpRequest.post(url, { ...input, directoryId, provider });
79
+ }
73
80
  };
74
81
  CardApi = __decorate([
75
82
  injectable(),
@@ -1,5 +1,5 @@
1
1
  import { ApiGatewayResponse } from "@fiado/gateway-adapter";
2
- import { CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
2
+ import { CardApplicationRequest, CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
3
3
  import { InfoSelfVerifiedStatus } from "@fiado/type-kit/bin/identity/index.js";
4
4
  import { Provider } from "@fiado/type-kit/bin/provider/index.js";
5
5
  export interface CardsByStatusResponse {
@@ -17,4 +17,27 @@ export interface ICardApi {
17
17
  updateIssuanceObservation(directoryId: string, input: CardIssuanceObservationUpdateRequest): Promise<ApiGatewayResponse<void>>;
18
18
  issuance(directoryId: string, USA_InfoSelfVerified: InfoSelfVerifiedStatus): Promise<void>;
19
19
  updateShipping(provider: Provider, shippingId: string, input: UpdateBankAccountCardRequest): Promise<ApiGatewayResponse<void>>;
20
+ /**
21
+ * Emite la tarjeta virtual de un directorio y la registra en
22
+ * `fiado-card-business`, invocable SERVICIO-A-SERVICIO.
23
+ *
24
+ * Hasta esta version esta interfaz declaraba diez metodos y **ninguno
25
+ * creaba tarjetas**: la creacion existia solo en tres rutas humanas
26
+ * (`publicCardApplication`, `agentsCardApplication`,
27
+ * `backofficeCardApplication`), detras de autorizacion de usuario o de
28
+ * agente. Eso dejaba a `account-issuance-business` —que crea la cuenta de
29
+ * Pomelo— sin forma de completarla con su tarjeta.
30
+ *
31
+ * `directoryId` y `provider` viajan en el BODY, no en la ruta: el endpoint
32
+ * es de servicio y no hay token del que sacar el directorio, a diferencia
33
+ * de `publicCardApplication`.
34
+ *
35
+ * **Es idempotente del lado del servidor**: si el directorio ya tiene una
36
+ * tarjeta virtual no cancelada para ese proveedor, no se llama al proveedor
37
+ * y responde 200 igual. El llamador puede reintentar sin duplicar.
38
+ *
39
+ * Devuelve `void`: la ruta no responde la tarjeta creada, asi que el exito
40
+ * es "no lanzo". Un fallo llega como error HTTP.
41
+ */
42
+ cardApplication(directoryId: string, provider: Provider, input: CardApplicationRequest): Promise<void>;
20
43
  }
@@ -38,7 +38,18 @@ let ApiInvokerPermissionResolver = class ApiInvokerPermissionResolver {
38
38
  }
39
39
  async resolve(input) {
40
40
  const cacheable = input.roleAssignmentsFromToken === undefined;
41
- const key = `${input.issuer ?? ""}#${input.cognitoSub}`;
41
+ // DEC-RBAC-124: el tipo de principal entra en la cache key. NO es defensa contra colisión
42
+ // —los subs de integración llevan prefijo `INT#` y no chocan con un UUID de Cognito— sino
43
+ // para que el canal quede explícito y un cambio futuro de formato no cause aliasing
44
+ // silencioso entre un usuario y una integración.
45
+ //
46
+ // Se lee con cast en vez de tiparlo desde `ResolveInput` A PROPÓSITO: `principalType` recién
47
+ // existe en `@fiado/gateway-adapter` 4.x, y esta lib la consume toda la flota. Atarla al
48
+ // prerelease obligaría a todos a subir de major. Así compila igual contra 3.x y 4.x, y el
49
+ // reenvío al endpoint privado funciona en runtime porque `resolvePermissions(input)` manda
50
+ // el objeto entero sin filtrar campos.
51
+ const principalType = input.principalType ?? "USER";
52
+ const key = `${principalType}#${input.issuer ?? ""}#${input.cognitoSub}`;
42
53
  if (cacheable) {
43
54
  const hit = this._cache.get(key);
44
55
  if (hit)
@@ -1,9 +1,9 @@
1
1
  import type { IHttpRequest } from "@fiado/http-client";
2
2
  import { StandardResponse } from "@fiado/gateway-adapter";
3
3
  import { RetailerValidationDto, StoreValidationDto, RetailUserValidationDto, CollectorValidationDto, PrivateSellerListResponse, RetailUserStatusEnum } from "@fiado/type-kit/bin/retailOrg/index.js";
4
- import { IRetailOrgBusinessApi } from "./interfaces/IRetailOrgBusinessApi.js";
4
+ import { IRetailOrgBusinessApi, StoreRosterStatusFilter } from "./interfaces/IRetailOrgBusinessApi.js";
5
5
  /**
6
- * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 6
6
+ * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 8
7
7
  * endpoints privados de validación. Contrato y semántica completos → `IRetailOrgBusinessApi`.
8
8
  *
9
9
  * Los paths y el `?tenantId=` fueron verificados contra dev (200 + negativos 400/404) el
@@ -23,6 +23,13 @@ export default class RetailOrgBusinessApi implements IRetailOrgBusinessApi {
23
23
  getRetailer(retailerId: string, tenantId: string): Promise<StandardResponse<RetailerValidationDto>>;
24
24
  getRetailerByStore(storeId: string, tenantId: string): Promise<StandardResponse<RetailerValidationDto>>;
25
25
  getStore(retailerId: string, storeId: string, tenantId: string): Promise<StandardResponse<StoreValidationDto>>;
26
+ /**
27
+ * SureKeep F3 — el padrón de tiendas que le faltaba al tablero de cohorte de «Análisis de
28
+ * ventas»: sin él, la tienda que lleva 60 días sin vender no existe para el tablero. Contrato y
29
+ * semántica completos (incluido por qué acá `status` SÍ tiene default y en `listSellers` no)
30
+ * → `IRetailOrgBusinessApi.listStoresByRetailer`.
31
+ */
32
+ listStoresByRetailer(retailerId: string, tenantId: string, status?: StoreRosterStatusFilter): Promise<StandardResponse<StoreValidationDto[]>>;
26
33
  getStoreUsers(retailerId: string, storeId: string, tenantId: string): Promise<StandardResponse<RetailUserValidationDto[]>>;
27
34
  getUserByCognitoSub(cognitoSub: string, tenantId: string): Promise<StandardResponse<RetailUserValidationDto>>;
28
35
  listActiveCollectorsByRetailer(retailerId: string, tenantId: string): Promise<StandardResponse<CollectorValidationDto[]>>;
@@ -12,7 +12,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  import { inject, injectable } from "inversify";
14
14
  /**
15
- * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 6
15
+ * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 8
16
16
  * endpoints privados de validación. Contrato y semántica completos → `IRetailOrgBusinessApi`.
17
17
  *
18
18
  * Los paths y el `?tenantId=` fueron verificados contra dev (200 + negativos 400/404) el
@@ -43,6 +43,21 @@ let RetailOrgBusinessApi = class RetailOrgBusinessApi {
43
43
  const url = `${this.baseUrl}/private/retailers/${encodeURIComponent(retailerId)}/stores/${encodeURIComponent(storeId)}?tenantId=${encodeURIComponent(tenantId)}`;
44
44
  return await this.httpRequest.get(url);
45
45
  }
46
+ /**
47
+ * SureKeep F3 — el padrón de tiendas que le faltaba al tablero de cohorte de «Análisis de
48
+ * ventas»: sin él, la tienda que lleva 60 días sin vender no existe para el tablero. Contrato y
49
+ * semántica completos (incluido por qué acá `status` SÍ tiene default y en `listSellers` no)
50
+ * → `IRetailOrgBusinessApi.listStoresByRetailer`.
51
+ */
52
+ async listStoresByRetailer(retailerId, tenantId, status) {
53
+ // `URLSearchParams` en vez de concatenar: `status` es opcional y armar el `&status=` a mano
54
+ // deja un `&status=undefined` en cuanto alguien se distrae.
55
+ const query = new URLSearchParams({ tenantId });
56
+ if (status)
57
+ query.set("status", status);
58
+ const path = `/private/retailers/${encodeURIComponent(retailerId)}/stores`;
59
+ return await this.httpRequest.get(`${this.baseUrl}${path}?${query.toString()}`);
60
+ }
46
61
  async getStoreUsers(retailerId, storeId, tenantId) {
47
62
  const url = `${this.baseUrl}/private/retailers/${encodeURIComponent(retailerId)}/users/by-store/${encodeURIComponent(storeId)}?tenantId=${encodeURIComponent(tenantId)}`;
48
63
  return await this.httpRequest.get(url);
@@ -1,8 +1,14 @@
1
1
  import { StandardResponse } from "@fiado/gateway-adapter";
2
- import { RetailerValidationDto, StoreValidationDto, RetailUserValidationDto, CollectorValidationDto, PrivateSellerListResponse, RetailUserStatusEnum } from "@fiado/type-kit/bin/retailOrg/index.js";
2
+ import { RetailerValidationDto, StoreValidationDto, RetailUserValidationDto, CollectorValidationDto, PrivateSellerListResponse, RetailUserStatusEnum, StoreStatusEnum } from "@fiado/type-kit/bin/retailOrg/index.js";
3
+ /**
4
+ * Filtro de `listStoresByRetailer`. `"ALL"` no es un estado de tienda: es la ausencia de filtro, y
5
+ * va en el MISMO parámetro que los estados porque los dos hablan del mismo eje — un segundo flag
6
+ * tipo `includeInactive` admitiría combinaciones sin sentido que alguien tendría que resolver.
7
+ */
8
+ export type StoreRosterStatusFilter = StoreStatusEnum | "ALL";
3
9
  /**
4
10
  * Contrato del publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1)
5
- * para sus 6 endpoints privados de VALIDACIÓN (service-to-service, VPC-only).
11
+ * para sus 8 endpoints privados de VALIDACIÓN (service-to-service, VPC-only).
6
12
  *
7
13
  * Consumidores previstos (regla R5 — cruces entre lambdas son SOLO lecturas de validación;
8
14
  * ningún lambda escribe en tabla ajena):
@@ -12,7 +18,7 @@ import { RetailerValidationDto, StoreValidationDto, RetailUserValidationDto, Col
12
18
  * - `loan-collector-assignment-business` → lista cobradores ACTIVE del retailer para asignar cartera
13
19
  *
14
20
  * ─────────────────────────────────────────────────────────────────────────────
15
- * `tenantId` es OBLIGATORIO en los 4 métodos — no es un detalle de firma.
21
+ * `tenantId` es OBLIGATORIO en TODOS los métodos — no es un detalle de firma.
16
22
  * ─────────────────────────────────────────────────────────────────────────────
17
23
  * Los privados NO llevan token de usuario (`Feature.ANONIMUS`; la protección es la red VPC),
18
24
  * así que el lambda destino no tiene `AuthContext.issuer` y NO puede inferir el silo. El caller
@@ -75,6 +81,42 @@ export interface IRetailOrgBusinessApi {
75
81
  * @throws si la tienda no existe o es de OTRO retailer (`404 STORE_NOT_FOUND` — verificado en dev).
76
82
  */
77
83
  getStore(retailerId: string, storeId: string, tenantId: string): Promise<StandardResponse<StoreValidationDto>>;
84
+ /**
85
+ * `GET /private/retailers/{retailerId}/stores` (SureKeep F3) — el **padrón de tiendas** de una
86
+ * cadena.
87
+ *
88
+ * ── 🔴 Por qué este método existe ────────────────────────────────────────────────────────────
89
+ * Lo consume el tablero de cohorte de «Análisis de ventas» (`retail-wizard-business`), que arma
90
+ * la lista de tiendas a partir de las VENTAS que leyó. Con eso, **la tienda que lleva 60 días sin
91
+ * vender no existe para el tablero** — y es exactamente la que hay que mirar. Este es el único
92
+ * endpoint que sirve el padrón completo: `getStore` resuelve UNA tienda y `publicListStores` va
93
+ * detrás del `W4Authorizer` (token de usuario, no invocable entre lambdas).
94
+ *
95
+ * NO se deriva de `listSellers`: ese endpoint escribe en `SharedPiiAccessLog_GT` y esa bitácora
96
+ * es la única traza de una exportación masiva de datos de empleados. Pedirle ids de tienda la
97
+ * llenaría de filas con una intención falsa.
98
+ *
99
+ * Devuelve el mismo `StoreValidationDto` que `getStore` — `storeId`, `retailerId`, `zoneId`,
100
+ * `zoneName`, `code`, `name`, `status` — con el `status` EFECTIVO (cascada del retailer padre ya
101
+ * aplicada). El `zoneName` sale de la tabla de zonas del propio retail-org: el consumidor no
102
+ * puede resolver un `zoneId` de otro dominio, y hoy lo saca de un snapshot congelado en la venta,
103
+ * así que una tienda re-zonificada muestra la zona vieja.
104
+ *
105
+ * ── ⚠️ `status` SÍ tiene default, al revés que `listSellers` ─────────────────────────────────
106
+ * Omitirlo devuelve **solo las `ACTIVE`**. La asimetría es deliberada: allá un vendedor dado de
107
+ * baja el día 20 sí tiene comisión por lo que vendió hasta el 19, así que filtrar lo perdería.
108
+ * Acá el consumidor cruza este padrón con ventas que YA leyó, o sea que la tienda cerrada que
109
+ * vendió dentro de la ventana entra igual por el lado de las ventas — filtrar no puede perder
110
+ * una tienda que vendió, solo evita agregar un cero permanente a un tablero cuyo trabajo es que
111
+ * las filas en cero llamen la atención. Pasa `"ALL"` para el padrón completo.
112
+ *
113
+ * @throws `404 RETAILER_NOT_FOUND` si el retailer no existe en ese silo. Un `[]` no distinguiría
114
+ * "esta cadena no tiene tiendas" de "preguntaste por una cadena que no existe", y lo segundo
115
+ * dejaría el tablero mudo sin que nadie sospeche del request.
116
+ * @throws `400 STORE_STATUS_FILTER_INVALID` si `status` no es del catálogo · `400 UNKNOWN_TENANT`
117
+ * sin `tenantId`.
118
+ */
119
+ listStoresByRetailer(retailerId: string, tenantId: string, status?: StoreRosterStatusFilter): Promise<StandardResponse<StoreValidationDto[]>>;
78
120
  /**
79
121
  * GET /private/retailers/{retailerId}/users/by-store/{storeId} — usuarios retail cuya tienda base
80
122
  * (`homeStoreId`) es esa. Devuelve `[]` (no 404) si la tienda no tiene usuarios asignados.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/api-invoker",
3
- "version": "5.22.0",
3
+ "version": "5.24.0",
4
4
  "description": "Sirve como un puente entre diferentes funciones lambda, facilitando la comunicación entre ellas a través de invocaciones http",
5
5
  "type": "module",
6
6
  "main": "bin/index.js",
@@ -1,6 +1,11 @@
1
1
  import { ApiGatewayResponse } from "@fiado/gateway-adapter";
2
- import { IHttpRequest } from "@fiado/http-client";
3
- import { CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
2
+ // `import type`: `IHttpRequest` es una interfaz, y el import de VALOR rompe
3
+ // bajo ESM en jest ("does not provide an export named 'IHttpRequest'"), que es
4
+ // lo que impedia testear esta clase. Mismo estilo que los demas Api del repo
5
+ // que si tienen suite (p. ej. `RetailCardsBusinessApi`). No cambia el runtime:
6
+ // tsc borra las dos formas por igual.
7
+ import type { IHttpRequest } from "@fiado/http-client";
8
+ import { CardApplicationRequest, CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
4
9
  import { inject, injectable } from "inversify";
5
10
  import { CardsByStatusResponse, ICardApi } from "./interfaces/ICardApi.js";
6
11
  import { Provider } from "@fiado/type-kit/bin/provider/index.js";
@@ -96,6 +101,15 @@ export class CardApi implements ICardApi {
96
101
  return await this.httpRequest.put(url, input);
97
102
  }
98
103
 
104
+ async cardApplication(directoryId: string, provider: Provider, input: CardApplicationRequest): Promise<void> {
105
+
106
+ const url = `${this.baseUrl}cards/virtual`;
107
+ // `directoryId` y `provider` van DESPUES del spread a proposito: son los
108
+ // dos campos que el endpoint exige, y un `input` que los trajera con
109
+ // otro valor no debe poder pisarlos.
110
+ await this.httpRequest.post(url, { ...input, directoryId, provider });
111
+ }
112
+
99
113
  }
100
114
 
101
115
 
@@ -1,5 +1,5 @@
1
1
  import { ApiGatewayResponse } from "@fiado/gateway-adapter";
2
- import { CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
2
+ import { CardApplicationRequest, CardIssuanceObservationUpdateRequest, CardResponse, CardUpdateIssuanceRequest, Status, UpdateBankAccountCardRequest } from "@fiado/type-kit/bin/card/index.js";
3
3
  import { InfoSelfVerifiedStatus } from "@fiado/type-kit/bin/identity/index.js";
4
4
  import { Provider } from "@fiado/type-kit/bin/provider/index.js";
5
5
 
@@ -30,4 +30,28 @@ export interface ICardApi {
30
30
 
31
31
  updateShipping(provider: Provider, shippingId: string, input: UpdateBankAccountCardRequest): Promise<ApiGatewayResponse<void>>;
32
32
 
33
+ /**
34
+ * Emite la tarjeta virtual de un directorio y la registra en
35
+ * `fiado-card-business`, invocable SERVICIO-A-SERVICIO.
36
+ *
37
+ * Hasta esta version esta interfaz declaraba diez metodos y **ninguno
38
+ * creaba tarjetas**: la creacion existia solo en tres rutas humanas
39
+ * (`publicCardApplication`, `agentsCardApplication`,
40
+ * `backofficeCardApplication`), detras de autorizacion de usuario o de
41
+ * agente. Eso dejaba a `account-issuance-business` —que crea la cuenta de
42
+ * Pomelo— sin forma de completarla con su tarjeta.
43
+ *
44
+ * `directoryId` y `provider` viajan en el BODY, no en la ruta: el endpoint
45
+ * es de servicio y no hay token del que sacar el directorio, a diferencia
46
+ * de `publicCardApplication`.
47
+ *
48
+ * **Es idempotente del lado del servidor**: si el directorio ya tiene una
49
+ * tarjeta virtual no cancelada para ese proveedor, no se llama al proveedor
50
+ * y responde 200 igual. El llamador puede reintentar sin duplicar.
51
+ *
52
+ * Devuelve `void`: la ruta no responde la tarjeta creada, asi que el exito
53
+ * es "no lanzo". Un fallo llega como error HTTP.
54
+ */
55
+ cardApplication(directoryId: string, provider: Provider, input: CardApplicationRequest): Promise<void>;
56
+
33
57
  }
@@ -31,7 +31,18 @@ export class ApiInvokerPermissionResolver {
31
31
  }
32
32
  async resolve(input: ResolveInput): Promise<ResolvedPermissions> {
33
33
  const cacheable = input.roleAssignmentsFromToken === undefined;
34
- const key = `${input.issuer ?? ""}#${input.cognitoSub}`;
34
+ // DEC-RBAC-124: el tipo de principal entra en la cache key. NO es defensa contra colisión
35
+ // —los subs de integración llevan prefijo `INT#` y no chocan con un UUID de Cognito— sino
36
+ // para que el canal quede explícito y un cambio futuro de formato no cause aliasing
37
+ // silencioso entre un usuario y una integración.
38
+ //
39
+ // Se lee con cast en vez de tiparlo desde `ResolveInput` A PROPÓSITO: `principalType` recién
40
+ // existe en `@fiado/gateway-adapter` 4.x, y esta lib la consume toda la flota. Atarla al
41
+ // prerelease obligaría a todos a subir de major. Así compila igual contra 3.x y 4.x, y el
42
+ // reenvío al endpoint privado funciona en runtime porque `resolvePermissions(input)` manda
43
+ // el objeto entero sin filtrar campos.
44
+ const principalType = (input as { principalType?: string }).principalType ?? "USER";
45
+ const key = `${principalType}#${input.issuer ?? ""}#${input.cognitoSub}`;
35
46
  if (cacheable) {
36
47
  const hit = this._cache.get(key);
37
48
  if (hit) return hit;
@@ -9,10 +9,13 @@ import {
9
9
  PrivateSellerListResponse,
10
10
  RetailUserStatusEnum,
11
11
  } from "@fiado/type-kit/bin/retailOrg/index.js";
12
- import { IRetailOrgBusinessApi } from "./interfaces/IRetailOrgBusinessApi.js";
12
+ import {
13
+ IRetailOrgBusinessApi,
14
+ StoreRosterStatusFilter,
15
+ } from "./interfaces/IRetailOrgBusinessApi.js";
13
16
 
14
17
  /**
15
- * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 6
18
+ * Publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1) para sus 8
16
19
  * endpoints privados de validación. Contrato y semántica completos → `IRetailOrgBusinessApi`.
17
20
  *
18
21
  * Los paths y el `?tenantId=` fueron verificados contra dev (200 + negativos 400/404) el
@@ -56,6 +59,25 @@ export default class RetailOrgBusinessApi implements IRetailOrgBusinessApi {
56
59
  return await this.httpRequest.get(url);
57
60
  }
58
61
 
62
+ /**
63
+ * SureKeep F3 — el padrón de tiendas que le faltaba al tablero de cohorte de «Análisis de
64
+ * ventas»: sin él, la tienda que lleva 60 días sin vender no existe para el tablero. Contrato y
65
+ * semántica completos (incluido por qué acá `status` SÍ tiene default y en `listSellers` no)
66
+ * → `IRetailOrgBusinessApi.listStoresByRetailer`.
67
+ */
68
+ async listStoresByRetailer(
69
+ retailerId: string,
70
+ tenantId: string,
71
+ status?: StoreRosterStatusFilter,
72
+ ): Promise<StandardResponse<StoreValidationDto[]>> {
73
+ // `URLSearchParams` en vez de concatenar: `status` es opcional y armar el `&status=` a mano
74
+ // deja un `&status=undefined` en cuanto alguien se distrae.
75
+ const query = new URLSearchParams({ tenantId });
76
+ if (status) query.set("status", status);
77
+ const path = `/private/retailers/${encodeURIComponent(retailerId)}/stores`;
78
+ return await this.httpRequest.get(`${this.baseUrl}${path}?${query.toString()}`);
79
+ }
80
+
59
81
  async getStoreUsers(
60
82
  retailerId: string,
61
83
  storeId: string,
@@ -6,11 +6,19 @@ import {
6
6
  CollectorValidationDto,
7
7
  PrivateSellerListResponse,
8
8
  RetailUserStatusEnum,
9
+ StoreStatusEnum,
9
10
  } from "@fiado/type-kit/bin/retailOrg/index.js";
10
11
 
12
+ /**
13
+ * Filtro de `listStoresByRetailer`. `"ALL"` no es un estado de tienda: es la ausencia de filtro, y
14
+ * va en el MISMO parámetro que los estados porque los dos hablan del mismo eje — un segundo flag
15
+ * tipo `includeInactive` admitiría combinaciones sin sentido que alguien tendría que resolver.
16
+ */
17
+ export type StoreRosterStatusFilter = StoreStatusEnum | "ALL";
18
+
11
19
  /**
12
20
  * Contrato del publisher HTTP del lambda `retail-org-business` (componente 04 SureKeep Fase 1)
13
- * para sus 6 endpoints privados de VALIDACIÓN (service-to-service, VPC-only).
21
+ * para sus 8 endpoints privados de VALIDACIÓN (service-to-service, VPC-only).
14
22
  *
15
23
  * Consumidores previstos (regla R5 — cruces entre lambdas son SOLO lecturas de validación;
16
24
  * ningún lambda escribe en tabla ajena):
@@ -20,7 +28,7 @@ import {
20
28
  * - `loan-collector-assignment-business` → lista cobradores ACTIVE del retailer para asignar cartera
21
29
  *
22
30
  * ─────────────────────────────────────────────────────────────────────────────
23
- * `tenantId` es OBLIGATORIO en los 4 métodos — no es un detalle de firma.
31
+ * `tenantId` es OBLIGATORIO en TODOS los métodos — no es un detalle de firma.
24
32
  * ─────────────────────────────────────────────────────────────────────────────
25
33
  * Los privados NO llevan token de usuario (`Feature.ANONIMUS`; la protección es la red VPC),
26
34
  * así que el lambda destino no tiene `AuthContext.issuer` y NO puede inferir el silo. El caller
@@ -96,6 +104,47 @@ export interface IRetailOrgBusinessApi {
96
104
  tenantId: string,
97
105
  ): Promise<StandardResponse<StoreValidationDto>>;
98
106
 
107
+ /**
108
+ * `GET /private/retailers/{retailerId}/stores` (SureKeep F3) — el **padrón de tiendas** de una
109
+ * cadena.
110
+ *
111
+ * ── 🔴 Por qué este método existe ────────────────────────────────────────────────────────────
112
+ * Lo consume el tablero de cohorte de «Análisis de ventas» (`retail-wizard-business`), que arma
113
+ * la lista de tiendas a partir de las VENTAS que leyó. Con eso, **la tienda que lleva 60 días sin
114
+ * vender no existe para el tablero** — y es exactamente la que hay que mirar. Este es el único
115
+ * endpoint que sirve el padrón completo: `getStore` resuelve UNA tienda y `publicListStores` va
116
+ * detrás del `W4Authorizer` (token de usuario, no invocable entre lambdas).
117
+ *
118
+ * NO se deriva de `listSellers`: ese endpoint escribe en `SharedPiiAccessLog_GT` y esa bitácora
119
+ * es la única traza de una exportación masiva de datos de empleados. Pedirle ids de tienda la
120
+ * llenaría de filas con una intención falsa.
121
+ *
122
+ * Devuelve el mismo `StoreValidationDto` que `getStore` — `storeId`, `retailerId`, `zoneId`,
123
+ * `zoneName`, `code`, `name`, `status` — con el `status` EFECTIVO (cascada del retailer padre ya
124
+ * aplicada). El `zoneName` sale de la tabla de zonas del propio retail-org: el consumidor no
125
+ * puede resolver un `zoneId` de otro dominio, y hoy lo saca de un snapshot congelado en la venta,
126
+ * así que una tienda re-zonificada muestra la zona vieja.
127
+ *
128
+ * ── ⚠️ `status` SÍ tiene default, al revés que `listSellers` ─────────────────────────────────
129
+ * Omitirlo devuelve **solo las `ACTIVE`**. La asimetría es deliberada: allá un vendedor dado de
130
+ * baja el día 20 sí tiene comisión por lo que vendió hasta el 19, así que filtrar lo perdería.
131
+ * Acá el consumidor cruza este padrón con ventas que YA leyó, o sea que la tienda cerrada que
132
+ * vendió dentro de la ventana entra igual por el lado de las ventas — filtrar no puede perder
133
+ * una tienda que vendió, solo evita agregar un cero permanente a un tablero cuyo trabajo es que
134
+ * las filas en cero llamen la atención. Pasa `"ALL"` para el padrón completo.
135
+ *
136
+ * @throws `404 RETAILER_NOT_FOUND` si el retailer no existe en ese silo. Un `[]` no distinguiría
137
+ * "esta cadena no tiene tiendas" de "preguntaste por una cadena que no existe", y lo segundo
138
+ * dejaría el tablero mudo sin que nadie sospeche del request.
139
+ * @throws `400 STORE_STATUS_FILTER_INVALID` si `status` no es del catálogo · `400 UNKNOWN_TENANT`
140
+ * sin `tenantId`.
141
+ */
142
+ listStoresByRetailer(
143
+ retailerId: string,
144
+ tenantId: string,
145
+ status?: StoreRosterStatusFilter,
146
+ ): Promise<StandardResponse<StoreValidationDto[]>>;
147
+
99
148
  /**
100
149
  * GET /private/retailers/{retailerId}/users/by-store/{storeId} — usuarios retail cuya tienda base
101
150
  * (`homeStoreId`) es esa. Devuelve `[]` (no 404) si la tienda no tiene usuarios asignados.