mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 09:00:57 +00:00
Merge branch 'dev' of https://github.com/Tria-plc/edr-platform into alpha
This commit is contained in:
43
packages/api-common/src/constants/apiModules.ts
Normal file
43
packages/api-common/src/constants/apiModules.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
export const flatResponseModules: string[] = [
|
||||
"/api/file-settings",
|
||||
"api/me",
|
||||
"/api/auth",
|
||||
"/api/sessions",
|
||||
"/api/users",
|
||||
"/api/roles",
|
||||
"/api/user-roles",
|
||||
"/api/permissions",
|
||||
"/api/role-permissions",
|
||||
"/api/user-documents",
|
||||
"/api/documentary-requirements",
|
||||
"/api/account-configurations",
|
||||
"/api/applications",
|
||||
"/api/organization-types",
|
||||
"/api/default-units",
|
||||
"/api/default-positions",
|
||||
"/api/organizations",
|
||||
"/api/units",
|
||||
"/api/unit-settings",
|
||||
"/api/organization-configurations",
|
||||
"/api/positions",
|
||||
"/api/employees",
|
||||
"/api/employee-positions",
|
||||
"/api/migrate",
|
||||
"/api/projects",
|
||||
"/api/position-permissions",
|
||||
"/api/position-type-permissions",
|
||||
"/api/position-types",
|
||||
"/api/position-configurations",
|
||||
"/api/organization-global-configurations",
|
||||
"/api/global-unit-configurations",
|
||||
"/api/position-type-configurations",
|
||||
"/api/organization-settings",
|
||||
"/api/location-types",
|
||||
"/api/locations",
|
||||
"/api/unit-clusters",
|
||||
"/api/seals",
|
||||
"/api/headers",
|
||||
"/api/footers",
|
||||
"/api/employee-signatures",
|
||||
"/api/employee-stamps",
|
||||
];
|
||||
@@ -17,3 +17,6 @@ export * from "./entities/base.entity";
|
||||
|
||||
// Repositories
|
||||
export * from "./repositories/base.repository";
|
||||
|
||||
// Services
|
||||
export * from "./services/exchange";
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { Readable } from "stream";
|
||||
import { Observable } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { flatResponseModules } from "../constants/apiModules";
|
||||
|
||||
export interface StandardResponse<T> {
|
||||
success: true;
|
||||
@@ -33,6 +34,25 @@ export class ResponseTransformInterceptor<T> implements NestInterceptor<
|
||||
) {
|
||||
return data;
|
||||
}
|
||||
const request = _context.switchToHttp().getRequest();
|
||||
const path = request.route?.path ?? request.originalUrl ?? "";
|
||||
|
||||
const shouldFlatten = flatResponseModules.some((module) =>
|
||||
path.startsWith(module),
|
||||
);
|
||||
|
||||
if (
|
||||
shouldFlatten &&
|
||||
data &&
|
||||
typeof data === "object" &&
|
||||
!Array.isArray(data)
|
||||
) {
|
||||
return {
|
||||
success: true,
|
||||
...data,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
|
||||
129
packages/api-common/src/services/exchange/cbe.provider.ts
Normal file
129
packages/api-common/src/services/exchange/cbe.provider.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Logger } from "@nestjs/common";
|
||||
|
||||
import { EXCHANGE_DEFAULTS, ExchangeOptions } from "./exchange.options";
|
||||
import {
|
||||
CurrencyPair,
|
||||
ExchangeRateProvider,
|
||||
} from "./exchange.types";
|
||||
|
||||
/** Matches USD buying/selling embedded in ethio.forex CBET page HTML (after entity unescape). */
|
||||
const USD_RATE_REGEX =
|
||||
/currency_code":\[0,"USD"\],"currency_name":\[0,"US DOLLAR"\],"buying":\[0,([\d.]+)\],"selling":\[0,([\d.]+)\]/;
|
||||
|
||||
/**
|
||||
* Central Bank of Ethiopia (CBE) rate provider.
|
||||
*
|
||||
* Sources a single canonical direction — **USD→ETB** (selling rate) — by
|
||||
* scraping ethio.forex, caching the result, and falling back to a configured
|
||||
* rate when the scrape fails. The inverse (ETB→USD) is derived by
|
||||
* {@link ExchangeService}, so this provider only ever reports USD→ETB.
|
||||
*/
|
||||
export class CbeExchangeProvider implements ExchangeRateProvider {
|
||||
readonly name = "CBE";
|
||||
|
||||
private readonly logger = new Logger(CbeExchangeProvider.name);
|
||||
private readonly options: Required<ExchangeOptions>;
|
||||
private cachedRate: number | null = null;
|
||||
private cacheExpiresAt = 0;
|
||||
|
||||
constructor(options: ExchangeOptions) {
|
||||
this.options = { ...EXCHANGE_DEFAULTS, ...stripUndefined(options) };
|
||||
}
|
||||
|
||||
async getBaseRate(pair: CurrencyPair): Promise<number | null> {
|
||||
// CBE only sources USD→ETB; everything else is derived upstream.
|
||||
if (pair.from !== "USD" || pair.to !== "ETB") {
|
||||
return null;
|
||||
}
|
||||
return this.getUsdToEtbRate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current CBE USD→ETB **selling** rate scraped from ethio.forex.
|
||||
* Cached for `cacheTtlMs`; on failure reuses the last cached rate, else
|
||||
* returns `fallbackRate`.
|
||||
*/
|
||||
private async getUsdToEtbRate(): Promise<number> {
|
||||
const now = Date.now();
|
||||
|
||||
if (this.cachedRate !== null && now < this.cacheExpiresAt) {
|
||||
return this.cachedRate;
|
||||
}
|
||||
|
||||
const { scrapeUrl, fallbackRate, cacheTtlMs, requestTimeoutMs } =
|
||||
this.options;
|
||||
|
||||
try {
|
||||
const response = await fetch(scrapeUrl, {
|
||||
signal: AbortSignal.timeout(requestTimeoutMs),
|
||||
headers: { "User-Agent": "Mozilla/5.0" },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`CBE scrape responded with status ${response.status}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const rates = this.parseScrapedRates(html);
|
||||
|
||||
if (!rates) {
|
||||
throw new Error("USD rate not found in ethio.forex page HTML");
|
||||
}
|
||||
|
||||
const rate = rates.selling;
|
||||
if (!Number.isFinite(rate) || rate <= 0) {
|
||||
throw new Error(`Invalid selling rate parsed: ${rate}`);
|
||||
}
|
||||
|
||||
this.cachedRate = rate;
|
||||
this.cacheExpiresAt = now + cacheTtlMs;
|
||||
this.logger.log(
|
||||
`CBE USD→ETB rate refreshed from ethio.forex — buying=${rates.buying} selling=${rate}`,
|
||||
);
|
||||
return rate;
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to scrape CBE exchange rate — using fallback ${fallbackRate} ETB/USD. Error: ${(err as Error).message}`,
|
||||
);
|
||||
|
||||
if (this.cachedRate !== null) {
|
||||
this.logger.warn(
|
||||
`Using previously cached CBE rate: ${this.cachedRate}`,
|
||||
);
|
||||
return this.cachedRate;
|
||||
}
|
||||
|
||||
return fallbackRate;
|
||||
}
|
||||
}
|
||||
|
||||
private parseScrapedRates(
|
||||
html: string,
|
||||
): { buying: number; selling: number } | null {
|
||||
const decoded = this.unescapeHtml(html);
|
||||
const match = USD_RATE_REGEX.exec(decoded);
|
||||
if (!match) return null;
|
||||
|
||||
const buying = Number(match[1]);
|
||||
const selling = Number(match[2]);
|
||||
if (!Number.isFinite(buying) || !Number.isFinite(selling)) return null;
|
||||
|
||||
return { buying, selling };
|
||||
}
|
||||
|
||||
private unescapeHtml(html: string): string {
|
||||
return html
|
||||
.replace(/"/g, '"')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
}
|
||||
}
|
||||
|
||||
/** Drops keys whose value is `undefined` so they don't override defaults via spread. */
|
||||
function stripUndefined(options: ExchangeOptions): ExchangeOptions {
|
||||
return Object.fromEntries(
|
||||
Object.entries(options).filter(([, value]) => value !== undefined),
|
||||
);
|
||||
}
|
||||
52
packages/api-common/src/services/exchange/exchange.module.ts
Normal file
52
packages/api-common/src/services/exchange/exchange.module.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { DynamicModule, Module, Provider } from "@nestjs/common";
|
||||
|
||||
import {
|
||||
EXCHANGE_OPTIONS,
|
||||
ExchangeAsyncOptions,
|
||||
ExchangeOptions,
|
||||
} from "./exchange.options";
|
||||
import { ExchangeService } from "./exchange.service";
|
||||
|
||||
/**
|
||||
* Provides {@link ExchangeService} (currency conversion, currently CBE-backed).
|
||||
*
|
||||
* Register once in the app root, then inject `ExchangeService` anywhere:
|
||||
*
|
||||
* ```ts
|
||||
* // static config
|
||||
* ExchangeModule.forRoot({ fallbackRate: 135 })
|
||||
*
|
||||
* // config resolved from ConfigService
|
||||
* ExchangeModule.forRootAsync({
|
||||
* inject: [ConfigService],
|
||||
* useFactory: (config: ConfigService) => config.get('app.exchange'),
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
@Module({})
|
||||
export class ExchangeModule {
|
||||
static forRoot(options: ExchangeOptions = {}): DynamicModule {
|
||||
return {
|
||||
module: ExchangeModule,
|
||||
providers: [
|
||||
{ provide: EXCHANGE_OPTIONS, useValue: options },
|
||||
ExchangeService,
|
||||
],
|
||||
exports: [ExchangeService],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: ExchangeAsyncOptions): DynamicModule {
|
||||
const optionsProvider: Provider = {
|
||||
provide: EXCHANGE_OPTIONS,
|
||||
useFactory: options.useFactory,
|
||||
inject: (options.inject ?? []) as never[],
|
||||
};
|
||||
|
||||
return {
|
||||
module: ExchangeModule,
|
||||
providers: [optionsProvider, ExchangeService],
|
||||
exports: [ExchangeService],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/** Injection token carrying the resolved {@link ExchangeOptions}. */
|
||||
export const EXCHANGE_OPTIONS = Symbol("EXCHANGE_OPTIONS");
|
||||
|
||||
/** Configuration for the {@link ExchangeService} and its CBE provider. */
|
||||
export interface ExchangeOptions {
|
||||
/**
|
||||
* ethio.forex CBET page scraped for USD buying/selling rates.
|
||||
* @default 'https://ethio.forex/bank/CBET'
|
||||
*/
|
||||
scrapeUrl?: string;
|
||||
|
||||
/**
|
||||
* Base USD→ETB rate used when scraping fails and no previously cached rate
|
||||
* exists. The ETB→USD direction is derived as its inverse.
|
||||
* @default 130
|
||||
*/
|
||||
fallbackRate?: number;
|
||||
|
||||
/**
|
||||
* How long a successfully fetched rate is cached, in milliseconds.
|
||||
* @default 3_600_000 (1 hour)
|
||||
*/
|
||||
cacheTtlMs?: number;
|
||||
|
||||
/**
|
||||
* Timeout for the scrape HTTP request, in milliseconds.
|
||||
* @default 8_000
|
||||
*/
|
||||
requestTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/** Defaults applied to any unset {@link ExchangeOptions} field. */
|
||||
export const EXCHANGE_DEFAULTS: Required<ExchangeOptions> = {
|
||||
scrapeUrl: "https://ethio.forex/bank/CBET",
|
||||
fallbackRate: 130,
|
||||
cacheTtlMs: 3_600_000,
|
||||
requestTimeoutMs: 8_000,
|
||||
};
|
||||
|
||||
/** Factory contract for {@link ExchangeModule.forRootAsync}. */
|
||||
export interface ExchangeAsyncOptions {
|
||||
/** Providers to inject into {@link useFactory} (e.g. `[ConfigService]`). */
|
||||
inject?: unknown[];
|
||||
/** Returns the options, possibly async. */
|
||||
useFactory: (...args: never[]) => ExchangeOptions | Promise<ExchangeOptions>;
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Inject, Injectable } from "@nestjs/common";
|
||||
|
||||
import { CbeExchangeProvider } from "./cbe.provider";
|
||||
import { EXCHANGE_OPTIONS, ExchangeOptions } from "./exchange.options";
|
||||
import { CurrencyCode } from "./exchange.types";
|
||||
|
||||
/**
|
||||
* Currency exchange service. Resolves the rate between any supported currency
|
||||
* pair and converts amounts, backed by a rate provider (currently CBE).
|
||||
*
|
||||
* Resolution order for `getRate(from, to)`:
|
||||
* 1. `from === to` → `1`.
|
||||
* 2. Provider supplies the pair directly (e.g. CBE → USD→ETB).
|
||||
* 3. Provider supplies the inverse → return `1 / inverse` (e.g. ETB→USD).
|
||||
*
|
||||
* Configure via {@link ExchangeModule.forRoot} / `forRootAsync`.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ExchangeService {
|
||||
private readonly provider: CbeExchangeProvider;
|
||||
|
||||
constructor(@Inject(EXCHANGE_OPTIONS) options: ExchangeOptions) {
|
||||
this.provider = new CbeExchangeProvider(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the rate to convert 1 unit of `from` into `to`
|
||||
* (i.e. `amountInTo = amountInFrom * getRate(from, to)`).
|
||||
*/
|
||||
async getRate(from: CurrencyCode, to: CurrencyCode): Promise<number> {
|
||||
if (from === to) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const direct = await this.provider.getBaseRate({ from, to });
|
||||
if (direct !== null) {
|
||||
return direct;
|
||||
}
|
||||
|
||||
const inverse = await this.provider.getBaseRate({ from: to, to: from });
|
||||
if (inverse !== null && inverse > 0) {
|
||||
return 1 / inverse;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No exchange rate available for ${from}→${to} from provider ${this.provider.name}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Converts `amount` from one currency to another using {@link getRate}. */
|
||||
async convert(
|
||||
amount: number,
|
||||
from: CurrencyCode,
|
||||
to: CurrencyCode,
|
||||
): Promise<number> {
|
||||
const rate = await this.getRate(from, to);
|
||||
return amount * rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience alias for `getRate('USD', 'ETB')`.
|
||||
* @deprecated Prefer {@link getRate}; kept for existing callers.
|
||||
*/
|
||||
getUsdToEtbRate(): Promise<number> {
|
||||
return this.getRate("USD", "ETB");
|
||||
}
|
||||
|
||||
/** Convenience alias for `getRate('ETB', 'USD')`. */
|
||||
getEtbToUsdRate(): Promise<number> {
|
||||
return this.getRate("ETB", "USD");
|
||||
}
|
||||
}
|
||||
31
packages/api-common/src/services/exchange/exchange.types.ts
Normal file
31
packages/api-common/src/services/exchange/exchange.types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* ISO-4217 currency codes the exchange service can handle.
|
||||
* Extend this union as new currencies are supported.
|
||||
*/
|
||||
export type CurrencyCode = "USD" | "ETB";
|
||||
|
||||
/** A directional currency pair, e.g. `{ from: 'USD', to: 'ETB' }`. */
|
||||
export interface CurrencyPair {
|
||||
from: CurrencyCode;
|
||||
to: CurrencyCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of base exchange rates. Implementations fetch (scrape/API) the rate
|
||||
* for a single canonical direction; the {@link ExchangeService} derives the
|
||||
* inverse and same-currency (1:1) cases on top.
|
||||
*
|
||||
* Today the only implementation is the CBE (Central Bank of Ethiopia) provider,
|
||||
* which sources USD→ETB. New providers (other banks, other base pairs) can be
|
||||
* added without touching consumers.
|
||||
*/
|
||||
export interface ExchangeRateProvider {
|
||||
/** Human-readable provider name, used in logs (e.g. `'CBE'`). */
|
||||
readonly name: string;
|
||||
|
||||
/**
|
||||
* Returns the rate for `pair` (units of `pair.to` per 1 unit of `pair.from`),
|
||||
* or `null` if this provider cannot supply that pair directly.
|
||||
*/
|
||||
getBaseRate(pair: CurrencyPair): Promise<number | null>;
|
||||
}
|
||||
10
packages/api-common/src/services/exchange/index.ts
Normal file
10
packages/api-common/src/services/exchange/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { ExchangeService } from "./exchange.service";
|
||||
export { ExchangeModule } from "./exchange.module";
|
||||
export { CbeExchangeProvider } from "./cbe.provider";
|
||||
export { EXCHANGE_OPTIONS, EXCHANGE_DEFAULTS } from "./exchange.options";
|
||||
export type { ExchangeOptions, ExchangeAsyncOptions } from "./exchange.options";
|
||||
export type {
|
||||
CurrencyCode,
|
||||
CurrencyPair,
|
||||
ExchangeRateProvider,
|
||||
} from "./exchange.types";
|
||||
79
packages/types/src/freight/etrade.ts
Normal file
79
packages/types/src/freight/etrade.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export interface ETradeAddressInfo {
|
||||
Region: string;
|
||||
Zone: string;
|
||||
Woreda: string;
|
||||
Kebele: string;
|
||||
HouseNo: string;
|
||||
MobilePhone: string;
|
||||
RegularPhone: string;
|
||||
}
|
||||
|
||||
export interface ETradeAssociateInfo {
|
||||
Position: string | null;
|
||||
ManagerName: string;
|
||||
ManagerNameEng: string;
|
||||
Photo: string | null;
|
||||
MobilePhone: string | null;
|
||||
RegularPhone: string | null;
|
||||
}
|
||||
|
||||
export interface ETradeBusinessInfo {
|
||||
MainGuid: string;
|
||||
OwnerTIN: string;
|
||||
DateRegistered: string;
|
||||
TradeName: string;
|
||||
LicenceNumber: string;
|
||||
Status: number;
|
||||
StatusDescription: string;
|
||||
Capital: number;
|
||||
AssociateShortInfos: ETradeAssociateInfo[];
|
||||
AddressInfo: ETradeAddressInfo;
|
||||
RenewedTo: string;
|
||||
RenewedToDateString: string;
|
||||
RenewalDate: string;
|
||||
RenewedFrom: string;
|
||||
CancellationDate: string | null;
|
||||
}
|
||||
|
||||
export interface ETradeCompanyInfo {
|
||||
Tin: string;
|
||||
LegalCondtion: string;
|
||||
RegNo: string;
|
||||
RegDate: string;
|
||||
BusinessName: string;
|
||||
BusinessNameAmh: string;
|
||||
PaidUpCapital: number;
|
||||
AssociateShortInfos: ETradeAssociateInfo[];
|
||||
Businesses: Array<{
|
||||
MainGuid: string;
|
||||
OwnerTIN: string;
|
||||
DateRegistered: string;
|
||||
TradeNameAmh: string;
|
||||
TradesName: string;
|
||||
LicenceNumber: string;
|
||||
RenewalDate: string;
|
||||
RenewedFrom: string;
|
||||
RenewedTo: string;
|
||||
BusinessLicensingGroupMain: string | null;
|
||||
SubGroups: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface CompanyRegistrationData {
|
||||
licenceNumber: string;
|
||||
statusDescription: string;
|
||||
dateRegistered: string;
|
||||
renewedFrom: string;
|
||||
renewalDate: string;
|
||||
renewedTo: string;
|
||||
region: string;
|
||||
zone: string;
|
||||
woreda: string;
|
||||
kebele: string;
|
||||
houseNo: string;
|
||||
mobilePhone: string;
|
||||
regularPhone: string;
|
||||
managerName: string;
|
||||
managerEmail?: string;
|
||||
managerPhone: string;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import type { BaseEntity } from "../common";
|
||||
export * from "./dropdown_settings";
|
||||
export * from "./file_upload_settings";
|
||||
export * from "./overview";
|
||||
export * from "./etrade";
|
||||
|
||||
export enum TradeDirection {
|
||||
IMPORT = "IMPORT",
|
||||
@@ -41,6 +42,26 @@ export enum FreightType {
|
||||
Bulk = "BULK",
|
||||
}
|
||||
|
||||
/**
|
||||
* Distinguishes a normal one-time booking from a general contract — an umbrella
|
||||
* commitment that is signed and paid once, then drawn down by many orders over
|
||||
* its period. Stored on the booking row.
|
||||
*/
|
||||
export enum BookingType {
|
||||
OneTime = "ONE_TIME",
|
||||
GeneralContract = "GENERAL_CONTRACT",
|
||||
}
|
||||
|
||||
/**
|
||||
* How a cargo type's quantity is measured. Bulk cargo is weighed in tons,
|
||||
* break-bulk is counted per item. Containerised freight is always counted by
|
||||
* container and carries no unit-of-measure.
|
||||
*/
|
||||
export enum CargoUnitOfMeasure {
|
||||
PerTon = "PER_TON",
|
||||
PerItem = "PER_ITEM",
|
||||
}
|
||||
|
||||
export enum BookingStatus {
|
||||
Draft = "DRAFT",
|
||||
Submitted = "SUBMITTED",
|
||||
@@ -67,6 +88,10 @@ export enum BookingStatus {
|
||||
Cancelled = "CANCELLED",
|
||||
PendingConsolidation = "PENDING_CONSOLIDATION",
|
||||
Consolidated = "CONSOLIDATED",
|
||||
/** General contract: paid umbrella contract that is accepting drawdown orders. */
|
||||
ContractActive = "CONTRACT_ACTIVE",
|
||||
/** General contract: closed because its quantity was exhausted (or period elapsed). */
|
||||
ContractClosed = "CONTRACT_CLOSED",
|
||||
}
|
||||
|
||||
export enum ConsignmentStatus {
|
||||
@@ -328,8 +353,25 @@ export interface IBooking extends BaseEntity {
|
||||
customerId: string;
|
||||
trainId?: string | null;
|
||||
status: BookingStatus;
|
||||
/** ONE_TIME for normal bookings; GENERAL_CONTRACT for umbrella contracts. */
|
||||
bookingType?: BookingType;
|
||||
/** General contracts only: when ordering closes (null until active / for one-time). */
|
||||
expiresAt?: string | null;
|
||||
/** Null for general contracts at creation — the date is chosen per order. */
|
||||
scheduledDate: string;
|
||||
totalAmount: number;
|
||||
/** Staff-adjusted total that overrides totalAmount for the customer, if set. */
|
||||
adjustedTotalAmount?: number | null;
|
||||
adjustedByStaffId?: string | null;
|
||||
adjustedAt?: string | null;
|
||||
adjustmentReason?: string | null;
|
||||
/**
|
||||
* Contract validity window set by the backoffice at the accept step. Valid
|
||||
* from contractValidFrom through contractValidUntil (validFrom + N days).
|
||||
*/
|
||||
contractValidityDays?: number | null;
|
||||
contractValidFrom?: string | null;
|
||||
contractValidUntil?: string | null;
|
||||
paymentStatus: PaymentStatus;
|
||||
|
||||
shippingLineId?: string | null;
|
||||
@@ -341,8 +383,15 @@ export interface IBooking extends BaseEntity {
|
||||
|
||||
firstMileEnabled: boolean;
|
||||
firstMilePickupAddress?: string | null;
|
||||
firstMilePickupLat?: number | null;
|
||||
firstMilePickupLng?: number | null;
|
||||
lastMileEnabled: boolean;
|
||||
lastMileDeliveryAddress?: string | null;
|
||||
lastMileDeliveryLat?: number | null;
|
||||
lastMileDeliveryLng?: number | null;
|
||||
|
||||
customsClearingEnabled?: boolean;
|
||||
customsClearingAgent?: string | null;
|
||||
|
||||
equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN";
|
||||
originYard?: IYard | null;
|
||||
@@ -357,7 +406,6 @@ export interface IBooking extends BaseEntity {
|
||||
|
||||
tradeDirection: "IMPORT" | "EXPORT";
|
||||
paymentCurrency: string;
|
||||
allowConsolidation: boolean;
|
||||
consolidationPartnerId?: string | null;
|
||||
|
||||
startDate?: string | null;
|
||||
@@ -400,7 +448,14 @@ export interface IBooking extends BaseEntity {
|
||||
|
||||
export interface PricingBreakdownLineItem {
|
||||
code: string;
|
||||
/** Computed line total (unitAmount × quantity). */
|
||||
amount: number;
|
||||
/** Price for a single unit of this charge (e.g. one 20ft container, one ton). */
|
||||
unitAmount?: number;
|
||||
/** Unit the rate is charged per: PER_CONTAINER | PER_TON | PER_WAGON | PER_KM | FLAT. */
|
||||
unit?: string;
|
||||
/** How many units this charge applies to (containers, tons, wagons; 1 for FLAT). */
|
||||
quantity?: number;
|
||||
currency: string;
|
||||
description: string;
|
||||
}
|
||||
@@ -412,6 +467,34 @@ export interface PricingBreakdown {
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
// ── Document clearance (post counter-sign GL workflow) ──────────────────────
|
||||
|
||||
export type DocumentReviewStatus = "PENDING" | "APPROVED" | "QUERIED";
|
||||
|
||||
/** One row of the clearance document grid (a required doc + its file + review). */
|
||||
export interface ClearanceDocument {
|
||||
fileKey: string;
|
||||
label: string;
|
||||
required: boolean;
|
||||
/** Who supplies this document: the customer, or Global Logistics staff. */
|
||||
uploadedBy: "customer" | "gl";
|
||||
settingCode: string;
|
||||
file: { id: string; name: string; url: string } | null;
|
||||
reviewStatus: DocumentReviewStatus | null;
|
||||
note: string | null;
|
||||
}
|
||||
|
||||
/** The clearance view for a booking, driving both portals' clearance UI. */
|
||||
export interface ClearanceView {
|
||||
status: string;
|
||||
includesCustoms: boolean;
|
||||
inputCode: string | null;
|
||||
outputCode: string | null;
|
||||
documents: ClearanceDocument[];
|
||||
/** True once every required customer document is APPROVED (the 100% gate). */
|
||||
allApproved: boolean;
|
||||
}
|
||||
|
||||
export interface IInvoice extends BaseEntity {
|
||||
bookingId: string;
|
||||
invoiceNumber: string;
|
||||
@@ -473,6 +556,8 @@ export interface BookingReferenceCargoTypeChild {
|
||||
name: string;
|
||||
code: string;
|
||||
show_free_text_box: boolean;
|
||||
/** How this cargo is measured (PER_TON / PER_ITEM); null when unset. */
|
||||
unit_of_measure?: CargoUnitOfMeasure | null;
|
||||
}
|
||||
|
||||
export interface BookingReferenceCargoTypeGroup {
|
||||
@@ -545,6 +630,14 @@ export interface CreateBookingContainerDto {
|
||||
vgmPerUnitTons: number;
|
||||
}
|
||||
|
||||
/** A contracted route+quantity line for a GENERAL contract. */
|
||||
export interface CreateContractRouteDto {
|
||||
originYardId: string;
|
||||
destinationYardId: string;
|
||||
containerTypeId?: string | undefined;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface CreateBookingDto {
|
||||
freightShapeValidation?: boolean | undefined;
|
||||
reference?: string | undefined;
|
||||
@@ -553,12 +646,21 @@ export interface CreateBookingDto {
|
||||
companyId?: string | undefined;
|
||||
trainId?: string | undefined;
|
||||
trainScheduleId?: string | undefined;
|
||||
scheduledDate: string;
|
||||
/** Optional for general contracts — they pick the date per order, not at creation. */
|
||||
scheduledDate?: string | undefined;
|
||||
/** Defaults to ONE_TIME. GENERAL_CONTRACT creates an umbrella contract. */
|
||||
bookingType?: BookingType | undefined;
|
||||
contractType: string;
|
||||
previousContractId?: string | undefined;
|
||||
serviceTypeId: string;
|
||||
firstMilePickupAddress?: string | undefined;
|
||||
firstMilePickupLat?: number | undefined;
|
||||
firstMilePickupLng?: number | undefined;
|
||||
lastMileDeliveryAddress?: string | undefined;
|
||||
lastMileDeliveryLat?: number | undefined;
|
||||
lastMileDeliveryLng?: number | undefined;
|
||||
customsClearingEnabled?: boolean | undefined;
|
||||
customsClearingAgent?: string | undefined;
|
||||
equipmentReturn: string;
|
||||
originYardId: string;
|
||||
destinationYardId: string;
|
||||
@@ -575,5 +677,100 @@ export interface CreateBookingDto {
|
||||
endDate?: string | undefined;
|
||||
financialTerms?: string | undefined;
|
||||
containers?: CreateBookingContainerDto[];
|
||||
allowConsolidation?: boolean;
|
||||
/** GENERAL_CONTRACT only: routes the contract reserves quantity across. */
|
||||
routes?: CreateContractRouteDto[];
|
||||
}
|
||||
|
||||
// ── General Contracts & Booking Orders ──────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A line of contracted quantity. For CONTAINER contracts there is one line per
|
||||
* container type (each its own drawdown pool); for BULK/BREAK_BULK a single line
|
||||
* with a null containerTypeId carries the total tons/items.
|
||||
*/
|
||||
export interface ContractQuantityLine {
|
||||
containerTypeId: string | null;
|
||||
containerTypeName?: string | null;
|
||||
/** PER_TON / PER_ITEM for bulk-style lines; null for container lines. */
|
||||
unitOfMeasure?: CargoUnitOfMeasure | null;
|
||||
/** Total contracted units on this line (containers, tons, or items). */
|
||||
contractedQuantity: number;
|
||||
/** Units already drawn down by non-cancelled orders. */
|
||||
orderedQuantity: number;
|
||||
/** contractedQuantity − orderedQuantity. */
|
||||
remainingQuantity: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customer-facing view of a general contract (a Booking with
|
||||
* bookingType = GENERAL_CONTRACT) and its remaining drawdown pool.
|
||||
*/
|
||||
export interface IGeneralContract extends IBooking {
|
||||
bookingType: BookingType;
|
||||
/** When the contract becomes ACTIVE; when ordering closes. Null until active. */
|
||||
expiresAt?: string | null;
|
||||
/** Per-line contracted / ordered / remaining quantities. */
|
||||
quantityLines: ContractQuantityLine[];
|
||||
}
|
||||
|
||||
export interface CreateBookingOrderLineDto {
|
||||
/** Null for bulk/break-bulk; the container type id for container contracts. */
|
||||
containerTypeId?: string | null;
|
||||
quantity: number;
|
||||
/** How much of this line is hazardous (≤ quantity). Defaults to 0. */
|
||||
hazardousQuantity?: number;
|
||||
/** How much of this line is refrigerated (≤ quantity). Defaults to 0. */
|
||||
reeferQuantity?: number;
|
||||
}
|
||||
|
||||
/** Per-route contracted / ordered / remaining pool line (multi-route contracts). */
|
||||
export interface ContractRouteLine {
|
||||
routeLineId: string;
|
||||
originYardId: string;
|
||||
originYardName?: string | null;
|
||||
destinationYardId: string;
|
||||
destinationYardName?: string | null;
|
||||
containerTypeId?: string | null;
|
||||
containerTypeName?: string | null;
|
||||
contractedQuantity: number;
|
||||
orderedQuantity: number;
|
||||
remainingQuantity: number;
|
||||
/** Road distance (km) for this route; used to bill road orders. Null for rail-only. */
|
||||
km?: number | null;
|
||||
}
|
||||
|
||||
export interface CreateBookingOrderDto {
|
||||
/** The general contract (booking) this order draws down from. */
|
||||
contractBookingId: string;
|
||||
/** For multi-route contracts: the route line being drawn from. */
|
||||
routeLineId?: string;
|
||||
/** The shipment day the customer wants for this order. */
|
||||
scheduledDate: string;
|
||||
lines: CreateBookingOrderLineDto[];
|
||||
}
|
||||
|
||||
export interface IBookingOrderLine {
|
||||
id: string;
|
||||
containerTypeId?: string | null;
|
||||
containerTypeName?: string | null;
|
||||
quantity: number;
|
||||
hazardousQuantity?: number;
|
||||
reeferQuantity?: number;
|
||||
}
|
||||
|
||||
export interface IBookingOrder {
|
||||
id: string;
|
||||
reference: string;
|
||||
contractBookingId: string;
|
||||
/** The child shipment booking spawned for this order (enters scheduling). */
|
||||
bookingId?: string | null;
|
||||
bookingReference?: string | null;
|
||||
companyId?: string | null;
|
||||
scheduledDate: string;
|
||||
status: BookingStatus;
|
||||
schedulingStatus: SchedulingStatus;
|
||||
trainScheduleId?: string | null;
|
||||
lines: IBookingOrderLine[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user