Merge pull request #1329 from Tria-plc/freight_feature/usermanagement

feat(freight): offer built-train wagons per boarding yard on multi-ya…
This commit is contained in:
marshal
2026-08-18 11:28:32 +03:00
committed by GitHub
33 changed files with 1234 additions and 231 deletions

View File

@@ -81,6 +81,7 @@ describe("BillingService.generateInvoice", () => {
{} as never, // invoiceDocuments
{} as never, // files
{ get: () => undefined } as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
});
@@ -163,6 +164,7 @@ describe("BillingService.issueMemo", () => {
{} as never,
{} as never,
{ get: () => undefined } as never,
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, manager, savedLines };
}
@@ -297,6 +299,7 @@ describe("BillingService.markInvoiceAsPaid", () => {
{} as never, // invoiceDocuments
{} as never, // files
{ get: () => undefined } as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
await service.markInvoiceAsPaid("inv-1", "pay-1", mg as never);
@@ -352,6 +355,7 @@ describe("BillingService.markInvoiceAsPaid", () => {
{} as never, // invoiceDocuments
{} as never, // files
{ get: () => undefined } as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
await service.markInvoiceAsPaid("inv-1", "pay-1", mg as never);
@@ -397,6 +401,7 @@ describe("BillingService.settleByPaymentId", () => {
{} as never, // invoiceDocuments
{} as never, // files
{ get: () => undefined } as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, mg, events };
}
@@ -510,6 +515,7 @@ describe("BillingService.recordPayment", () => {
{} as never, // invoiceDocuments
{} as never, // files
{ get: () => undefined } as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, mg, events };
}
@@ -627,6 +633,7 @@ describe("BillingService.expirePayable — locked write runs in a transaction",
{} as never,
{} as never,
{} as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, defaultManager, txManager, transaction };
};
@@ -700,6 +707,7 @@ describe("BillingService.issuePayable", () => {
{} as never,
{} as never,
{} as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, manager };
};
@@ -791,6 +799,7 @@ describe("BillingService — CAC Bank (OTP debit)", () => {
{} as never,
{} as never,
{} as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, repo };
};
@@ -874,6 +883,7 @@ describe("BillingService — CBE bill amounts carry cents, never rounded", () =>
{} as never,
{} as never,
{} as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, repo };
};
@@ -943,6 +953,7 @@ describe("BillingService.document", () => {
? { tin: "0053481357", invoice: { sellerVatNumber: "43256663343256663322" } }
: undefined,
} as never, // config
{ isEnabled: async () => true, enabledCurrencies: async () => ["ETB", "USD"] } as never, // manualPaymentSettings
);
return { service, render, renderThermal };
};

View File

@@ -17,6 +17,7 @@ import { Booking } from "../bookings/entities/booking.entity";
// payers straight off the table.
import { ShippingLineCompany } from "../shipping-lines/entities/shipping-line-company.entity";
import { ShippingLineCredit } from "../shipping-lines/entities/shipping-line-credit.entity";
import { ManualPaymentSettingsService } from "../payment-settings/manual-payment-settings.service";
import { EimsConfig } from "../../config/eims.config";
import { CompaniesService } from "../companies/companies.service";
import { EimsInvoiceStatus } from "../eims/eims-registration.types";
@@ -201,6 +202,7 @@ export class BillingService {
private readonly invoiceDocuments: InvoiceDocumentService,
private readonly files: FilesService,
private readonly config: ConfigService,
private readonly manualPaymentSettings: ManualPaymentSettingsService,
) { }
// ── Reads ──────────────────────────────────────────────────────────────────
@@ -370,20 +372,24 @@ export class BillingService {
const pageSize =
filter.pageSize && filter.pageSize > 0 ? filter.pageSize : 20;
// Only currencies whose manual-payment channel is switched on are listed:
// a row Finance cannot act on is noise, and the confirm endpoint would
// refuse it anyway. All off → nothing to work.
const enabled = await this.manualPaymentSettings.enabledCurrencies();
if (!enabled.length) return { items: [], total: 0 };
const currencies = filter.currency
? enabled.filter((c) => c === filter.currency)
: enabled;
if (!currencies.length) return { items: [], total: 0 };
const qb = this.dataSource
.getRepository(Invoice)
.createQueryBuilder("invoice")
.leftJoinAndSelect("invoice.company", "company")
.where("UPPER(invoice.currency) IN ('USD', 'ETB')")
.where("UPPER(invoice.currency) IN (:...currencies)", { currencies })
.orderBy("invoice.issuedAt", "DESC")
.skip((page - 1) * pageSize)
.take(pageSize);
if (filter.currency) {
qb.andWhere("UPPER(invoice.currency) = :currency", {
currency: filter.currency,
});
}
if (filter.status) {
qb.andWhere("invoice.status = :status", { status: filter.status });
} else {
@@ -465,7 +471,8 @@ export class BillingService {
/**
* Finance confirms an invoice (USD or ETB) as paid manually — bank transfer
* or counter payment: stores the slip against the invoice and settles the
* or counter payment. Refused when that currency's manual-payment channel is
* switched off in settings. Stores the slip against the invoice and settles the
* FULL outstanding balance through
* {@link recordPayment}, which flips the invoice to PAID and (for bookings)
* emits `booking.invoice.paid` — the same event an online payment fires, so
@@ -485,6 +492,13 @@ export class BillingService {
): Promise<Invoice> {
const invoice = await this.invoices.findById(invoiceId);
if (!invoice) throw new NotFoundException(`Invoice ${invoiceId} not found`);
// The channel is a setting, not a role: even a permitted user cannot
// settle by hand in a currency whose channel is switched off.
if (!(await this.manualPaymentSettings.isEnabled(invoice.currency))) {
throw new BadRequestException(
`Manual payment is disabled for ${invoice.currency ?? "this"} invoices. Enable it in Configuration → Manual payments first.`,
);
}
if (!file) {
throw new BadRequestException("The bank payment slip file is required.");
}