mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 08:32:54 +00:00
Add manual single-invoice registration, verification and reconciliation. Nothing submits automatically; invoice creation is untouched. Sequencing uses a durable reservation. The counter is consumed and the holder recorded in a committed transaction before the request leaves the process, and the HTTP call runs outside every transaction. A counter is therefore never reused once an attempt begins, a crash mid-flight leaves the reservation standing instead of inviting a blind resubmission, and an ambiguous result blocks the whole system number rather than one invoice -- PreviousIrn is unknown, so any later document would chain to a stale IRN. Deterministic rejections (400/406/401/403) mark the invoice FAILED and clear the block. Timeouts and 5xx mark it UNKNOWN and keep it. Since /v1/verify takes an IRN we never received in that case, POST :id/eims/resolve is the exit: record the IRN confirmed in the MoR portal, or discard. A recorded IRN is verified against the gateway first and refused unless EIMS reports it against this invoice's document number. Business and tax configuration is validated locally before anything is locked, allocated or sent, so a missing tax code fails naming the exact environment variables instead of at the gateway. No tax value is defaulted. Filing gets its own permission (invoices:eims_register) rather than riding on invoices:export -- registration is irreversible at MoR and must not follow from the right to download a PDF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
3.0 KiB
TypeScript
81 lines
3.0 KiB
TypeScript
import { HttpService } from "@nestjs/axios";
|
|
import { Injectable, Logger } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import { firstValueFrom } from "rxjs";
|
|
import { EimsConfig } from "../../config/eims.config";
|
|
import { EimsAuthService } from "./eims-auth.service";
|
|
import { EimsSignerService, toSignedBody } from "./eims-signer.service";
|
|
import { toEimsApiException } from "./eims.errors";
|
|
|
|
/**
|
|
* Foundation for EIMS's bearer-authenticated endpoints (`/v1/register`, `/v1/verify`, …).
|
|
*
|
|
* Login is not routed through here: `/auth/login` carries no bearer token and lives in
|
|
* `EimsAuthService`. Nothing calls `postSigned` yet — invoice registration is a later phase.
|
|
*/
|
|
@Injectable()
|
|
export class EimsClientService {
|
|
private readonly logger = new Logger(EimsClientService.name);
|
|
|
|
constructor(
|
|
private readonly http: HttpService,
|
|
private readonly config: ConfigService,
|
|
private readonly auth: EimsAuthService,
|
|
private readonly signer: EimsSignerService,
|
|
) {}
|
|
|
|
private get cfg(): EimsConfig {
|
|
return this.config.get<EimsConfig>("eims")!;
|
|
}
|
|
|
|
/**
|
|
* Sign `request`, POST it to `path` with a valid bearer token, and return the parsed response.
|
|
* A 401 invalidates the cached token and retries exactly once.
|
|
*/
|
|
async postSigned<TRequest, TResponse>(path: string, request: TRequest): Promise<TResponse> {
|
|
return this.send<TRequest, TResponse>(path, request, false, true);
|
|
}
|
|
|
|
/**
|
|
* POST `request` verbatim — bearer-authenticated but **not** wrapped in a signed envelope.
|
|
*
|
|
* `/v1/verify` is the only endpoint observed to work this way: the supplied collection sends a
|
|
* raw `{"irn":"…"}` body with no `signature`/`certificate` siblings. Kept as its own entry point
|
|
* so that if the live gateway turns out to require signing after all, exactly one call site
|
|
* changes — `postSigned` is already the alternative.
|
|
*/
|
|
async postBearer<TRequest, TResponse>(path: string, request: TRequest): Promise<TResponse> {
|
|
return this.send<TRequest, TResponse>(path, request, false, false);
|
|
}
|
|
|
|
private async send<TRequest, TResponse>(
|
|
path: string,
|
|
request: TRequest,
|
|
isRetry: boolean,
|
|
signed: boolean,
|
|
): Promise<TResponse> {
|
|
const cfg = this.cfg;
|
|
const token = await this.auth.getValidAccessToken();
|
|
const body = signed ? toSignedBody(this.signer.signRequest(request)) : request;
|
|
|
|
try {
|
|
const res = await firstValueFrom(
|
|
this.http.post<TResponse>(`${cfg.baseUrl}${path}`, body, {
|
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
timeout: cfg.httpTimeoutMs,
|
|
}),
|
|
);
|
|
return res.data;
|
|
} catch (err) {
|
|
const mapped = toEimsApiException(err, `POST ${path}`);
|
|
if (mapped.kind === "AUTH" && !isRetry) {
|
|
this.logger.warn(`EIMS rejected the token on ${path}; re-authenticating once`);
|
|
this.auth.invalidate();
|
|
return this.send<TRequest, TResponse>(path, request, true, signed);
|
|
}
|
|
this.logger.error(mapped.message);
|
|
throw mapped;
|
|
}
|
|
}
|
|
}
|