PR #26 (squash-merged via Gitea API)
Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Orchestrator Unit Tests (push) Has been cancelled
CI / Orchestrator E2E (Testcontainers) (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
Some checks failed
CI / Frontend Lint (push) Has been cancelled
CI / Frontend Type Check (push) Has been cancelled
CI / Frontend Build (push) Has been cancelled
CI / Frontend E2E Tests (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
CI / Orchestrator Unit Tests (push) Has been cancelled
CI / Orchestrator E2E (Testcontainers) (push) Has been cancelled
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
This commit was merged in pull request #26.
This commit is contained in:
215
orchestrator/src/services/dbisCore/client.ts
Normal file
215
orchestrator/src/services/dbisCore/client.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* HTTP client adapter for `d-bis/dbis_core`.
|
||||
*
|
||||
* Provider-switched: when `DBIS_CORE_URL` is set the client makes real
|
||||
* HTTP calls to the upstream DBIS Core Banking API; otherwise every
|
||||
* method returns a deterministic mock response so unit tests, local
|
||||
* dev, and CI still work.
|
||||
*
|
||||
* This is intentionally minimal — only the endpoints the orchestrator
|
||||
* actually calls from its settlement / obligation / compliance paths.
|
||||
* Extend the client surface as new orchestrator capabilities need more
|
||||
* of the dbis_core API.
|
||||
*/
|
||||
|
||||
import { logger } from "../../logging/logger";
|
||||
import type {
|
||||
AccountBalance,
|
||||
AriDecisionRequest,
|
||||
AriDecisionResponse,
|
||||
AtomicSettleRequest,
|
||||
AtomicSettleResponse,
|
||||
Pacs008DispatchRequest,
|
||||
Pacs008DispatchResponse,
|
||||
RouteRequest,
|
||||
RouteResponse,
|
||||
SettlementStatus,
|
||||
} from "./types";
|
||||
|
||||
export interface DbisCoreConfig {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
timeoutMs?: number;
|
||||
fetchImpl?: typeof fetch;
|
||||
}
|
||||
|
||||
export interface DbisCoreClient {
|
||||
mode: "live" | "mock";
|
||||
getAccountBalance(accountId: string): Promise<AccountBalance>;
|
||||
findSettlementRoute(req: RouteRequest): Promise<RouteResponse>;
|
||||
atomicSettle(req: AtomicSettleRequest): Promise<AtomicSettleResponse>;
|
||||
getSettlementStatus(settlementId: string): Promise<SettlementStatus>;
|
||||
requestAriDecision(req: AriDecisionRequest): Promise<AriDecisionResponse>;
|
||||
dispatchPacs008(req: Pacs008DispatchRequest): Promise<Pacs008DispatchResponse>;
|
||||
}
|
||||
|
||||
function loadConfigFromEnv(): DbisCoreConfig {
|
||||
return {
|
||||
baseUrl: process.env.DBIS_CORE_URL,
|
||||
apiKey: process.env.DBIS_CORE_API_KEY,
|
||||
timeoutMs: process.env.DBIS_CORE_TIMEOUT_MS
|
||||
? parseInt(process.env.DBIS_CORE_TIMEOUT_MS, 10)
|
||||
: 10_000,
|
||||
};
|
||||
}
|
||||
|
||||
class HttpDbisCoreClient implements DbisCoreClient {
|
||||
readonly mode = "live" as const;
|
||||
private readonly baseUrl: string;
|
||||
private readonly apiKey?: string;
|
||||
private readonly timeoutMs: number;
|
||||
private readonly fetchImpl: typeof fetch;
|
||||
|
||||
constructor(cfg: Required<Pick<DbisCoreConfig, "baseUrl">> & DbisCoreConfig) {
|
||||
this.baseUrl = cfg.baseUrl.replace(/\/+$/, "");
|
||||
this.apiKey = cfg.apiKey;
|
||||
this.timeoutMs = cfg.timeoutMs ?? 10_000;
|
||||
this.fetchImpl = cfg.fetchImpl ?? fetch;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
Accept: "application/json",
|
||||
};
|
||||
if (body !== undefined) headers["Content-Type"] = "application/json";
|
||||
if (this.apiKey) headers["X-API-Key"] = this.apiKey;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
||||
try {
|
||||
const resp = await this.fetchImpl(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
signal: controller.signal,
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const text = await resp.text().catch(() => "");
|
||||
throw new Error(
|
||||
`dbis_core ${method} ${path} failed: HTTP ${resp.status} ${text.slice(0, 200)}`,
|
||||
);
|
||||
}
|
||||
return (await resp.json()) as T;
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
getAccountBalance(accountId: string): Promise<AccountBalance> {
|
||||
return this.request<AccountBalance>(
|
||||
"GET",
|
||||
`/api/accounts/${encodeURIComponent(accountId)}/balance`,
|
||||
);
|
||||
}
|
||||
|
||||
findSettlementRoute(req: RouteRequest): Promise<RouteResponse> {
|
||||
return this.request<RouteResponse>("POST", "/api/isn/route", req);
|
||||
}
|
||||
|
||||
atomicSettle(req: AtomicSettleRequest): Promise<AtomicSettleResponse> {
|
||||
return this.request<AtomicSettleResponse>("POST", "/api/isn/atomic", req);
|
||||
}
|
||||
|
||||
getSettlementStatus(settlementId: string): Promise<SettlementStatus> {
|
||||
return this.request<SettlementStatus>(
|
||||
"GET",
|
||||
`/api/isn/settlements/${encodeURIComponent(settlementId)}`,
|
||||
);
|
||||
}
|
||||
|
||||
requestAriDecision(req: AriDecisionRequest): Promise<AriDecisionResponse> {
|
||||
return this.request<AriDecisionResponse>("POST", "/api/ari/decision", req);
|
||||
}
|
||||
|
||||
dispatchPacs008(req: Pacs008DispatchRequest): Promise<Pacs008DispatchResponse> {
|
||||
return this.request<Pacs008DispatchResponse>(
|
||||
"POST",
|
||||
"/api/v1/gpn/message/pacs008",
|
||||
req,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MockDbisCoreClient implements DbisCoreClient {
|
||||
readonly mode = "mock" as const;
|
||||
|
||||
async getAccountBalance(accountId: string): Promise<AccountBalance> {
|
||||
return {
|
||||
accountId,
|
||||
currency: "USD",
|
||||
available: "1000000.00",
|
||||
held: "0.00",
|
||||
asOf: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async findSettlementRoute(req: RouteRequest): Promise<RouteResponse> {
|
||||
return {
|
||||
routeId: `mock-route-${req.sourceBankId}-${req.destinationBankId}`,
|
||||
hops: [
|
||||
{ bankId: req.sourceBankId, latencyMs: 20, feeBps: 0 },
|
||||
{ bankId: req.destinationBankId, latencyMs: 40, feeBps: 5 },
|
||||
],
|
||||
estimatedLatencyMs: 60,
|
||||
estimatedFeeBps: 5,
|
||||
};
|
||||
}
|
||||
|
||||
async atomicSettle(req: AtomicSettleRequest): Promise<AtomicSettleResponse> {
|
||||
return {
|
||||
settlementId: `mock-stlm-${req.reference}`,
|
||||
status: "settled",
|
||||
completedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getSettlementStatus(settlementId: string): Promise<SettlementStatus> {
|
||||
return {
|
||||
settlementId,
|
||||
status: "settled",
|
||||
legs: [
|
||||
{ legId: `${settlementId}-leg1`, bankId: "mock-src", status: "confirmed" },
|
||||
{ legId: `${settlementId}-leg2`, bankId: "mock-dst", status: "confirmed" },
|
||||
],
|
||||
lastUpdated: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async requestAriDecision(req: AriDecisionRequest): Promise<AriDecisionResponse> {
|
||||
return {
|
||||
txId: req.txId,
|
||||
outcome: "allow",
|
||||
riskScore: 0.1,
|
||||
reasons: ["mock: default allow"],
|
||||
evaluatedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async dispatchPacs008(req: Pacs008DispatchRequest): Promise<Pacs008DispatchResponse> {
|
||||
return {
|
||||
messageId: req.messageId,
|
||||
status: "accepted",
|
||||
acknowledgmentRef: `mock-ack-${req.messageId}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory. Call once per process (or per test run) to get a client
|
||||
* wired to whichever backend the env selects.
|
||||
*/
|
||||
export function createDbisCoreClient(
|
||||
cfg: DbisCoreConfig = loadConfigFromEnv(),
|
||||
): DbisCoreClient {
|
||||
if (cfg.baseUrl) {
|
||||
logger.info({ baseUrl: cfg.baseUrl, mode: "live" }, "[DbisCore] HTTP client");
|
||||
return new HttpDbisCoreClient({ ...cfg, baseUrl: cfg.baseUrl });
|
||||
}
|
||||
logger.info({ mode: "mock" }, "[DbisCore] HTTP client (no DBIS_CORE_URL — mock mode)");
|
||||
return new MockDbisCoreClient();
|
||||
}
|
||||
9
orchestrator/src/services/dbisCore/index.ts
Normal file
9
orchestrator/src/services/dbisCore/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Public surface for the dbis_core client adapter.
|
||||
* See ./client.ts for implementation and ./types.ts for the shared
|
||||
* request/response shapes.
|
||||
*/
|
||||
|
||||
export * from "./types";
|
||||
export { createDbisCoreClient } from "./client";
|
||||
export type { DbisCoreClient, DbisCoreConfig } from "./client";
|
||||
106
orchestrator/src/services/dbisCore/types.ts
Normal file
106
orchestrator/src/services/dbisCore/types.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Canonical request/response shapes for the subset of `d-bis/dbis_core`
|
||||
* endpoints the orchestrator actually calls. Kept small and focused —
|
||||
* this is a client adapter, not a mirror of the upstream service.
|
||||
*
|
||||
* Upstream endpoint references (from dbis_core/src/integration/api-
|
||||
* gateway/app.ts mount points):
|
||||
*
|
||||
* GET /api/accounts/:accountId/balance
|
||||
* POST /api/isn/route
|
||||
* POST /api/isn/atomic
|
||||
* POST /api/ari/decision
|
||||
* POST /api/v1/gpn/message/pacs008
|
||||
* GET /api/isn/settlements/:settlementId
|
||||
*/
|
||||
|
||||
export interface AccountBalance {
|
||||
accountId: string;
|
||||
currency: string;
|
||||
available: string;
|
||||
held: string;
|
||||
asOf: string;
|
||||
}
|
||||
|
||||
export interface RouteRequest {
|
||||
sourceBankId: string;
|
||||
destinationBankId: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
}
|
||||
|
||||
export interface SettlementHop {
|
||||
bankId: string;
|
||||
latencyMs: number;
|
||||
feeBps: number;
|
||||
}
|
||||
|
||||
export interface RouteResponse {
|
||||
routeId: string;
|
||||
hops: SettlementHop[];
|
||||
estimatedLatencyMs: number;
|
||||
estimatedFeeBps: number;
|
||||
}
|
||||
|
||||
export interface AtomicSettleRequest {
|
||||
routeId: string;
|
||||
sourceAccountId: string;
|
||||
destinationAccountId: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
reference: string;
|
||||
}
|
||||
|
||||
export interface AtomicSettleResponse {
|
||||
settlementId: string;
|
||||
status: "accepted" | "settled" | "rejected";
|
||||
completedAt?: string;
|
||||
rejectionReason?: string;
|
||||
}
|
||||
|
||||
export interface AriDecisionRequest {
|
||||
txId: string;
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
creator: string;
|
||||
counterparty?: string;
|
||||
metadata?: Record<string, string | number | boolean>;
|
||||
}
|
||||
|
||||
export type AriOutcome = "allow" | "deny" | "review";
|
||||
|
||||
export interface AriDecisionResponse {
|
||||
txId: string;
|
||||
outcome: AriOutcome;
|
||||
riskScore: number;
|
||||
reasons: string[];
|
||||
evaluatedAt: string;
|
||||
}
|
||||
|
||||
export interface Pacs008DispatchRequest {
|
||||
messageId: string;
|
||||
creationDateTime: string;
|
||||
debtor: { name: string; bic: string; account: string };
|
||||
creditor: { name: string; bic: string; account: string };
|
||||
amount: string;
|
||||
currencyCode: string;
|
||||
remittanceInfo?: string;
|
||||
}
|
||||
|
||||
export interface Pacs008DispatchResponse {
|
||||
messageId: string;
|
||||
status: "accepted" | "rejected";
|
||||
acknowledgmentRef?: string;
|
||||
rejectionReason?: string;
|
||||
}
|
||||
|
||||
export interface SettlementStatus {
|
||||
settlementId: string;
|
||||
status: "pending" | "routing" | "executing" | "settled" | "failed" | "reversed";
|
||||
legs: {
|
||||
legId: string;
|
||||
bankId: string;
|
||||
status: "pending" | "dispatched" | "confirmed" | "failed";
|
||||
}[];
|
||||
lastUpdated: string;
|
||||
}
|
||||
Reference in New Issue
Block a user