Files
edr-platform/apps/finance-api/src/modules/revenue/revenue.module.ts
2026-08-25 00:11:39 +03:00

103 lines
3.8 KiB
TypeScript

import { DynamicModule, Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { RabbitMQModule } from "@golevelup/nestjs-rabbitmq";
import { PAYMENT_EVENTS_DLX, PAYMENT_EVENTS_EXCHANGE } from "@edr/types";
import { CutoverModule } from "../cutover/cutover.module";
import { RevenueMapping } from "./entities/revenue-mapping.entity";
import { InboundEvent } from "./entities/inbound-event.entity";
import {
InboundEventsRepository,
RevenueMappingsRepository,
} from "./revenue.repository";
import { RevenueMappingsService } from "./revenue-mappings.service";
import { RevenueProjectionService } from "./revenue-projection.service";
import { RevenuePostingService } from "./revenue-posting.service";
import { RevenueController } from "./revenue.controller";
import {
FINANCE_PAYMENT_BINDING,
FINANCE_PAYMENT_DLQ,
PaymentEventsConsumer,
} from "./payment-events.consumer";
import { AccountsModule } from "../accounts/accounts.module";
import { JournalsModule } from "../journals/journals.module";
/**
* The broker is OPTIONAL.
*
* Without `PAYMENT_RABBITMQ_URL` this returns nothing and the app boots with
* every screen and projection working — only live payment ingest is off. That
* matters because Finance is useful long before it is wired to a broker (the
* chart, journals, periods and projections need no messaging at all), and
* because a developer without RabbitMQ should not be blocked from running it.
*
* Same gate freight and passenger use, so all three behave alike.
*/
function rabbitMQImport(): DynamicModule[] {
if (!process.env.PAYMENT_RABBITMQ_URL) return [];
return [
RabbitMQModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
uri:
config.get<string>("rabbitmq.url") ??
process.env.PAYMENT_RABBITMQ_URL ??
"",
exchanges: [
{ name: PAYMENT_EVENTS_EXCHANGE, type: "topic", options: { durable: true } },
{ name: PAYMENT_EVENTS_DLX, type: "topic", options: { durable: true } },
],
// The dead-letter queue is declared HERE, not by @RabbitSubscribe —
// that decorator only declares the main queue and its DLX pointer. With
// no bound DLQ, a dead-lettered message is silently discarded by the
// broker. Freight and passenger declare theirs the same way.
queues: [
{
name: FINANCE_PAYMENT_DLQ,
exchange: PAYMENT_EVENTS_DLX,
routingKey: FINANCE_PAYMENT_BINDING,
options: { durable: true },
},
],
prefetchCount: Number(process.env.PAYMENT_EVENTS_PREFETCH ?? 10),
// Do not block boot on the broker: the API must still serve its
// read-only screens when messaging is down.
connectionInitOptions: { wait: false },
}),
}),
];
}
@Module({
imports: [
TypeOrmModule.forFeature([RevenueMapping, InboundEvent]),
AccountsModule,
JournalsModule,
// For the cutover boundary: a payment settled before go-live is already
// inside the opening balances and must not post again.
CutoverModule,
...rabbitMQImport(),
],
controllers: [RevenueController],
providers: [
RevenueMappingsRepository,
InboundEventsRepository,
RevenueMappingsService,
RevenueProjectionService,
RevenuePostingService,
// Registered unconditionally. Without RabbitMQModule the @RabbitSubscribe
// decorator is inert, so the class is simply never invoked — and it stays
// directly callable, which is how the consumer's idempotency is tested
// without standing up a broker.
PaymentEventsConsumer,
],
exports: [
RevenueProjectionService,
RevenuePostingService,
RevenueMappingsRepository,
],
})
export class RevenueModule {}