Compare commits
8
Commits
01736f1a16
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1811820fb7 | ||
|
|
f1ecd83e9c | ||
|
|
c3464e5f3f | ||
|
|
e709c332a6 | ||
|
|
952253dceb | ||
|
|
d34db71b51 | ||
|
|
2f1aa30d6b | ||
|
|
3a2bb76dbc |
+1
-1
Submodule graphql-contracts updated: 88e1aa58bf...4fffef4915
@@ -0,0 +1,23 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "orders_quotation" ADD COLUMN "exchange_rate" DECIMAL(24,10),
|
||||||
|
ADD COLUMN "exchange_rate_effective_date" DATE,
|
||||||
|
ADD COLUMN "exchange_rate_locked_at" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "exchange_rate_provider" VARCHAR(255),
|
||||||
|
ADD COLUMN "exchange_rate_quoted_at" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "exchange_rate_snapshot_uuid" VARCHAR(100),
|
||||||
|
ADD COLUMN "pricing_country_code" VARCHAR(2),
|
||||||
|
ADD COLUMN "pricing_kyc_profile_uuid" VARCHAR(100),
|
||||||
|
ADD COLUMN "source_amount" DECIMAL(12,2),
|
||||||
|
ADD COLUMN "source_currency" VARCHAR(10);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "orders_order" ADD COLUMN "exchange_rate" DECIMAL(24,10),
|
||||||
|
ADD COLUMN "exchange_rate_effective_date" DATE,
|
||||||
|
ADD COLUMN "exchange_rate_locked_at" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "exchange_rate_provider" VARCHAR(255),
|
||||||
|
ADD COLUMN "exchange_rate_quoted_at" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "exchange_rate_snapshot_uuid" VARCHAR(100),
|
||||||
|
ADD COLUMN "pricing_country_code" VARCHAR(2),
|
||||||
|
ADD COLUMN "pricing_kyc_profile_uuid" VARCHAR(100),
|
||||||
|
ADD COLUMN "source_amount" DECIMAL(12,2),
|
||||||
|
ADD COLUMN "source_currency" VARCHAR(10);
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "orders_order" ADD COLUMN "destination_hub_uuid" VARCHAR(100),
|
||||||
|
ADD COLUMN "source_hub_uuid" VARCHAR(100);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "orders_quotation_route_stage" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"uuid" TEXT NOT NULL,
|
||||||
|
"quotation_id" INTEGER NOT NULL,
|
||||||
|
"sequence" INTEGER NOT NULL,
|
||||||
|
"stage_type" VARCHAR(30) NOT NULL,
|
||||||
|
"transport_type" VARCHAR(50),
|
||||||
|
"source_point_uuid" VARCHAR(100),
|
||||||
|
"source_point_type" VARCHAR(30) NOT NULL,
|
||||||
|
"source_location_name" VARCHAR(255) NOT NULL,
|
||||||
|
"source_latitude" DOUBLE PRECISION,
|
||||||
|
"source_longitude" DOUBLE PRECISION,
|
||||||
|
"destination_point_uuid" VARCHAR(100),
|
||||||
|
"destination_point_type" VARCHAR(30) NOT NULL,
|
||||||
|
"destination_location_name" VARCHAR(255) NOT NULL,
|
||||||
|
"destination_latitude" DOUBLE PRECISION,
|
||||||
|
"destination_longitude" DOUBLE PRECISION,
|
||||||
|
"distance_km" DOUBLE PRECISION,
|
||||||
|
"travel_time_seconds" INTEGER,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "orders_quotation_route_stage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "orders_order_route_stage" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"uuid" TEXT NOT NULL,
|
||||||
|
"order_id" INTEGER NOT NULL,
|
||||||
|
"quotation_route_stage_uuid" VARCHAR(100),
|
||||||
|
"sequence" INTEGER NOT NULL,
|
||||||
|
"stage_type" VARCHAR(30) NOT NULL,
|
||||||
|
"transport_type" VARCHAR(50),
|
||||||
|
"source_point_uuid" VARCHAR(100),
|
||||||
|
"source_point_type" VARCHAR(30) NOT NULL,
|
||||||
|
"source_location_name" VARCHAR(255) NOT NULL,
|
||||||
|
"source_latitude" DOUBLE PRECISION,
|
||||||
|
"source_longitude" DOUBLE PRECISION,
|
||||||
|
"destination_point_uuid" VARCHAR(100),
|
||||||
|
"destination_point_type" VARCHAR(30) NOT NULL,
|
||||||
|
"destination_location_name" VARCHAR(255) NOT NULL,
|
||||||
|
"destination_latitude" DOUBLE PRECISION,
|
||||||
|
"destination_longitude" DOUBLE PRECISION,
|
||||||
|
"distance_km" DOUBLE PRECISION,
|
||||||
|
"travel_time_seconds" INTEGER,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "orders_order_route_stage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "orders_quotation_route_stage_uuid_key" ON "orders_quotation_route_stage"("uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "orders_quotation_route_stage_quotation_id_sequence_idx" ON "orders_quotation_route_stage"("quotation_id", "sequence");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "orders_quotation_route_stage_quotation_id_sequence_key" ON "orders_quotation_route_stage"("quotation_id", "sequence");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "orders_order_route_stage_uuid_key" ON "orders_order_route_stage"("uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "orders_order_route_stage_order_id_sequence_idx" ON "orders_order_route_stage"("order_id", "sequence");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "orders_order_route_stage_order_id_sequence_key" ON "orders_order_route_stage"("order_id", "sequence");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "orders_quotation_route_stage" ADD CONSTRAINT "orders_quotation_route_stage_quotation_id_fkey" FOREIGN KEY ("quotation_id") REFERENCES "orders_quotation"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "orders_order_route_stage" ADD CONSTRAINT "orders_order_route_stage_order_id_fkey" FOREIGN KEY ("order_id") REFERENCES "orders_order"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- Backfill immutable quotation route snapshots for existing calculations.
|
||||||
|
INSERT INTO "orders_quotation_route_stage" (
|
||||||
|
"uuid", "quotation_id", "sequence", "stage_type", "transport_type",
|
||||||
|
"source_point_uuid", "source_point_type", "source_location_name", "source_latitude", "source_longitude",
|
||||||
|
"destination_point_uuid", "destination_point_type", "destination_location_name", "destination_latitude", "destination_longitude",
|
||||||
|
"distance_km", "updated_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
quotation."uuid" || '-first-mile', quotation."id", 1, 'FIRST_MILE', 'AUTO',
|
||||||
|
quotation."source_location_uuid", 'ADDRESS', COALESCE(quotation."source_location_name", 'Origin'), quotation."source_latitude", quotation."source_longitude",
|
||||||
|
quotation."source_hub_uuid", 'HUB', COALESCE(quotation."source_hub_uuid", 'Origin hub'), NULL, NULL,
|
||||||
|
NULL, CURRENT_TIMESTAMP
|
||||||
|
FROM "orders_quotation" quotation
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
quotation."uuid" || '-main-haul', quotation."id", 2, 'MAIN_HAUL', COALESCE(quotation."transport_type_code", 'MULTIMODAL'),
|
||||||
|
quotation."source_hub_uuid", 'HUB', COALESCE(quotation."source_hub_uuid", 'Origin hub'), NULL, NULL,
|
||||||
|
quotation."destination_hub_uuid", 'HUB', COALESCE(quotation."destination_hub_uuid", 'Destination hub'), NULL, NULL,
|
||||||
|
quotation."route_distance_km", CURRENT_TIMESTAMP
|
||||||
|
FROM "orders_quotation" quotation
|
||||||
|
UNION ALL
|
||||||
|
SELECT
|
||||||
|
quotation."uuid" || '-last-mile', quotation."id", 3, 'LAST_MILE', 'AUTO',
|
||||||
|
quotation."destination_hub_uuid", 'HUB', COALESCE(quotation."destination_hub_uuid", 'Destination hub'), NULL, NULL,
|
||||||
|
quotation."destination_location_uuid", 'ADDRESS', COALESCE(quotation."destination_location_name", 'Destination'), quotation."destination_latitude", quotation."destination_longitude",
|
||||||
|
NULL, CURRENT_TIMESTAMP
|
||||||
|
FROM "orders_quotation" quotation;
|
||||||
|
|
||||||
|
UPDATE "orders_order" order_row
|
||||||
|
SET
|
||||||
|
"source_hub_uuid" = quotation."source_hub_uuid",
|
||||||
|
"destination_hub_uuid" = quotation."destination_hub_uuid"
|
||||||
|
FROM "orders_quotation" quotation
|
||||||
|
WHERE order_row."quotation_id" = quotation."id";
|
||||||
|
|
||||||
|
INSERT INTO "orders_order_route_stage" (
|
||||||
|
"uuid", "order_id", "quotation_route_stage_uuid", "sequence", "stage_type", "transport_type",
|
||||||
|
"source_point_uuid", "source_point_type", "source_location_name", "source_latitude", "source_longitude",
|
||||||
|
"destination_point_uuid", "destination_point_type", "destination_location_name", "destination_latitude", "destination_longitude",
|
||||||
|
"distance_km", "travel_time_seconds", "updated_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
order_row."uuid" || '-stage-' || route_stage."sequence", order_row."id", route_stage."uuid", route_stage."sequence", route_stage."stage_type", route_stage."transport_type",
|
||||||
|
route_stage."source_point_uuid", route_stage."source_point_type", route_stage."source_location_name", route_stage."source_latitude", route_stage."source_longitude",
|
||||||
|
route_stage."destination_point_uuid", route_stage."destination_point_type", route_stage."destination_location_name", route_stage."destination_latitude", route_stage."destination_longitude",
|
||||||
|
route_stage."distance_km", route_stage."travel_time_seconds", CURRENT_TIMESTAMP
|
||||||
|
FROM "orders_order" order_row
|
||||||
|
JOIN "orders_quotation_route_stage" route_stage ON route_stage."quotation_id" = order_row."quotation_id";
|
||||||
|
|
||||||
|
INSERT INTO "orders_order_route_stage" (
|
||||||
|
"uuid", "order_id", "sequence", "stage_type", "transport_type",
|
||||||
|
"source_point_type", "source_location_name", "source_latitude", "source_longitude",
|
||||||
|
"destination_point_type", "destination_location_name", "destination_latitude", "destination_longitude", "updated_at"
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
order_row."uuid" || '-stage-1', order_row."id", 1, 'MAIN_HAUL', 'MULTIMODAL',
|
||||||
|
'ADDRESS', COALESCE(order_row."source_location_name", 'Origin'), order_row."source_latitude", order_row."source_longitude",
|
||||||
|
'ADDRESS', COALESCE(order_row."destination_location_name", 'Destination'), order_row."destination_latitude", order_row."destination_longitude", CURRENT_TIMESTAMP
|
||||||
|
FROM "orders_order" order_row
|
||||||
|
WHERE order_row."quotation_id" IS NULL;
|
||||||
+89
-7
@@ -26,11 +26,11 @@ model TariffReference {
|
|||||||
maxVolumeCbm Decimal? @map("max_volume_cbm") @db.Decimal(12, 3)
|
maxVolumeCbm Decimal? @map("max_volume_cbm") @db.Decimal(12, 3)
|
||||||
minDistanceKm Int? @map("min_distance_km")
|
minDistanceKm Int? @map("min_distance_km")
|
||||||
maxDistanceKm Int? @map("max_distance_km")
|
maxDistanceKm Int? @map("max_distance_km")
|
||||||
amountUsd Decimal @map("amount_usd") @db.Decimal(12, 2)
|
sourceAmount Decimal @map("amount_usd") @db.Decimal(12, 2)
|
||||||
minPriceUsd Decimal? @map("min_price_usd") @db.Decimal(12, 2)
|
minSourcePrice Decimal? @map("min_price_usd") @db.Decimal(12, 2)
|
||||||
etaDays Int? @map("eta_days")
|
etaDays Int? @map("eta_days")
|
||||||
priority Int @default(100)
|
priority Int @default(100)
|
||||||
currency String @default("USD") @db.VarChar(10)
|
sourceCurrency String @default("USD") @map("currency") @db.VarChar(10)
|
||||||
dmnExpression String? @map("dmn_expression")
|
dmnExpression String? @map("dmn_expression")
|
||||||
notes String?
|
notes String?
|
||||||
createdByUserId String? @map("created_by_user_id") @db.VarChar(255)
|
createdByUserId String? @map("created_by_user_id") @db.VarChar(255)
|
||||||
@@ -76,8 +76,18 @@ model Quotation {
|
|||||||
selectedTariff TariffReference? @relation("QuotationSelectedTariff", fields: [selectedTariffId], references: [id], onDelete: SetNull)
|
selectedTariff TariffReference? @relation("QuotationSelectedTariff", fields: [selectedTariffId], references: [id], onDelete: SetNull)
|
||||||
tariffMatchSummary String? @map("tariff_match_summary")
|
tariffMatchSummary String? @map("tariff_match_summary")
|
||||||
tariffSnapshot String? @map("tariff_snapshot")
|
tariffSnapshot String? @map("tariff_snapshot")
|
||||||
totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(12, 2)
|
sourceAmount Decimal? @map("source_amount") @db.Decimal(12, 2)
|
||||||
currency String @default("USD") @db.VarChar(10)
|
sourceCurrency String? @map("source_currency") @db.VarChar(10)
|
||||||
|
clientAmount Decimal @default(0) @map("total_amount") @db.Decimal(12, 2)
|
||||||
|
clientCurrency String @default("USD") @map("currency") @db.VarChar(10)
|
||||||
|
exchangeRate Decimal? @map("exchange_rate") @db.Decimal(24, 10)
|
||||||
|
exchangeRateSnapshotUuid String? @map("exchange_rate_snapshot_uuid") @db.VarChar(100)
|
||||||
|
exchangeRateProvider String? @map("exchange_rate_provider") @db.VarChar(255)
|
||||||
|
exchangeRateEffectiveDate DateTime? @map("exchange_rate_effective_date") @db.Date
|
||||||
|
exchangeRateQuotedAt DateTime? @map("exchange_rate_quoted_at")
|
||||||
|
exchangeRateLockedAt DateTime? @map("exchange_rate_locked_at")
|
||||||
|
pricingCountryCode String? @map("pricing_country_code") @db.VarChar(2)
|
||||||
|
pricingKycProfileUuid String? @map("pricing_kyc_profile_uuid") @db.VarChar(100)
|
||||||
etaDays Int? @map("eta_days")
|
etaDays Int? @map("eta_days")
|
||||||
notes String?
|
notes String?
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
@@ -85,12 +95,41 @@ model Quotation {
|
|||||||
|
|
||||||
orders Order[]
|
orders Order[]
|
||||||
changes QuotationChange[]
|
changes QuotationChange[]
|
||||||
|
routeStages QuotationRouteStage[]
|
||||||
|
|
||||||
@@index([teamUuid, status, createdAt])
|
@@index([teamUuid, status, createdAt])
|
||||||
@@index([teamUuid, sourceCountryCode, destinationCountryCode])
|
@@index([teamUuid, sourceCountryCode, destinationCountryCode])
|
||||||
@@map("orders_quotation")
|
@@map("orders_quotation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model QuotationRouteStage {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String @unique @default(uuid())
|
||||||
|
quotationId Int @map("quotation_id")
|
||||||
|
quotation Quotation @relation(fields: [quotationId], references: [id], onDelete: Cascade)
|
||||||
|
sequence Int
|
||||||
|
stageType String @map("stage_type") @db.VarChar(30)
|
||||||
|
transportType String? @map("transport_type") @db.VarChar(50)
|
||||||
|
sourcePointUuid String? @map("source_point_uuid") @db.VarChar(100)
|
||||||
|
sourcePointType String @map("source_point_type") @db.VarChar(30)
|
||||||
|
sourceLocationName String @map("source_location_name") @db.VarChar(255)
|
||||||
|
sourceLatitude Float? @map("source_latitude")
|
||||||
|
sourceLongitude Float? @map("source_longitude")
|
||||||
|
destinationPointUuid String? @map("destination_point_uuid") @db.VarChar(100)
|
||||||
|
destinationPointType String @map("destination_point_type") @db.VarChar(30)
|
||||||
|
destinationLocationName String @map("destination_location_name") @db.VarChar(255)
|
||||||
|
destinationLatitude Float? @map("destination_latitude")
|
||||||
|
destinationLongitude Float? @map("destination_longitude")
|
||||||
|
distanceKm Float? @map("distance_km")
|
||||||
|
travelTimeSeconds Int? @map("travel_time_seconds")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@unique([quotationId, sequence])
|
||||||
|
@@index([quotationId, sequence])
|
||||||
|
@@map("orders_quotation_route_stage")
|
||||||
|
}
|
||||||
|
|
||||||
model QuotationChange {
|
model QuotationChange {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
uuid String @unique @default(uuid())
|
uuid String @unique @default(uuid())
|
||||||
@@ -116,8 +155,19 @@ model Order {
|
|||||||
createdByUserId String? @map("created_by_user_id") @db.VarChar(255)
|
createdByUserId String? @map("created_by_user_id") @db.VarChar(255)
|
||||||
name String @db.VarChar(255)
|
name String @db.VarChar(255)
|
||||||
status String @default("draft") @db.VarChar(30)
|
status String @default("draft") @db.VarChar(30)
|
||||||
totalAmount Decimal @default(0) @map("total_amount") @db.Decimal(12, 2)
|
sourceAmount Decimal? @map("source_amount") @db.Decimal(12, 2)
|
||||||
currency String @default("USD") @db.VarChar(10)
|
sourceCurrency String? @map("source_currency") @db.VarChar(10)
|
||||||
|
clientAmount Decimal @default(0) @map("total_amount") @db.Decimal(12, 2)
|
||||||
|
clientCurrency String @default("USD") @map("currency") @db.VarChar(10)
|
||||||
|
exchangeRate Decimal? @map("exchange_rate") @db.Decimal(24, 10)
|
||||||
|
exchangeRateSnapshotUuid String? @map("exchange_rate_snapshot_uuid") @db.VarChar(100)
|
||||||
|
exchangeRateProvider String? @map("exchange_rate_provider") @db.VarChar(255)
|
||||||
|
exchangeRateEffectiveDate DateTime? @map("exchange_rate_effective_date") @db.Date
|
||||||
|
exchangeRateQuotedAt DateTime? @map("exchange_rate_quoted_at")
|
||||||
|
exchangeRateLockedAt DateTime? @map("exchange_rate_locked_at")
|
||||||
|
pricingCountryCode String? @map("pricing_country_code") @db.VarChar(2)
|
||||||
|
pricingKycProfileUuid String? @map("pricing_kyc_profile_uuid") @db.VarChar(100)
|
||||||
|
sourceHubUuid String? @map("source_hub_uuid") @db.VarChar(100)
|
||||||
sourceLocationUuid String? @map("source_location_uuid") @db.VarChar(100)
|
sourceLocationUuid String? @map("source_location_uuid") @db.VarChar(100)
|
||||||
sourceLocationName String? @map("source_location_name") @db.VarChar(255)
|
sourceLocationName String? @map("source_location_name") @db.VarChar(255)
|
||||||
sourceCountryCode String? @map("source_country_code") @db.VarChar(10)
|
sourceCountryCode String? @map("source_country_code") @db.VarChar(10)
|
||||||
@@ -126,6 +176,7 @@ model Order {
|
|||||||
destinationLocationUuid String? @map("destination_location_uuid") @db.VarChar(100)
|
destinationLocationUuid String? @map("destination_location_uuid") @db.VarChar(100)
|
||||||
destinationLocationName String? @map("destination_location_name") @db.VarChar(255)
|
destinationLocationName String? @map("destination_location_name") @db.VarChar(255)
|
||||||
destinationCountryCode String? @map("destination_country_code") @db.VarChar(10)
|
destinationCountryCode String? @map("destination_country_code") @db.VarChar(10)
|
||||||
|
destinationHubUuid String? @map("destination_hub_uuid") @db.VarChar(100)
|
||||||
destinationLatitude Float? @map("destination_latitude")
|
destinationLatitude Float? @map("destination_latitude")
|
||||||
destinationLongitude Float? @map("destination_longitude")
|
destinationLongitude Float? @map("destination_longitude")
|
||||||
etaDays Int? @map("eta_days")
|
etaDays Int? @map("eta_days")
|
||||||
@@ -133,6 +184,37 @@ model Order {
|
|||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime @updatedAt @map("updated_at")
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
routeStages OrderRouteStage[]
|
||||||
|
|
||||||
@@index([teamUuid, createdAt])
|
@@index([teamUuid, createdAt])
|
||||||
@@map("orders_order")
|
@@map("orders_order")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model OrderRouteStage {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String @unique @default(uuid())
|
||||||
|
orderId Int @map("order_id")
|
||||||
|
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
|
||||||
|
quotationRouteStageUuid String? @map("quotation_route_stage_uuid") @db.VarChar(100)
|
||||||
|
sequence Int
|
||||||
|
stageType String @map("stage_type") @db.VarChar(30)
|
||||||
|
transportType String? @map("transport_type") @db.VarChar(50)
|
||||||
|
sourcePointUuid String? @map("source_point_uuid") @db.VarChar(100)
|
||||||
|
sourcePointType String @map("source_point_type") @db.VarChar(30)
|
||||||
|
sourceLocationName String @map("source_location_name") @db.VarChar(255)
|
||||||
|
sourceLatitude Float? @map("source_latitude")
|
||||||
|
sourceLongitude Float? @map("source_longitude")
|
||||||
|
destinationPointUuid String? @map("destination_point_uuid") @db.VarChar(100)
|
||||||
|
destinationPointType String @map("destination_point_type") @db.VarChar(30)
|
||||||
|
destinationLocationName String @map("destination_location_name") @db.VarChar(255)
|
||||||
|
destinationLatitude Float? @map("destination_latitude")
|
||||||
|
destinationLongitude Float? @map("destination_longitude")
|
||||||
|
distanceKm Float? @map("distance_km")
|
||||||
|
travelTimeSeconds Int? @map("travel_time_seconds")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @updatedAt @map("updated_at")
|
||||||
|
|
||||||
|
@@unique([orderId, sequence])
|
||||||
|
@@index([orderId, sequence])
|
||||||
|
@@map("orders_order_route_stage")
|
||||||
|
}
|
||||||
|
|||||||
+209
-17
@@ -1,6 +1,7 @@
|
|||||||
import { createHash } from 'node:crypto'
|
import { createHash } from 'node:crypto'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import { prisma } from './db.js'
|
import { prisma } from './db.js'
|
||||||
|
import { ensureDemoKycPricingContext, lockCurrencyPrice } from './pricing-context.js'
|
||||||
|
|
||||||
interface DemoWorkspaceAccess {
|
interface DemoWorkspaceAccess {
|
||||||
teamUuid: string
|
teamUuid: string
|
||||||
@@ -23,6 +24,26 @@ interface DemoRoute {
|
|||||||
grossWeightKg: number
|
grossWeightKg: number
|
||||||
volumeCbm: number
|
volumeCbm: number
|
||||||
routeDistanceKm: number
|
routeDistanceKm: number
|
||||||
|
sourceHubUuid: string
|
||||||
|
destinationHubUuid: string
|
||||||
|
stages: DemoRouteStage[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DemoRouteStage {
|
||||||
|
stageType: 'FIRST_MILE' | 'MAIN_HAUL' | 'LAST_MILE'
|
||||||
|
transportType: 'AUTO' | 'RAIL' | 'SEA' | 'AIR'
|
||||||
|
sourcePointUuid: string
|
||||||
|
sourcePointType: string
|
||||||
|
sourceLocationName: string
|
||||||
|
sourceLatitude: number
|
||||||
|
sourceLongitude: number
|
||||||
|
destinationPointUuid: string
|
||||||
|
destinationPointType: string
|
||||||
|
destinationLocationName: string
|
||||||
|
destinationLatitude: number
|
||||||
|
destinationLongitude: number
|
||||||
|
distanceKm: number
|
||||||
|
travelTimeSeconds: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const demoRoutes: DemoRoute[] = [
|
const demoRoutes: DemoRoute[] = [
|
||||||
@@ -42,6 +63,14 @@ const demoRoutes: DemoRoute[] = [
|
|||||||
grossWeightKg: 18400,
|
grossWeightKg: 18400,
|
||||||
volumeCbm: 68,
|
volumeCbm: 68,
|
||||||
routeDistanceKm: 8240,
|
routeDistanceKm: 8240,
|
||||||
|
sourceHubUuid: 'hub-mombasa-port',
|
||||||
|
destinationHubUuid: 'hub-almaty-terminal',
|
||||||
|
stages: [
|
||||||
|
{ stageType: 'FIRST_MILE', transportType: 'AUTO', sourcePointUuid: 'supplier-mombasa', sourcePointType: 'ADDRESS', sourceLocationName: 'Mombasa supplier', sourceLatitude: -4.0507, sourceLongitude: 39.6672, destinationPointUuid: 'hub-mombasa-port', destinationPointType: 'PORT', destinationLocationName: 'Port of Mombasa', destinationLatitude: -4.0435, destinationLongitude: 39.6682, distanceKm: 18, travelTimeSeconds: 3600 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'SEA', sourcePointUuid: 'hub-mombasa-port', sourcePointType: 'PORT', sourceLocationName: 'Port of Mombasa', sourceLatitude: -4.0435, sourceLongitude: 39.6682, destinationPointUuid: 'hub-jebel-ali-port', destinationPointType: 'PORT', destinationLocationName: 'Jebel Ali Port', destinationLatitude: 24.9857, destinationLongitude: 55.0273, distanceKm: 4420, travelTimeSeconds: 691200 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'RAIL', sourcePointUuid: 'hub-jebel-ali-port', sourcePointType: 'PORT', sourceLocationName: 'Jebel Ali Port', sourceLatitude: 24.9857, sourceLongitude: 55.0273, destinationPointUuid: 'hub-almaty-terminal', destinationPointType: 'RAIL_STATION', destinationLocationName: 'Almaty freight terminal', destinationLatitude: 43.222, destinationLongitude: 76.8512, distanceKm: 3782, travelTimeSeconds: 518400 },
|
||||||
|
{ stageType: 'LAST_MILE', transportType: 'AUTO', sourcePointUuid: 'hub-almaty-terminal', sourcePointType: 'RAIL_STATION', sourceLocationName: 'Almaty freight terminal', sourceLatitude: 43.222, sourceLongitude: 76.8512, destinationPointUuid: 'recipient-almaty', destinationPointType: 'ADDRESS', destinationLocationName: 'Almaty recipient', destinationLatitude: 43.2389, destinationLongitude: 76.8897, distanceKm: 20, travelTimeSeconds: 3600 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Casablanca → Moscow',
|
title: 'Casablanca → Moscow',
|
||||||
@@ -59,6 +88,14 @@ const demoRoutes: DemoRoute[] = [
|
|||||||
grossWeightKg: 12600,
|
grossWeightKg: 12600,
|
||||||
volumeCbm: 52,
|
volumeCbm: 52,
|
||||||
routeDistanceKm: 5060,
|
routeDistanceKm: 5060,
|
||||||
|
sourceHubUuid: 'hub-casablanca-port',
|
||||||
|
destinationHubUuid: 'hub-moscow-terminal',
|
||||||
|
stages: [
|
||||||
|
{ stageType: 'FIRST_MILE', transportType: 'AUTO', sourcePointUuid: 'supplier-casablanca', sourcePointType: 'ADDRESS', sourceLocationName: 'Casablanca supplier', sourceLatitude: 33.5899, sourceLongitude: -7.6039, destinationPointUuid: 'hub-casablanca-port', destinationPointType: 'PORT', destinationLocationName: 'Port of Casablanca', destinationLatitude: 33.6047, destinationLongitude: -7.6122, distanceKm: 15, travelTimeSeconds: 2700 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'SEA', sourcePointUuid: 'hub-casablanca-port', sourcePointType: 'PORT', sourceLocationName: 'Port of Casablanca', sourceLatitude: 33.6047, sourceLongitude: -7.6122, destinationPointUuid: 'hub-algeciras-port', destinationPointType: 'PORT', destinationLocationName: 'Port of Algeciras', destinationLatitude: 36.1307, destinationLongitude: -5.4488, distanceKm: 455, travelTimeSeconds: 129600 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'RAIL', sourcePointUuid: 'hub-algeciras-port', sourcePointType: 'PORT', sourceLocationName: 'Port of Algeciras', sourceLatitude: 36.1307, sourceLongitude: -5.4488, destinationPointUuid: 'hub-moscow-terminal', destinationPointType: 'RAIL_STATION', destinationLocationName: 'Moscow freight terminal', destinationLatitude: 55.775, destinationLongitude: 37.655, distanceKm: 4560, travelTimeSeconds: 691200 },
|
||||||
|
{ stageType: 'LAST_MILE', transportType: 'AUTO', sourcePointUuid: 'hub-moscow-terminal', sourcePointType: 'RAIL_STATION', sourceLocationName: 'Moscow freight terminal', sourceLatitude: 55.775, sourceLongitude: 37.655, destinationPointUuid: 'recipient-moscow', destinationPointType: 'ADDRESS', destinationLocationName: 'Moscow recipient', destinationLatitude: 55.7558, destinationLongitude: 37.6173, distanceKm: 30, travelTimeSeconds: 5400 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Nairobi → Dubai',
|
title: 'Nairobi → Dubai',
|
||||||
@@ -76,6 +113,13 @@ const demoRoutes: DemoRoute[] = [
|
|||||||
grossWeightKg: 4200,
|
grossWeightKg: 4200,
|
||||||
volumeCbm: 18,
|
volumeCbm: 18,
|
||||||
routeDistanceKm: 3560,
|
routeDistanceKm: 3560,
|
||||||
|
sourceHubUuid: 'hub-nairobi-airport',
|
||||||
|
destinationHubUuid: 'hub-dubai-airport',
|
||||||
|
stages: [
|
||||||
|
{ stageType: 'FIRST_MILE', transportType: 'AUTO', sourcePointUuid: 'supplier-nairobi', sourcePointType: 'ADDRESS', sourceLocationName: 'Nairobi supplier', sourceLatitude: -1.2921, sourceLongitude: 36.8219, destinationPointUuid: 'hub-nairobi-airport', destinationPointType: 'AIRPORT', destinationLocationName: 'Jomo Kenyatta International Airport', destinationLatitude: -1.3192, destinationLongitude: 36.9278, distanceKm: 18, travelTimeSeconds: 2700 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'AIR', sourcePointUuid: 'hub-nairobi-airport', sourcePointType: 'AIRPORT', sourceLocationName: 'Jomo Kenyatta International Airport', sourceLatitude: -1.3192, sourceLongitude: 36.9278, destinationPointUuid: 'hub-dubai-airport', destinationPointType: 'AIRPORT', destinationLocationName: 'Dubai International Airport', destinationLatitude: 25.2532, destinationLongitude: 55.3657, distanceKm: 3550, travelTimeSeconds: 18000 },
|
||||||
|
{ stageType: 'LAST_MILE', transportType: 'AUTO', sourcePointUuid: 'hub-dubai-airport', sourcePointType: 'AIRPORT', sourceLocationName: 'Dubai International Airport', sourceLatitude: 25.2532, sourceLongitude: 55.3657, destinationPointUuid: 'recipient-dubai', destinationPointType: 'ADDRESS', destinationLocationName: 'Dubai recipient', destinationLatitude: 25.2048, destinationLongitude: 55.2708, distanceKm: 22, travelTimeSeconds: 2700 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Johannesburg → Warsaw',
|
title: 'Johannesburg → Warsaw',
|
||||||
@@ -93,10 +137,19 @@ const demoRoutes: DemoRoute[] = [
|
|||||||
grossWeightKg: 7600,
|
grossWeightKg: 7600,
|
||||||
volumeCbm: 31,
|
volumeCbm: 31,
|
||||||
routeDistanceKm: 8690,
|
routeDistanceKm: 8690,
|
||||||
|
sourceHubUuid: 'hub-johannesburg-airport',
|
||||||
|
destinationHubUuid: 'hub-warsaw-airport',
|
||||||
|
stages: [
|
||||||
|
{ stageType: 'FIRST_MILE', transportType: 'AUTO', sourcePointUuid: 'supplier-johannesburg', sourcePointType: 'ADDRESS', sourceLocationName: 'Johannesburg supplier', sourceLatitude: -26.2041, sourceLongitude: 28.0473, destinationPointUuid: 'hub-johannesburg-airport', destinationPointType: 'AIRPORT', destinationLocationName: 'O. R. Tambo International Airport', destinationLatitude: -26.1337, destinationLongitude: 28.242, distanceKm: 26, travelTimeSeconds: 3600 },
|
||||||
|
{ stageType: 'MAIN_HAUL', transportType: 'AIR', sourcePointUuid: 'hub-johannesburg-airport', sourcePointType: 'AIRPORT', sourceLocationName: 'O. R. Tambo International Airport', sourceLatitude: -26.1337, sourceLongitude: 28.242, destinationPointUuid: 'hub-warsaw-airport', destinationPointType: 'AIRPORT', destinationLocationName: 'Warsaw Chopin Airport', destinationLatitude: 52.1657, destinationLongitude: 20.9671, distanceKm: 8660, travelTimeSeconds: 39600 },
|
||||||
|
{ stageType: 'LAST_MILE', transportType: 'AUTO', sourcePointUuid: 'hub-warsaw-airport', sourcePointType: 'AIRPORT', sourceLocationName: 'Warsaw Chopin Airport', sourceLatitude: 52.1657, sourceLongitude: 20.9671, destinationPointUuid: 'recipient-warsaw', destinationPointType: 'ADDRESS', destinationLocationName: 'Warsaw recipient', destinationLatitude: 52.2297, destinationLongitude: 21.0122, distanceKm: 14, travelTimeSeconds: 2400 },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const demoNote = JSON.stringify({ seed: 'empty-team-workspace' })
|
const demoNote = JSON.stringify({ seed: 'empty-team-workspace' })
|
||||||
|
const demoPricingCountryCode = process.env.DEMO_PRICING_COUNTRY_CODE ?? 'RU'
|
||||||
|
const demoClientCurrency = process.env.DEMO_CLIENT_CURRENCY ?? 'RUB'
|
||||||
|
|
||||||
function stableUuid(teamUuid: string, entity: string, index: number): string {
|
function stableUuid(teamUuid: string, entity: string, index: number): string {
|
||||||
const hex = createHash('sha256')
|
const hex = createHash('sha256')
|
||||||
@@ -111,12 +164,57 @@ function stableUuid(teamUuid: string, entity: string, index: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Promise<void> {
|
export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Promise<void> {
|
||||||
const [quotationCount, orderCount] = await Promise.all([
|
const [
|
||||||
|
quotationCount,
|
||||||
|
orderCount,
|
||||||
|
demoQuotationPricingCount,
|
||||||
|
demoOrderPricingCount,
|
||||||
|
demoQuotationCount,
|
||||||
|
demoOrderCount,
|
||||||
|
demoQuotationRouteStageCount,
|
||||||
|
demoOrderRouteStageCount,
|
||||||
|
] = await Promise.all([
|
||||||
prisma.quotation.count({ where: { teamUuid: access.teamUuid } }),
|
prisma.quotation.count({ where: { teamUuid: access.teamUuid } }),
|
||||||
prisma.order.count({ where: { teamUuid: access.teamUuid } }),
|
prisma.order.count({ where: { teamUuid: access.teamUuid } }),
|
||||||
|
prisma.quotation.count({
|
||||||
|
where: { teamUuid: access.teamUuid, notes: demoNote, sourceAmount: null },
|
||||||
|
}),
|
||||||
|
prisma.order.count({
|
||||||
|
where: { teamUuid: access.teamUuid, notes: demoNote, sourceAmount: null },
|
||||||
|
}),
|
||||||
|
prisma.quotation.count({
|
||||||
|
where: { teamUuid: access.teamUuid, notes: demoNote },
|
||||||
|
}),
|
||||||
|
prisma.order.count({
|
||||||
|
where: { teamUuid: access.teamUuid, notes: demoNote },
|
||||||
|
}),
|
||||||
|
prisma.quotationRouteStage.count({
|
||||||
|
where: { quotation: { teamUuid: access.teamUuid, notes: demoNote } },
|
||||||
|
}),
|
||||||
|
prisma.orderRouteStage.count({
|
||||||
|
where: { order: { teamUuid: access.teamUuid, notes: demoNote } },
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
const needsCalculations = quotationCount === 0
|
const needsCalculations = quotationCount === 0
|
||||||
const needsOrders = orderCount === 0
|
const needsOrders = orderCount === 0
|
||||||
|
const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0
|
||||||
|
const expectedQuotationRouteStageCount = demoRoutes.reduce(
|
||||||
|
(total, route) => total + route.stages.length,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
const expectedOrderRouteStageCount = demoRoutes
|
||||||
|
.slice(0, 2)
|
||||||
|
.reduce((total, route) => total + route.stages.length, 0)
|
||||||
|
const needsRouteBackfill =
|
||||||
|
(demoQuotationCount > 0 && demoQuotationRouteStageCount < expectedQuotationRouteStageCount) ||
|
||||||
|
(demoOrderCount > 0 && demoOrderRouteStageCount < expectedOrderRouteStageCount)
|
||||||
|
const kyc = needsCalculations || needsOrders || needsPricingBackfill || needsRouteBackfill
|
||||||
|
? await ensureDemoKycPricingContext({
|
||||||
|
teamUuid: access.teamUuid,
|
||||||
|
userId: access.userId,
|
||||||
|
countryCode: demoPricingCountryCode,
|
||||||
|
})
|
||||||
|
: null
|
||||||
|
|
||||||
for (const [index, route] of demoRoutes.entries()) {
|
for (const [index, route] of demoRoutes.entries()) {
|
||||||
const createsOrder = index < 2
|
const createsOrder = index < 2
|
||||||
@@ -132,11 +230,13 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
status: 'active',
|
status: 'active',
|
||||||
sourceCountryCode: route.sourceCountryCode,
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
destinationCountryCode: route.destinationCountryCode,
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
amountUsd: new Prisma.Decimal(route.amountUsd),
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
minPriceUsd: new Prisma.Decimal(route.amountUsd),
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
|
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
||||||
etaDays: route.etaDays,
|
etaDays: route.etaDays,
|
||||||
priority: 50 + index,
|
priority: 50 + index,
|
||||||
currency: 'USD',
|
sourceCurrency: 'USD',
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
@@ -150,16 +250,28 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
tareTypeCode: null,
|
tareTypeCode: null,
|
||||||
sourceCountryCode: route.sourceCountryCode,
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
destinationCountryCode: route.destinationCountryCode,
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
amountUsd: new Prisma.Decimal(route.amountUsd),
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
minPriceUsd: new Prisma.Decimal(route.amountUsd),
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
|
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
||||||
|
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
||||||
etaDays: route.etaDays,
|
etaDays: route.etaDays,
|
||||||
priority: 50 + index,
|
priority: 50 + index,
|
||||||
currency: 'USD',
|
sourceCurrency: 'USD',
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!needsCalculations && !(needsOrders && createsOrder)) continue
|
if (!needsCalculations && !(needsOrders && createsOrder) && !needsPricingBackfill && !needsRouteBackfill) continue
|
||||||
|
if (kyc === null) throw new Error('KYC pricing context was not resolved for demo data')
|
||||||
|
const price = await lockCurrencyPrice({
|
||||||
|
sourceAmount: route.amountUsd,
|
||||||
|
sourceCurrency: 'USD',
|
||||||
|
clientCurrency: demoClientCurrency,
|
||||||
|
pricingCountryCode: kyc.countryCode,
|
||||||
|
})
|
||||||
|
const pricingEffectiveDate = new Date(`${price.rateEffectiveDate}T00:00:00.000Z`)
|
||||||
|
const pricingQuotedAt = new Date(price.rateQuotedAt)
|
||||||
|
const pricingLockedAt = new Date(price.rateLockedAt)
|
||||||
|
|
||||||
const quotationUuid = stableUuid(access.teamUuid, 'demo-quotation', index)
|
const quotationUuid = stableUuid(access.teamUuid, 'demo-quotation', index)
|
||||||
const quotation = await prisma.quotation.upsert({
|
const quotation = await prisma.quotation.upsert({
|
||||||
@@ -174,10 +286,12 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
incotermsCode: 'DAP',
|
incotermsCode: 'DAP',
|
||||||
transportTypeCode: route.transportTypeCode,
|
transportTypeCode: route.transportTypeCode,
|
||||||
sourceCountryCode: route.sourceCountryCode,
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
sourceLocationName: route.sourceLocationName,
|
sourceLocationName: route.sourceLocationName,
|
||||||
sourceLatitude: route.sourceLatitude,
|
sourceLatitude: route.sourceLatitude,
|
||||||
sourceLongitude: route.sourceLongitude,
|
sourceLongitude: route.sourceLongitude,
|
||||||
destinationCountryCode: route.destinationCountryCode,
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
destinationLocationName: route.destinationLocationName,
|
destinationLocationName: route.destinationLocationName,
|
||||||
destinationLatitude: route.destinationLatitude,
|
destinationLatitude: route.destinationLatitude,
|
||||||
destinationLongitude: route.destinationLongitude,
|
destinationLongitude: route.destinationLongitude,
|
||||||
@@ -188,23 +302,60 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
routeDistanceKm: route.routeDistanceKm,
|
routeDistanceKm: route.routeDistanceKm,
|
||||||
selectedTariffId: tariff.id,
|
selectedTariffId: tariff.id,
|
||||||
tariffMatchSummary: `Demo tariff: ${route.title}`,
|
tariffMatchSummary: `Demo tariff: ${route.title}`,
|
||||||
totalAmount: new Prisma.Decimal(route.amountUsd),
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
||||||
currency: 'USD',
|
sourceCurrency: price.sourceCurrency,
|
||||||
|
clientAmount: new Prisma.Decimal(price.clientAmount),
|
||||||
|
clientCurrency: price.clientCurrency,
|
||||||
|
exchangeRate: new Prisma.Decimal(price.exchangeRate),
|
||||||
|
exchangeRateSnapshotUuid: price.rateSnapshotUuid,
|
||||||
|
exchangeRateProvider: price.provider,
|
||||||
|
exchangeRateEffectiveDate: pricingEffectiveDate,
|
||||||
|
exchangeRateQuotedAt: pricingQuotedAt,
|
||||||
|
exchangeRateLockedAt: pricingLockedAt,
|
||||||
|
pricingCountryCode: price.pricingCountryCode,
|
||||||
|
pricingKycProfileUuid: kyc.profileUuid,
|
||||||
etaDays: route.etaDays,
|
etaDays: route.etaDays,
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
|
routeStages: {
|
||||||
|
create: route.stages.map((stage, stageIndex) => ({
|
||||||
|
uuid: stableUuid(access.teamUuid, `demo-quotation-${index}-stage`, stageIndex),
|
||||||
|
sequence: stageIndex + 1,
|
||||||
|
...stage,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
teamUuid: access.teamUuid,
|
teamUuid: access.teamUuid,
|
||||||
createdByUserId: access.userId,
|
createdByUserId: access.userId,
|
||||||
selectedTariffId: tariff.id,
|
selectedTariffId: tariff.id,
|
||||||
totalAmount: new Prisma.Decimal(route.amountUsd),
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
||||||
currency: 'USD',
|
sourceCurrency: price.sourceCurrency,
|
||||||
|
clientAmount: new Prisma.Decimal(price.clientAmount),
|
||||||
|
clientCurrency: price.clientCurrency,
|
||||||
|
exchangeRate: new Prisma.Decimal(price.exchangeRate),
|
||||||
|
exchangeRateSnapshotUuid: price.rateSnapshotUuid,
|
||||||
|
exchangeRateProvider: price.provider,
|
||||||
|
exchangeRateEffectiveDate: pricingEffectiveDate,
|
||||||
|
exchangeRateQuotedAt: pricingQuotedAt,
|
||||||
|
exchangeRateLockedAt: pricingLockedAt,
|
||||||
|
pricingCountryCode: price.pricingCountryCode,
|
||||||
|
pricingKycProfileUuid: kyc.profileUuid,
|
||||||
etaDays: route.etaDays,
|
etaDays: route.etaDays,
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
|
routeStages: {
|
||||||
|
deleteMany: {},
|
||||||
|
create: route.stages.map((stage, stageIndex) => ({
|
||||||
|
uuid: stableUuid(access.teamUuid, `demo-quotation-${index}-stage`, stageIndex),
|
||||||
|
sequence: stageIndex + 1,
|
||||||
|
...stage,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!needsOrders || !createsOrder) continue
|
if ((!needsOrders && !needsPricingBackfill && !needsRouteBackfill) || !createsOrder) continue
|
||||||
await prisma.order.upsert({
|
await prisma.order.upsert({
|
||||||
where: { uuid: stableUuid(access.teamUuid, 'demo-order', index) },
|
where: { uuid: stableUuid(access.teamUuid, 'demo-order', index) },
|
||||||
create: {
|
create: {
|
||||||
@@ -214,26 +365,67 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
|||||||
createdByUserId: access.userId,
|
createdByUserId: access.userId,
|
||||||
name: route.title,
|
name: route.title,
|
||||||
status: index === 0 ? 'in_transit' : 'processing',
|
status: index === 0 ? 'in_transit' : 'processing',
|
||||||
totalAmount: new Prisma.Decimal(route.amountUsd),
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
||||||
currency: 'USD',
|
sourceCurrency: price.sourceCurrency,
|
||||||
|
clientAmount: new Prisma.Decimal(price.clientAmount),
|
||||||
|
clientCurrency: price.clientCurrency,
|
||||||
|
exchangeRate: new Prisma.Decimal(price.exchangeRate),
|
||||||
|
exchangeRateSnapshotUuid: price.rateSnapshotUuid,
|
||||||
|
exchangeRateProvider: price.provider,
|
||||||
|
exchangeRateEffectiveDate: pricingEffectiveDate,
|
||||||
|
exchangeRateQuotedAt: pricingQuotedAt,
|
||||||
|
exchangeRateLockedAt: pricingLockedAt,
|
||||||
|
pricingCountryCode: price.pricingCountryCode,
|
||||||
|
pricingKycProfileUuid: kyc.profileUuid,
|
||||||
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
sourceLocationName: route.sourceLocationName,
|
sourceLocationName: route.sourceLocationName,
|
||||||
sourceCountryCode: route.sourceCountryCode,
|
sourceCountryCode: route.sourceCountryCode,
|
||||||
sourceLatitude: route.sourceLatitude,
|
sourceLatitude: route.sourceLatitude,
|
||||||
sourceLongitude: route.sourceLongitude,
|
sourceLongitude: route.sourceLongitude,
|
||||||
destinationLocationName: route.destinationLocationName,
|
destinationLocationName: route.destinationLocationName,
|
||||||
destinationCountryCode: route.destinationCountryCode,
|
destinationCountryCode: route.destinationCountryCode,
|
||||||
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
destinationLatitude: route.destinationLatitude,
|
destinationLatitude: route.destinationLatitude,
|
||||||
destinationLongitude: route.destinationLongitude,
|
destinationLongitude: route.destinationLongitude,
|
||||||
etaDays: route.etaDays,
|
etaDays: route.etaDays,
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
|
routeStages: {
|
||||||
|
create: route.stages.map((stage, stageIndex) => ({
|
||||||
|
uuid: stableUuid(access.teamUuid, `demo-order-${index}-stage`, stageIndex),
|
||||||
|
quotationRouteStageUuid: stableUuid(access.teamUuid, `demo-quotation-${index}-stage`, stageIndex),
|
||||||
|
sequence: stageIndex + 1,
|
||||||
|
...stage,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
teamUuid: access.teamUuid,
|
teamUuid: access.teamUuid,
|
||||||
createdByUserId: access.userId,
|
createdByUserId: access.userId,
|
||||||
status: index === 0 ? 'in_transit' : 'processing',
|
status: index === 0 ? 'in_transit' : 'processing',
|
||||||
totalAmount: new Prisma.Decimal(route.amountUsd),
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
||||||
currency: 'USD',
|
sourceCurrency: price.sourceCurrency,
|
||||||
|
clientAmount: new Prisma.Decimal(price.clientAmount),
|
||||||
|
clientCurrency: price.clientCurrency,
|
||||||
|
exchangeRate: new Prisma.Decimal(price.exchangeRate),
|
||||||
|
exchangeRateSnapshotUuid: price.rateSnapshotUuid,
|
||||||
|
exchangeRateProvider: price.provider,
|
||||||
|
exchangeRateEffectiveDate: pricingEffectiveDate,
|
||||||
|
exchangeRateQuotedAt: pricingQuotedAt,
|
||||||
|
exchangeRateLockedAt: pricingLockedAt,
|
||||||
|
pricingCountryCode: price.pricingCountryCode,
|
||||||
|
pricingKycProfileUuid: kyc.profileUuid,
|
||||||
notes: demoNote,
|
notes: demoNote,
|
||||||
|
sourceHubUuid: route.sourceHubUuid,
|
||||||
|
destinationHubUuid: route.destinationHubUuid,
|
||||||
|
routeStages: {
|
||||||
|
deleteMany: {},
|
||||||
|
create: route.stages.map((stage, stageIndex) => ({
|
||||||
|
uuid: stableUuid(access.teamUuid, `demo-order-${index}-stage`, stageIndex),
|
||||||
|
quotationRouteStageUuid: stableUuid(access.teamUuid, `demo-quotation-${index}-stage`, stageIndex),
|
||||||
|
sequence: stageIndex + 1,
|
||||||
|
...stage,
|
||||||
|
})),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
type GraphqlEnvelope<T> = {
|
||||||
|
data?: T
|
||||||
|
errors?: Array<{ message?: string }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KycPricingContext = {
|
||||||
|
teamUuid: string
|
||||||
|
profileUuid: string
|
||||||
|
countryCode: string
|
||||||
|
resolvedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LockedCurrencyPrice = {
|
||||||
|
sourceAmount: number
|
||||||
|
sourceCurrency: string
|
||||||
|
clientAmount: number
|
||||||
|
clientCurrency: string
|
||||||
|
exchangeRate: number
|
||||||
|
rateSnapshotUuid: string | null
|
||||||
|
rateEffectiveDate: string
|
||||||
|
rateQuotedAt: string
|
||||||
|
rateLockedAt: string
|
||||||
|
provider: string
|
||||||
|
attributionUrl: string | null
|
||||||
|
pricingCountryCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DisplayCurrencyConversion = {
|
||||||
|
sourceAmount: number
|
||||||
|
sourceCurrency: string
|
||||||
|
displayAmount: number
|
||||||
|
displayCurrency: string
|
||||||
|
exchangeRate: number
|
||||||
|
rateSnapshotUuid: string | null
|
||||||
|
rateEffectiveDate: string
|
||||||
|
rateQuotedAt: string
|
||||||
|
provider: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const kycM2mEndpoint =
|
||||||
|
process.env.KYC_M2M_GRAPHQL_ENDPOINT ?? 'https://kyc.optovia.ru/graphql/m2m'
|
||||||
|
const exchangeM2mEndpoint =
|
||||||
|
process.env.EXCHANGE_M2M_GRAPHQL_ENDPOINT ?? 'https://exchange.optovia.ru/graphql/m2m'
|
||||||
|
|
||||||
|
async function requestGraphql<T>(
|
||||||
|
endpoint: string,
|
||||||
|
query: string,
|
||||||
|
variables: Record<string, unknown>,
|
||||||
|
): Promise<T> {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify({ query, variables }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Pricing dependency request failed: ${response.status} ${endpoint}`)
|
||||||
|
}
|
||||||
|
const envelope = (await response.json()) as GraphqlEnvelope<T>
|
||||||
|
if (envelope.errors && envelope.errors.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
envelope.errors.map(error => error.message ?? 'Unknown GraphQL error').join('\n'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (envelope.data === undefined) {
|
||||||
|
throw new Error(`Pricing dependency returned no data: ${endpoint}`)
|
||||||
|
}
|
||||||
|
return envelope.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveKycPricingContext(
|
||||||
|
teamUuid: string,
|
||||||
|
): Promise<KycPricingContext> {
|
||||||
|
const data = await requestGraphql<{
|
||||||
|
pricingContextForTeam: KycPricingContext | null
|
||||||
|
}>(
|
||||||
|
kycM2mEndpoint,
|
||||||
|
`query OrdersKycPricingContext($teamUuid: String!) {
|
||||||
|
pricingContextForTeam(teamUuid: $teamUuid) {
|
||||||
|
teamUuid
|
||||||
|
profileUuid
|
||||||
|
countryCode
|
||||||
|
resolvedAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ teamUuid },
|
||||||
|
)
|
||||||
|
if (data.pricingContextForTeam === null) {
|
||||||
|
throw new Error(`Active KYC pricing context is required for team ${teamUuid}`)
|
||||||
|
}
|
||||||
|
return data.pricingContextForTeam
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureDemoKycPricingContext(input: {
|
||||||
|
teamUuid: string
|
||||||
|
userId: string
|
||||||
|
countryCode: string
|
||||||
|
}): Promise<KycPricingContext> {
|
||||||
|
const data = await requestGraphql<{
|
||||||
|
ensureDemoPricingContextForTeam: KycPricingContext
|
||||||
|
}>(
|
||||||
|
kycM2mEndpoint,
|
||||||
|
`mutation OrdersEnsureDemoKycPricingContext($teamUuid: String!, $userId: String!, $countryCode: String!) {
|
||||||
|
ensureDemoPricingContextForTeam(teamUuid: $teamUuid, userId: $userId, countryCode: $countryCode) {
|
||||||
|
teamUuid
|
||||||
|
profileUuid
|
||||||
|
countryCode
|
||||||
|
resolvedAt
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
input,
|
||||||
|
)
|
||||||
|
return data.ensureDemoPricingContextForTeam
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function lockCurrencyPrice(input: {
|
||||||
|
sourceAmount: number
|
||||||
|
sourceCurrency: string
|
||||||
|
clientCurrency: string
|
||||||
|
pricingCountryCode: string
|
||||||
|
rateDate?: string
|
||||||
|
}): Promise<LockedCurrencyPrice> {
|
||||||
|
const data = await requestGraphql<{
|
||||||
|
lockCurrencyPrice: LockedCurrencyPrice
|
||||||
|
}>(
|
||||||
|
exchangeM2mEndpoint,
|
||||||
|
`mutation OrdersLockCurrencyPrice($input: LockCurrencyPriceInput!) {
|
||||||
|
lockCurrencyPrice(input: $input) {
|
||||||
|
sourceAmount
|
||||||
|
sourceCurrency
|
||||||
|
clientAmount
|
||||||
|
clientCurrency
|
||||||
|
exchangeRate
|
||||||
|
rateSnapshotUuid
|
||||||
|
rateEffectiveDate
|
||||||
|
rateQuotedAt
|
||||||
|
rateLockedAt
|
||||||
|
provider
|
||||||
|
attributionUrl
|
||||||
|
pricingCountryCode
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ input },
|
||||||
|
)
|
||||||
|
return data.lockCurrencyPrice
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertCurrencyForDisplay(input: {
|
||||||
|
amount: number
|
||||||
|
sourceCurrency: string
|
||||||
|
displayCurrency: string
|
||||||
|
}): Promise<DisplayCurrencyConversion> {
|
||||||
|
const data = await requestGraphql<{
|
||||||
|
convertCurrencyForDisplay: DisplayCurrencyConversion
|
||||||
|
}>(
|
||||||
|
exchangeM2mEndpoint,
|
||||||
|
`query OrdersConvertCurrencyForDisplay($input: ConvertCurrencyForDisplayInput!) {
|
||||||
|
convertCurrencyForDisplay(input: $input) {
|
||||||
|
sourceAmount
|
||||||
|
sourceCurrency
|
||||||
|
displayAmount
|
||||||
|
displayCurrency
|
||||||
|
exchangeRate
|
||||||
|
rateSnapshotUuid
|
||||||
|
rateEffectiveDate
|
||||||
|
rateQuotedAt
|
||||||
|
provider
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ input },
|
||||||
|
)
|
||||||
|
return data.convertCurrencyForDisplay
|
||||||
|
}
|
||||||
+587
-79
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user