feat: add expiration to invoice

This commit is contained in:
Nathnael
2026-07-01 06:19:07 +00:00
parent 0056dec924
commit 8ab27a6721
6 changed files with 105 additions and 0 deletions

View File

@@ -110,6 +110,7 @@ describe('BookingBatchService — PAID reconcile', () => {
notifier as never,
{ addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never,
trainSchedulingService as never,
{ syncPayableDueDate: jest.fn(), expirePayable: jest.fn() } as never,
);
});

View File

@@ -8,7 +8,9 @@ import {
import { InjectDataSource } from '@nestjs/typeorm';
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
import { DataSource } from 'typeorm';
import { Freight } from '@edr/types';
import { BillingService } from '../billing/billing.service';
import { Booking } from '../bookings/entities/booking.entity';
import { BookingsRepository } from '../bookings/bookings.repository';
import { Locomotive } from '../locomotives/entities/locomotive.entity';
@@ -179,6 +181,7 @@ export class BookingBatchService implements OnModuleInit {
private readonly notifier: BookingNotifierService,
private readonly scheduler: SchedulerRegistry,
private readonly trainSchedulingService: TrainSchedulingService,
private readonly billing: BillingService,
) {}
/** On boot, reconcile OPEN route-days and re-arm settle timers. */
@@ -972,6 +975,13 @@ export class BookingBatchService implements OnModuleInit {
paymentDeadline: deadline,
} as never);
booking.trainScheduleId = scheduleId;
// The invoice was generated at booking creation/approval, before this pay
// window opened — refresh its printed due date to the real deadline.
await this.billing.syncPayableDueDate(
Freight.InvoiceSource.Booking,
booking.id,
deadline,
);
await this.notifier.payNow(booking, deadline);
}
@@ -1018,6 +1028,10 @@ export class BookingBatchService implements OnModuleInit {
selectedForBatchAt: null,
} as never);
booking.trainScheduleId = null;
// Pay window closed before settlement → expire the booking's open invoice too
// (emits `booking.invoice.expired`). Domain owns the reaction; billing stays
// source-agnostic.
await this.billing.expirePayable(Freight.InvoiceSource.Booking, booking.id);
this.notifier.expired(booking);
}
@@ -1057,6 +1071,13 @@ export class BookingBatchService implements OnModuleInit {
paymentDeadline: null,
selectedForBatchAt: null,
} as never);
// Displaced → EXPIRED: close its open invoice too, so a dead booking
// can't still be paid (mirrors `expire()`; enlisted in this txn).
await this.billing.expirePayable(
Freight.InvoiceSource.Booking,
victim.id,
manager,
);
});
this.notifier.displaced(victim);
freed = this.add(freed, this.needFor(victim, wagonLengths));

View File

@@ -1,6 +1,7 @@
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BillingModule } from '../billing/billing.module';
import { BookingsModule } from '../bookings/bookings.module';
import { Container } from '../container-management/entities/container.entity';
import { LocomotivesModule } from '../locomotives/locomotives.module';
@@ -42,6 +43,7 @@ import { NotificationsModule } from '../notifications/notifications.module';
ImportDjiboutiOperation,
]),
forwardRef(() => BookingsModule),
BillingModule,
NotificationsModule,
LocomotivesModule,
WagonTypesModule,