mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 03:05:42 +00:00
7
.github/workflows/deploy.yml
vendored
7
.github/workflows/deploy.yml
vendored
@@ -144,15 +144,16 @@ jobs:
|
|||||||
run: ./scripts/deploy/create-npmrc.sh
|
run: ./scripts/deploy/create-npmrc.sh
|
||||||
|
|
||||||
- name: Resolve env file path for ${{ matrix.service }}
|
- name: Resolve env file path for ${{ matrix.service }}
|
||||||
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
|
if: contains(fromJson('["freight-api", "passenger-api", "payment-api"]'), matrix.service)
|
||||||
run: |
|
run: |
|
||||||
case "${{ matrix.service }}" in
|
case "${{ matrix.service }}" in
|
||||||
|
freight-api) echo "SERVICE_ENV_FILE=apps/edr-freight-api/.env" >> "$GITHUB_ENV" ;;
|
||||||
passenger-api) echo "SERVICE_ENV_FILE=apps/edr-passenger-api/.env" >> "$GITHUB_ENV" ;;
|
passenger-api) echo "SERVICE_ENV_FILE=apps/edr-passenger-api/.env" >> "$GITHUB_ENV" ;;
|
||||||
payment-api) echo "SERVICE_ENV_FILE=apps/edr-payment-api/.env" >> "$GITHUB_ENV" ;;
|
payment-api) echo "SERVICE_ENV_FILE=apps/edr-payment-api/.env" >> "$GITHUB_ENV" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
- name: Build migration image for ${{ matrix.service }}
|
- name: Build migration image for ${{ matrix.service }}
|
||||||
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
|
if: contains(fromJson('["freight-api", "passenger-api", "payment-api"]'), matrix.service)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
docker build \
|
docker build \
|
||||||
@@ -163,7 +164,7 @@ jobs:
|
|||||||
.
|
.
|
||||||
|
|
||||||
- name: Run migrations for ${{ matrix.service }}
|
- name: Run migrations for ${{ matrix.service }}
|
||||||
if: contains(fromJson('["passenger-api", "payment-api"]'), matrix.service)
|
if: contains(fromJson('["freight-api", "passenger-api", "payment-api"]'), matrix.service)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
docker run --rm --env-file "${SERVICE_ENV_FILE}" "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration"
|
docker run --rm --env-file "${SERVICE_ENV_FILE}" "${COMPOSE_PROJECT_NAME}-${{ matrix.service }}-migration"
|
||||||
|
|||||||
@@ -154,13 +154,32 @@ no timeout and will wait forever.
|
|||||||
Migrations are the most dangerous surface in this repo. Two production-grade incidents have
|
Migrations are the most dangerous surface in this repo. Two production-grade incidents have
|
||||||
already come from it.
|
already come from it.
|
||||||
|
|
||||||
- `migrationsRun: true` — **migrations run automatically on API boot**, with
|
- `migrationsRun: false` — **migrations do NOT run on API boot.** They run as a separate
|
||||||
`migrationsTransactionMode: 'each'`.
|
one-shot step, via the Dockerfile's `migration` build target (`docker build --target
|
||||||
|
migration`), with `migrationsTransactionMode: 'each'`.
|
||||||
|
- CI: `.github/workflows/deploy.yml` builds the `migration` image and runs it
|
||||||
|
(`docker run --rm --env-file ...`) *before* building/deploying the app image.
|
||||||
|
- e2e: `docker-compose.e2e.yaml`'s `freight-migration-e2e` service runs once and
|
||||||
|
`freight-api-e2e` depends on it (`condition: service_completed_successfully`).
|
||||||
|
- Local dev (`docker-compose.yaml`) has no equivalent migration service yet — run
|
||||||
|
migrations yourself before `docker compose up freight-api`, e.g.
|
||||||
|
`docker build --target migration -f apps/edr-freight-api/Dockerfile -t freight-migration .`
|
||||||
|
then `docker run --rm --env-file apps/edr-freight-api/.env freight-migration`. Don't
|
||||||
|
use `pnpm run migrate` for this — it runs via `ts-node`, which never writes compiled
|
||||||
|
output to `dist/`, and the freight migrations glob only matches `dist/migrations/*.js`.
|
||||||
|
It silently applies zero freight migrations while exiting 0.
|
||||||
- Consequences you must design for:
|
- Consequences you must design for:
|
||||||
- Running several `nest start --watch` instances races `migrationsRun`. A non-idempotent
|
|
||||||
data migration can execute twice. Keep one instance.
|
|
||||||
- A watch-mode hot reload does **not** re-run migrations. If you add a column that new
|
- A watch-mode hot reload does **not** re-run migrations. If you add a column that new
|
||||||
code reads, apply it to the dev database yourself (idempotently) or fully restart.
|
code reads, apply it to the dev database yourself (idempotently) or fully restart.
|
||||||
|
- `apps/edr-freight-api/src/config/database.config.ts`'s `iamEntities` array is a
|
||||||
|
hand-maintained list of `@tria-plc/iamapi-common` entity classes. The live app never
|
||||||
|
notices when it's stale (`autoLoadEntities: true` papers over gaps via IAM's own
|
||||||
|
`forFeature()` registrations), but the standalone migration `DataSource`
|
||||||
|
(`data-source.ts`, no `autoLoadEntities`) does not have that fallback — a missing
|
||||||
|
entity throws `Entity metadata for X#y was not found` at `initialize()`, before a
|
||||||
|
single migration runs. **Every `@tria-plc/iamapi-common` version bump is a candidate
|
||||||
|
for this to break again** — diff the package's entity classes against `iamEntities`
|
||||||
|
when bumping it.
|
||||||
- **Give every migration a unique timestamp.** 34 timestamps are currently shared by two or
|
- **Give every migration a unique timestamp.** 34 timestamps are currently shared by two or
|
||||||
more migrations. TypeORM orders by timestamp and breaks ties non-deterministically. Before
|
more migrations. TypeORM orders by timestamp and breaks ties non-deterministically. Before
|
||||||
adding one, check the filename prefix is unused *and* higher than the newest recorded row.
|
adding one, check the filename prefix is unused *and* higher than the newest recorded row.
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ COPY --from=builder /app/ .
|
|||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store \
|
||||||
pnpm deploy --filter="@edr/freight-api" --prod --legacy /deploy
|
pnpm deploy --filter="@edr/freight-api" --prod --legacy /deploy
|
||||||
|
|
||||||
|
FROM deployer AS migration
|
||||||
|
WORKDIR /deploy
|
||||||
|
CMD ["node", "dist/scripts/migrate.js"]
|
||||||
|
|
||||||
FROM base AS runner
|
FROM base AS runner
|
||||||
|
RUN apk add --no-cache libc6-compat
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"seed:negad-indode-arrived-train": "ts-node -r tsconfig-paths/register src/scripts/seed-negad-indode-arrived-train.ts",
|
"seed:negad-indode-arrived-train": "ts-node -r tsconfig-paths/register src/scripts/seed-negad-indode-arrived-train.ts",
|
||||||
"seed:gate-pass-train-scenarios": "ts-node -r tsconfig-paths/register src/scripts/seed-gate-pass-train-scenarios.ts",
|
"seed:gate-pass-train-scenarios": "ts-node -r tsconfig-paths/register src/scripts/seed-gate-pass-train-scenarios.ts",
|
||||||
"auto-unload:arrived-import-trains": "ts-node -r tsconfig-paths/register src/scripts/auto-unload-arrived-import-trains.ts",
|
"auto-unload:arrived-import-trains": "ts-node -r tsconfig-paths/register src/scripts/auto-unload-arrived-import-trains.ts",
|
||||||
|
"backfill:missing-unload-inventory": "ts-node -r tsconfig-paths/register src/scripts/backfill-missing-unload-inventory.ts",
|
||||||
"seed:file-upload-settings": "ts-node -r tsconfig-paths/register src/scripts/seed-file-upload-settings.ts",
|
"seed:file-upload-settings": "ts-node -r tsconfig-paths/register src/scripts/seed-file-upload-settings.ts",
|
||||||
"seed:dropdown-settings": "ts-node -r tsconfig-paths/register src/scripts/seed-dropdown-settings.ts",
|
"seed:dropdown-settings": "ts-node -r tsconfig-paths/register src/scripts/seed-dropdown-settings.ts",
|
||||||
"seed:gov-companies": "ts-node -r tsconfig-paths/register src/scripts/seed-gov-companies.ts",
|
"seed:gov-companies": "ts-node -r tsconfig-paths/register src/scripts/seed-gov-companies.ts",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"iam:migration:revert": "pnpm run iam:typeorm:cli migration:revert",
|
"iam:migration:revert": "pnpm run iam:typeorm:cli migration:revert",
|
||||||
"iam:migration:show": "pnpm run iam:typeorm:cli migration:show",
|
"iam:migration:show": "pnpm run iam:typeorm:cli migration:show",
|
||||||
"migrate": "ts-node -r tsconfig-paths/register src/scripts/run-migrations.ts",
|
"migrate": "ts-node -r tsconfig-paths/register src/scripts/run-migrations.ts",
|
||||||
|
"migration:run": "node dist/scripts/migrate.js",
|
||||||
"script": "ts-node -r tsconfig-paths/register src/scripts/main.ts"
|
"script": "ts-node -r tsconfig-paths/register src/scripts/main.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -57,8 +59,8 @@
|
|||||||
"@nestjs/swagger": "^11.4.2",
|
"@nestjs/swagger": "^11.4.2",
|
||||||
"@nestjs/typeorm": "^11.0.1",
|
"@nestjs/typeorm": "^11.0.1",
|
||||||
"@nestjs/websockets": "^11.1.27",
|
"@nestjs/websockets": "^11.1.27",
|
||||||
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.4.3.tgz",
|
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz",
|
||||||
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-0.7.15.tgz",
|
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz",
|
||||||
"amqp-connection-manager": "^5.0.0",
|
"amqp-connection-manager": "^5.0.0",
|
||||||
"amqplib": "^2.0.1",
|
"amqplib": "^2.0.1",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { registerAs } from "@nestjs/config";
|
import { registerAs } from "@nestjs/config";
|
||||||
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
|
import { TypeOrmModuleOptions } from "@nestjs/typeorm";
|
||||||
|
import { DataSourceOptions } from "typeorm";
|
||||||
import { join, dirname } from "path";
|
import { join, dirname } from "path";
|
||||||
import {
|
import {
|
||||||
DefaultPosition,
|
DefaultPosition,
|
||||||
@@ -44,8 +45,30 @@ import {
|
|||||||
NotificationTemplate,
|
NotificationTemplate,
|
||||||
} from "@tria-plc/iamapi-common";
|
} from "@tria-plc/iamapi-common";
|
||||||
import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity";
|
import { OrganizationSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-setting.entity";
|
||||||
|
import { UnitSetting } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-setting.entity";
|
||||||
|
import { UnitDetail } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-detail.entity";
|
||||||
|
import { OrganizationDetail } from "@tria-plc/iamapi-common/entities/iam/organization-structure/organization-detail.entity";
|
||||||
|
import { UnitCluster } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-cluster.entity";
|
||||||
|
import { Location } from "@tria-plc/iamapi-common/entities/iam/organization-structure/location.entity";
|
||||||
|
import { LocationType } from "@tria-plc/iamapi-common/entities/iam/organization-structure/location-type.entity";
|
||||||
|
import { DelegationTerminationReason } from "@tria-plc/iamapi-common/entities/iam/organization-structure/delegation-termination-reason.entity";
|
||||||
|
import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/iam/organization-structure/employee-position-active-period.entity";
|
||||||
|
import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity";
|
||||||
|
import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity";
|
||||||
|
import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity";
|
||||||
|
|
||||||
const iamEntities = [
|
const iamEntities = [
|
||||||
|
UnitSetting,
|
||||||
|
UnitDetail,
|
||||||
|
OrganizationDetail,
|
||||||
|
UnitCluster,
|
||||||
|
Location,
|
||||||
|
LocationType,
|
||||||
|
DelegationTerminationReason,
|
||||||
|
EmployeePositionActivePeriod,
|
||||||
|
UnitConfiguration,
|
||||||
|
Site,
|
||||||
|
SiteSetting,
|
||||||
DefaultPosition,
|
DefaultPosition,
|
||||||
DefaultUnit,
|
DefaultUnit,
|
||||||
EmployeePosition,
|
EmployeePosition,
|
||||||
@@ -95,7 +118,7 @@ const iamMigrationsGlob = join(
|
|||||||
);
|
);
|
||||||
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
|
const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
|
||||||
|
|
||||||
export default registerAs("database", (): TypeOrmModuleOptions => {
|
export function buildDataSourceOptions(): DataSourceOptions {
|
||||||
return {
|
return {
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
host: process.env.DB_HOST ?? "localhost",
|
host: process.env.DB_HOST ?? "localhost",
|
||||||
@@ -111,17 +134,19 @@ export default registerAs("database", (): TypeOrmModuleOptions => {
|
|||||||
// The search_path is instead applied per-connection via a pool `connect`
|
// The search_path is instead applied per-connection via a pool `connect`
|
||||||
// handler in app.module.ts (see setPoolSearchPath).
|
// handler in app.module.ts (see setPoolSearchPath).
|
||||||
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
||||||
autoLoadEntities: true,
|
|
||||||
migrations: [
|
migrations: [
|
||||||
// IAM schema + tables must be created before freight migrations
|
|
||||||
iamMigrationsGlob,
|
iamMigrationsGlob,
|
||||||
freightMigrationsGlob,
|
freightMigrationsGlob,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
// Schema changes via migrations only (synchronize breaks ITMLS backfill on existing rows).
|
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging:
|
logging:
|
||||||
process.env.TYPEORM_LOGGING === "true" ? true : ["error", "warn"],
|
process.env.TYPEORM_LOGGING === "true" ? true : ["error", "warn"],
|
||||||
};
|
};
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default registerAs("database", (): TypeOrmModuleOptions => ({
|
||||||
|
...buildDataSourceOptions(),
|
||||||
|
autoLoadEntities: true,
|
||||||
|
migrationsRun: false,
|
||||||
|
}));
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
// apps/edr-freight-api/src/data-source.ts
|
// apps/edr-freight-api/src/data-source.ts
|
||||||
import 'dotenv/config';
|
import "dotenv/config";
|
||||||
import { DataSource } from 'typeorm';
|
import { DataSource } from "typeorm";
|
||||||
//import { ensurePostgresSchemas } from './utils/ensure-postgres-schemas'; // adjust path if needed
|
import { buildDataSourceOptions } from "./config/database.config";
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource(buildDataSourceOptions());
|
||||||
type: 'postgres',
|
|
||||||
host: process.env.DB_HOST ?? 'localhost',
|
|
||||||
port: Number(process.env.DB_PORT ?? 5433),
|
|
||||||
username: process.env.DB_USER ?? 'postgres',
|
|
||||||
password: process.env.DB_PASSWORD ?? '',
|
|
||||||
database: process.env.DB_NAME ?? 'edr_freight',
|
|
||||||
schema: 'freight', // default schema for entities without an explicit schema
|
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
|
||||||
migrations: [__dirname + '/migrations/*{.ts,.js}'],
|
|
||||||
synchronize: false,
|
|
||||||
logging: process.env.TYPEORM_LOGGING === 'true',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Optional: call ensurePostgresSchemas before initializing
|
export default AppDataSource;
|
||||||
// But you can also run it separately.
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break-bulk (PER_ITEM) cargo needs a physical items-fit per allowed wagon
|
||||||
|
* type (e.g. cars → NW5: 4, NW7: 6): a wagon runs out of floor space before it
|
||||||
|
* runs out of rated tonnage, so allocation must respect BOTH limits. Stored as
|
||||||
|
* a jsonb map { [wagonTypeId]: itemsFit } on cargo_types — keys mirror the
|
||||||
|
* cargo_type_wagon_types join rows, kept in sync by the cargo-types service.
|
||||||
|
*/
|
||||||
|
export class AddCargoTypeItemsPerWagonMap3120000000000 implements MigrationInterface {
|
||||||
|
name = 'AddCargoTypeItemsPerWagonMap3120000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "freight"."cargo_types" ADD COLUMN IF NOT EXISTS "items_per_wagon_map" jsonb`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "freight"."cargo_types" DROP COLUMN IF EXISTS "items_per_wagon_map"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rates created before their commodity's unit_of_measure was flipped kept the
|
||||||
|
* old bulk-quantity unit, so bookings of a PER_ITEM commodity (e.g. Machinery)
|
||||||
|
* quoted "per ton". PER_TON and PER_ITEM bill the same stored quantity — only
|
||||||
|
* the name differs — so renaming is safe. Going forward the cargo-types
|
||||||
|
* service syncs rates on every uom change; this backfills the drift.
|
||||||
|
*/
|
||||||
|
export class SyncBulkRateUnitsToCargoUom3140000000000 implements MigrationInterface {
|
||||||
|
name = 'SyncBulkRateUnitsToCargoUom3140000000000';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "freight"."rates" r
|
||||||
|
SET "rate_unit" = 'PER_ITEM'
|
||||||
|
FROM "freight"."cargo_types" ct
|
||||||
|
WHERE ct."id" = r."cargo_type_id"
|
||||||
|
AND ct."unit_of_measure" = 'PER_ITEM'
|
||||||
|
AND r."rate_unit" = 'PER_TON'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`UPDATE "freight"."rates" r
|
||||||
|
SET "rate_unit" = 'PER_TON'
|
||||||
|
FROM "freight"."cargo_types" ct
|
||||||
|
WHERE ct."id" = r."cargo_type_id"
|
||||||
|
AND ct."unit_of_measure" = 'PER_TON'
|
||||||
|
AND r."rate_unit" = 'PER_ITEM'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(): Promise<void> {
|
||||||
|
// Irreversible rename-by-join: the pre-sync unit is not recorded. Both
|
||||||
|
// units bill identically, so rolling back the code needs no data change.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
containersPerWagonForSize,
|
containersPerWagonForSize,
|
||||||
wagonsPerUnitForSize,
|
wagonsPerUnitForSize,
|
||||||
} from '../rule-engine/container-type.util';
|
} from '../rule-engine/container-type.util';
|
||||||
import { bulkItemWagonsRequired } from '../train-scheduling/train-capacity.util';
|
import { bulkItemWagonsForAllowedTypes } from '../train-scheduling/train-capacity.util';
|
||||||
import { BookingsRepository } from './bookings.repository';
|
import { BookingsRepository } from './bookings.repository';
|
||||||
import { wagonRemainder } from './consolidation.service';
|
import { wagonRemainder } from './consolidation.service';
|
||||||
import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price-response.dto';
|
import { GeneratePriceResponseDto, PriceLineItemDto } from './dto/generate-price-response.dto';
|
||||||
@@ -1188,8 +1188,9 @@ export class BookingPricingService {
|
|||||||
);
|
);
|
||||||
if (!(capacity > 0)) return null;
|
if (!(capacity > 0)) return null;
|
||||||
// Break-bulk (PER_ITEM): `tons` above is the item count; size by
|
// Break-bulk (PER_ITEM): `tons` above is the item count; size by
|
||||||
// indivisible items instead of pretending the count is tonnage.
|
// indivisible items instead of pretending the count is tonnage. Best
|
||||||
const byItems = bulkItemWagonsRequired(booking, capacity);
|
// count across allowed wagon types, each capped by its items-fit.
|
||||||
|
const byItems = bulkItemWagonsForAllowedTypes(booking, cargo, capacity);
|
||||||
if (byItems > 0) return byItems;
|
if (byItems > 0) return byItems;
|
||||||
return Math.max(1, Math.ceil(tons / capacity));
|
return Math.max(1, Math.ceil(tons / capacity));
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { CargoUnitOfMeasure } from '@edr/types';
|
import { CargoUnitOfMeasure } from '@edr/types';
|
||||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
import { IsArray, IsBoolean, IsEnum, IsInt, IsObject, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
||||||
|
|
||||||
export class CreateCargoTypeDto {
|
export class CreateCargoTypeDto {
|
||||||
@ApiProperty({ description: 'Cargo type display name', maxLength: 255 })
|
@ApiProperty({ description: 'Cargo type display name', maxLength: 255 })
|
||||||
@@ -32,6 +32,18 @@ export class CreateCargoTypeDto {
|
|||||||
@IsUUID('4', { each: true })
|
@IsUUID('4', { each: true })
|
||||||
wagonTypeIds?: string[];
|
wagonTypeIds?: string[];
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description:
|
||||||
|
'PER_ITEM cargo only: items that physically fit one wagon, keyed by wagon-type id ' +
|
||||||
|
'(e.g. { "<nw5-id>": 4, "<nw7-id>": 6 }). Required for every wagonTypeId when ' +
|
||||||
|
'unitOfMeasure is PER_ITEM.',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: { type: 'integer', minimum: 1 },
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsObject()
|
||||||
|
itemsPerWagonMap?: Record<string, number> | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ default: false })
|
@ApiPropertyOptional({ default: false })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ export class CargoType extends BaseEntity {
|
|||||||
})
|
})
|
||||||
wagonTypes?: WagonType[];
|
wagonTypes?: WagonType[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PER_ITEM (break-bulk) only: how many whole items physically fit each
|
||||||
|
* allowed wagon type, keyed by wagon-type id (e.g. cars → { NW5: 4, NW7: 6 }).
|
||||||
|
* Allocation loads min(this fit, floor(capacityTons / perItemTons)) per
|
||||||
|
* wagon — floor space and rated tonnage bind independently. Keys are kept a
|
||||||
|
* subset of the wagonTypes join rows by the cargo-types service.
|
||||||
|
*/
|
||||||
|
@Column({ name: 'items_per_wagon_map', type: 'jsonb', nullable: true })
|
||||||
|
itemsPerWagonMap?: Record<string, number> | null;
|
||||||
|
|
||||||
@Column({ name: 'requires_director_approval', type: 'boolean', default: false })
|
@Column({ name: 'requires_director_approval', type: 'boolean', default: false })
|
||||||
requiresDirectorApproval!: boolean;
|
requiresDirectorApproval!: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,13 @@ export interface IRatesRepository {
|
|||||||
create(data: Partial<Rate>): Promise<Rate>;
|
create(data: Partial<Rate>): Promise<Rate>;
|
||||||
update(id: string, data: Partial<Rate>): Promise<Rate | null>;
|
update(id: string, data: Partial<Rate>): Promise<Rate | null>;
|
||||||
softDelete(id: string): Promise<void>;
|
softDelete(id: string): Promise<void>;
|
||||||
|
/**
|
||||||
|
* Flip a commodity's PER_TON↔PER_ITEM rates to match its unit of measure.
|
||||||
|
* Both units bill the same stored quantity — only the name differs — so a
|
||||||
|
* uom change must rename the units or bookings keep quoting "per ton" for
|
||||||
|
* counted cargo. Returns the number of rates flipped.
|
||||||
|
*/
|
||||||
|
syncBulkQuantityUnit(cargoTypeId: string, unitOfMeasure: 'PER_TON' | 'PER_ITEM'): Promise<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RATES_REPOSITORY = Symbol('RATES_REPOSITORY');
|
export const RATES_REPOSITORY = Symbol('RATES_REPOSITORY');
|
||||||
|
|||||||
@@ -177,4 +177,16 @@ export class RatesRepository implements IRatesRepository {
|
|||||||
async softDelete(id: string): Promise<void> {
|
async softDelete(id: string): Promise<void> {
|
||||||
await this.repo.softDelete(id);
|
await this.repo.softDelete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncBulkQuantityUnit(
|
||||||
|
cargoTypeId: string,
|
||||||
|
unitOfMeasure: 'PER_TON' | 'PER_ITEM',
|
||||||
|
): Promise<number> {
|
||||||
|
const from = unitOfMeasure === 'PER_ITEM' ? 'PER_TON' : 'PER_ITEM';
|
||||||
|
const result = await this.repo.update(
|
||||||
|
{ cargoTypeId, rateUnit: from },
|
||||||
|
{ rateUnit: unitOfMeasure },
|
||||||
|
);
|
||||||
|
return result.affected ?? 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { PaginatedResponse } from '@edr/types';
|
import { CargoUnitOfMeasure, PaginatedResponse } from '@edr/types';
|
||||||
import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { generateCode } from '../../../common/utils/generate-code.util';
|
import { generateCode } from '../../../common/utils/generate-code.util';
|
||||||
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto';
|
||||||
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||||
@@ -11,6 +17,7 @@ import {
|
|||||||
CARGO_TYPES_REPOSITORY,
|
CARGO_TYPES_REPOSITORY,
|
||||||
ICargoTypesRepository,
|
ICargoTypesRepository,
|
||||||
} from '../interfaces/cargo-types.repository.interface';
|
} from '../interfaces/cargo-types.repository.interface';
|
||||||
|
import { IRatesRepository, RATES_REPOSITORY } from '../interfaces/rates.repository.interface';
|
||||||
import { DisplayOrderService } from './display-order.service';
|
import { DisplayOrderService } from './display-order.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -18,6 +25,8 @@ export class CargoTypesService {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(CARGO_TYPES_REPOSITORY)
|
@Inject(CARGO_TYPES_REPOSITORY)
|
||||||
private readonly repository: ICargoTypesRepository,
|
private readonly repository: ICargoTypesRepository,
|
||||||
|
@Inject(RATES_REPOSITORY)
|
||||||
|
private readonly ratesRepository: IRatesRepository,
|
||||||
private readonly displayOrder: DisplayOrderService,
|
private readonly displayOrder: DisplayOrderService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -38,6 +47,34 @@ export class CargoTypesService {
|
|||||||
return this.repository.findByCode(code);
|
return this.repository.findByCode(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PER_ITEM (break-bulk) cargo must carry a whole-items-fit for EVERY allowed
|
||||||
|
* wagon type — allocation caps each wagon at min(fit, tonnage) and a missing
|
||||||
|
* fit would silently fall back to tonnage-only loading. Returns the map
|
||||||
|
* trimmed to the allowed ids (stale keys from a removed wagon type drop out);
|
||||||
|
* null when the cargo is not PER_ITEM or has no wagon types.
|
||||||
|
*/
|
||||||
|
private resolveItemsPerWagonMap(input: {
|
||||||
|
unitOfMeasure?: CargoUnitOfMeasure | null;
|
||||||
|
wagonTypeIds: string[];
|
||||||
|
itemsPerWagonMap?: Record<string, number> | null;
|
||||||
|
}): Record<string, number> | null {
|
||||||
|
if (input.unitOfMeasure !== CargoUnitOfMeasure.PerItem || !input.wagonTypeIds.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const map: Record<string, number> = {};
|
||||||
|
for (const wagonTypeId of input.wagonTypeIds) {
|
||||||
|
const fit = Number(input.itemsPerWagonMap?.[wagonTypeId]);
|
||||||
|
if (!Number.isInteger(fit) || fit < 1) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`itemsPerWagonMap must define how many items fit wagon type ${wagonTypeId} (integer >= 1) for PER_ITEM cargo`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
map[wagonTypeId] = fit;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a new cargo type. */
|
/** Create a new cargo type. */
|
||||||
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
|
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
|
||||||
const code = generateCode(dto.cargoTypeName);
|
const code = generateCode(dto.cargoTypeName);
|
||||||
@@ -62,26 +99,58 @@ export class CargoTypesService {
|
|||||||
unitOfMeasure: dto.unitOfMeasure ?? null,
|
unitOfMeasure: dto.unitOfMeasure ?? null,
|
||||||
// Join rows are written by the save (RESTRICT FK rejects unknown ids).
|
// Join rows are written by the save (RESTRICT FK rejects unknown ids).
|
||||||
wagonTypes: (dto.wagonTypeIds ?? []).map((id) => ({ id }) as WagonType),
|
wagonTypes: (dto.wagonTypeIds ?? []).map((id) => ({ id }) as WagonType),
|
||||||
|
itemsPerWagonMap: this.resolveItemsPerWagonMap({
|
||||||
|
unitOfMeasure: dto.unitOfMeasure ?? null,
|
||||||
|
wagonTypeIds: dto.wagonTypeIds ?? [],
|
||||||
|
itemsPerWagonMap: dto.itemsPerWagonMap,
|
||||||
|
}),
|
||||||
displayOrder,
|
displayOrder,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Update an existing cargo type. */
|
/** Update an existing cargo type. */
|
||||||
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
|
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
|
||||||
await this.findById(id);
|
const existing = await this.findById(id);
|
||||||
if (dto.parentGroupId) {
|
if (dto.parentGroupId) {
|
||||||
if (dto.parentGroupId === id) throw new ConflictException('A cargo type cannot be its own parent');
|
if (dto.parentGroupId === id) throw new ConflictException('A cargo type cannot be its own parent');
|
||||||
const parent = await this.repository.findById(dto.parentGroupId);
|
const parent = await this.repository.findById(dto.parentGroupId);
|
||||||
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
||||||
}
|
}
|
||||||
const { wagonTypeIds, insertAfterId: _insertAfterId, ...columns } = dto;
|
const { wagonTypeIds, itemsPerWagonMap, insertAfterId: _insertAfterId, ...columns } = dto;
|
||||||
|
// Re-validate the fit map whenever anything it depends on moves — a partial
|
||||||
|
// update merges with the stored values so e.g. adding a wagon type without
|
||||||
|
// its fit still 400s. Untouched fields leave the stored map alone.
|
||||||
|
const touchesItemsFit =
|
||||||
|
wagonTypeIds !== undefined || itemsPerWagonMap !== undefined || dto.unitOfMeasure !== undefined;
|
||||||
const updated = await this.repository.update(id, {
|
const updated = await this.repository.update(id, {
|
||||||
...columns,
|
...columns,
|
||||||
...(wagonTypeIds
|
...(wagonTypeIds
|
||||||
? { wagonTypes: wagonTypeIds.map((wagonTypeId) => ({ id: wagonTypeId }) as WagonType) }
|
? { wagonTypes: wagonTypeIds.map((wagonTypeId) => ({ id: wagonTypeId }) as WagonType) }
|
||||||
: {}),
|
: {}),
|
||||||
|
...(touchesItemsFit
|
||||||
|
? {
|
||||||
|
itemsPerWagonMap: this.resolveItemsPerWagonMap({
|
||||||
|
unitOfMeasure:
|
||||||
|
dto.unitOfMeasure !== undefined ? dto.unitOfMeasure : existing.unitOfMeasure,
|
||||||
|
wagonTypeIds: wagonTypeIds ?? (existing.wagonTypes ?? []).map((wt) => wt.id),
|
||||||
|
itemsPerWagonMap:
|
||||||
|
itemsPerWagonMap !== undefined ? itemsPerWagonMap : existing.itemsPerWagonMap,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
});
|
});
|
||||||
if (!updated) throw new NotFoundException(`Cargo type ${id} not found`);
|
if (!updated) throw new NotFoundException(`Cargo type ${id} not found`);
|
||||||
|
// A uom flip renames how existing rates bill (PER_TON ↔ PER_ITEM name the
|
||||||
|
// same stored quantity) — sync them or bookings keep quoting "per ton" for
|
||||||
|
// counted cargo.
|
||||||
|
if (
|
||||||
|
dto.unitOfMeasure !== undefined &&
|
||||||
|
dto.unitOfMeasure !== existing.unitOfMeasure &&
|
||||||
|
(dto.unitOfMeasure === CargoUnitOfMeasure.PerTon ||
|
||||||
|
dto.unitOfMeasure === CargoUnitOfMeasure.PerItem)
|
||||||
|
) {
|
||||||
|
await this.ratesRepository.syncBulkQuantityUnit(id, dto.unitOfMeasure);
|
||||||
|
}
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ import {
|
|||||||
LocomotiveLimits,
|
LocomotiveLimits,
|
||||||
WagonTypeDimensions,
|
WagonTypeDimensions,
|
||||||
bookingCargoTons,
|
bookingCargoTons,
|
||||||
|
bulkItemsFitFor,
|
||||||
bulkItemWagonsRequired,
|
bulkItemWagonsRequired,
|
||||||
bookingGrossWeightTons,
|
bookingGrossWeightTons,
|
||||||
deriveTrainCapacityFromLocomotive,
|
deriveTrainCapacityFromLocomotive,
|
||||||
@@ -4060,7 +4061,13 @@ export class BookingBatchService implements OnModuleInit {
|
|||||||
|
|
||||||
// Break-bulk (PER_ITEM): indivisible items can need more wagons than raw
|
// Break-bulk (PER_ITEM): indivisible items can need more wagons than raw
|
||||||
// tonnage suggests (floor items-per-wagon loses the fractional capacity).
|
// tonnage suggests (floor items-per-wagon loses the fractional capacity).
|
||||||
const byItems = bulkItemWagonsRequired(booking, capacityTons);
|
// `dimsFor` resolved dims from the first allowed wagon type, so charge that
|
||||||
|
// same type's configured items-fit alongside its capacity.
|
||||||
|
const byItems = bulkItemWagonsRequired(
|
||||||
|
booking,
|
||||||
|
capacityTons,
|
||||||
|
bulkItemsFitFor(booking.cargoType, booking.cargoType?.wagonTypes?.[0]?.id),
|
||||||
|
);
|
||||||
|
|
||||||
return Math.max(DEFAULT_WAGONS_PER_BOOKING, stored, byLength, byWeight, byItems);
|
return Math.max(DEFAULT_WAGONS_PER_BOOKING, stored, byLength, byWeight, byItems);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { bookingCargoTons, bulkItemWagonsRequired } from './train-capacity.util';
|
import { bookingCargoTons, bulkItemWagonsForAllowedTypes } from './train-capacity.util';
|
||||||
import type { Booking } from '../bookings/entities/booking.entity';
|
import type { Booking } from '../bookings/entities/booking.entity';
|
||||||
import type { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
import type { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||||
import {
|
import {
|
||||||
@@ -53,8 +53,10 @@ export function wagonsRequiredForBooking(booking: Booking, bulkWagonCapacity?: n
|
|||||||
if (booking.freightType === 'BULK') {
|
if (booking.freightType === 'BULK') {
|
||||||
const capacity = bulkWagonCapacity && bulkWagonCapacity > 0 ? bulkWagonCapacity : 1;
|
const capacity = bulkWagonCapacity && bulkWagonCapacity > 0 ? bulkWagonCapacity : 1;
|
||||||
// Break-bulk (PER_ITEM) sizes by indivisible items; `cargoTotalWeightVgm`
|
// Break-bulk (PER_ITEM) sizes by indivisible items; `cargoTotalWeightVgm`
|
||||||
// holds the item count there, not tons.
|
// holds the item count there, not tons. No wagon type is fixed yet, so use
|
||||||
const byItems = bulkItemWagonsRequired(booking, capacity);
|
// the best count across the cargo's allowed types (per-type items-fit
|
||||||
|
// respected); falls back to `capacity` when the relation isn't loaded.
|
||||||
|
const byItems = bulkItemWagonsForAllowedTypes(booking, booking.cargoType, capacity);
|
||||||
if (byItems > 0) return byItems;
|
if (byItems > 0) return byItems;
|
||||||
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
|
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
|
||||||
return Math.max(1, Math.ceil(weight / capacity));
|
return Math.max(1, Math.ceil(weight / capacity));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
bookingCargoTons,
|
bookingCargoTons,
|
||||||
bookingGrossWeightTons,
|
bookingGrossWeightTons,
|
||||||
bookingTrainLengthMeters,
|
bookingTrainLengthMeters,
|
||||||
|
bulkItemWagonsForAllowedTypes,
|
||||||
bulkItemWagonsRequired,
|
bulkItemWagonsRequired,
|
||||||
consistUsage,
|
consistUsage,
|
||||||
consistViolations,
|
consistViolations,
|
||||||
@@ -76,6 +77,62 @@ describe('train-capacity.util', () => {
|
|||||||
expect(bulkItemWagonsRequired(breakBulk(0, 800), 69)).toBe(0);
|
expect(bulkItemWagonsRequired(breakBulk(0, 800), 69)).toBe(0);
|
||||||
expect(bulkItemWagonsRequired(breakBulk(400, 0), 69)).toBe(0);
|
expect(bulkItemWagonsRequired(breakBulk(400, 0), 69)).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('configured items-fit (floor space vs tonnage)', () => {
|
||||||
|
it('weight binds: 50 cars × 20T on a 70T wagon that fits 4 → 3 per wagon → 17', () => {
|
||||||
|
// floor(70/20) = 3 by tonnage < 4 by floor space.
|
||||||
|
expect(bulkItemWagonsRequired(breakBulk(50, 1000), 70, 4)).toBe(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('floor space binds: 50 cars × 10T on a 70T wagon that fits 4 → 4 per wagon → 13', () => {
|
||||||
|
// floor(70/10) = 7 by tonnage, but only 4 fit physically.
|
||||||
|
expect(bulkItemWagonsRequired(breakBulk(50, 500), 70, 4)).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores an absent/invalid fit (legacy cargo types): tonnage-only', () => {
|
||||||
|
expect(bulkItemWagonsRequired(breakBulk(50, 1000), 70, null)).toBe(17);
|
||||||
|
expect(bulkItemWagonsRequired(breakBulk(50, 1000), 70, 0)).toBe(17);
|
||||||
|
// floor(70/10) = 7 per wagon → ceil(50/7) = 8 wagons.
|
||||||
|
expect(bulkItemWagonsRequired(breakBulk(50, 500), 70)).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bulkItemWagonsForAllowedTypes', () => {
|
||||||
|
const breakBulk = (quantity: number, weightTons: number) => ({
|
||||||
|
freightType: 'BULK',
|
||||||
|
cargoTotalWeightVgm: quantity,
|
||||||
|
bulkTotalWeightTons: weightTons,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('picks the fewest-wagon allowed type, each capped by its own fit', () => {
|
||||||
|
const cargoType = {
|
||||||
|
wagonTypes: [
|
||||||
|
{ id: 'nw5', capacityTons: 70 },
|
||||||
|
{ id: 'nw7', capacityTons: 80 },
|
||||||
|
],
|
||||||
|
itemsPerWagonMap: { nw5: 4, nw7: 6 },
|
||||||
|
};
|
||||||
|
// 50 cars × 20T: NW5 → min(4, floor(70/20)=3) = 3/wagon = 17 wagons;
|
||||||
|
// NW7 → min(6, floor(80/20)=4) = 4/wagon = 13 wagons. Best = 13.
|
||||||
|
expect(bulkItemWagonsForAllowedTypes(breakBulk(50, 1000), cargoType, 70)).toBe(13);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('equals the old max-capacity estimate when no fits are configured', () => {
|
||||||
|
const cargoType = {
|
||||||
|
wagonTypes: [
|
||||||
|
{ id: 'a', capacityTons: 50 },
|
||||||
|
{ id: 'b', capacityTons: 70 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
// Tonnage-only best = biggest wagon: floor(70/20) = 3/wagon → 17.
|
||||||
|
expect(bulkItemWagonsForAllowedTypes(breakBulk(50, 1000), cargoType, 1)).toBe(17);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the given capacity when the relation is missing', () => {
|
||||||
|
expect(bulkItemWagonsForAllowedTypes(breakBulk(50, 1000), null, 70)).toBe(17);
|
||||||
|
expect(bulkItemWagonsForAllowedTypes(breakBulk(50, 1000), { wagonTypes: [] }, 70)).toBe(17);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('bookingCargoTons (break-bulk weight preference)', () => {
|
describe('bookingCargoTons (break-bulk weight preference)', () => {
|
||||||
|
|||||||
@@ -120,6 +120,12 @@ export function bookingCargoTons(booking: {
|
|||||||
* 400 items / 800T on 69T wagons → 2T per item → 34 items per wagon → 12 wagons.
|
* 400 items / 800T on 69T wagons → 2T per item → 34 items per wagon → 12 wagons.
|
||||||
* Returns 0 when the booking is not item-counted (PER_TON bulk, containers) —
|
* Returns 0 when the booking is not item-counted (PER_TON bulk, containers) —
|
||||||
* callers then fall back to the pooled-tonnage math.
|
* callers then fall back to the pooled-tonnage math.
|
||||||
|
*
|
||||||
|
* `itemsFit` is the wagon type's PHYSICAL item capacity (floor space — from
|
||||||
|
* cargoType.itemsPerWagonMap). It binds independently of tonnage: a 70T wagon
|
||||||
|
* that fits 4 cars takes 3 cars of 20T (weight binds) but only 4 cars of 10T
|
||||||
|
* (floor binds, 30T of rated capacity ride empty). Absent/invalid fit falls
|
||||||
|
* back to tonnage-only (legacy cargo types without a configured fit).
|
||||||
*/
|
*/
|
||||||
export function bulkItemWagonsRequired(
|
export function bulkItemWagonsRequired(
|
||||||
booking: {
|
booking: {
|
||||||
@@ -128,6 +134,7 @@ export function bulkItemWagonsRequired(
|
|||||||
bulkTotalWeightTons?: number | string | null;
|
bulkTotalWeightTons?: number | string | null;
|
||||||
},
|
},
|
||||||
capacityTons: number,
|
capacityTons: number,
|
||||||
|
itemsFit?: number | null,
|
||||||
): number {
|
): number {
|
||||||
if (booking.freightType !== 'BULK' || !(capacityTons > 0)) return 0;
|
if (booking.freightType !== 'BULK' || !(capacityTons > 0)) return 0;
|
||||||
const quantity = num(booking.cargoTotalWeightVgm);
|
const quantity = num(booking.cargoTotalWeightVgm);
|
||||||
@@ -136,10 +143,56 @@ export function bulkItemWagonsRequired(
|
|||||||
const perItemTons = totalWeightTons / quantity;
|
const perItemTons = totalWeightTons / quantity;
|
||||||
// ponytail: an item heavier than a whole wagon still charges 1 wagon per
|
// ponytail: an item heavier than a whole wagon still charges 1 wagon per
|
||||||
// item; reject such bookings at creation time if the case turns real.
|
// item; reject such bookings at creation time if the case turns real.
|
||||||
const itemsPerWagon = Math.max(1, Math.floor(capacityTons / perItemTons));
|
const byTonnage = Math.max(1, Math.floor(capacityTons / perItemTons));
|
||||||
|
const byFloor = num(itemsFit) >= 1 ? Math.floor(num(itemsFit)) : Infinity;
|
||||||
|
const itemsPerWagon = Math.min(byTonnage, byFloor);
|
||||||
return Math.max(1, Math.ceil(quantity / itemsPerWagon));
|
return Math.max(1, Math.ceil(quantity / itemsPerWagon));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ItemFitCargoType = {
|
||||||
|
wagonTypes?: Array<{ id: string; capacityTons?: number | string | null }> | null;
|
||||||
|
itemsPerWagonMap?: Record<string, number> | null;
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
/** Configured whole-items fit of one wagon type for a cargo type; null if unset. */
|
||||||
|
export function bulkItemsFitFor(
|
||||||
|
cargoType: ItemFitCargoType | undefined,
|
||||||
|
wagonTypeId: string | null | undefined,
|
||||||
|
): number | null {
|
||||||
|
const fit = wagonTypeId ? Number(cargoType?.itemsPerWagonMap?.[wagonTypeId]) : NaN;
|
||||||
|
return Number.isFinite(fit) && fit >= 1 ? fit : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Break-bulk wagon count when no single wagon type is fixed yet: the best
|
||||||
|
* (fewest-wagon) count across the cargo type's allowed wagon types, each
|
||||||
|
* respecting its own items-fit. With no fits configured this equals the old
|
||||||
|
* max-capacity estimate; with no allowed types it degrades to
|
||||||
|
* `fallbackCapacityTons` tonnage-only.
|
||||||
|
*/
|
||||||
|
export function bulkItemWagonsForAllowedTypes(
|
||||||
|
booking: {
|
||||||
|
freightType?: string | null;
|
||||||
|
cargoTotalWeightVgm?: number | string | null;
|
||||||
|
bulkTotalWeightTons?: number | string | null;
|
||||||
|
},
|
||||||
|
cargoType: ItemFitCargoType | undefined,
|
||||||
|
fallbackCapacityTons: number,
|
||||||
|
): number {
|
||||||
|
const allowed = (cargoType?.wagonTypes ?? []).filter((wt) => num(wt.capacityTons) > 0);
|
||||||
|
if (!allowed.length) return bulkItemWagonsRequired(booking, fallbackCapacityTons);
|
||||||
|
let best = 0;
|
||||||
|
for (const wagonType of allowed) {
|
||||||
|
const wagons = bulkItemWagonsRequired(
|
||||||
|
booking,
|
||||||
|
num(wagonType.capacityTons),
|
||||||
|
bulkItemsFitFor(cargoType, wagonType.id),
|
||||||
|
);
|
||||||
|
if (wagons > 0 && (best === 0 || wagons < best)) best = wagons;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
/** Gross weight of one loaded wagon: it hauls itself plus its cargo. */
|
/** Gross weight of one loaded wagon: it hauls itself plus its cargo. */
|
||||||
export function grossWagonWeightTons(slot: Pick<ConsistSlot, 'tareWeightTons' | 'cargoTons'>): number {
|
export function grossWagonWeightTons(slot: Pick<ConsistSlot, 'tareWeightTons' | 'cargoTons'>): number {
|
||||||
return num(slot.tareWeightTons) + num(slot.cargoTons);
|
return num(slot.tareWeightTons) + num(slot.cargoTons);
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ import { AllocationLoadType } from '@edr/types';
|
|||||||
import { Booking } from '../bookings/entities/booking.entity';
|
import { Booking } from '../bookings/entities/booking.entity';
|
||||||
import { containersPerWagonForSize, wagonsPerUnitForSize } from '../rule-engine/container-type.util';
|
import { containersPerWagonForSize, wagonsPerUnitForSize } from '../rule-engine/container-type.util';
|
||||||
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
import { WagonType } from '../wagon-types/entities/wagon-type.entity';
|
||||||
import { bookingCargoTons, bulkItemWagonsRequired, consistViolations } from './train-capacity.util';
|
import {
|
||||||
|
bookingCargoTons,
|
||||||
|
bulkItemsFitFor,
|
||||||
|
bulkItemWagonsRequired,
|
||||||
|
consistViolations,
|
||||||
|
} from './train-capacity.util';
|
||||||
|
|
||||||
export const MAX_TRAIN_WEIGHT_TONS = 3500;
|
export const MAX_TRAIN_WEIGHT_TONS = 3500;
|
||||||
export const MAX_TRAIN_LENGTH_METERS = 760;
|
export const MAX_TRAIN_LENGTH_METERS = 760;
|
||||||
@@ -175,7 +180,11 @@ export function buildBulkWagonPlan(
|
|||||||
// Break-bulk (PER_ITEM) bookings size by indivisible items per booking —
|
// Break-bulk (PER_ITEM) bookings size by indivisible items per booking —
|
||||||
// their tonnage must NOT pool with PER_TON cargo (an item can't split
|
// their tonnage must NOT pool with PER_TON cargo (an item can't split
|
||||||
// across wagons the way loose tonnage can).
|
// across wagons the way loose tonnage can).
|
||||||
const itemSlotsByBooking = bookings.map((b) => bulkItemWagonsRequired(b, capacity));
|
const itemSlotsByBooking = bookings.map((b) =>
|
||||||
|
// The plan fixed THIS wagon type, so its configured items-fit binds — not
|
||||||
|
// the best fit across the cargo's allowed types.
|
||||||
|
bulkItemWagonsRequired(b, capacity, bulkItemsFitFor(b.cargoType, wagonType.id)),
|
||||||
|
);
|
||||||
const itemSlots = itemSlotsByBooking.reduce((sum, n) => sum + n, 0);
|
const itemSlots = itemSlotsByBooking.reduce((sum, n) => sum + n, 0);
|
||||||
const totalWeight = roundTons(
|
const totalWeight = roundTons(
|
||||||
bookings.reduce(
|
bookings.reduce(
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'reflect-metadata';
|
||||||
|
import { config } from 'dotenv';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
|
||||||
|
config({ path: resolve(__dirname, '../../.env') });
|
||||||
|
process.env.TYPEORM_LOGGING = 'false';
|
||||||
|
|
||||||
|
import { AppModule } from '../app.module';
|
||||||
|
import { WarehouseInventoryService } from '../modules/warehouses/warehouse-inventory.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* One-off backfill for bookings caught by the autoArriveAtFinalYard bug
|
||||||
|
* (fixed in booking-journey.service.ts): the bulk final-yard arrival used to
|
||||||
|
* flip booking status to ARRIVED/COMPLETED without ever emitting
|
||||||
|
* booking.unloadedAtYard, so WarehouseInventoryService never created their
|
||||||
|
* warehouse_inventory row. Reuses the same idempotent listener the live
|
||||||
|
* event now calls, so it's safe to re-run.
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
const app = await NestFactory.createApplicationContext(AppModule, {
|
||||||
|
logger: ['error', 'warn'],
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dataSource = app.get(DataSource);
|
||||||
|
const inventory = app.get(WarehouseInventoryService);
|
||||||
|
|
||||||
|
const bookings: { id: string; tradeDirection: string }[] = await dataSource.query(
|
||||||
|
`SELECT b.id, b.trade_direction AS "tradeDirection"
|
||||||
|
FROM freight.bookings b
|
||||||
|
WHERE b.deleted_at IS NULL
|
||||||
|
AND b.trade_direction IN ('IMPORT', 'DOMESTIC')
|
||||||
|
AND b.status IN ('ARRIVED', 'COMPLETED')
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM freight.warehouse_inventory wi
|
||||||
|
WHERE wi.booking_id = b.id AND wi.deleted_at IS NULL
|
||||||
|
)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (bookings.length === 0) {
|
||||||
|
console.log('No bookings missing their unload inventory row.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Backfilling ${bookings.length} booking(s)...`);
|
||||||
|
for (const booking of bookings) {
|
||||||
|
await inventory.handleBookingUnloadedAtYard({
|
||||||
|
bookingId: booking.id,
|
||||||
|
tradeDirection: booking.tradeDirection,
|
||||||
|
});
|
||||||
|
console.log(` - ${booking.id} (${booking.tradeDirection})`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await app.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
24
apps/edr-freight-api/src/scripts/migrate.ts
Normal file
24
apps/edr-freight-api/src/scripts/migrate.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import "dotenv/config";
|
||||||
|
import { AppDataSource } from "../data-source";
|
||||||
|
import { ensurePostgresSchemas } from "../config/ensure-postgres-schemas";
|
||||||
|
import { buildDataSourceOptions } from "../config/database.config";
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
await ensurePostgresSchemas(buildDataSourceOptions());
|
||||||
|
|
||||||
|
await AppDataSource.initialize();
|
||||||
|
try {
|
||||||
|
const applied = await AppDataSource.runMigrations();
|
||||||
|
for (const migration of applied) {
|
||||||
|
console.log(`applied: ${migration.name}`);
|
||||||
|
}
|
||||||
|
if (applied.length === 0) console.log("no pending migrations");
|
||||||
|
} finally {
|
||||||
|
await AppDataSource.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -29,7 +29,6 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import type { Freight } from "@edr/types";
|
import type { Freight } from "@edr/types";
|
||||||
import { isViewable } from "@edr/ui-common";
|
import { isViewable } from "@edr/ui-common";
|
||||||
@@ -299,7 +298,14 @@ function DocumentRow({
|
|||||||
<Text size="xs" c="dimmed" mt={4} truncate>
|
<Text size="xs" c="dimmed" mt={4} truncate>
|
||||||
{doc.file.name} · {formatBytes(doc.file.size)} ·{" "}
|
{doc.file.name} · {formatBytes(doc.file.size)} ·{" "}
|
||||||
{doc.uploadedByName ?? "Global Logistics"} ·{" "}
|
{doc.uploadedByName ?? "Global Logistics"} ·{" "}
|
||||||
{dayjs(doc.uploadedAt).format("D MMM YYYY, HH:mm")}
|
{new Date(doc.uploadedAt).toLocaleString("en-GB", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import "@edr/ui-common/display-timezone";
|
||||||
|
|
||||||
|
// The pin must hold on ANY machine timezone — these assertions are the bug:
|
||||||
|
// before the patch they only passed on a PC already set to UTC+3.
|
||||||
|
describe("display-timezone pin (EAT, UTC+3)", () => {
|
||||||
|
const utcMidnight = new Date("2026-01-01T00:00:00Z");
|
||||||
|
|
||||||
|
it("formats Date.toLocale* in EAT regardless of machine timezone", () => {
|
||||||
|
expect(utcMidnight.toLocaleTimeString("en-GB", { hour12: false })).toBe(
|
||||||
|
"03:00:00",
|
||||||
|
);
|
||||||
|
// 22:00 UTC is already the NEXT day in EAT.
|
||||||
|
expect(new Date("2026-01-01T22:00:00Z").toLocaleDateString("en-CA")).toBe(
|
||||||
|
"2026-01-02",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats Intl.DateTimeFormat in EAT and keeps instanceof/statics", () => {
|
||||||
|
const fmt = new Intl.DateTimeFormat("en-GB", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
expect(fmt.format(utcMidnight)).toBe("03:00");
|
||||||
|
expect(fmt).toBeInstanceOf(Intl.DateTimeFormat);
|
||||||
|
expect(Intl.DateTimeFormat.supportedLocalesOf(["en-GB"])).toContain(
|
||||||
|
"en-GB",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects an explicit timeZone option", () => {
|
||||||
|
expect(
|
||||||
|
utcMidnight.toLocaleTimeString("en-GB", {
|
||||||
|
hour12: false,
|
||||||
|
timeZone: "UTC",
|
||||||
|
}),
|
||||||
|
).toBe("00:00:00");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Must stay the first import: pins all date/time display to EAT before any
|
||||||
|
// module can create a formatter in the PC's local timezone.
|
||||||
|
import "@edr/ui-common/display-timezone";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { Input } from "@/shared/common/ui/input";
|
import { Input } from "@/shared/common/ui/input";
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
@@ -219,7 +218,7 @@ const buildQuery = (): CollectionQueryDTO => {
|
|||||||
<TableCell>{log.message}</TableCell>
|
<TableCell>{log.message}</TableCell>
|
||||||
<TableCell className="text-muted-foreground">
|
<TableCell className="text-muted-foreground">
|
||||||
{log.timestamp && !isNaN(new Date(log.timestamp).getTime())
|
{log.timestamp && !isNaN(new Date(log.timestamp).getTime())
|
||||||
? format(new Date(log.timestamp), "yyyy-MM-dd HH:mm:ss")
|
? new Date(log.timestamp).toLocaleString("sv-SE")
|
||||||
: "N/A"}
|
: "N/A"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -58,10 +58,15 @@ interface CargoNode extends RuleEngineRecord {
|
|||||||
unitOfMeasure?: string | null;
|
unitOfMeasure?: string | null;
|
||||||
/** Wagon types that can carry this bulk cargo during scheduling; empty if unset. */
|
/** Wagon types that can carry this bulk cargo during scheduling; empty if unset. */
|
||||||
wagonTypes?: { id: string; code?: string; name?: string }[];
|
wagonTypes?: { id: string; code?: string; name?: string }[];
|
||||||
|
/** PER_ITEM only: whole items that physically fit one wagon, keyed by wagon-type id. */
|
||||||
|
itemsPerWagonMap?: Record<string, number> | null;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
displayOrder?: number;
|
displayOrder?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Form-value prefix for the per-wagon-type items-fit inputs (PER_ITEM cargo). */
|
||||||
|
const ITEMS_FIT_PREFIX = "itemsFit__";
|
||||||
|
|
||||||
const str = (v: unknown): string => (v == null ? "" : String(v));
|
const str = (v: unknown): string => (v == null ? "" : String(v));
|
||||||
const orderOf = (n: CargoNode): number => Number(n.displayOrder ?? 0);
|
const orderOf = (n: CargoNode): number => Number(n.displayOrder ?? 0);
|
||||||
|
|
||||||
@@ -131,15 +136,33 @@ const CargoTypesPage = () => {
|
|||||||
|
|
||||||
// Wagon-type options for the "Wagon types" picker (bulk cargo → allowed list).
|
// Wagon-type options for the "Wagon types" picker (bulk cargo → allowed list).
|
||||||
const { data: wagonTypeOptions } = useWagonTypeOptions(canCreate || canUpdate);
|
const { data: wagonTypeOptions } = useWagonTypeOptions(canCreate || canUpdate);
|
||||||
const formFields = useMemo<FormFieldDef[]>(
|
const formFields = useMemo<FormFieldDef[]>(() => {
|
||||||
() =>
|
const base = FORM_FIELDS.map((field) =>
|
||||||
FORM_FIELDS.map((field) =>
|
field.name === "wagonTypeIds"
|
||||||
field.name === "wagonTypeIds"
|
? { ...field, options: wagonTypeOptions ?? [] }
|
||||||
? { ...field, options: wagonTypeOptions ?? [] }
|
: field,
|
||||||
: field,
|
);
|
||||||
),
|
// PER_ITEM cargo: one "items per wagon" input per SELECTED wagon type — how
|
||||||
[wagonTypeOptions],
|
// many whole items physically fit that wagon (floor space binds before
|
||||||
);
|
// tonnage). Shown only while the wagon type is picked; the API requires a
|
||||||
|
// fit for every selected type on PER_ITEM cargo.
|
||||||
|
const fitFields: FormFieldDef[] = (wagonTypeOptions ?? []).map((opt) => ({
|
||||||
|
name: `${ITEMS_FIT_PREFIX}${opt.value}`,
|
||||||
|
label: `Items per ${opt.label} wagon`,
|
||||||
|
type: "number",
|
||||||
|
required: true,
|
||||||
|
placeholder: "e.g. 4",
|
||||||
|
showIf: (values) =>
|
||||||
|
values.unitOfMeasure === "PER_ITEM" &&
|
||||||
|
Array.isArray(values.wagonTypeIds) &&
|
||||||
|
(values.wagonTypeIds as string[]).includes(opt.value),
|
||||||
|
getInitialValue: (record) =>
|
||||||
|
(record as CargoNode).itemsPerWagonMap?.[opt.value],
|
||||||
|
}));
|
||||||
|
const wagonTypesAt = base.findIndex((field) => field.name === "wagonTypeIds");
|
||||||
|
base.splice(wagonTypesAt + 1, 0, ...fitFields);
|
||||||
|
return base;
|
||||||
|
}, [wagonTypeOptions]);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [formMode, setFormMode] = useState<FormMode | null>(null);
|
const [formMode, setFormMode] = useState<FormMode | null>(null);
|
||||||
@@ -203,7 +226,18 @@ const CargoTypesPage = () => {
|
|||||||
const countAtRoot = (childrenOf.get("__root__") ?? []).length;
|
const countAtRoot = (childrenOf.get("__root__") ?? []).length;
|
||||||
|
|
||||||
const handleSubmit = (values: Record<string, unknown>) => {
|
const handleSubmit = (values: Record<string, unknown>) => {
|
||||||
const payload: Record<string, unknown> = { ...values };
|
// Fold the per-wagon-type fit inputs into the API's map shape. Null when
|
||||||
|
// none are visible (not PER_ITEM) so an update clears stale fits.
|
||||||
|
const payload: Record<string, unknown> = {};
|
||||||
|
const itemsPerWagonMap: Record<string, number> = {};
|
||||||
|
for (const [key, value] of Object.entries(values)) {
|
||||||
|
if (key.startsWith(ITEMS_FIT_PREFIX)) {
|
||||||
|
itemsPerWagonMap[key.slice(ITEMS_FIT_PREFIX.length)] = Number(value);
|
||||||
|
} else {
|
||||||
|
payload[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
payload.itemsPerWagonMap = Object.keys(itemsPerWagonMap).length ? itemsPerWagonMap : null;
|
||||||
// Add always attaches to the page we're on; edit keeps the node's parent.
|
// Add always attaches to the page we're on; edit keeps the node's parent.
|
||||||
if (formMode?.kind === "create" && current) {
|
if (formMode?.kind === "create" && current) {
|
||||||
payload.parentGroupId = current.id;
|
payload.parentGroupId = current.id;
|
||||||
|
|||||||
@@ -107,7 +107,14 @@ const ReminderList = () => {
|
|||||||
Remind {dayjs(reminder.remindAt).fromNow()}
|
Remind {dayjs(reminder.remindAt).fromNow()}
|
||||||
</p>
|
</p>
|
||||||
<span className="text-[11px] text-gray-400">
|
<span className="text-[11px] text-gray-400">
|
||||||
({dayjs(reminder.remindAt).format("MMM D, h:mm A")})
|
(
|
||||||
|
{new Date(reminder.remindAt).toLocaleString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import * as React from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Card, CardContent } from "@/shared/common/ui/card";
|
import { Card, CardContent } from "@/shared/common/ui/card";
|
||||||
import { Badge } from "@/shared/common/ui/badge";
|
import { Badge } from "@/shared/common/ui/badge";
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/common/ui/avatar";
|
import { Avatar, AvatarFallback, AvatarImage } from "@/shared/common/ui/avatar";
|
||||||
import { format } from "date-fns";
|
|
||||||
import {
|
import {
|
||||||
Clock,
|
Clock,
|
||||||
User,
|
User,
|
||||||
@@ -101,8 +99,14 @@ export function ActivityCard({ activity }: { activity: ActivityCardProps }) {
|
|||||||
const iconBg = "bg-gray-100 dark:bg-gray-800";
|
const iconBg = "bg-gray-100 dark:bg-gray-800";
|
||||||
const whiteBg = "bg-white dark:bg-gray-900";
|
const whiteBg = "bg-white dark:bg-gray-900";
|
||||||
|
|
||||||
const formattedDate = format(new Date(activity.timestamp), "MMM d, yyyy");
|
const formattedDate = new Date(activity.timestamp).toLocaleDateString(
|
||||||
const formattedTime = format(new Date(activity.timestamp), "HH:mm:ss");
|
"en-US",
|
||||||
|
{ month: "short", day: "numeric", year: "numeric" },
|
||||||
|
);
|
||||||
|
const formattedTime = new Date(activity.timestamp).toLocaleTimeString(
|
||||||
|
"en-GB",
|
||||||
|
{ hour12: false },
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
|
|||||||
@@ -213,7 +213,6 @@ export default function AuditLogPageShared({
|
|||||||
const hoverSoft = "hover:bg-gray-100 dark:hover:bg-gray-800";
|
const hoverSoft = "hover:bg-gray-100 dark:hover:bg-gray-800";
|
||||||
const primaryBtn =
|
const primaryBtn =
|
||||||
"bg-gray-900 hover:bg-gray-800 dark:bg-gray-100 dark:hover:bg-gray-200 text-white dark:text-gray-900";
|
"bg-gray-900 hover:bg-gray-800 dark:bg-gray-100 dark:hover:bg-gray-200 text-white dark:text-gray-900";
|
||||||
const primaryIcon = "text-gray-900 dark:text-gray-100";
|
|
||||||
|
|
||||||
const getSeverityColor = (severity: string) => {
|
const getSeverityColor = (severity: string) => {
|
||||||
switch (severity) {
|
switch (severity) {
|
||||||
@@ -472,10 +471,14 @@ export default function AuditLogPageShared({
|
|||||||
textSubtle,
|
textSubtle,
|
||||||
)}>
|
)}>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{format(new Date(log.timestamp), "MMM d, yyyy")}
|
{new Date(log.timestamp).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="whitespace-nowrap">
|
<span className="whitespace-nowrap">
|
||||||
{format(new Date(log.timestamp), "HH:mm:ss")}
|
{new Date(log.timestamp).toLocaleTimeString("en-GB", { hour12: false })}
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-2 min-w-0">
|
<span className="inline-flex items-center gap-2 min-w-0">
|
||||||
<span className={cn(textSubtle)}>•</span>
|
<span className={cn(textSubtle)}>•</span>
|
||||||
@@ -817,13 +820,17 @@ export default function AuditLogPageShared({
|
|||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className={cn("text-sm", textStrong)}>
|
<span className={cn("text-sm", textStrong)}>
|
||||||
{format(
|
{new Date(log.timestamp).toLocaleDateString(
|
||||||
new Date(log.timestamp),
|
"en-US",
|
||||||
"MMM d, yyyy",
|
{
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
},
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className={cn("text-xs", textSubtle)}>
|
<span className={cn("text-xs", textSubtle)}>
|
||||||
{format(new Date(log.timestamp), "HH:mm:ss")}
|
{new Date(log.timestamp).toLocaleTimeString("en-GB", { hour12: false })}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edr/types": "workspace:*",
|
"@edr/types": "workspace:*",
|
||||||
"@edr/ui-common": "workspace:*",
|
"@edr/ui-common": "workspace:*",
|
||||||
"@hookform/resolvers": "^5.4.0",
|
"@hookform/resolvers": "^5.6.0",
|
||||||
"@mantine/core": "^9.3.0",
|
"@mantine/core": "^9.3.0",
|
||||||
"@mantine/dates": "^9.3.0",
|
"@mantine/dates": "^9.3.0",
|
||||||
"@mantine/hooks": "^9.3.0",
|
"@mantine/hooks": "^9.3.0",
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ export const URL_CONSTANTS = {
|
|||||||
CANCEL: (id: string | number) => `/api/bookings/${id}/cancel`,
|
CANCEL: (id: string | number) => `/api/bookings/${id}/cancel`,
|
||||||
CONFIRM: (id: string | number) => `/api/bookings/${id}/confirm`,
|
CONFIRM: (id: string | number) => `/api/bookings/${id}/confirm`,
|
||||||
CUSTOMER_TRUCKS: (id: string) => `/api/bookings/${id}/customer-trucks`,
|
CUSTOMER_TRUCKS: (id: string) => `/api/bookings/${id}/customer-trucks`,
|
||||||
|
CUSTOMER_TRUCKS_BULK: (id: string) =>
|
||||||
|
`/api/bookings/${id}/customer-trucks/bulk`,
|
||||||
CUSTOMER_TRUCK: (id: string, assignmentId: string) =>
|
CUSTOMER_TRUCK: (id: string, assignmentId: string) =>
|
||||||
`/api/bookings/${id}/customer-trucks/${assignmentId}`,
|
`/api/bookings/${id}/customer-trucks/${assignmentId}`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Must stay the first import: pins all date/time display to EAT before any
|
||||||
|
// module can create a formatter in the PC's local timezone.
|
||||||
|
import "@edr/ui-common/display-timezone";
|
||||||
import { StrictMode } from "react";
|
import { StrictMode } from "react";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Box, Group, Text } from "@mantine/core";
|
import { Box, Group, Text } from "@mantine/core";
|
||||||
import { format } from "date-fns";
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { STATUS_CONFIG, cv } from "../constants";
|
import { STATUS_CONFIG, cv } from "../constants";
|
||||||
|
|
||||||
@@ -56,7 +55,10 @@ export const ActivityRow = memo(function ActivityRow({
|
|||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Text fz={11} c="edr-muted" className="shrink-0">
|
<Text fz={11} c="edr-muted" className="shrink-0">
|
||||||
{format(new Date(booking.createdAt), "MMM d")}
|
{new Date(booking.createdAt).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -106,7 +106,8 @@ function Countdown({
|
|||||||
day: "numeric",
|
day: "numeric",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
})}
|
})}{" "}
|
||||||
|
EAT
|
||||||
</Text>
|
</Text>
|
||||||
{onPay && (
|
{onPay && (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Upload, Download, AlertCircle, CheckCircle, AlertTriangle } from "lucid
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { client } from "@/utils/api";
|
import { client } from "@/utils/api";
|
||||||
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||||
import { generateTruckAssignmentTemplate, parseTruckAssignmentFile } from "@/utils/truck-assignment-template";
|
import { generateTruckAssignmentTemplate, parseTruckAssignmentFile } from "@/utils/truck-assignment-template";
|
||||||
|
|
||||||
interface BulkTruckUploadModalProps {
|
interface BulkTruckUploadModalProps {
|
||||||
@@ -32,7 +33,7 @@ export function BulkTruckUploadModal({
|
|||||||
|
|
||||||
const uploadMutation = useMutation({
|
const uploadMutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const { data } = await client.post(`/bookings/${bookingId}/customer-trucks/bulk`, {
|
const { data } = await client.post(URL_CONSTANTS.BOOKINGS.CUSTOMER_TRUCKS_BULK(bookingId), {
|
||||||
trucks: parsed,
|
trucks: parsed,
|
||||||
});
|
});
|
||||||
return data;
|
return data;
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function formatPriceUnit(unit: string): string {
|
|||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
PER_CONTAINER: "per container",
|
PER_CONTAINER: "per container",
|
||||||
PER_TON: "per ton",
|
PER_TON: "per ton",
|
||||||
|
PER_ITEM: "per item",
|
||||||
PER_WAGON: "per wagon",
|
PER_WAGON: "per wagon",
|
||||||
PER_KM: "per km",
|
PER_KM: "per km",
|
||||||
FLAT: "flat",
|
FLAT: "flat",
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Textarea,
|
Textarea,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { format } from "date-fns";
|
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -239,7 +238,12 @@ export function Step8Review({
|
|||||||
values.destinationYard;
|
values.destinationYard;
|
||||||
|
|
||||||
const scheduleLabel = values.scheduledDate
|
const scheduleLabel = values.scheduledDate
|
||||||
? format(new Date(values.scheduledDate), "EEEE, MMM d, yyyy")
|
? new Date(values.scheduledDate).toLocaleDateString("en-US", {
|
||||||
|
weekday: "long",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
: "—";
|
: "—";
|
||||||
|
|
||||||
const directionLabel = direction
|
const directionLabel = direction
|
||||||
|
|||||||
@@ -61,6 +61,27 @@ services:
|
|||||||
- mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc
|
- mc alias set e2e http://minio-e2e:9000 e2e-minio e2e-minio-secret && mc mb --ignore-existing e2e/fhc
|
||||||
restart: "no"
|
restart: "no"
|
||||||
|
|
||||||
|
# One-shot: runs schema creation + migrations against postgres-freight-e2e,
|
||||||
|
# then exits. freight-api-e2e no longer migrates itself on boot (migrationsRun
|
||||||
|
# is false) — this is the CI "migration" stage, run here the same way.
|
||||||
|
freight-migration-e2e:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: apps/edr-freight-api/Dockerfile
|
||||||
|
target: migration
|
||||||
|
secrets:
|
||||||
|
- npmrc
|
||||||
|
depends_on:
|
||||||
|
postgres-freight-e2e:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DB_HOST: postgres-freight-e2e
|
||||||
|
DB_PORT: "5432"
|
||||||
|
DB_USER: edr_e2e
|
||||||
|
DB_PASSWORD: edr_e2e
|
||||||
|
DB_NAME: edr_freight_e2e
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
# Stand-in for eSignet's token/userinfo endpoints (see fayda-mock/server.js).
|
# Stand-in for eSignet's token/userinfo endpoints (see fayda-mock/server.js).
|
||||||
# The real Fayda authorization step (phone + SMS OTP) can't run in e2e —
|
# The real Fayda authorization step (phone + SMS OTP) can't run in e2e —
|
||||||
# Cypress bypasses the popup and drives POST start / POST complete directly,
|
# Cypress bypasses the popup and drives POST start / POST complete directly,
|
||||||
@@ -139,6 +160,8 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
minio-init-e2e:
|
minio-init-e2e:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
freight-migration-e2e:
|
||||||
|
condition: service_completed_successfully
|
||||||
fayda-mock-e2e:
|
fayda-mock-e2e:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
etrade-mock-e2e:
|
etrade-mock-e2e:
|
||||||
@@ -195,7 +218,8 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${E2E_API_PORT:-3101}:3001"
|
- "${E2E_API_PORT:-3101}:3001"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
# Boot runs 240+ migrations + seeders on first start — generous start_period.
|
# Migrations run in freight-migration-e2e before this container even
|
||||||
|
# starts (depends_on above) — boot here is just Nest bootstrap + seeders.
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
"CMD",
|
"CMD",
|
||||||
@@ -206,7 +230,7 @@ services:
|
|||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 12
|
retries: 12
|
||||||
start_period: 180s
|
start_period: 60s
|
||||||
|
|
||||||
freight-portal-e2e:
|
freight-portal-e2e:
|
||||||
build:
|
build:
|
||||||
|
|||||||
BIN
local-packages/tria-plc-api-common-1.6.0.tgz
Normal file
BIN
local-packages/tria-plc-api-common-1.6.0.tgz
Normal file
Binary file not shown.
BIN
local-packages/tria-plc-iamapi-common-1.0.0.tgz
Normal file
BIN
local-packages/tria-plc-iamapi-common-1.0.0.tgz
Normal file
Binary file not shown.
@@ -7,6 +7,7 @@
|
|||||||
"types": "./src/index.ts",
|
"types": "./src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
"./display-timezone": "./src/lib/display-timezone.ts",
|
||||||
"./styles.css": "./dist/index.css",
|
"./styles.css": "./dist/index.css",
|
||||||
"./theme.css": "./src/styles/theme.css"
|
"./theme.css": "./src/styles/theme.css"
|
||||||
},
|
},
|
||||||
|
|||||||
48
packages/ui-common/src/lib/display-timezone.ts
Normal file
48
packages/ui-common/src/lib/display-timezone.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Side-effect module: pins every Intl-based date/time display to East Africa
|
||||||
|
* Time (UTC+3) so all users see the same wall-clock times no matter what
|
||||||
|
* timezone their PC is set to. Import it FIRST in the app entry, before any
|
||||||
|
* other app module, so no module-scope formatter is created unpatched:
|
||||||
|
*
|
||||||
|
* import "@edr/ui-common/display-timezone";
|
||||||
|
*
|
||||||
|
* Call sites that pass an explicit `timeZone` option keep it. date-fns and
|
||||||
|
* dayjs `format()` do NOT go through Intl and stay PC-local — don't use them
|
||||||
|
* to display API timestamps.
|
||||||
|
*/
|
||||||
|
export const DISPLAY_TIME_ZONE = "Africa/Addis_Ababa";
|
||||||
|
|
||||||
|
type Locales = string | string[] | undefined;
|
||||||
|
type LocaleMethod = (
|
||||||
|
this: Date,
|
||||||
|
locales?: Locales,
|
||||||
|
options?: Intl.DateTimeFormatOptions,
|
||||||
|
) => string;
|
||||||
|
|
||||||
|
function pinned(original: LocaleMethod): LocaleMethod {
|
||||||
|
return function (locales, options) {
|
||||||
|
return original.call(this, locales, {
|
||||||
|
...options,
|
||||||
|
timeZone: options?.timeZone ?? DISPLAY_TIME_ZONE,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Date.prototype.toLocaleString = pinned(Date.prototype.toLocaleString);
|
||||||
|
Date.prototype.toLocaleDateString = pinned(Date.prototype.toLocaleDateString);
|
||||||
|
Date.prototype.toLocaleTimeString = pinned(Date.prototype.toLocaleTimeString);
|
||||||
|
|
||||||
|
const OriginalDateTimeFormat = Intl.DateTimeFormat;
|
||||||
|
function PinnedDateTimeFormat(
|
||||||
|
locales?: Locales,
|
||||||
|
options?: Intl.DateTimeFormatOptions,
|
||||||
|
): Intl.DateTimeFormat {
|
||||||
|
return new OriginalDateTimeFormat(locales, {
|
||||||
|
...options,
|
||||||
|
timeZone: options?.timeZone ?? DISPLAY_TIME_ZONE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Keep `instanceof Intl.DateTimeFormat` and the static method working.
|
||||||
|
PinnedDateTimeFormat.prototype = OriginalDateTimeFormat.prototype;
|
||||||
|
PinnedDateTimeFormat.supportedLocalesOf = OriginalDateTimeFormat.supportedLocalesOf;
|
||||||
|
Intl.DateTimeFormat = PinnedDateTimeFormat as unknown as typeof Intl.DateTimeFormat;
|
||||||
230
pnpm-lock.yaml
generated
230
pnpm-lock.yaml
generated
@@ -97,11 +97,11 @@ importers:
|
|||||||
specifier: ^11.1.27
|
specifier: ^11.1.27
|
||||||
version: 11.1.27(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
version: 11.1.27(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/platform-socket.io@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@tria-plc/api-common':
|
'@tria-plc/api-common':
|
||||||
specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz
|
specifier: file:../../local-packages/tria-plc-api-common-1.6.0.tgz
|
||||||
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(3400edbb67a81ad1009ef960f3b47a6d)
|
version: file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489)
|
||||||
'@tria-plc/iamapi-common':
|
'@tria-plc/iamapi-common':
|
||||||
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.15.tgz
|
specifier: file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz
|
||||||
version: file:local-packages/tria-plc-iamapi-common-0.7.15.tgz(578386f46cf99fd4720e3e99f196f69e)
|
version: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(2079b56e9fb8fa788c1a142167448388)
|
||||||
amqp-connection-manager:
|
amqp-connection-manager:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.0(amqplib@2.0.1)
|
version: 5.0.0(amqplib@2.0.1)
|
||||||
@@ -570,8 +570,8 @@ importers:
|
|||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../../packages/ui-common
|
version: link:../../../packages/ui-common
|
||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^5.4.0
|
specifier: ^5.6.0
|
||||||
version: 5.4.0(react-hook-form@7.77.0(react@19.2.6))
|
version: 5.6.0(@sinclair/typebox@0.27.10)(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(class-transformer@0.5.1)(class-validator@0.14.4)(effect@3.21.0)(react-hook-form@7.77.0(react@19.2.6))(zod@4.4.3)
|
||||||
'@mantine/core':
|
'@mantine/core':
|
||||||
specifier: ^9.3.0
|
specifier: ^9.3.0
|
||||||
version: 9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 9.3.0(@mantine/hooks@9.3.0(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -2146,6 +2146,84 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react-hook-form: ^7.55.0
|
react-hook-form: ^7.55.0
|
||||||
|
|
||||||
|
'@hookform/resolvers@5.6.0':
|
||||||
|
resolution: {integrity: sha512-qtgE4NUK/WQFPq8aDe+GOhr0/UiUSKT0m9ta9SMnDS5ZE63yC850ShAGz1lxtwO+dBjhUKvUxmHwkp4eN7/kBQ==}
|
||||||
|
peerDependencies:
|
||||||
|
'@sinclair/typebox': '>=0.25.24'
|
||||||
|
'@standard-schema/spec': ^1.0.0
|
||||||
|
'@typeschema/main': '>=0.13.7'
|
||||||
|
'@vinejs/vine': ^2.0.0 || ^3.0.0
|
||||||
|
ajv: ^8.12.0
|
||||||
|
ajv-errors: ^3.0.0
|
||||||
|
ajv-formats: ^2.1.1
|
||||||
|
arktype: ^2.0.0
|
||||||
|
ata-validator: ^0.7.0
|
||||||
|
class-transformer: '>=0.4.0'
|
||||||
|
class-validator: '>=0.12.0'
|
||||||
|
computed-types: ^1.0.0
|
||||||
|
effect: ^3.10.3
|
||||||
|
fluentvalidation-ts: ^3.0.0
|
||||||
|
fp-ts: ^2.7.0
|
||||||
|
io-ts: ^2.0.0
|
||||||
|
joi: ^17.0.0
|
||||||
|
nope-validator: '>=0.12.0'
|
||||||
|
react-hook-form: ^7.55.0
|
||||||
|
superstruct: '>=0.12.0'
|
||||||
|
typanion: ^3.3.2
|
||||||
|
valibot: '>=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc'
|
||||||
|
vest: '>=3.0.0'
|
||||||
|
yup: ^1.0.0
|
||||||
|
zod: ^3.25.0 || ^4.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@sinclair/typebox':
|
||||||
|
optional: true
|
||||||
|
'@standard-schema/spec':
|
||||||
|
optional: true
|
||||||
|
'@typeschema/main':
|
||||||
|
optional: true
|
||||||
|
'@vinejs/vine':
|
||||||
|
optional: true
|
||||||
|
ajv:
|
||||||
|
optional: true
|
||||||
|
ajv-errors:
|
||||||
|
optional: true
|
||||||
|
ajv-formats:
|
||||||
|
optional: true
|
||||||
|
arktype:
|
||||||
|
optional: true
|
||||||
|
ata-validator:
|
||||||
|
optional: true
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
computed-types:
|
||||||
|
optional: true
|
||||||
|
effect:
|
||||||
|
optional: true
|
||||||
|
fluentvalidation-ts:
|
||||||
|
optional: true
|
||||||
|
fp-ts:
|
||||||
|
optional: true
|
||||||
|
io-ts:
|
||||||
|
optional: true
|
||||||
|
joi:
|
||||||
|
optional: true
|
||||||
|
nope-validator:
|
||||||
|
optional: true
|
||||||
|
superstruct:
|
||||||
|
optional: true
|
||||||
|
typanion:
|
||||||
|
optional: true
|
||||||
|
valibot:
|
||||||
|
optional: true
|
||||||
|
vest:
|
||||||
|
optional: true
|
||||||
|
yup:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -4537,9 +4615,25 @@ packages:
|
|||||||
rxjs: ^7.8.0
|
rxjs: ^7.8.0
|
||||||
typeorm: ^0.3.0
|
typeorm: ^0.3.0
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.15.tgz':
|
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.6.0.tgz':
|
||||||
resolution: {integrity: sha512-3I2rMhQ30ok446WQHS5X0PYZ0HdaAapeT7tPrU6zxemL8ClURGgeUPckYLtL+BFQIrMMQDoM0S6+RvflDIXhyA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.15.tgz}
|
resolution: {integrity: sha512-SZomla65xesBQZ12n8xH+9eX0TRbXNWToQ3SNURLhP1zlHJWUMTVHoRXTd5zWoe4mqah2Lr83L8ueHERsqCTFw==, tarball: file:local-packages/tria-plc-api-common-1.6.0.tgz}
|
||||||
version: 0.7.15
|
version: 1.6.0
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^11.0.0
|
||||||
|
'@nestjs/core': ^11.0.0
|
||||||
|
'@nestjs/jwt': ^11.0.0
|
||||||
|
'@nestjs/microservices': ^11.0.0
|
||||||
|
'@nestjs/passport': ^11.0.0
|
||||||
|
'@nestjs/swagger': ^11.0.0
|
||||||
|
'@nestjs/throttler': ^6.0.0
|
||||||
|
'@nestjs/typeorm': ^11.0.0
|
||||||
|
reflect-metadata: ^0.2.0
|
||||||
|
rxjs: ^7.8.0
|
||||||
|
typeorm: ^0.3.0
|
||||||
|
|
||||||
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz':
|
||||||
|
resolution: {integrity: sha512-Y6SDEJUR4NcwLXrRJFZ+SbknpczybMwp59cR000vjFEu09RClLr5Gzv8TaJbOeDytyhdgjkZriVNPb2Dt6tipA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz}
|
||||||
|
version: 0.7.9
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nestjs/axios': ^4.0.0
|
'@nestjs/axios': ^4.0.0
|
||||||
@@ -4559,9 +4653,9 @@ packages:
|
|||||||
rxjs: ^7.8.0
|
rxjs: ^7.8.0
|
||||||
typeorm: ^0.3.0
|
typeorm: ^0.3.0
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz':
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz':
|
||||||
resolution: {integrity: sha512-Y6SDEJUR4NcwLXrRJFZ+SbknpczybMwp59cR000vjFEu09RClLr5Gzv8TaJbOeDytyhdgjkZriVNPb2Dt6tipA==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.9.tgz}
|
resolution: {integrity: sha512-rfHSXOm/0VUMTj7HrvYysrEAqxItqfPs4Rd35qWJyaAk+snF1wTKFRVz6cRGPoQNj8fhplkVAefnvuKBuHT+xQ==, tarball: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz}
|
||||||
version: 0.7.9
|
version: 1.0.0
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@nestjs/axios': ^4.0.0
|
'@nestjs/axios': ^4.0.0
|
||||||
@@ -13017,6 +13111,20 @@ snapshots:
|
|||||||
'@standard-schema/utils': 0.3.0
|
'@standard-schema/utils': 0.3.0
|
||||||
react-hook-form: 7.77.0(react@19.2.6)
|
react-hook-form: 7.77.0(react@19.2.6)
|
||||||
|
|
||||||
|
'@hookform/resolvers@5.6.0(@sinclair/typebox@0.27.10)(@standard-schema/spec@1.1.0)(ajv-formats@2.1.1(ajv@8.20.0))(ajv@8.20.0)(class-transformer@0.5.1)(class-validator@0.14.4)(effect@3.21.0)(react-hook-form@7.77.0(react@19.2.6))(zod@4.4.3)':
|
||||||
|
dependencies:
|
||||||
|
'@standard-schema/utils': 0.3.0
|
||||||
|
react-hook-form: 7.77.0(react@19.2.6)
|
||||||
|
optionalDependencies:
|
||||||
|
'@sinclair/typebox': 0.27.10
|
||||||
|
'@standard-schema/spec': 1.1.0
|
||||||
|
ajv: 8.20.0
|
||||||
|
ajv-formats: 2.1.1(ajv@8.20.0)
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.4
|
||||||
|
effect: 3.21.0
|
||||||
|
zod: 4.4.3
|
||||||
|
|
||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
@@ -16282,50 +16390,6 @@ snapshots:
|
|||||||
|
|
||||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||||
|
|
||||||
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(3400edbb67a81ad1009ef960f3b47a6d)':
|
|
||||||
dependencies:
|
|
||||||
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
|
||||||
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
|
||||||
'@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(@nestjs/websockets@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
|
||||||
'@nestjs/jwt': 11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
|
||||||
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
|
||||||
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
|
||||||
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
|
||||||
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
|
||||||
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
|
||||||
'@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.15.tgz(578386f46cf99fd4720e3e99f196f69e)
|
|
||||||
argon2: 0.43.1
|
|
||||||
axios: 1.17.0
|
|
||||||
change-case: 5.4.4
|
|
||||||
class-transformer: 0.5.1
|
|
||||||
class-validator: 0.14.4
|
|
||||||
dotenv: 16.6.1
|
|
||||||
ethiopian-calendar-date-converter: 2.1.6
|
|
||||||
ethiopian-date: 0.0.6
|
|
||||||
exceljs: 4.4.0
|
|
||||||
file-type: 21.3.4
|
|
||||||
handlebars: 4.7.9
|
|
||||||
handlebars-helpers: 0.10.0
|
|
||||||
jmespath: 0.16.0
|
|
||||||
jose: 5.10.0
|
|
||||||
jsonwebtoken: 9.0.3
|
|
||||||
libphonenumber-js: 1.13.6
|
|
||||||
libreoffice-convert: 1.8.1
|
|
||||||
nestjs-minio-client: 2.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)
|
|
||||||
passport-jwt: 4.0.1
|
|
||||||
qrcode: 1.5.4
|
|
||||||
reflect-metadata: 0.2.2
|
|
||||||
rxjs: 7.8.2
|
|
||||||
style-object-to-css-string: 1.1.3
|
|
||||||
typeorm: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))
|
|
||||||
typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
|
||||||
uuid: 11.1.1
|
|
||||||
xlsx: 0.18.5
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@faker-js/faker'
|
|
||||||
- debug
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65)':
|
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
||||||
@@ -16370,7 +16434,7 @@ snapshots:
|
|||||||
- debug
|
- debug
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.15.tgz(578386f46cf99fd4720e3e99f196f69e)':
|
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
||||||
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -16381,7 +16445,50 @@ snapshots:
|
|||||||
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(3400edbb67a81ad1009ef960f3b47a6d)
|
argon2: 0.43.1
|
||||||
|
axios: 1.17.0
|
||||||
|
change-case: 5.4.4
|
||||||
|
class-transformer: 0.5.1
|
||||||
|
class-validator: 0.14.4
|
||||||
|
dotenv: 16.6.1
|
||||||
|
ethiopian-calendar-date-converter: 2.1.6
|
||||||
|
ethiopian-date: 0.0.6
|
||||||
|
exceljs: 4.4.0
|
||||||
|
file-type: 21.3.4
|
||||||
|
handlebars: 4.7.9
|
||||||
|
handlebars-helpers: 0.10.0
|
||||||
|
jmespath: 0.16.0
|
||||||
|
jose: 5.10.0
|
||||||
|
jsonwebtoken: 9.0.3
|
||||||
|
libphonenumber-js: 1.13.6
|
||||||
|
libreoffice-convert: 1.8.1
|
||||||
|
nestjs-minio-client: 2.2.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)
|
||||||
|
passport-jwt: 4.0.1
|
||||||
|
qrcode: 1.5.4
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
style-object-to-css-string: 1.1.3
|
||||||
|
typeorm: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))
|
||||||
|
typeorm-extension: 3.9.0(@faker-js/faker@10.4.0)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
|
uuid: 11.1.1
|
||||||
|
xlsx: 0.18.5
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@faker-js/faker'
|
||||||
|
- debug
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(c97ba831ddde82920910406ab5262991)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
||||||
|
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(@nestjs/websockets@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/jwt': 11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
||||||
|
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
|
'@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
||||||
|
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
||||||
|
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
|
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(59a15a37c5b1c12685ed78e172f27e65)
|
||||||
api-common: 1.2.2
|
api-common: 1.2.2
|
||||||
argon2: 0.43.1
|
argon2: 0.43.1
|
||||||
axios: 1.17.0
|
axios: 1.17.0
|
||||||
@@ -16405,7 +16512,7 @@ snapshots:
|
|||||||
- '@faker-js/faker'
|
- '@faker-js/faker'
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.9.tgz(4d1d275441e80423228c2d8370f9d999)':
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(2079b56e9fb8fa788c1a142167448388)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
||||||
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
@@ -16413,11 +16520,10 @@ snapshots:
|
|||||||
'@nestjs/jwt': 11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
'@nestjs/jwt': 11.0.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))
|
||||||
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
'@nestjs/passport': 10.0.3(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)
|
||||||
'@nestjs/swagger': 7.4.2(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
'@nestjs/throttler': 6.5.0(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)
|
||||||
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(aaad3d77da283ea37b052677c39644c3)
|
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.6.0.tgz(d194f7ef21135288330b07fecd9a2489)
|
||||||
api-common: 1.2.2
|
|
||||||
argon2: 0.43.1
|
argon2: 0.43.1
|
||||||
axios: 1.17.0
|
axios: 1.17.0
|
||||||
class-transformer: 0.5.1
|
class-transformer: 0.5.1
|
||||||
|
|||||||
Reference in New Issue
Block a user