mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
changes
This commit is contained in:
@@ -1016,7 +1016,7 @@ export class BillingService {
|
||||
// in the domain via `${source}.invoice.paid`. Neither billing nor the payment
|
||||
// service branches on a domain-specific reference type.
|
||||
referenceType: PaymentReferenceType.SHIPMENT,
|
||||
orderRef: invoice.invoiceNumber.replace("-", "_"),
|
||||
orderRef: invoice.invoiceNumber.replace(/-/g, "_"),
|
||||
amountMinor: Math.round(Number(invoice.balanceAmount)),
|
||||
currency: invoice.currency,
|
||||
reason: `Payment for invoice ${invoice.invoiceNumber}`,
|
||||
@@ -1032,20 +1032,18 @@ export class BillingService {
|
||||
.getRepository(Invoice)
|
||||
.update({ id: invoice.id }, { paymentId: result.intentId });
|
||||
|
||||
// DEMO: manually fire the gateway `payment.succeeded` callback here, without
|
||||
// waiting for real gateway settlement. Runs AFTER the paymentId link above so
|
||||
// `handlePaymentEvent → settleByPaymentId` can correlate the invoice. TODO:
|
||||
// remove — real settlement flips this via the `${source}.invoice.paid` handler.
|
||||
if (!result.immediateSuccess) {
|
||||
await this.payment.handlePaymentEvent({
|
||||
eventType: "payment.succeeded",
|
||||
eventId: `demo-${result.intentId}`,
|
||||
referenceId: invoice.sourceId,
|
||||
intentId: result.intentId,
|
||||
providerTxnId: result.providerTxnId,
|
||||
paidAt: (result.paidAt ?? new Date()).toISOString(),
|
||||
});
|
||||
}
|
||||
// Settlement is driven by the payment API (webhook/outbox → payment.succeeded);
|
||||
// billing must not simulate it. Kept commented for local demos only.
|
||||
// if (!result.immediateSuccess) {
|
||||
// await this.payment.handlePaymentEvent({
|
||||
// eventType: "payment.succeeded",
|
||||
// eventId: `demo-${result.intentId}`,
|
||||
// referenceId: invoice.sourceId,
|
||||
// intentId: result.intentId,
|
||||
// providerTxnId: result.providerTxnId,
|
||||
// paidAt: (result.paidAt ?? new Date()).toISOString(),
|
||||
// });
|
||||
// }
|
||||
|
||||
if (result.immediateSuccess) {
|
||||
await this.settleByPaymentId(
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
InvoiceLineInput,
|
||||
} from "../billing/billing.service";
|
||||
import { Invoice } from "../billing/entities/invoice.entity";
|
||||
import { CLEARANCE_BOOKING_INVOICE_TYPE } from "../contracts/clearance-fee.service";
|
||||
import { FirstMileService } from "../first-mile/first-mile.service";
|
||||
import { BookingBatchService } from "../train-scheduling/booking-batch.service";
|
||||
import { PriceLineItemDto } from "./dto/generate-price-response.dto";
|
||||
@@ -120,17 +121,27 @@ export class BookingInvoiceService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Expire the booking's currently-open prepaid invoice when the booking is
|
||||
* Expire the booking's currently-open invoices (freight PREPAID and the
|
||||
* per-shipment clearance fee) when the booking is
|
||||
* cancelled or rejected — the counterpart to the pay-window-expiry path
|
||||
* (which also calls {@link BillingService.expirePayable}). Stops a terminated
|
||||
* booking from leaving a payable invoice open. No-op when the booking has no
|
||||
* open invoice (never invoiced, already paid/cancelled/expired). Pass a
|
||||
* caller `manager` to enlist in its transaction.
|
||||
*/
|
||||
expireOpenInvoices(
|
||||
async expireOpenInvoices(
|
||||
bookingId: string,
|
||||
manager?: EntityManager,
|
||||
): Promise<Invoice | null> {
|
||||
// The per-shipment clearance fee (GENERAL contracts) bills this same booking
|
||||
// id under its own source/type — retire it alongside the freight invoice, or
|
||||
// a cancelled shipment keeps a payable clearance invoice open.
|
||||
await this.billing.expirePayable(
|
||||
Freight.InvoiceSource.Clearance,
|
||||
bookingId,
|
||||
CLEARANCE_BOOKING_INVOICE_TYPE,
|
||||
manager,
|
||||
);
|
||||
return this.billing.expirePayable(
|
||||
Freight.InvoiceSource.Booking,
|
||||
bookingId,
|
||||
|
||||
@@ -161,6 +161,21 @@ export class ClearanceFeeService {
|
||||
return invoice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire (idempotently) the unpaid contract-level fee invoice when the
|
||||
* contract reaches a terminal state — a dead contract must not leave a
|
||||
* payable clearance invoice open for the customer to settle. No-op when the
|
||||
* fee was already paid or never invoiced (mirrors the booking cancel path,
|
||||
* {@link BillingService.expirePayable}).
|
||||
*/
|
||||
async expireForContract(contractId: string): Promise<Invoice | null> {
|
||||
return this.billing.expirePayable(
|
||||
Freight.InvoiceSource.Clearance,
|
||||
contractId,
|
||||
CLEARANCE_CONTRACT_INVOICE_TYPE,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settlement branch point for `clearance`-source invoices: unlock the
|
||||
* document-upload step the fee was gating. Idempotent — a replayed event on
|
||||
|
||||
@@ -419,6 +419,10 @@ export class ContractTransitionService {
|
||||
actorId,
|
||||
'STAFF',
|
||||
);
|
||||
// Stop the open-invoice leak: a rejected contract must not leave a payable
|
||||
// clearance fee invoice open. Mirror the booking cancel path (billing.expirePayable).
|
||||
await this.clearanceFeeService.expireForContract(contractId);
|
||||
|
||||
await this.contractsRepository.update(contractId, {
|
||||
status: 'REJECTED',
|
||||
} as never);
|
||||
@@ -457,6 +461,10 @@ export class ContractTransitionService {
|
||||
'STAFF',
|
||||
);
|
||||
|
||||
// Stop the open-invoice leak: a rejected contract must not leave a payable
|
||||
// clearance fee invoice open. Mirror the booking cancel path (billing.expirePayable).
|
||||
await this.clearanceFeeService.expireForContract(contractId);
|
||||
|
||||
await this.contractsRepository.update(contractId, {
|
||||
status: 'REJECTED',
|
||||
} as never);
|
||||
|
||||
Reference in New Issue
Block a user