// health.controller.ts import { Controller, Get, HttpStatus, Res } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; import { InjectDataSource } from "@nestjs/typeorm"; import { Public } from "@edr/api-common"; import { Response } from "express"; import { DataSource } from "typeorm"; import { EmailClientService } from "../notifications/email-client.service"; import { SmsClientService } from "../notifications/sms-client.service"; type CheckStatus = "ok" | "error" | "unknown"; /** * Readiness normally stays green when only the broker is down. * * A 503 pulls the pod out of the load balancer, which would take booking, * tracking and billing offline because SMS is unreachable — a strictly worse * outcome than degraded notifications. The broker check is therefore reported, * not enforced, and `READINESS_REQUIRES_BROKER=true` opts into hard-failing for * deployments where a silent OTP black hole is the greater risk. */ const READINESS_REQUIRES_BROKER = process.env.READINESS_REQUIRES_BROKER === "true"; @ApiTags("Health") @Controller("health") export class HealthController { constructor( @InjectDataSource() private readonly dataSource: DataSource, private readonly smsClient: SmsClientService, private readonly emailClient: EmailClientService, ) {} @Get() @Public() @ApiOperation({ summary: "Liveness probe" }) liveness() { return { status: "ok", timestamp: new Date().toISOString() }; } @Get("ready") @Public() @ApiOperation({ summary: "Readiness probe — database plus SMS/email broker connectivity. Broker failures report as degraded unless READINESS_REQUIRES_BROKER=true.", }) async readiness(@Res() res: Response) { const startedAt = Date.now(); let database: { status: CheckStatus; latencyMs: number; error?: string }; try { await this.dataSource.query("SELECT 1"); database = { status: "ok", latencyMs: Date.now() - startedAt }; } catch (error) { database = { status: "error", latencyMs: Date.now() - startedAt, error: error instanceof Error ? error.message : "Unknown error", }; } // `null` from the client means the connection manager was not reachable // through Nest's internals — surfaced as "unknown" so a shape change in // @nestjs/microservices degrades to honest ignorance, not a false "ok". const toStatus = (connected: boolean | null): CheckStatus => connected === null ? "unknown" : connected ? "ok" : "error"; const broker = { sms: { status: toStatus(this.smsClient.brokerConnected) }, email: { status: toStatus(this.emailClient.brokerConnected) }, // Every OTP, and every booking/billing notification, publishes through // these. `error` here means codes are being generated and silently dropped. enabled: process.env.RABBITMQ_ENABLED !== "false", }; const brokerDown = broker.sms.status === "error" || broker.email.status === "error"; const failed = database.status === "error" || (READINESS_REQUIRES_BROKER && brokerDown); const status = failed ? "error" : brokerDown ? "degraded" : "ok"; return res .status(failed ? HttpStatus.SERVICE_UNAVAILABLE : HttpStatus.OK) .json({ status, timestamp: new Date().toISOString(), checks: { database, broker }, }); } @Get("info") @Public() @ApiOperation({ summary: "App info — version, environment, uptime" }) info() { return { name: "edr-freight-api", version: process.env.npm_package_version ?? "1.0.0", environment: process.env.NODE_ENV ?? "development", uptimeSeconds: Math.floor(process.uptime()), timestamp: new Date().toISOString(), }; } }