mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
feat(freight-api): enrich canonical request log line
Emit the request line as raw JSON on stdout (level/time/logger as fields) instead of through Nest's console logger, whose prefix made it unparsable. Collect data points via logCtx at the flow chokepoints: BaseRepository writes (status changes, creates, deletes), invoice transitions, payment intent lifecycle + outbound payment-service calls, booking/contract entry state, review-note reasons, signatures and OTP verify outcomes.
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
import { HttpService } from "@nestjs/axios";
|
||||
import { AxiosError } from "axios";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { logCtx } from "@edr/api-common";
|
||||
import {
|
||||
InitiatePaymentRequest,
|
||||
PaymentIntentSnapshot,
|
||||
@@ -111,6 +112,16 @@ export class PaymentClientService {
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
// Every hop to the payment service lands on the request log line: which
|
||||
// call, how slow, and what it answered. A settle that never happened is
|
||||
// almost always one of these coming back 4xx/5xx or timing out.
|
||||
const startedAt = Date.now();
|
||||
const trace = (extra: Record<string, unknown>) =>
|
||||
logCtx(
|
||||
{ method, path, ms: Date.now() - startedAt, ...extra },
|
||||
{ path: "outbound.payment", mode: "push" },
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.http.request<T>({
|
||||
@@ -122,9 +133,11 @@ export class PaymentClientService {
|
||||
: {},
|
||||
}),
|
||||
);
|
||||
trace({ status: response.status });
|
||||
return response.data;
|
||||
} catch (err) {
|
||||
if (err instanceof AxiosError && err.response) {
|
||||
trace({ status: err.response.status });
|
||||
if (err.response.status === 404) throw err;
|
||||
const detail =
|
||||
(err.response.data as { message?: string | string[] })?.message ??
|
||||
@@ -136,6 +149,7 @@ export class PaymentClientService {
|
||||
}
|
||||
const message =
|
||||
err instanceof Error && err.message ? err.message : String(err);
|
||||
trace({ unreachable: true, error: message });
|
||||
this.logger.error(
|
||||
`payment service unreachable (${method} ${path}): ${message}`,
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
Logger,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { logCtx } from "@edr/api-common";
|
||||
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
|
||||
import { PaymentEntity } from "./entities/payment.entity";
|
||||
import { PaymentRepository } from "./payment.repository";
|
||||
@@ -214,8 +215,16 @@ export class PaymentService {
|
||||
PaymentReferenceType.SHIPMENT,
|
||||
referenceId,
|
||||
);
|
||||
logCtx(
|
||||
{ referenceId, paid: result.paid, unverifiable: result.unverifiable },
|
||||
{ path: "payment.reconcile" },
|
||||
);
|
||||
return { paid: result.paid, unverifiable: result.unverifiable };
|
||||
} catch (err) {
|
||||
logCtx(
|
||||
{ referenceId, unverifiable: true, error: (err as Error).message },
|
||||
{ path: "payment.reconcile" },
|
||||
);
|
||||
this.logger.warn(
|
||||
`Reconcile for shipment ${referenceId} failed: ${(err as Error).message}`,
|
||||
);
|
||||
@@ -224,6 +233,18 @@ export class PaymentService {
|
||||
}
|
||||
|
||||
async initiate(input: InitiateIntentInput): Promise<InitiateIntentResult> {
|
||||
logCtx(
|
||||
{
|
||||
referenceId: input.referenceId,
|
||||
source: input.source,
|
||||
orderRef: input.orderRef,
|
||||
method: input.method,
|
||||
amountMinor: input.amountMinor,
|
||||
currency: input.currency,
|
||||
platform: input.platform,
|
||||
},
|
||||
{ path: "payment.initiate" },
|
||||
);
|
||||
try {
|
||||
const isCbeBill = input.method === ProviderMethod.CBE_BILL;
|
||||
// CBE settles ETB only (docs/cbe/CBE_IMPLEMENTATION_PLAN.md D8).
|
||||
@@ -268,6 +289,17 @@ export class PaymentService {
|
||||
|
||||
const intent = await this.upsertIntent(input, snapshot);
|
||||
|
||||
logCtx(
|
||||
{
|
||||
intentId: intent.id,
|
||||
providerStatus: snapshot.status,
|
||||
providerTxnId: snapshot.providerTxnId,
|
||||
merchantOrderId: snapshot.merchantOrderId,
|
||||
immediateSuccess,
|
||||
},
|
||||
{ path: "payment.initiate" },
|
||||
);
|
||||
|
||||
if (immediateSuccess) {
|
||||
// Settle the projection but DO NOT notify billing — billing settles
|
||||
// inline once it has stored intentId on the invoice (see payInvoice),
|
||||
@@ -428,6 +460,17 @@ export class PaymentService {
|
||||
otp,
|
||||
);
|
||||
|
||||
logCtx(
|
||||
{
|
||||
intentId: local.id,
|
||||
refId: local.refId,
|
||||
gatewayIntentId: snapshot.intentId,
|
||||
providerStatus: confirmed.status,
|
||||
failureCode: confirmed.failureCode,
|
||||
},
|
||||
{ path: "payment.confirmOtp" },
|
||||
);
|
||||
|
||||
if (confirmed.status === ProviderPaymentStatus.SUCCEEDED) {
|
||||
await this.markIntentSucceeded(local.id, {
|
||||
providerTxnId: confirmed.providerTxnId,
|
||||
@@ -460,6 +503,16 @@ export class PaymentService {
|
||||
): Promise<{ alreadyFinalized: boolean }> {
|
||||
const intent = await this.paymentRepo.findOneBy({ id: intentId });
|
||||
if (!intent) throw new NotFoundException("PaymentIntent not found");
|
||||
logCtx(
|
||||
{
|
||||
intentId,
|
||||
refId: intent.refId,
|
||||
priorStatus: intent.status,
|
||||
providerTxnId: opts.providerTxnId,
|
||||
notifyBilling: opts.notify !== false,
|
||||
},
|
||||
{ path: "payment.settle" },
|
||||
);
|
||||
if (intent.status === "success") {
|
||||
// Still notify billing: a prior delivery may have flipped the intent to
|
||||
// success and then died before the invoice settled (the two steps are not
|
||||
@@ -471,6 +524,7 @@ export class PaymentService {
|
||||
opts.paidAt ?? intent.paidAt ?? undefined,
|
||||
);
|
||||
}
|
||||
logCtx(true, { path: "payment.settle.alreadyFinalized", mode: "set" });
|
||||
return { alreadyFinalized: true };
|
||||
}
|
||||
|
||||
@@ -507,6 +561,15 @@ export class PaymentService {
|
||||
referenceId: string,
|
||||
): Promise<{ acknowledged: boolean }> {
|
||||
const intent = await this.paymentRepo.findOneBy({ refId: referenceId });
|
||||
logCtx(
|
||||
{
|
||||
referenceId,
|
||||
intentId: intent?.id,
|
||||
intentStatus: intent?.status ?? "none",
|
||||
method: intent?.method,
|
||||
},
|
||||
{ path: "payment.successRedirect" },
|
||||
);
|
||||
if (!intent || intent.method === "cbe-bill") {
|
||||
return { acknowledged: false };
|
||||
}
|
||||
@@ -533,6 +596,16 @@ export class PaymentService {
|
||||
}): Promise<void> {
|
||||
const intent = await this.paymentRepo.findOneBy({ id: input.intentId });
|
||||
if (!intent) throw new NotFoundException("PaymentIntent not found");
|
||||
logCtx(
|
||||
{
|
||||
intentId: intent.id,
|
||||
refId: intent.refId,
|
||||
priorStatus: intent.status,
|
||||
failureCode: input.failureCode,
|
||||
failureMessage: input.failureMessage,
|
||||
},
|
||||
{ path: "payment.failed" },
|
||||
);
|
||||
if (intent.status === "success" || intent.status === "canceled") return;
|
||||
|
||||
await this.paymentRepo.update(
|
||||
|
||||
Reference in New Issue
Block a user