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:
Nathnael
2026-08-12 09:31:31 +00:00
parent 879f0b890d
commit bd9f7f354a
11 changed files with 295 additions and 14 deletions

View File

@@ -7,9 +7,16 @@ import {
Repository,
} from "typeorm";
import { logCtx } from "../logging/request-context";
export abstract class BaseRepository<T extends ObjectLiteral> {
protected constructor(protected readonly repository: Repository<T>) {}
/** Table name, for the write trail on the canonical request log line. */
private get table(): string {
return this.repository.metadata.tableName;
}
/** Find a single entity by its primary key. */
async findById(
id: string,
@@ -34,22 +41,43 @@ export abstract class BaseRepository<T extends ObjectLiteral> {
/** Create and persist a new entity. */
async create(data: DeepPartial<T>): Promise<T> {
const entity = this.repository.create(data);
return this.repository.save(entity);
const saved = await this.repository.save(entity);
logCtx(1, { path: `db.created.${this.table}`, mode: "count" });
return saved;
}
/** Patch an entity in place and return the reloaded row. */
/**
* Patch an entity in place and return the reloaded row.
*
* Every domain status machine in this app (booking, contract, wagon,
* warehouse, transfer request…) lands here, so this is the one place that can
* record "what state did this request actually move" without every service
* remembering to log it.
*/
async update(id: string, data: DeepPartial<T>): Promise<T | null> {
await this.repository.update(id, data as never);
logCtx(1, { path: `db.updated.${this.table}`, mode: "count" });
if (data && typeof data === "object" && "status" in data) {
logCtx(
{ entity: this.table, id, status: (data as { status: unknown }).status },
{ path: "statusChanges", mode: "push" },
);
}
return this.findById(id);
}
/** Soft-delete an entity by primary key (sets deleted_at). */
async softDelete(id: string): Promise<void> {
await this.repository.softDelete(id);
logCtx({ entity: this.table, id }, { path: "deleted", mode: "push" });
}
/** Permanently delete an entity. Avoid in domain code; prefer softDelete. */
async hardDelete(id: string): Promise<void> {
await this.repository.delete(id);
logCtx(
{ entity: this.table, id, hard: true },
{ path: "deleted", mode: "push" },
);
}
}