@fiado/api-invoker 5.115.0 → 5.116.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 (39) hide show
  1. package/bin/container.config.js +12 -0
  2. package/bin/index.d.ts +2 -0
  3. package/bin/index.js +2 -0
  4. package/bin/rewards-business/api/RewardsBusinessApi.d.ts +12 -0
  5. package/bin/rewards-business/api/RewardsBusinessApi.js +38 -0
  6. package/bin/rewards-business/api/interfaces/IRewardsBusinessApi.d.ts +33 -0
  7. package/bin/rewards-business/api/interfaces/IRewardsBusinessApi.js +1 -0
  8. package/bin/rewards-business/grantIdFor.d.ts +17 -0
  9. package/bin/rewards-business/grantIdFor.js +30 -0
  10. package/bin/rewards-business/index.d.ts +5 -0
  11. package/bin/rewards-business/index.js +5 -0
  12. package/bin/rewards-business/queue/RewardsGrantPublisher.d.ts +7 -0
  13. package/bin/rewards-business/queue/RewardsGrantPublisher.js +83 -0
  14. package/bin/rewards-business/queue/interfaces/IRewardsGrantPublisher.d.ts +32 -0
  15. package/bin/rewards-business/queue/interfaces/IRewardsGrantPublisher.js +1 -0
  16. package/bin/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.d.ts +12 -0
  17. package/bin/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.js +37 -0
  18. package/bin/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.d.ts +34 -0
  19. package/bin/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.js +1 -0
  20. package/bin/rewards-eligibility-business/index.d.ts +4 -0
  21. package/bin/rewards-eligibility-business/index.js +4 -0
  22. package/bin/rewards-eligibility-business/queue/RewardsEligibilityPublisher.d.ts +6 -0
  23. package/bin/rewards-eligibility-business/queue/RewardsEligibilityPublisher.js +34 -0
  24. package/bin/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.d.ts +15 -0
  25. package/bin/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.js +1 -0
  26. package/package.json +2 -2
  27. package/src/container.config.ts +14 -0
  28. package/src/index.ts +2 -0
  29. package/src/rewards-business/api/RewardsBusinessApi.ts +27 -0
  30. package/src/rewards-business/api/interfaces/IRewardsBusinessApi.ts +35 -0
  31. package/src/rewards-business/grantIdFor.ts +36 -0
  32. package/src/rewards-business/index.ts +5 -0
  33. package/src/rewards-business/queue/RewardsGrantPublisher.ts +80 -0
  34. package/src/rewards-business/queue/interfaces/IRewardsGrantPublisher.ts +35 -0
  35. package/src/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.ts +26 -0
  36. package/src/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.ts +36 -0
  37. package/src/rewards-eligibility-business/index.ts +4 -0
  38. package/src/rewards-eligibility-business/queue/RewardsEligibilityPublisher.ts +28 -0
  39. package/src/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.ts +16 -0
@@ -139,6 +139,10 @@ import { KycVerificationsBusinessApi } from "./kyc/index.js";
139
139
  // Webhook business — publisher de la cola compartida de entrega de eventos a suscriptores externos
140
140
  import { WebhookBusinessPublisher } from "./webhook-business/index.js";
141
141
  import { CentralPaymentsWebhookApi } from "./central-payments-webhook/index.js";
142
+ // Rewards — publisher de pedidos de entrega + cliente de lecturas privadas (rewards-business) y
143
+ // publisher de GRANT_FAILED + cliente del validador (rewards-eligibility-business)
144
+ import { RewardsGrantPublisher, RewardsBusinessApi } from "./rewards-business/index.js";
145
+ import { RewardsEligibilityPublisher, RewardsEligibilityBusinessApi } from "./rewards-eligibility-business/index.js";
142
146
  import CollectionEngineApi from "./collection-engine/api/CollectionEngineApi.js";
143
147
  import DomainCallbackApi from "./domain-callback/api/DomainCallbackApi.js";
144
148
  export const apiInvokerBindings = new ContainerModule(({ bind }) => {
@@ -298,6 +302,14 @@ export const apiInvokerBindings = new ContainerModule(({ bind }) => {
298
302
  // El productor necesita WEBHOOK_BUSINESS_QUEUE_URL y sqs:SendMessage sobre la cola.
299
303
  bind("IWebhookBusinessPublisher").to(WebhookBusinessPublisher);
300
304
  bind("ICentralPaymentsWebhookApi").to(CentralPaymentsWebhookApi);
305
+ // Rewards — cola compartida RewardsGrantQueue (pedidos de entrega) y lecturas privadas de rewards-business.
306
+ // El productor necesita REWARDS_GRANT_QUEUE_URL y sqs:SendMessage sobre la cola.
307
+ bind("IRewardsGrantPublisher").to(RewardsGrantPublisher);
308
+ bind("IRewardsBusinessApi").to(RewardsBusinessApi);
309
+ // Rewards eligibility — GRANT_FAILED hacia la cola del validador y cliente de POST /events.
310
+ // El productor necesita REWARDS_ELIGIBILITY_QUEUE_URL y sqs:SendMessage sobre la cola.
311
+ bind("IRewardsEligibilityPublisher").to(RewardsEligibilityPublisher);
312
+ bind("IRewardsEligibilityBusinessApi").to(RewardsEligibilityBusinessApi);
301
313
  // Collection engine — motor de cobro central multitenant (collect/deregister)
302
314
  bind("ICollectionEngineApi").to(CollectionEngineApi);
303
315
  // Domain callback — el motor notifica al dominio dueño del cargo (applyCollectionResult)
package/bin/index.d.ts CHANGED
@@ -122,3 +122,5 @@ export * from "./domain-callback/index.js";
122
122
  export * from "./datacultrConnector/index.js";
123
123
  export * from "./webhook-business/index.js";
124
124
  export * from "./trustonicConnector/index.js";
125
+ export * from "./rewards-business/index.js";
126
+ export * from "./rewards-eligibility-business/index.js";
package/bin/index.js CHANGED
@@ -122,3 +122,5 @@ export * from "./domain-callback/index.js";
122
122
  export * from "./datacultrConnector/index.js";
123
123
  export * from "./webhook-business/index.js";
124
124
  export * from "./trustonicConnector/index.js";
125
+ export * from "./rewards-business/index.js";
126
+ export * from "./rewards-eligibility-business/index.js";
@@ -0,0 +1,12 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { IHttpRequest } from "@fiado/http-client";
3
+ import type { GrantResponse, PackageContractResponse } from "@fiado/type-kit/bin/rewards/index.js";
4
+ import type { IRewardsBusinessApi } from "./interfaces/IRewardsBusinessApi.js";
5
+ /** Contrato y semántica completos → `IRewardsBusinessApi` (`StandardResponse` → leer `result.data`). */
6
+ export declare class RewardsBusinessApi implements IRewardsBusinessApi {
7
+ private httpRequest;
8
+ private readonly baseUrl;
9
+ constructor(httpRequest: IHttpRequest);
10
+ getPackage(packageId: string): Promise<StandardResponse<PackageContractResponse>>;
11
+ getGrant(grantId: string): Promise<StandardResponse<GrantResponse>>;
12
+ }
@@ -0,0 +1,38 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { inject, injectable } from "inversify";
14
+ /** Contrato y semántica completos → `IRewardsBusinessApi` (`StandardResponse` → leer `result.data`). */
15
+ let RewardsBusinessApi = class RewardsBusinessApi {
16
+ httpRequest;
17
+ // El buildspec publica la URL en SSM con "/" final (`.../v1/`): se normaliza para no armar "//packages".
18
+ baseUrl = (process.env.REWARDS_BUSINESS_LAMBDA_URL || "").replace(/\/+$/, "");
19
+ constructor(httpRequest) {
20
+ this.httpRequest = httpRequest;
21
+ }
22
+ async getPackage(packageId) {
23
+ // PROVISIONAL: sin prefijo `/private` (la API que resuelve el SSM es la PRIVADA de rewards-business).
24
+ // La ruta base exacta (con o sin `/private`) la fija el plan de rewards-business; si cambia, ajustar aquí.
25
+ const url = `${this.baseUrl}/packages/${encodeURIComponent(packageId)}`;
26
+ return await this.httpRequest.get(url);
27
+ }
28
+ async getGrant(grantId) {
29
+ const url = `${this.baseUrl}/grants/${encodeURIComponent(grantId)}`;
30
+ return await this.httpRequest.get(url);
31
+ }
32
+ };
33
+ RewardsBusinessApi = __decorate([
34
+ injectable(),
35
+ __param(0, inject("IHttpRequest")),
36
+ __metadata("design:paramtypes", [Object])
37
+ ], RewardsBusinessApi);
38
+ export { RewardsBusinessApi };
@@ -0,0 +1,33 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { GrantResponse, PackageContractResponse } from "@fiado/type-kit/bin/rewards/index.js";
3
+ /**
4
+ * Cliente de la API privada de `rewards-business`. SOLO LECTURA: no hay endpoint privado de
5
+ * escritura (spec 2026-10-05, D11); los pedidos de entrega se publican con `IRewardsGrantPublisher`.
6
+ *
7
+ * Devuelve `StandardResponse<T>` → leer `result.data`. NO uses `FiadoApiResponse`: los dos tipos no
8
+ * son intercambiables en TypeScript (`code`/`date` admiten `null` en uno y no en el otro).
9
+ *
10
+ * Env var requerida en el consumer: `REWARDS_BUSINESS_LAMBDA_URL`. El template.yml del consumer la
11
+ * setea con:
12
+ *
13
+ * REWARDS_BUSINESS_LAMBDA_URL: '{{resolve:ssm:fiado-rewardsbusiness-lambda}}'
14
+ *
15
+ * El nombre del parámetro SSM es el `stack_name` del `samconfig.toml` de rewards-business (el
16
+ * buildspec registra el SSM con ese nombre): debe coincidir con el de arriba.
17
+ */
18
+ export interface IRewardsBusinessApi {
19
+ /**
20
+ * Contrato de entrada de un paquete: por `itemKey`, las `dependencies` de su recompensa, más
21
+ * nombre, descripción, costo y `totalCost` por moneda. El backoffice del validador lo usa para
22
+ * no dejar activar una evaluación con un paquete inactivo o con inputs que no cubren el contrato.
23
+ *
24
+ * @throws propaga el error del destino (404 `PACKAGE_NOT_FOUND`); nunca devuelve fallback.
25
+ */
26
+ getPackage(packageId: string): Promise<StandardResponse<PackageContractResponse>>;
27
+ /**
28
+ * Estado de una entrega y de cada ítem. El `grantId` se calcula con `grantIdFor`.
29
+ *
30
+ * @throws propaga el error del destino (404 `GRANT_NOT_FOUND`); nunca devuelve fallback.
31
+ */
32
+ getGrant(grantId: string): Promise<StandardResponse<GrantResponse>>;
33
+ }
@@ -0,0 +1,17 @@
1
+ /** Largo (hex) del `grantId`. 128 bits: colisión despreciable. */
2
+ export declare const REWARDS_GRANT_ID_LENGTH = 32;
3
+ /** Límite de `BenefitPaymentRequest.transactionNumber`. */
4
+ export declare const REWARDS_TRANSACTION_NUMBER_MAX_LENGTH = 64;
5
+ /**
6
+ * Largo máximo del `itemKey` de un paquete para que `RW-{grantId}-{itemKey}` quepa en 64:
7
+ * 64 − len('RW-') − len('-') − 32 = 28. rewards-business debe validarlo al guardar un paquete.
8
+ */
9
+ export declare const REWARDS_MAX_ITEM_KEY_LENGTH: number;
10
+ /**
11
+ * `grantId` determinístico de una entrega de rewards (spec A 5.3, D11). Lo calcula el llamador al
12
+ * publicar `GRANT_REQUESTED` (para conocerlo sin esperar respuesta) y el worker de rewards al
13
+ * consumirlo: tienen que dar lo mismo, por eso es UNA función compartida.
14
+ *
15
+ * El largo de `service` va como prefijo para que ('a:b','c') y ('a','b:c') no colisionen.
16
+ */
17
+ export declare function grantIdFor(service: string, idempotencyKey: string): string;
@@ -0,0 +1,30 @@
1
+ import { createHash } from 'node:crypto';
2
+ /** Largo (hex) del `grantId`. 128 bits: colisión despreciable. */
3
+ export const REWARDS_GRANT_ID_LENGTH = 32;
4
+ /** Límite de `BenefitPaymentRequest.transactionNumber`. */
5
+ export const REWARDS_TRANSACTION_NUMBER_MAX_LENGTH = 64;
6
+ const TRANSACTION_NUMBER_PREFIX = 'RW-';
7
+ /**
8
+ * Largo máximo del `itemKey` de un paquete para que `RW-{grantId}-{itemKey}` quepa en 64:
9
+ * 64 − len('RW-') − len('-') − 32 = 28. rewards-business debe validarlo al guardar un paquete.
10
+ */
11
+ export const REWARDS_MAX_ITEM_KEY_LENGTH = REWARDS_TRANSACTION_NUMBER_MAX_LENGTH - TRANSACTION_NUMBER_PREFIX.length - 1 - REWARDS_GRANT_ID_LENGTH;
12
+ /**
13
+ * `grantId` determinístico de una entrega de rewards (spec A 5.3, D11). Lo calcula el llamador al
14
+ * publicar `GRANT_REQUESTED` (para conocerlo sin esperar respuesta) y el worker de rewards al
15
+ * consumirlo: tienen que dar lo mismo, por eso es UNA función compartida.
16
+ *
17
+ * El largo de `service` va como prefijo para que ('a:b','c') y ('a','b:c') no colisionen.
18
+ */
19
+ export function grantIdFor(service, idempotencyKey) {
20
+ if (!service) {
21
+ throw new Error('grantIdFor: service es obligatorio');
22
+ }
23
+ if (!idempotencyKey) {
24
+ throw new Error('grantIdFor: idempotencyKey es obligatoria');
25
+ }
26
+ return createHash('sha256')
27
+ .update(`${service.length}:${service}:${idempotencyKey}`)
28
+ .digest('hex')
29
+ .slice(0, REWARDS_GRANT_ID_LENGTH);
30
+ }
@@ -0,0 +1,5 @@
1
+ export * from './grantIdFor.js';
2
+ export * from './queue/interfaces/IRewardsGrantPublisher.js';
3
+ export * from './queue/RewardsGrantPublisher.js';
4
+ export * from './api/interfaces/IRewardsBusinessApi.js';
5
+ export * from './api/RewardsBusinessApi.js';
@@ -0,0 +1,5 @@
1
+ export * from './grantIdFor.js';
2
+ export * from './queue/interfaces/IRewardsGrantPublisher.js';
3
+ export * from './queue/RewardsGrantPublisher.js';
4
+ export * from './api/interfaces/IRewardsBusinessApi.js';
5
+ export * from './api/RewardsBusinessApi.js';
@@ -0,0 +1,7 @@
1
+ import type { GrantRequestedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+ import { IRewardsGrantPublisher, RewardsGrantBatchResult } from "./interfaces/IRewardsGrantPublisher.js";
3
+ export declare class RewardsGrantPublisher implements IRewardsGrantPublisher {
4
+ private readonly REWARDS_GRANT_QUEUE_URL;
5
+ publish(message: GrantRequestedMessage): Promise<void>;
6
+ publishBatch(messages: GrantRequestedMessage[]): Promise<RewardsGrantBatchResult>;
7
+ }
@@ -0,0 +1,83 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { SendMessageBatchCommand, SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
8
+ import { injectable } from "inversify";
9
+ const SQS_BATCH_MAX_ENTRIES = 10;
10
+ // Límite SQS: 262 144 bytes por SendMessageBatch. Margen para no rozarlo.
11
+ const SQS_BATCH_MAX_BYTES = 250_000;
12
+ let RewardsGrantPublisher = class RewardsGrantPublisher {
13
+ REWARDS_GRANT_QUEUE_URL = process.env.REWARDS_GRANT_QUEUE_URL;
14
+ async publish(message) {
15
+ if (!this.REWARDS_GRANT_QUEUE_URL) {
16
+ throw new Error("Error publishing message to RewardsGrantQueue: REWARDS_GRANT_QUEUE_URL no está configurada");
17
+ }
18
+ try {
19
+ const client = new SQSClient();
20
+ // Cola ESTÁNDAR (no FIFO): sin MessageGroupId/MessageDeduplicationId ni orden garantizado.
21
+ // La idempotencia la hace el consumidor con el `grantId` derivado de (source.service +
22
+ // idempotencyKey) y un PutItem condicional.
23
+ const sendMessageRequest = {
24
+ QueueUrl: this.REWARDS_GRANT_QUEUE_URL,
25
+ MessageBody: JSON.stringify(message),
26
+ };
27
+ await client.send(new SendMessageCommand(sendMessageRequest));
28
+ }
29
+ catch (error) {
30
+ const errorMessage = error instanceof Error ? error.message : String(error);
31
+ throw new Error(`Error publishing message to RewardsGrantQueue: ${errorMessage}`);
32
+ }
33
+ }
34
+ async publishBatch(messages) {
35
+ const failedIdempotencyKeys = [];
36
+ const senderFaultIdempotencyKeys = [];
37
+ if (messages.length === 0) {
38
+ return { failedIdempotencyKeys, senderFaultIdempotencyKeys };
39
+ }
40
+ if (!this.REWARDS_GRANT_QUEUE_URL) {
41
+ throw new Error("Error publishing batch to RewardsGrantQueue: REWARDS_GRANT_QUEUE_URL no está configurada");
42
+ }
43
+ try {
44
+ const client = new SQSClient();
45
+ const keyByEntryId = new Map();
46
+ let current = [];
47
+ let currentBytes = 0;
48
+ const flush = async () => {
49
+ if (current.length === 0)
50
+ return;
51
+ const result = await client.send(new SendMessageBatchCommand({ QueueUrl: this.REWARDS_GRANT_QUEUE_URL, Entries: current }));
52
+ for (const failed of result.Failed ?? []) {
53
+ const key = failed.Id ? keyByEntryId.get(failed.Id) : undefined;
54
+ if (key)
55
+ (failed.SenderFault ? senderFaultIdempotencyKeys : failedIdempotencyKeys).push(key);
56
+ }
57
+ current = [];
58
+ currentBytes = 0;
59
+ };
60
+ for (let index = 0; index < messages.length; index += 1) {
61
+ const body = JSON.stringify(messages[index]);
62
+ const bytes = Buffer.byteLength(body, "utf8");
63
+ if (current.length >= SQS_BATCH_MAX_ENTRIES || (current.length > 0 && currentBytes + bytes > SQS_BATCH_MAX_BYTES)) {
64
+ await flush();
65
+ }
66
+ const id = `m${index}`;
67
+ keyByEntryId.set(id, messages[index].idempotencyKey);
68
+ current.push({ Id: id, MessageBody: body });
69
+ currentBytes += bytes;
70
+ }
71
+ await flush();
72
+ return { failedIdempotencyKeys, senderFaultIdempotencyKeys };
73
+ }
74
+ catch (error) {
75
+ const errorMessage = error instanceof Error ? error.message : String(error);
76
+ throw new Error(`Error publishing batch to RewardsGrantQueue: ${errorMessage}`);
77
+ }
78
+ }
79
+ };
80
+ RewardsGrantPublisher = __decorate([
81
+ injectable()
82
+ ], RewardsGrantPublisher);
83
+ export { RewardsGrantPublisher };
@@ -0,0 +1,32 @@
1
+ import type { GrantRequestedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+ /**
3
+ * Resultado de `publishBatch`: las `idempotencyKey` que SQS NO aceptó.
4
+ * - `failedIdempotencyKeys`: `SenderFault=false` (falla transitoria de SQS) → el llamador PUEDE reintentar.
5
+ * - `senderFaultIdempotencyKeys`: `SenderFault=true` (el mensaje es inválido para SQS) → NO reintentar:
6
+ * volver a mandarlo falla igual.
7
+ */
8
+ export interface RewardsGrantBatchResult {
9
+ failedIdempotencyKeys: string[];
10
+ senderFaultIdempotencyKeys: string[];
11
+ }
12
+ /**
13
+ * Publisher de pedidos de entrega hacia `RewardsGrantQueue`, que consume `rewards-business`
14
+ * (spec 2026-10-05, D11). La cola es COMPARTIDA entre lambdas: el estándar Fiado publica a una cola
15
+ * compartida con un publisher de `@fiado/api-invoker` (mismo patrón que `ITransactionPublisher`).
16
+ *
17
+ * Que `publish` funcione ES la aceptación del pedido: no hay respuesta síncrona. El `grantId` no
18
+ * viaja en el mensaje; el llamador lo conoce con `grantIdFor(source.service, idempotencyKey)`.
19
+ *
20
+ * Env var requerida en el consumer: `REWARDS_GRANT_QUEUE_URL`
21
+ * (`!ImportValue SqsRewardsGrantQueueUrl`) y el permiso `sqs:SendMessage` sobre la cola.
22
+ */
23
+ export interface IRewardsGrantPublisher {
24
+ publish(message: GrantRequestedMessage): Promise<void>;
25
+ /**
26
+ * Publica varios pedidos con `SendMessageBatch` (tandas de 10 y de ≤ 250 000 bytes). El validador
27
+ * suelta lo masivo por tramos con esto (spec B 7.1 y 7.6). Un fallo parcial NO lanza: se devuelve
28
+ * en `failedIdempotencyKeys` (reintentables) o `senderFaultIdempotencyKeys` (NO reintentar). Si la
29
+ * llamada entera falla, lanza (reintento idempotente por `grantId`).
30
+ */
31
+ publishBatch(messages: GrantRequestedMessage[]): Promise<RewardsGrantBatchResult>;
32
+ }
@@ -0,0 +1,12 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { IHttpRequest } from "@fiado/http-client";
3
+ import type { GetRunResponse, PostEventRequest, PostEventResponse } from "@fiado/type-kit/bin/rewardsEligibility/index.js";
4
+ import type { IRewardsEligibilityBusinessApi } from "./interfaces/IRewardsEligibilityBusinessApi.js";
5
+ /** Contrato y semántica completos → `IRewardsEligibilityBusinessApi` (`StandardResponse` → leer `result.data`). */
6
+ export declare class RewardsEligibilityBusinessApi implements IRewardsEligibilityBusinessApi {
7
+ private httpRequest;
8
+ private readonly baseUrl;
9
+ constructor(httpRequest: IHttpRequest);
10
+ postEvent(request: PostEventRequest): Promise<StandardResponse<PostEventResponse>>;
11
+ getRun(runId: string): Promise<StandardResponse<GetRunResponse>>;
12
+ }
@@ -0,0 +1,37 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { inject, injectable } from "inversify";
14
+ /** Contrato y semántica completos → `IRewardsEligibilityBusinessApi` (`StandardResponse` → leer `result.data`). */
15
+ let RewardsEligibilityBusinessApi = class RewardsEligibilityBusinessApi {
16
+ httpRequest;
17
+ // El buildspec publica la URL en SSM con "/" final (`.../v1/`): se normaliza para no armar "//events".
18
+ baseUrl = (process.env.REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL || "").replace(/\/+$/, "");
19
+ constructor(httpRequest) {
20
+ this.httpRequest = httpRequest;
21
+ }
22
+ async postEvent(request) {
23
+ // PROVISIONAL: sin prefijo `/private` (la API que resuelve el SSM es la PRIVADA).
24
+ // La ruta base exacta (con o sin `/private`) la fija el plan del validador; si cambia, ajustar aquí.
25
+ return await this.httpRequest.post(`${this.baseUrl}/events`, request);
26
+ }
27
+ async getRun(runId) {
28
+ const url = `${this.baseUrl}/runs/${encodeURIComponent(runId)}`;
29
+ return await this.httpRequest.get(url);
30
+ }
31
+ };
32
+ RewardsEligibilityBusinessApi = __decorate([
33
+ injectable(),
34
+ __param(0, inject("IHttpRequest")),
35
+ __metadata("design:paramtypes", [Object])
36
+ ], RewardsEligibilityBusinessApi);
37
+ export { RewardsEligibilityBusinessApi };
@@ -0,0 +1,34 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { GetRunResponse, PostEventRequest, PostEventResponse } from "@fiado/type-kit/bin/rewardsEligibility/index.js";
3
+ /**
4
+ * Cliente de la API privada de `rewards-eligibility-business` (spec 2026-10-06, §6).
5
+ *
6
+ * Devuelve `StandardResponse<T>` → leer `result.data`. Env var requerida en el consumer:
7
+ * `REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL`, seteada con
8
+ *
9
+ * REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL: '{{resolve:ssm:fiado-rewardseligibilitybusiness-lambda}}'
10
+ *
11
+ * (el nombre del SSM es el `stack_name` del `samconfig.toml` del validador).
12
+ *
13
+ * El emisor NO decide la elegibilidad: informa qué pasó y quién participó (E3). La llamada es NO
14
+ * bloqueante para el flujo del emisor: si falla, el emisor sigue y reintenta con el mismo
15
+ * `idempotencyKey`.
16
+ *
17
+ * RUTA BASE PROVISIONAL: se fija en el plan del validador (`rewards-eligibility-business`); si cambia
18
+ * (con o sin `/private`), se ajusta este cliente y se republica.
19
+ */
20
+ export interface IRewardsEligibilityBusinessApi {
21
+ /**
22
+ * Dispara las evaluaciones suscritas al `eventKey`. Responde 202 con una corrida por evaluación
23
+ * (`runs` vacío si nadie escucha el evento).
24
+ *
25
+ * @throws propaga el error del destino (400 `INPUT_INVALID`, 404 `EVENT_TYPE_NOT_FOUND`).
26
+ */
27
+ postEvent(request: PostEventRequest): Promise<StandardResponse<PostEventResponse>>;
28
+ /**
29
+ * Estado de una corrida y una página de sus resultados.
30
+ *
31
+ * @throws propaga el error del destino (404 `RUN_NOT_FOUND`); nunca devuelve fallback.
32
+ */
33
+ getRun(runId: string): Promise<StandardResponse<GetRunResponse>>;
34
+ }
@@ -0,0 +1,4 @@
1
+ export * from './queue/interfaces/IRewardsEligibilityPublisher.js';
2
+ export * from './queue/RewardsEligibilityPublisher.js';
3
+ export * from './api/interfaces/IRewardsEligibilityBusinessApi.js';
4
+ export * from './api/RewardsEligibilityBusinessApi.js';
@@ -0,0 +1,4 @@
1
+ export * from './queue/interfaces/IRewardsEligibilityPublisher.js';
2
+ export * from './queue/RewardsEligibilityPublisher.js';
3
+ export * from './api/interfaces/IRewardsEligibilityBusinessApi.js';
4
+ export * from './api/RewardsEligibilityBusinessApi.js';
@@ -0,0 +1,6 @@
1
+ import type { GrantFailedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+ import { IRewardsEligibilityPublisher } from "./interfaces/IRewardsEligibilityPublisher.js";
3
+ export declare class RewardsEligibilityPublisher implements IRewardsEligibilityPublisher {
4
+ private readonly REWARDS_ELIGIBILITY_QUEUE_URL;
5
+ publish(message: GrantFailedMessage): Promise<void>;
6
+ }
@@ -0,0 +1,34 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ 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;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
8
+ import { injectable } from "inversify";
9
+ let RewardsEligibilityPublisher = class RewardsEligibilityPublisher {
10
+ REWARDS_ELIGIBILITY_QUEUE_URL = process.env.REWARDS_ELIGIBILITY_QUEUE_URL;
11
+ async publish(message) {
12
+ if (!this.REWARDS_ELIGIBILITY_QUEUE_URL) {
13
+ throw new Error("Error publishing message to RewardsEligibilityQueue: REWARDS_ELIGIBILITY_QUEUE_URL no está configurada");
14
+ }
15
+ try {
16
+ const client = new SQSClient();
17
+ // Cola ESTÁNDAR: el consumidor descarta avisos repetidos o atrasados comparando el `n`
18
+ // de la idempotencyKey con el del ledger (spec B 7.7), así que no hace falta dedup en la cola.
19
+ const sendMessageRequest = {
20
+ QueueUrl: this.REWARDS_ELIGIBILITY_QUEUE_URL,
21
+ MessageBody: JSON.stringify(message),
22
+ };
23
+ await client.send(new SendMessageCommand(sendMessageRequest));
24
+ }
25
+ catch (error) {
26
+ const errorMessage = error instanceof Error ? error.message : String(error);
27
+ throw new Error(`Error publishing message to RewardsEligibilityQueue: ${errorMessage}`);
28
+ }
29
+ }
30
+ };
31
+ RewardsEligibilityPublisher = __decorate([
32
+ injectable()
33
+ ], RewardsEligibilityPublisher);
34
+ export { RewardsEligibilityPublisher };
@@ -0,0 +1,15 @@
1
+ import type { GrantFailedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+ /**
3
+ * Publisher del aviso `GRANT_FAILED` hacia la cola del validador (`RewardsEligibilityQueue`), que
4
+ * consume `rewards-eligibility-business` para liberar ledger y cupo (spec A 6.5, spec B 7.7).
5
+ *
6
+ * Lo produce `rewards-business` cuando una entrega termina SIN ningún ítem entregado (FAILED o
7
+ * REJECTED). Una entrega PARTIAL no avisa. El tipo es una unión cerrada a propósito: meter otro
8
+ * mensaje en una cola compartida obliga a tocar también al consumidor.
9
+ *
10
+ * Env var requerida en el consumer: `REWARDS_ELIGIBILITY_QUEUE_URL`
11
+ * (`!ImportValue SqsRewardsEligibilityQueueUrl`) y el permiso `sqs:SendMessage` sobre la cola.
12
+ */
13
+ export interface IRewardsEligibilityPublisher {
14
+ publish(message: GrantFailedMessage): Promise<void>;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fiado/api-invoker",
3
- "version": "5.115.0",
3
+ "version": "5.116.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",
@@ -34,7 +34,7 @@
34
34
  "@fiado/gateway-adapter": "^3.14.0",
35
35
  "@fiado/http-client": "^2.0.1",
36
36
  "@fiado/logger": "^1.1.3",
37
- "@fiado/type-kit": "^3.509.0",
37
+ "@fiado/type-kit": "^3.510.0",
38
38
  "dotenv": "^16.4.7"
39
39
  },
40
40
  "peerDependencies": {
@@ -204,6 +204,10 @@ import { IKycVerificationsBusinessApi, KycVerificationsBusinessApi } from "./kyc
204
204
  // Webhook business — publisher de la cola compartida de entrega de eventos a suscriptores externos
205
205
  import { IWebhookBusinessPublisher, WebhookBusinessPublisher } from "./webhook-business/index.js";
206
206
  import { ICentralPaymentsWebhookApi, CentralPaymentsWebhookApi } from "./central-payments-webhook/index.js";
207
+ // Rewards — publisher de pedidos de entrega + cliente de lecturas privadas (rewards-business) y
208
+ // publisher de GRANT_FAILED + cliente del validador (rewards-eligibility-business)
209
+ import { IRewardsGrantPublisher, IRewardsBusinessApi, RewardsGrantPublisher, RewardsBusinessApi } from "./rewards-business/index.js";
210
+ import { IRewardsEligibilityPublisher, IRewardsEligibilityBusinessApi, RewardsEligibilityPublisher, RewardsEligibilityBusinessApi } from "./rewards-eligibility-business/index.js";
207
211
  // Collection engine — cliente del motor de cobro central (collect/deregister)
208
212
  import { ICollectionEngineApi } from "./collection-engine/index.js";
209
213
  import CollectionEngineApi from "./collection-engine/api/CollectionEngineApi.js";
@@ -395,6 +399,16 @@ export const apiInvokerBindings = new ContainerModule(({ bind }) => {
395
399
  bind<IWebhookBusinessPublisher>("IWebhookBusinessPublisher").to(WebhookBusinessPublisher);
396
400
  bind<ICentralPaymentsWebhookApi>("ICentralPaymentsWebhookApi").to(CentralPaymentsWebhookApi);
397
401
 
402
+ // Rewards — cola compartida RewardsGrantQueue (pedidos de entrega) y lecturas privadas de rewards-business.
403
+ // El productor necesita REWARDS_GRANT_QUEUE_URL y sqs:SendMessage sobre la cola.
404
+ bind<IRewardsGrantPublisher>("IRewardsGrantPublisher").to(RewardsGrantPublisher);
405
+ bind<IRewardsBusinessApi>("IRewardsBusinessApi").to(RewardsBusinessApi);
406
+
407
+ // Rewards eligibility — GRANT_FAILED hacia la cola del validador y cliente de POST /events.
408
+ // El productor necesita REWARDS_ELIGIBILITY_QUEUE_URL y sqs:SendMessage sobre la cola.
409
+ bind<IRewardsEligibilityPublisher>("IRewardsEligibilityPublisher").to(RewardsEligibilityPublisher);
410
+ bind<IRewardsEligibilityBusinessApi>("IRewardsEligibilityBusinessApi").to(RewardsEligibilityBusinessApi);
411
+
398
412
  // Collection engine — motor de cobro central multitenant (collect/deregister)
399
413
  bind<ICollectionEngineApi>("ICollectionEngineApi").to(CollectionEngineApi);
400
414
 
package/src/index.ts CHANGED
@@ -122,3 +122,5 @@ export * from "./domain-callback/index.js";
122
122
  export * from "./datacultrConnector/index.js";
123
123
  export * from "./webhook-business/index.js";
124
124
  export * from "./trustonicConnector/index.js";
125
+ export * from "./rewards-business/index.js";
126
+ export * from "./rewards-eligibility-business/index.js";
@@ -0,0 +1,27 @@
1
+ import { inject, injectable } from "inversify";
2
+ import type { StandardResponse } from "@fiado/gateway-adapter";
3
+ import type { IHttpRequest } from "@fiado/http-client";
4
+ import type { GrantResponse, PackageContractResponse } from "@fiado/type-kit/bin/rewards/index.js";
5
+ import type { IRewardsBusinessApi } from "./interfaces/IRewardsBusinessApi.js";
6
+
7
+ /** Contrato y semántica completos → `IRewardsBusinessApi` (`StandardResponse` → leer `result.data`). */
8
+ @injectable()
9
+ export class RewardsBusinessApi implements IRewardsBusinessApi {
10
+
11
+ // El buildspec publica la URL en SSM con "/" final (`.../v1/`): se normaliza para no armar "//packages".
12
+ private readonly baseUrl = (process.env.REWARDS_BUSINESS_LAMBDA_URL || "").replace(/\/+$/, "");
13
+
14
+ constructor(@inject("IHttpRequest") private httpRequest: IHttpRequest) { }
15
+
16
+ async getPackage(packageId: string): Promise<StandardResponse<PackageContractResponse>> {
17
+ // PROVISIONAL: sin prefijo `/private` (la API que resuelve el SSM es la PRIVADA de rewards-business).
18
+ // La ruta base exacta (con o sin `/private`) la fija el plan de rewards-business; si cambia, ajustar aquí.
19
+ const url = `${this.baseUrl}/packages/${encodeURIComponent(packageId)}`;
20
+ return await this.httpRequest.get<StandardResponse<PackageContractResponse>>(url);
21
+ }
22
+
23
+ async getGrant(grantId: string): Promise<StandardResponse<GrantResponse>> {
24
+ const url = `${this.baseUrl}/grants/${encodeURIComponent(grantId)}`;
25
+ return await this.httpRequest.get<StandardResponse<GrantResponse>>(url);
26
+ }
27
+ }
@@ -0,0 +1,35 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { GrantResponse, PackageContractResponse } from "@fiado/type-kit/bin/rewards/index.js";
3
+
4
+ /**
5
+ * Cliente de la API privada de `rewards-business`. SOLO LECTURA: no hay endpoint privado de
6
+ * escritura (spec 2026-10-05, D11); los pedidos de entrega se publican con `IRewardsGrantPublisher`.
7
+ *
8
+ * Devuelve `StandardResponse<T>` → leer `result.data`. NO uses `FiadoApiResponse`: los dos tipos no
9
+ * son intercambiables en TypeScript (`code`/`date` admiten `null` en uno y no en el otro).
10
+ *
11
+ * Env var requerida en el consumer: `REWARDS_BUSINESS_LAMBDA_URL`. El template.yml del consumer la
12
+ * setea con:
13
+ *
14
+ * REWARDS_BUSINESS_LAMBDA_URL: '{{resolve:ssm:fiado-rewardsbusiness-lambda}}'
15
+ *
16
+ * El nombre del parámetro SSM es el `stack_name` del `samconfig.toml` de rewards-business (el
17
+ * buildspec registra el SSM con ese nombre): debe coincidir con el de arriba.
18
+ */
19
+ export interface IRewardsBusinessApi {
20
+ /**
21
+ * Contrato de entrada de un paquete: por `itemKey`, las `dependencies` de su recompensa, más
22
+ * nombre, descripción, costo y `totalCost` por moneda. El backoffice del validador lo usa para
23
+ * no dejar activar una evaluación con un paquete inactivo o con inputs que no cubren el contrato.
24
+ *
25
+ * @throws propaga el error del destino (404 `PACKAGE_NOT_FOUND`); nunca devuelve fallback.
26
+ */
27
+ getPackage(packageId: string): Promise<StandardResponse<PackageContractResponse>>;
28
+
29
+ /**
30
+ * Estado de una entrega y de cada ítem. El `grantId` se calcula con `grantIdFor`.
31
+ *
32
+ * @throws propaga el error del destino (404 `GRANT_NOT_FOUND`); nunca devuelve fallback.
33
+ */
34
+ getGrant(grantId: string): Promise<StandardResponse<GrantResponse>>;
35
+ }
@@ -0,0 +1,36 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ /** Largo (hex) del `grantId`. 128 bits: colisión despreciable. */
4
+ export const REWARDS_GRANT_ID_LENGTH = 32;
5
+
6
+ /** Límite de `BenefitPaymentRequest.transactionNumber`. */
7
+ export const REWARDS_TRANSACTION_NUMBER_MAX_LENGTH = 64;
8
+
9
+ const TRANSACTION_NUMBER_PREFIX = 'RW-';
10
+
11
+ /**
12
+ * Largo máximo del `itemKey` de un paquete para que `RW-{grantId}-{itemKey}` quepa en 64:
13
+ * 64 − len('RW-') − len('-') − 32 = 28. rewards-business debe validarlo al guardar un paquete.
14
+ */
15
+ export const REWARDS_MAX_ITEM_KEY_LENGTH =
16
+ REWARDS_TRANSACTION_NUMBER_MAX_LENGTH - TRANSACTION_NUMBER_PREFIX.length - 1 - REWARDS_GRANT_ID_LENGTH;
17
+
18
+ /**
19
+ * `grantId` determinístico de una entrega de rewards (spec A 5.3, D11). Lo calcula el llamador al
20
+ * publicar `GRANT_REQUESTED` (para conocerlo sin esperar respuesta) y el worker de rewards al
21
+ * consumirlo: tienen que dar lo mismo, por eso es UNA función compartida.
22
+ *
23
+ * El largo de `service` va como prefijo para que ('a:b','c') y ('a','b:c') no colisionen.
24
+ */
25
+ export function grantIdFor(service: string, idempotencyKey: string): string {
26
+ if (!service) {
27
+ throw new Error('grantIdFor: service es obligatorio');
28
+ }
29
+ if (!idempotencyKey) {
30
+ throw new Error('grantIdFor: idempotencyKey es obligatoria');
31
+ }
32
+ return createHash('sha256')
33
+ .update(`${service.length}:${service}:${idempotencyKey}`)
34
+ .digest('hex')
35
+ .slice(0, REWARDS_GRANT_ID_LENGTH);
36
+ }
@@ -0,0 +1,5 @@
1
+ export * from './grantIdFor.js';
2
+ export * from './queue/interfaces/IRewardsGrantPublisher.js';
3
+ export * from './queue/RewardsGrantPublisher.js';
4
+ export * from './api/interfaces/IRewardsBusinessApi.js';
5
+ export * from './api/RewardsBusinessApi.js';
@@ -0,0 +1,80 @@
1
+ import { SendMessageBatchCommand, SendMessageBatchRequestEntry, SendMessageCommand, SendMessageRequest, SQSClient } from "@aws-sdk/client-sqs";
2
+ import { injectable } from "inversify";
3
+ import type { GrantRequestedMessage } from "@fiado/type-kit/bin/rewards/index.js";
4
+ import { IRewardsGrantPublisher, RewardsGrantBatchResult } from "./interfaces/IRewardsGrantPublisher.js";
5
+
6
+ const SQS_BATCH_MAX_ENTRIES = 10;
7
+ // Límite SQS: 262 144 bytes por SendMessageBatch. Margen para no rozarlo.
8
+ const SQS_BATCH_MAX_BYTES = 250_000;
9
+
10
+ @injectable()
11
+ export class RewardsGrantPublisher implements IRewardsGrantPublisher {
12
+ private readonly REWARDS_GRANT_QUEUE_URL = process.env.REWARDS_GRANT_QUEUE_URL;
13
+
14
+ async publish(message: GrantRequestedMessage): Promise<void> {
15
+ if (!this.REWARDS_GRANT_QUEUE_URL) {
16
+ throw new Error("Error publishing message to RewardsGrantQueue: REWARDS_GRANT_QUEUE_URL no está configurada");
17
+ }
18
+ try {
19
+ const client: SQSClient = new SQSClient();
20
+ // Cola ESTÁNDAR (no FIFO): sin MessageGroupId/MessageDeduplicationId ni orden garantizado.
21
+ // La idempotencia la hace el consumidor con el `grantId` derivado de (source.service +
22
+ // idempotencyKey) y un PutItem condicional.
23
+ const sendMessageRequest: SendMessageRequest = {
24
+ QueueUrl: this.REWARDS_GRANT_QUEUE_URL,
25
+ MessageBody: JSON.stringify(message),
26
+ };
27
+ await client.send(new SendMessageCommand(sendMessageRequest));
28
+ } catch (error) {
29
+ const errorMessage = error instanceof Error ? error.message : String(error);
30
+ throw new Error(`Error publishing message to RewardsGrantQueue: ${errorMessage}`);
31
+ }
32
+ }
33
+
34
+ async publishBatch(messages: GrantRequestedMessage[]): Promise<RewardsGrantBatchResult> {
35
+ const failedIdempotencyKeys: string[] = [];
36
+ const senderFaultIdempotencyKeys: string[] = [];
37
+ if (messages.length === 0) {
38
+ return { failedIdempotencyKeys, senderFaultIdempotencyKeys };
39
+ }
40
+ if (!this.REWARDS_GRANT_QUEUE_URL) {
41
+ throw new Error("Error publishing batch to RewardsGrantQueue: REWARDS_GRANT_QUEUE_URL no está configurada");
42
+ }
43
+ try {
44
+ const client: SQSClient = new SQSClient();
45
+ const keyByEntryId = new Map<string, string>();
46
+ let current: SendMessageBatchRequestEntry[] = [];
47
+ let currentBytes = 0;
48
+
49
+ const flush = async (): Promise<void> => {
50
+ if (current.length === 0) return;
51
+ const result = await client.send(
52
+ new SendMessageBatchCommand({ QueueUrl: this.REWARDS_GRANT_QUEUE_URL, Entries: current }),
53
+ );
54
+ for (const failed of result.Failed ?? []) {
55
+ const key = failed.Id ? keyByEntryId.get(failed.Id) : undefined;
56
+ if (key) (failed.SenderFault ? senderFaultIdempotencyKeys : failedIdempotencyKeys).push(key);
57
+ }
58
+ current = [];
59
+ currentBytes = 0;
60
+ };
61
+
62
+ for (let index = 0; index < messages.length; index += 1) {
63
+ const body = JSON.stringify(messages[index]);
64
+ const bytes = Buffer.byteLength(body, "utf8");
65
+ if (current.length >= SQS_BATCH_MAX_ENTRIES || (current.length > 0 && currentBytes + bytes > SQS_BATCH_MAX_BYTES)) {
66
+ await flush();
67
+ }
68
+ const id = `m${index}`;
69
+ keyByEntryId.set(id, messages[index].idempotencyKey);
70
+ current.push({ Id: id, MessageBody: body });
71
+ currentBytes += bytes;
72
+ }
73
+ await flush();
74
+ return { failedIdempotencyKeys, senderFaultIdempotencyKeys };
75
+ } catch (error) {
76
+ const errorMessage = error instanceof Error ? error.message : String(error);
77
+ throw new Error(`Error publishing batch to RewardsGrantQueue: ${errorMessage}`);
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,35 @@
1
+ import type { GrantRequestedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+
3
+ /**
4
+ * Resultado de `publishBatch`: las `idempotencyKey` que SQS NO aceptó.
5
+ * - `failedIdempotencyKeys`: `SenderFault=false` (falla transitoria de SQS) → el llamador PUEDE reintentar.
6
+ * - `senderFaultIdempotencyKeys`: `SenderFault=true` (el mensaje es inválido para SQS) → NO reintentar:
7
+ * volver a mandarlo falla igual.
8
+ */
9
+ export interface RewardsGrantBatchResult {
10
+ failedIdempotencyKeys: string[];
11
+ senderFaultIdempotencyKeys: string[];
12
+ }
13
+
14
+ /**
15
+ * Publisher de pedidos de entrega hacia `RewardsGrantQueue`, que consume `rewards-business`
16
+ * (spec 2026-10-05, D11). La cola es COMPARTIDA entre lambdas: el estándar Fiado publica a una cola
17
+ * compartida con un publisher de `@fiado/api-invoker` (mismo patrón que `ITransactionPublisher`).
18
+ *
19
+ * Que `publish` funcione ES la aceptación del pedido: no hay respuesta síncrona. El `grantId` no
20
+ * viaja en el mensaje; el llamador lo conoce con `grantIdFor(source.service, idempotencyKey)`.
21
+ *
22
+ * Env var requerida en el consumer: `REWARDS_GRANT_QUEUE_URL`
23
+ * (`!ImportValue SqsRewardsGrantQueueUrl`) y el permiso `sqs:SendMessage` sobre la cola.
24
+ */
25
+ export interface IRewardsGrantPublisher {
26
+ publish(message: GrantRequestedMessage): Promise<void>;
27
+
28
+ /**
29
+ * Publica varios pedidos con `SendMessageBatch` (tandas de 10 y de ≤ 250 000 bytes). El validador
30
+ * suelta lo masivo por tramos con esto (spec B 7.1 y 7.6). Un fallo parcial NO lanza: se devuelve
31
+ * en `failedIdempotencyKeys` (reintentables) o `senderFaultIdempotencyKeys` (NO reintentar). Si la
32
+ * llamada entera falla, lanza (reintento idempotente por `grantId`).
33
+ */
34
+ publishBatch(messages: GrantRequestedMessage[]): Promise<RewardsGrantBatchResult>;
35
+ }
@@ -0,0 +1,26 @@
1
+ import { inject, injectable } from "inversify";
2
+ import type { StandardResponse } from "@fiado/gateway-adapter";
3
+ import type { IHttpRequest } from "@fiado/http-client";
4
+ import type { GetRunResponse, PostEventRequest, PostEventResponse } from "@fiado/type-kit/bin/rewardsEligibility/index.js";
5
+ import type { IRewardsEligibilityBusinessApi } from "./interfaces/IRewardsEligibilityBusinessApi.js";
6
+
7
+ /** Contrato y semántica completos → `IRewardsEligibilityBusinessApi` (`StandardResponse` → leer `result.data`). */
8
+ @injectable()
9
+ export class RewardsEligibilityBusinessApi implements IRewardsEligibilityBusinessApi {
10
+
11
+ // El buildspec publica la URL en SSM con "/" final (`.../v1/`): se normaliza para no armar "//events".
12
+ private readonly baseUrl = (process.env.REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL || "").replace(/\/+$/, "");
13
+
14
+ constructor(@inject("IHttpRequest") private httpRequest: IHttpRequest) { }
15
+
16
+ async postEvent(request: PostEventRequest): Promise<StandardResponse<PostEventResponse>> {
17
+ // PROVISIONAL: sin prefijo `/private` (la API que resuelve el SSM es la PRIVADA).
18
+ // La ruta base exacta (con o sin `/private`) la fija el plan del validador; si cambia, ajustar aquí.
19
+ return await this.httpRequest.post<StandardResponse<PostEventResponse>>(`${this.baseUrl}/events`, request);
20
+ }
21
+
22
+ async getRun(runId: string): Promise<StandardResponse<GetRunResponse>> {
23
+ const url = `${this.baseUrl}/runs/${encodeURIComponent(runId)}`;
24
+ return await this.httpRequest.get<StandardResponse<GetRunResponse>>(url);
25
+ }
26
+ }
@@ -0,0 +1,36 @@
1
+ import type { StandardResponse } from "@fiado/gateway-adapter";
2
+ import type { GetRunResponse, PostEventRequest, PostEventResponse } from "@fiado/type-kit/bin/rewardsEligibility/index.js";
3
+
4
+ /**
5
+ * Cliente de la API privada de `rewards-eligibility-business` (spec 2026-10-06, §6).
6
+ *
7
+ * Devuelve `StandardResponse<T>` → leer `result.data`. Env var requerida en el consumer:
8
+ * `REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL`, seteada con
9
+ *
10
+ * REWARDS_ELIGIBILITY_BUSINESS_LAMBDA_URL: '{{resolve:ssm:fiado-rewardseligibilitybusiness-lambda}}'
11
+ *
12
+ * (el nombre del SSM es el `stack_name` del `samconfig.toml` del validador).
13
+ *
14
+ * El emisor NO decide la elegibilidad: informa qué pasó y quién participó (E3). La llamada es NO
15
+ * bloqueante para el flujo del emisor: si falla, el emisor sigue y reintenta con el mismo
16
+ * `idempotencyKey`.
17
+ *
18
+ * RUTA BASE PROVISIONAL: se fija en el plan del validador (`rewards-eligibility-business`); si cambia
19
+ * (con o sin `/private`), se ajusta este cliente y se republica.
20
+ */
21
+ export interface IRewardsEligibilityBusinessApi {
22
+ /**
23
+ * Dispara las evaluaciones suscritas al `eventKey`. Responde 202 con una corrida por evaluación
24
+ * (`runs` vacío si nadie escucha el evento).
25
+ *
26
+ * @throws propaga el error del destino (400 `INPUT_INVALID`, 404 `EVENT_TYPE_NOT_FOUND`).
27
+ */
28
+ postEvent(request: PostEventRequest): Promise<StandardResponse<PostEventResponse>>;
29
+
30
+ /**
31
+ * Estado de una corrida y una página de sus resultados.
32
+ *
33
+ * @throws propaga el error del destino (404 `RUN_NOT_FOUND`); nunca devuelve fallback.
34
+ */
35
+ getRun(runId: string): Promise<StandardResponse<GetRunResponse>>;
36
+ }
@@ -0,0 +1,4 @@
1
+ export * from './queue/interfaces/IRewardsEligibilityPublisher.js';
2
+ export * from './queue/RewardsEligibilityPublisher.js';
3
+ export * from './api/interfaces/IRewardsEligibilityBusinessApi.js';
4
+ export * from './api/RewardsEligibilityBusinessApi.js';
@@ -0,0 +1,28 @@
1
+ import { SendMessageCommand, SendMessageRequest, SQSClient } from "@aws-sdk/client-sqs";
2
+ import { injectable } from "inversify";
3
+ import type { GrantFailedMessage } from "@fiado/type-kit/bin/rewards/index.js";
4
+ import { IRewardsEligibilityPublisher } from "./interfaces/IRewardsEligibilityPublisher.js";
5
+
6
+ @injectable()
7
+ export class RewardsEligibilityPublisher implements IRewardsEligibilityPublisher {
8
+ private readonly REWARDS_ELIGIBILITY_QUEUE_URL = process.env.REWARDS_ELIGIBILITY_QUEUE_URL;
9
+
10
+ async publish(message: GrantFailedMessage): Promise<void> {
11
+ if (!this.REWARDS_ELIGIBILITY_QUEUE_URL) {
12
+ throw new Error("Error publishing message to RewardsEligibilityQueue: REWARDS_ELIGIBILITY_QUEUE_URL no está configurada");
13
+ }
14
+ try {
15
+ const client: SQSClient = new SQSClient();
16
+ // Cola ESTÁNDAR: el consumidor descarta avisos repetidos o atrasados comparando el `n`
17
+ // de la idempotencyKey con el del ledger (spec B 7.7), así que no hace falta dedup en la cola.
18
+ const sendMessageRequest: SendMessageRequest = {
19
+ QueueUrl: this.REWARDS_ELIGIBILITY_QUEUE_URL,
20
+ MessageBody: JSON.stringify(message),
21
+ };
22
+ await client.send(new SendMessageCommand(sendMessageRequest));
23
+ } catch (error) {
24
+ const errorMessage = error instanceof Error ? error.message : String(error);
25
+ throw new Error(`Error publishing message to RewardsEligibilityQueue: ${errorMessage}`);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,16 @@
1
+ import type { GrantFailedMessage } from "@fiado/type-kit/bin/rewards/index.js";
2
+
3
+ /**
4
+ * Publisher del aviso `GRANT_FAILED` hacia la cola del validador (`RewardsEligibilityQueue`), que
5
+ * consume `rewards-eligibility-business` para liberar ledger y cupo (spec A 6.5, spec B 7.7).
6
+ *
7
+ * Lo produce `rewards-business` cuando una entrega termina SIN ningún ítem entregado (FAILED o
8
+ * REJECTED). Una entrega PARTIAL no avisa. El tipo es una unión cerrada a propósito: meter otro
9
+ * mensaje en una cola compartida obliga a tocar también al consumidor.
10
+ *
11
+ * Env var requerida en el consumer: `REWARDS_ELIGIBILITY_QUEUE_URL`
12
+ * (`!ImportValue SqsRewardsEligibilityQueueUrl`) y el permiso `sqs:SendMessage` sobre la cola.
13
+ */
14
+ export interface IRewardsEligibilityPublisher {
15
+ publish(message: GrantFailedMessage): Promise<void>;
16
+ }