mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
feat(freight): offer built-train wagons per boarding yard on multi-yard consists
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional } from "class-validator";
|
||||
|
||||
/**
|
||||
* Partial update: the UI flips one currency at a time, so an omitted field
|
||||
* leaves that currency's channel exactly as it was.
|
||||
*/
|
||||
export class UpdateManualPaymentSettingDto {
|
||||
@ApiPropertyOptional({ description: "Allow manual settlement of ETB invoices" })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
etbEnabled?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Allow manual settlement of USD invoices" })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
usdEnabled?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Column, Entity } from "typeorm";
|
||||
|
||||
/**
|
||||
* Single-row table controlling whether Finance may settle invoices by hand
|
||||
* (bank transfer / counter payment) instead of the customer paying online.
|
||||
*
|
||||
* Per currency on purpose: the two channels are operationally different — USD
|
||||
* bookings have always been bank-transfer-only, while ETB normally goes
|
||||
* through the gateway and manual settlement is the exception. Switching one
|
||||
* off must not switch off the other.
|
||||
*/
|
||||
@Entity({ schema: "freight", name: "manual_payment_settings" })
|
||||
export class ManualPaymentSetting extends BaseEntity {
|
||||
/** Manual settlement allowed for ETB invoices. */
|
||||
@Column({ name: "etb_enabled", type: "boolean", default: false })
|
||||
etbEnabled!: boolean;
|
||||
|
||||
/** Manual settlement allowed for USD invoices. */
|
||||
@Column({ name: "usd_enabled", type: "boolean", default: true })
|
||||
usdEnabled!: boolean;
|
||||
|
||||
/** IAM user id of the last operator to change either toggle. */
|
||||
@Column({ name: "updated_by_id", type: "uuid", nullable: true })
|
||||
updatedById?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Body, Controller, Get, Patch } from "@nestjs/common";
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { CurrentUser } from "@edr/api-common";
|
||||
import type { TCurrentUser } from "@tria-plc/api-common/modules/auth/types/current-user.type";
|
||||
|
||||
import { BookingStaff } from "../../common/booking-guards";
|
||||
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||
import { UpdateManualPaymentSettingDto } from "./dto/update-manual-payment-setting.dto";
|
||||
import { ManualPaymentSettingsService } from "./manual-payment-settings.service";
|
||||
|
||||
@ApiTags("payment-settings")
|
||||
@ApiBearerAuth()
|
||||
@Controller("payment-settings/manual")
|
||||
export class ManualPaymentSettingsController {
|
||||
constructor(private readonly service: ManualPaymentSettingsService) {}
|
||||
|
||||
/**
|
||||
* Read is gated on `manual_payment:view`, which Finance also holds — the
|
||||
* Manual Payments worklist reads this to know which currency tabs to offer.
|
||||
*/
|
||||
@Get()
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.settings.manualPayment.view,
|
||||
FREIGHT_PERMS.admin,
|
||||
])
|
||||
@ApiOperation({
|
||||
summary: "Whether manual (offline) invoice settlement is enabled, per currency",
|
||||
})
|
||||
get() {
|
||||
return this.service.get();
|
||||
}
|
||||
|
||||
@Patch()
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.settings.manualPayment.manage,
|
||||
FREIGHT_PERMS.admin,
|
||||
])
|
||||
@ApiOperation({
|
||||
summary: "Enable or disable manual invoice settlement for ETB and/or USD",
|
||||
})
|
||||
update(
|
||||
@Body() dto: UpdateManualPaymentSettingDto,
|
||||
@CurrentUser() user: TCurrentUser,
|
||||
) {
|
||||
return this.service.update(dto, user?.id ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
|
||||
import { ManualPaymentSetting } from "./entities/manual-payment-setting.entity";
|
||||
|
||||
/** The two currencies an invoice can be settled by hand in. */
|
||||
export type ManualPaymentCurrency = "ETB" | "USD";
|
||||
|
||||
/**
|
||||
* Owns the single `manual_payment_settings` row: whether Finance may settle
|
||||
* invoices by hand, per currency.
|
||||
*
|
||||
* Defaults mirror how the platform behaved before the toggles existed — USD
|
||||
* has always been bank-transfer-only so it starts ON; ETB manual settlement is
|
||||
* the new capability and starts OFF, so enabling it is a deliberate act.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ManualPaymentSettingsService {
|
||||
private readonly logger = new Logger(ManualPaymentSettingsService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ManualPaymentSetting)
|
||||
private readonly repository: Repository<ManualPaymentSetting>,
|
||||
) {}
|
||||
|
||||
/** The settings row, created at the defaults on first access. */
|
||||
async get(): Promise<ManualPaymentSetting> {
|
||||
const existing = await this.repository.findOne({ where: {} });
|
||||
if (existing) return existing;
|
||||
|
||||
return this.repository.save(
|
||||
this.repository.create({ etbEnabled: false, usdEnabled: true }),
|
||||
);
|
||||
}
|
||||
|
||||
/** Currencies manual settlement is currently allowed for. */
|
||||
async enabledCurrencies(): Promise<ManualPaymentCurrency[]> {
|
||||
const setting = await this.get();
|
||||
const enabled: ManualPaymentCurrency[] = [];
|
||||
if (setting.etbEnabled) enabled.push("ETB");
|
||||
if (setting.usdEnabled) enabled.push("USD");
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/** Whether one currency may be settled by hand right now. */
|
||||
async isEnabled(currency: string | null | undefined): Promise<boolean> {
|
||||
const upper = currency?.toUpperCase();
|
||||
if (upper !== "ETB" && upper !== "USD") return false;
|
||||
const setting = await this.get();
|
||||
return upper === "ETB" ? setting.etbEnabled : setting.usdEnabled;
|
||||
}
|
||||
|
||||
/** Flip either toggle; an omitted field leaves that currency unchanged. */
|
||||
async update(
|
||||
patch: { etbEnabled?: boolean; usdEnabled?: boolean },
|
||||
updatedById?: string | null,
|
||||
): Promise<ManualPaymentSetting> {
|
||||
const current = await this.get();
|
||||
await this.repository.update(current.id, {
|
||||
...(patch.etbEnabled === undefined ? {} : { etbEnabled: patch.etbEnabled }),
|
||||
...(patch.usdEnabled === undefined ? {} : { usdEnabled: patch.usdEnabled }),
|
||||
updatedById: updatedById ?? null,
|
||||
});
|
||||
const updated = await this.get();
|
||||
this.logger.warn(
|
||||
`Manual payment channels set to ETB=${updated.etbEnabled} USD=${updated.usdEnabled} by ${updatedById ?? "unknown user"}`,
|
||||
);
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Global, Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
|
||||
import { ManualPaymentSetting } from "./entities/manual-payment-setting.entity";
|
||||
import { ManualPaymentSettingsController } from "./manual-payment-settings.controller";
|
||||
import { ManualPaymentSettingsService } from "./manual-payment-settings.service";
|
||||
|
||||
/**
|
||||
* Global so billing can inject {@link ManualPaymentSettingsService} to gate
|
||||
* the manual-settlement worklist and confirmation endpoint without importing
|
||||
* this module (and without a cycle, since this module needs nothing back).
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([ManualPaymentSetting])],
|
||||
controllers: [ManualPaymentSettingsController],
|
||||
providers: [ManualPaymentSettingsService],
|
||||
exports: [ManualPaymentSettingsService],
|
||||
})
|
||||
export class PaymentSettingsModule {}
|
||||
Reference in New Issue
Block a user