@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.
- package/bin/container.config.js +12 -0
- package/bin/index.d.ts +2 -0
- package/bin/index.js +2 -0
- package/bin/rewards-business/api/RewardsBusinessApi.d.ts +12 -0
- package/bin/rewards-business/api/RewardsBusinessApi.js +38 -0
- package/bin/rewards-business/api/interfaces/IRewardsBusinessApi.d.ts +33 -0
- package/bin/rewards-business/api/interfaces/IRewardsBusinessApi.js +1 -0
- package/bin/rewards-business/grantIdFor.d.ts +17 -0
- package/bin/rewards-business/grantIdFor.js +30 -0
- package/bin/rewards-business/index.d.ts +5 -0
- package/bin/rewards-business/index.js +5 -0
- package/bin/rewards-business/queue/RewardsGrantPublisher.d.ts +7 -0
- package/bin/rewards-business/queue/RewardsGrantPublisher.js +83 -0
- package/bin/rewards-business/queue/interfaces/IRewardsGrantPublisher.d.ts +32 -0
- package/bin/rewards-business/queue/interfaces/IRewardsGrantPublisher.js +1 -0
- package/bin/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.d.ts +12 -0
- package/bin/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.js +37 -0
- package/bin/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.d.ts +34 -0
- package/bin/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.js +1 -0
- package/bin/rewards-eligibility-business/index.d.ts +4 -0
- package/bin/rewards-eligibility-business/index.js +4 -0
- package/bin/rewards-eligibility-business/queue/RewardsEligibilityPublisher.d.ts +6 -0
- package/bin/rewards-eligibility-business/queue/RewardsEligibilityPublisher.js +34 -0
- package/bin/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.d.ts +15 -0
- package/bin/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.js +1 -0
- package/package.json +2 -2
- package/src/container.config.ts +14 -0
- package/src/index.ts +2 -0
- package/src/rewards-business/api/RewardsBusinessApi.ts +27 -0
- package/src/rewards-business/api/interfaces/IRewardsBusinessApi.ts +35 -0
- package/src/rewards-business/grantIdFor.ts +36 -0
- package/src/rewards-business/index.ts +5 -0
- package/src/rewards-business/queue/RewardsGrantPublisher.ts +80 -0
- package/src/rewards-business/queue/interfaces/IRewardsGrantPublisher.ts +35 -0
- package/src/rewards-eligibility-business/api/RewardsEligibilityBusinessApi.ts +26 -0
- package/src/rewards-eligibility-business/api/interfaces/IRewardsEligibilityBusinessApi.ts +36 -0
- package/src/rewards-eligibility-business/index.ts +4 -0
- package/src/rewards-eligibility-business/queue/RewardsEligibilityPublisher.ts +28 -0
- package/src/rewards-eligibility-business/queue/interfaces/IRewardsEligibilityPublisher.ts +16 -0
package/bin/container.config.js
CHANGED
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fiado/api-invoker",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
37
|
+
"@fiado/type-kit": "^3.510.0",
|
|
38
38
|
"dotenv": "^16.4.7"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
package/src/container.config.ts
CHANGED
|
@@ -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,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,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
|
+
}
|