This commit is contained in:
+1
-1
Submodule graphql-contracts updated: a599040ca8...4fffef4915
@@ -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;
|
||||
@@ -95,12 +95,41 @@ model Quotation {
|
||||
|
||||
orders Order[]
|
||||
changes QuotationChange[]
|
||||
routeStages QuotationRouteStage[]
|
||||
|
||||
@@index([teamUuid, status, createdAt])
|
||||
@@index([teamUuid, sourceCountryCode, destinationCountryCode])
|
||||
@@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 {
|
||||
id Int @id @default(autoincrement())
|
||||
uuid String @unique @default(uuid())
|
||||
@@ -138,6 +167,7 @@ model Order {
|
||||
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)
|
||||
sourceLocationName String? @map("source_location_name") @db.VarChar(255)
|
||||
sourceCountryCode String? @map("source_country_code") @db.VarChar(10)
|
||||
@@ -146,6 +176,7 @@ model Order {
|
||||
destinationLocationUuid String? @map("destination_location_uuid") @db.VarChar(100)
|
||||
destinationLocationName String? @map("destination_location_name") @db.VarChar(255)
|
||||
destinationCountryCode String? @map("destination_country_code") @db.VarChar(10)
|
||||
destinationHubUuid String? @map("destination_hub_uuid") @db.VarChar(100)
|
||||
destinationLatitude Float? @map("destination_latitude")
|
||||
destinationLongitude Float? @map("destination_longitude")
|
||||
etaDays Int? @map("eta_days")
|
||||
@@ -153,6 +184,37 @@ model Order {
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
routeStages OrderRouteStage[]
|
||||
|
||||
@@index([teamUuid, createdAt])
|
||||
@@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")
|
||||
}
|
||||
|
||||
+112
-4
@@ -24,6 +24,26 @@ interface DemoRoute {
|
||||
grossWeightKg: number
|
||||
volumeCbm: 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[] = [
|
||||
@@ -43,6 +63,14 @@ const demoRoutes: DemoRoute[] = [
|
||||
grossWeightKg: 18400,
|
||||
volumeCbm: 68,
|
||||
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',
|
||||
@@ -60,6 +88,14 @@ const demoRoutes: DemoRoute[] = [
|
||||
grossWeightKg: 12600,
|
||||
volumeCbm: 52,
|
||||
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',
|
||||
@@ -77,6 +113,13 @@ const demoRoutes: DemoRoute[] = [
|
||||
grossWeightKg: 4200,
|
||||
volumeCbm: 18,
|
||||
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',
|
||||
@@ -94,6 +137,13 @@ const demoRoutes: DemoRoute[] = [
|
||||
grossWeightKg: 7600,
|
||||
volumeCbm: 31,
|
||||
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 },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -114,7 +164,14 @@ function stableUuid(teamUuid: string, entity: string, index: number): string {
|
||||
}
|
||||
|
||||
export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Promise<void> {
|
||||
const [quotationCount, orderCount, demoQuotationPricingCount, demoOrderPricingCount] = await Promise.all([
|
||||
const [
|
||||
quotationCount,
|
||||
orderCount,
|
||||
demoQuotationPricingCount,
|
||||
demoOrderPricingCount,
|
||||
demoQuotationWithoutRouteCount,
|
||||
demoOrderWithoutRouteCount,
|
||||
] = await Promise.all([
|
||||
prisma.quotation.count({ where: { teamUuid: access.teamUuid } }),
|
||||
prisma.order.count({ where: { teamUuid: access.teamUuid } }),
|
||||
prisma.quotation.count({
|
||||
@@ -123,11 +180,18 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
prisma.order.count({
|
||||
where: { teamUuid: access.teamUuid, notes: demoNote, sourceAmount: null },
|
||||
}),
|
||||
prisma.quotation.count({
|
||||
where: { teamUuid: access.teamUuid, notes: demoNote, routeStages: { none: {} } },
|
||||
}),
|
||||
prisma.order.count({
|
||||
where: { teamUuid: access.teamUuid, notes: demoNote, routeStages: { none: {} } },
|
||||
}),
|
||||
])
|
||||
const needsCalculations = quotationCount === 0
|
||||
const needsOrders = orderCount === 0
|
||||
const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0
|
||||
const kyc = needsCalculations || needsOrders || needsPricingBackfill
|
||||
const needsRouteBackfill = demoQuotationWithoutRouteCount > 0 || demoOrderWithoutRouteCount > 0
|
||||
const kyc = needsCalculations || needsOrders || needsPricingBackfill || needsRouteBackfill
|
||||
? await ensureDemoKycPricingContext({
|
||||
teamUuid: access.teamUuid,
|
||||
userId: access.userId,
|
||||
@@ -149,6 +213,8 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
status: 'active',
|
||||
sourceCountryCode: route.sourceCountryCode,
|
||||
destinationCountryCode: route.destinationCountryCode,
|
||||
sourceHubUuid: route.sourceHubUuid,
|
||||
destinationHubUuid: route.destinationHubUuid,
|
||||
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
||||
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
||||
etaDays: route.etaDays,
|
||||
@@ -167,6 +233,8 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
tareTypeCode: null,
|
||||
sourceCountryCode: route.sourceCountryCode,
|
||||
destinationCountryCode: route.destinationCountryCode,
|
||||
sourceHubUuid: route.sourceHubUuid,
|
||||
destinationHubUuid: route.destinationHubUuid,
|
||||
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
||||
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
||||
etaDays: route.etaDays,
|
||||
@@ -176,7 +244,7 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
},
|
||||
})
|
||||
|
||||
if (!needsCalculations && !(needsOrders && createsOrder) && !needsPricingBackfill) 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,
|
||||
@@ -201,10 +269,12 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
incotermsCode: 'DAP',
|
||||
transportTypeCode: route.transportTypeCode,
|
||||
sourceCountryCode: route.sourceCountryCode,
|
||||
sourceHubUuid: route.sourceHubUuid,
|
||||
sourceLocationName: route.sourceLocationName,
|
||||
sourceLatitude: route.sourceLatitude,
|
||||
sourceLongitude: route.sourceLongitude,
|
||||
destinationCountryCode: route.destinationCountryCode,
|
||||
destinationHubUuid: route.destinationHubUuid,
|
||||
destinationLocationName: route.destinationLocationName,
|
||||
destinationLatitude: route.destinationLatitude,
|
||||
destinationLongitude: route.destinationLongitude,
|
||||
@@ -229,6 +299,13 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
pricingKycProfileUuid: kyc.profileUuid,
|
||||
etaDays: route.etaDays,
|
||||
notes: demoNote,
|
||||
routeStages: {
|
||||
create: route.stages.map((stage, stageIndex) => ({
|
||||
uuid: stableUuid(access.teamUuid, `demo-quotation-${index}-stage`, stageIndex),
|
||||
sequence: stageIndex + 1,
|
||||
...stage,
|
||||
})),
|
||||
},
|
||||
},
|
||||
update: {
|
||||
teamUuid: access.teamUuid,
|
||||
@@ -248,10 +325,20 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
pricingKycProfileUuid: kyc.profileUuid,
|
||||
etaDays: route.etaDays,
|
||||
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 && !needsPricingBackfill) || !createsOrder) continue
|
||||
if ((!needsOrders && !needsPricingBackfill && !needsRouteBackfill) || !createsOrder) continue
|
||||
await prisma.order.upsert({
|
||||
where: { uuid: stableUuid(access.teamUuid, 'demo-order', index) },
|
||||
create: {
|
||||
@@ -273,16 +360,26 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
exchangeRateLockedAt: pricingLockedAt,
|
||||
pricingCountryCode: price.pricingCountryCode,
|
||||
pricingKycProfileUuid: kyc.profileUuid,
|
||||
sourceHubUuid: route.sourceHubUuid,
|
||||
sourceLocationName: route.sourceLocationName,
|
||||
sourceCountryCode: route.sourceCountryCode,
|
||||
sourceLatitude: route.sourceLatitude,
|
||||
sourceLongitude: route.sourceLongitude,
|
||||
destinationLocationName: route.destinationLocationName,
|
||||
destinationCountryCode: route.destinationCountryCode,
|
||||
destinationHubUuid: route.destinationHubUuid,
|
||||
destinationLatitude: route.destinationLatitude,
|
||||
destinationLongitude: route.destinationLongitude,
|
||||
etaDays: route.etaDays,
|
||||
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: {
|
||||
teamUuid: access.teamUuid,
|
||||
@@ -301,6 +398,17 @@ export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Prom
|
||||
pricingCountryCode: price.pricingCountryCode,
|
||||
pricingKycProfileUuid: kyc.profileUuid,
|
||||
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,
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
+259
-23
@@ -20,6 +20,11 @@ import {
|
||||
|
||||
const quotationInclude = {
|
||||
selectedTariff: true,
|
||||
routeStages: {
|
||||
orderBy: {
|
||||
sequence: 'asc' as const,
|
||||
},
|
||||
},
|
||||
changes: {
|
||||
orderBy: {
|
||||
createdAt: 'desc' as const,
|
||||
@@ -29,6 +34,11 @@ const quotationInclude = {
|
||||
|
||||
const orderInclude = {
|
||||
quotation: true,
|
||||
routeStages: {
|
||||
orderBy: {
|
||||
sequence: 'asc' as const,
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.OrderInclude
|
||||
|
||||
type QuotationWithRelations = Prisma.QuotationGetPayload<{ include: typeof quotationInclude }>
|
||||
@@ -46,6 +56,7 @@ type CreateTariffReferenceInput = Record<string, unknown>
|
||||
type UpdateTariffReferenceInput = Record<string, unknown>
|
||||
type CreateQuotationInput = Record<string, unknown>
|
||||
type UpdateQuotationInput = Record<string, unknown>
|
||||
type RouteStageInput = Record<string, unknown>
|
||||
|
||||
export const teamTypeDefs = `#graphql
|
||||
type Company {
|
||||
@@ -78,12 +89,18 @@ export const teamTypeDefs = `#graphql
|
||||
sequence: Int
|
||||
stageType: String
|
||||
transportType: String
|
||||
sourcePointUuid: String
|
||||
sourcePointType: String
|
||||
sourceLocationName: String
|
||||
sourceLatitude: Float
|
||||
sourceLongitude: Float
|
||||
destinationPointUuid: String
|
||||
destinationPointType: String
|
||||
destinationLocationName: String
|
||||
destinationLatitude: Float
|
||||
destinationLongitude: Float
|
||||
distanceKm: Float
|
||||
travelTimeSeconds: Int
|
||||
locationName: String
|
||||
locationLatitude: Float
|
||||
locationLongitude: Float
|
||||
@@ -127,13 +144,17 @@ export const teamTypeDefs = `#graphql
|
||||
totalAmount: Float @deprecated(reason: "Use clientAmount")
|
||||
currency: String @deprecated(reason: "Use clientCurrency")
|
||||
sourceCountryCode: String
|
||||
sourceHubUuid: String
|
||||
sourceLocationUuid: String
|
||||
sourceLocationName: String
|
||||
sourceLatitude: Float
|
||||
sourceLongitude: Float
|
||||
destinationCountryCode: String
|
||||
destinationHubUuid: String
|
||||
destinationLocationUuid: String
|
||||
destinationLocationName: String
|
||||
destinationLatitude: Float
|
||||
destinationLongitude: Float
|
||||
etaDays: Int
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
@@ -237,6 +258,7 @@ export const teamTypeDefs = `#graphql
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
changes: [QuotationChange!]!
|
||||
routeStages: [Stage!]!
|
||||
}
|
||||
|
||||
type QuotationTariffMatch {
|
||||
@@ -336,6 +358,7 @@ export const teamTypeDefs = `#graphql
|
||||
volumeCbm: Float
|
||||
unitsCount: Int
|
||||
routeDistanceKm: Int
|
||||
routeStages: [RouteStageInput!]
|
||||
notes: String
|
||||
}
|
||||
|
||||
@@ -364,9 +387,28 @@ export const teamTypeDefs = `#graphql
|
||||
volumeCbm: Float
|
||||
unitsCount: Int
|
||||
routeDistanceKm: Int
|
||||
routeStages: [RouteStageInput!]
|
||||
notes: String
|
||||
}
|
||||
|
||||
input RouteStageInput {
|
||||
sequence: Int!
|
||||
stageType: String!
|
||||
transportType: String
|
||||
sourcePointUuid: String
|
||||
sourcePointType: String!
|
||||
sourceLocationName: String!
|
||||
sourceLatitude: Float
|
||||
sourceLongitude: Float
|
||||
destinationPointUuid: String
|
||||
destinationPointType: String!
|
||||
destinationLocationName: String!
|
||||
destinationLatitude: Float
|
||||
destinationLongitude: Float
|
||||
distanceKm: Float
|
||||
travelTimeSeconds: Int
|
||||
}
|
||||
|
||||
type Query {
|
||||
getTeamOrders(displayCurrency: String! = "USD"): [Order]
|
||||
getOrder(orderUuid: String!, displayCurrency: String! = "USD"): Order
|
||||
@@ -610,6 +652,62 @@ function mapQuotation(
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: item.updatedAt.toISOString(),
|
||||
changes: item.changes.map(mapQuotationChange),
|
||||
routeStages: item.routeStages.map(stage => mapRouteStage(stage)),
|
||||
}
|
||||
}
|
||||
|
||||
type RouteStageRecord = {
|
||||
uuid: string
|
||||
sequence: number
|
||||
stageType: string
|
||||
transportType: string | null
|
||||
sourcePointUuid: string | null
|
||||
sourcePointType: string
|
||||
sourceLocationName: string
|
||||
sourceLatitude: number | null
|
||||
sourceLongitude: number | null
|
||||
destinationPointUuid: string | null
|
||||
destinationPointType: string
|
||||
destinationLocationName: string
|
||||
destinationLatitude: number | null
|
||||
destinationLongitude: number | null
|
||||
distanceKm: number | null
|
||||
travelTimeSeconds: number | null
|
||||
}
|
||||
|
||||
function routeStageName(stageType: string): string {
|
||||
if (stageType === 'FIRST_MILE') return 'First mile'
|
||||
if (stageType === 'LAST_MILE') return 'Last mile'
|
||||
return 'Main haul'
|
||||
}
|
||||
|
||||
function mapRouteStage(
|
||||
stage: RouteStageRecord,
|
||||
location?: { name: string | null; latitude: number | null; longitude: number | null },
|
||||
) {
|
||||
return {
|
||||
uuid: stage.uuid,
|
||||
name: routeStageName(stage.stageType),
|
||||
sequence: stage.sequence,
|
||||
stageType: stage.stageType,
|
||||
transportType: stage.transportType,
|
||||
sourcePointUuid: stage.sourcePointUuid,
|
||||
sourcePointType: stage.sourcePointType,
|
||||
sourceLocationName: stage.sourceLocationName,
|
||||
sourceLatitude: stage.sourceLatitude,
|
||||
sourceLongitude: stage.sourceLongitude,
|
||||
destinationPointUuid: stage.destinationPointUuid,
|
||||
destinationPointType: stage.destinationPointType,
|
||||
destinationLocationName: stage.destinationLocationName,
|
||||
destinationLatitude: stage.destinationLatitude,
|
||||
destinationLongitude: stage.destinationLongitude,
|
||||
distanceKm: stage.distanceKm,
|
||||
travelTimeSeconds: stage.travelTimeSeconds,
|
||||
locationName: location?.name ?? stage.sourceLocationName,
|
||||
locationLatitude: location?.latitude ?? stage.sourceLatitude,
|
||||
locationLongitude: location?.longitude ?? stage.sourceLongitude,
|
||||
selectedCompany: null,
|
||||
trips: [],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -620,8 +718,7 @@ function mapLocalOrder(
|
||||
const displayAmount = display?.displayAmount ?? Number(item.clientAmount)
|
||||
const displayCurrency = display?.displayCurrency ?? item.clientCurrency
|
||||
const progress = orderLocationProgress(item.status)
|
||||
const locationLatitude = interpolateCoordinate(item.sourceLatitude, item.destinationLatitude, progress)
|
||||
const locationLongitude = interpolateCoordinate(item.sourceLongitude, item.destinationLongitude, progress)
|
||||
const stageCount = item.routeStages.length
|
||||
return {
|
||||
uuid: item.uuid,
|
||||
quotationUuid: item.quotation?.uuid ?? null,
|
||||
@@ -646,38 +743,34 @@ function mapLocalOrder(
|
||||
totalAmount: Number(item.clientAmount),
|
||||
currency: item.clientCurrency,
|
||||
sourceCountryCode: item.sourceCountryCode,
|
||||
sourceHubUuid: item.sourceHubUuid,
|
||||
sourceLocationUuid: item.sourceLocationUuid,
|
||||
sourceLocationName: item.sourceLocationName,
|
||||
sourceLatitude: item.sourceLatitude,
|
||||
sourceLongitude: item.sourceLongitude,
|
||||
destinationCountryCode: item.destinationCountryCode,
|
||||
destinationHubUuid: item.destinationHubUuid,
|
||||
destinationLocationUuid: item.destinationLocationUuid,
|
||||
destinationLocationName: item.destinationLocationName,
|
||||
destinationLatitude: item.destinationLatitude,
|
||||
destinationLongitude: item.destinationLongitude,
|
||||
etaDays: item.etaDays,
|
||||
createdAt: item.createdAt.toISOString(),
|
||||
updatedAt: item.updatedAt.toISOString(),
|
||||
notes: item.notes ?? '',
|
||||
orderLines: [],
|
||||
stages: [
|
||||
{
|
||||
uuid: `${item.uuid}-route`,
|
||||
name: item.name,
|
||||
sequence: 1,
|
||||
stageType: 'route',
|
||||
transportType: item.quotation?.transportTypeCode ?? null,
|
||||
sourceLocationName: item.sourceLocationName,
|
||||
sourceLatitude: item.sourceLatitude,
|
||||
sourceLongitude: item.sourceLongitude,
|
||||
destinationLocationName: item.destinationLocationName,
|
||||
destinationLatitude: item.destinationLatitude,
|
||||
destinationLongitude: item.destinationLongitude,
|
||||
locationName: orderLocationName(item, progress),
|
||||
locationLatitude,
|
||||
locationLongitude,
|
||||
selectedCompany: null,
|
||||
trips: [],
|
||||
},
|
||||
],
|
||||
stages: item.routeStages.map((stage, index) => {
|
||||
const stageProgress = Math.max(0, Math.min(1, progress * stageCount - index))
|
||||
return mapRouteStage(stage, {
|
||||
name: stageProgress >= 1
|
||||
? stage.destinationLocationName
|
||||
: stageProgress <= 0
|
||||
? stage.sourceLocationName
|
||||
: orderLocationName(item, progress),
|
||||
latitude: interpolateCoordinate(stage.sourceLatitude, stage.destinationLatitude, stageProgress),
|
||||
longitude: interpolateCoordinate(stage.sourceLongitude, stage.destinationLongitude, stageProgress),
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -797,6 +890,109 @@ function buildUpdateTariffData(input: UpdateTariffReferenceInput): Prisma.Tariff
|
||||
return data
|
||||
}
|
||||
|
||||
function buildRouteStageData(input: RouteStageInput): Prisma.QuotationRouteStageCreateWithoutQuotationInput {
|
||||
return {
|
||||
sequence: normalizeInteger(input.sequence) ?? 0,
|
||||
stageType: requiredString(normalizeUpper(input.stageType), 'Route stage type'),
|
||||
transportType: normalizeUpper(input.transportType),
|
||||
sourcePointUuid: normalizeString(input.sourcePointUuid),
|
||||
sourcePointType: requiredString(normalizeUpper(input.sourcePointType), 'Source point type'),
|
||||
sourceLocationName: requiredString(input.sourceLocationName, 'Source location name'),
|
||||
sourceLatitude: normalizeNumber(input.sourceLatitude),
|
||||
sourceLongitude: normalizeNumber(input.sourceLongitude),
|
||||
destinationPointUuid: normalizeString(input.destinationPointUuid),
|
||||
destinationPointType: requiredString(normalizeUpper(input.destinationPointType), 'Destination point type'),
|
||||
destinationLocationName: requiredString(input.destinationLocationName, 'Destination location name'),
|
||||
destinationLatitude: normalizeNumber(input.destinationLatitude),
|
||||
destinationLongitude: normalizeNumber(input.destinationLongitude),
|
||||
distanceKm: normalizeNumber(input.distanceKm),
|
||||
travelTimeSeconds: normalizeInteger(input.travelTimeSeconds),
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRouteStages(input: Record<string, unknown>): Prisma.QuotationRouteStageCreateWithoutQuotationInput[] {
|
||||
const sourceLocationName = normalizeString(input.sourceLocationName) ?? 'Origin'
|
||||
const destinationLocationName = normalizeString(input.destinationLocationName) ?? 'Destination'
|
||||
const sourceHubUuid = normalizeString(input.sourceHubUuid)
|
||||
const destinationHubUuid = normalizeString(input.destinationHubUuid)
|
||||
const sourceHubName = sourceHubUuid ?? 'Origin hub'
|
||||
const destinationHubName = destinationHubUuid ?? 'Destination hub'
|
||||
return [
|
||||
{
|
||||
sequence: 1,
|
||||
stageType: 'FIRST_MILE',
|
||||
transportType: 'AUTO',
|
||||
sourcePointUuid: normalizeString(input.sourceLocationUuid),
|
||||
sourcePointType: 'ADDRESS',
|
||||
sourceLocationName,
|
||||
sourceLatitude: normalizeNumber(input.sourceLatitude),
|
||||
sourceLongitude: normalizeNumber(input.sourceLongitude),
|
||||
destinationPointUuid: sourceHubUuid,
|
||||
destinationPointType: 'HUB',
|
||||
destinationLocationName: sourceHubName,
|
||||
},
|
||||
{
|
||||
sequence: 2,
|
||||
stageType: 'MAIN_HAUL',
|
||||
transportType: normalizeUpper(input.transportTypeCode) ?? 'MULTIMODAL',
|
||||
sourcePointUuid: sourceHubUuid,
|
||||
sourcePointType: 'HUB',
|
||||
sourceLocationName: sourceHubName,
|
||||
destinationPointUuid: destinationHubUuid,
|
||||
destinationPointType: 'HUB',
|
||||
destinationLocationName: destinationHubName,
|
||||
distanceKm: normalizeNumber(input.routeDistanceKm),
|
||||
},
|
||||
{
|
||||
sequence: 3,
|
||||
stageType: 'LAST_MILE',
|
||||
transportType: 'AUTO',
|
||||
sourcePointUuid: destinationHubUuid,
|
||||
sourcePointType: 'HUB',
|
||||
sourceLocationName: destinationHubName,
|
||||
destinationPointUuid: normalizeString(input.destinationLocationUuid),
|
||||
destinationPointType: 'ADDRESS',
|
||||
destinationLocationName,
|
||||
destinationLatitude: normalizeNumber(input.destinationLatitude),
|
||||
destinationLongitude: normalizeNumber(input.destinationLongitude),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function buildRouteStages(input: Record<string, unknown>): Prisma.QuotationRouteStageCreateWithoutQuotationInput[] {
|
||||
if (!hasOwn(input, 'routeStages') || input.routeStages == null) {
|
||||
return defaultRouteStages(input)
|
||||
}
|
||||
if (!Array.isArray(input.routeStages) || input.routeStages.length === 0) {
|
||||
throw new GraphQLError('Route must contain at least one stage')
|
||||
}
|
||||
const stages = input.routeStages
|
||||
.map(item => buildRouteStageData(item as RouteStageInput))
|
||||
.sort((left, right) => left.sequence - right.sequence)
|
||||
stages.forEach((stage, index) => {
|
||||
if (stage.sequence !== index + 1) {
|
||||
throw new GraphQLError('Route stage sequence must start at 1 and be continuous')
|
||||
}
|
||||
})
|
||||
return stages
|
||||
}
|
||||
|
||||
const quotationRouteKeys = new Set([
|
||||
'transportTypeCode',
|
||||
'sourceHubUuid',
|
||||
'sourceLocationUuid',
|
||||
'sourceLocationName',
|
||||
'sourceLatitude',
|
||||
'sourceLongitude',
|
||||
'destinationHubUuid',
|
||||
'destinationLocationUuid',
|
||||
'destinationLocationName',
|
||||
'destinationLatitude',
|
||||
'destinationLongitude',
|
||||
'routeDistanceKm',
|
||||
'routeStages',
|
||||
])
|
||||
|
||||
function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid: string; userId: string }): Prisma.QuotationCreateInput {
|
||||
return {
|
||||
teamUuid: ctx.teamUuid,
|
||||
@@ -826,6 +1022,9 @@ function buildCreateQuotationData(input: CreateQuotationInput, ctx: { teamUuid:
|
||||
unitsCount: normalizeInteger(input.unitsCount),
|
||||
routeDistanceKm: normalizeInteger(input.routeDistanceKm),
|
||||
notes: normalizeString(input.notes),
|
||||
routeStages: {
|
||||
create: buildRouteStages(input),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1370,11 +1569,26 @@ export const teamResolvers = {
|
||||
}
|
||||
const teamAccess = { teamUuid: current.teamUuid, userId: access.userId }
|
||||
const actor = { userId: access.userId, label: actorLabel(teamAccess) }
|
||||
const routeChanged = Object.keys(args.input).some(key => quotationRouteKeys.has(key))
|
||||
const routeInput: Record<string, unknown> = { ...current, ...args.input }
|
||||
if (!hasOwn(args.input, 'routeStages')) {
|
||||
delete routeInput.routeStages
|
||||
}
|
||||
await prisma.quotation.update({
|
||||
where: {
|
||||
id: current.id,
|
||||
},
|
||||
data: buildUpdateQuotationData(args.input),
|
||||
data: {
|
||||
...buildUpdateQuotationData(args.input),
|
||||
...(routeChanged
|
||||
? {
|
||||
routeStages: {
|
||||
deleteMany: {},
|
||||
create: buildRouteStages(routeInput),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
|
||||
await addQuotationChange(current.id, actor, 'quotation', 'Quotation updated', {
|
||||
@@ -1498,18 +1712,40 @@ export const teamResolvers = {
|
||||
exchangeRateLockedAt: quotation.exchangeRateLockedAt,
|
||||
pricingCountryCode: quotation.pricingCountryCode,
|
||||
pricingKycProfileUuid: quotation.pricingKycProfileUuid,
|
||||
sourceHubUuid: quotation.sourceHubUuid,
|
||||
sourceLocationUuid: quotation.sourceLocationUuid,
|
||||
sourceLocationName: quotation.sourceLocationName,
|
||||
sourceCountryCode: quotation.sourceCountryCode,
|
||||
sourceLatitude: quotation.sourceLatitude,
|
||||
sourceLongitude: quotation.sourceLongitude,
|
||||
destinationLocationUuid: quotation.destinationLocationUuid,
|
||||
destinationHubUuid: quotation.destinationHubUuid,
|
||||
destinationLocationName: quotation.destinationLocationName,
|
||||
destinationCountryCode: quotation.destinationCountryCode,
|
||||
destinationLatitude: quotation.destinationLatitude,
|
||||
destinationLongitude: quotation.destinationLongitude,
|
||||
etaDays: quotation.etaDays,
|
||||
notes: quotation.notes,
|
||||
routeStages: {
|
||||
create: quotation.routeStages.map(stage => ({
|
||||
quotationRouteStageUuid: stage.uuid,
|
||||
sequence: stage.sequence,
|
||||
stageType: stage.stageType,
|
||||
transportType: stage.transportType,
|
||||
sourcePointUuid: stage.sourcePointUuid,
|
||||
sourcePointType: stage.sourcePointType,
|
||||
sourceLocationName: stage.sourceLocationName,
|
||||
sourceLatitude: stage.sourceLatitude,
|
||||
sourceLongitude: stage.sourceLongitude,
|
||||
destinationPointUuid: stage.destinationPointUuid,
|
||||
destinationPointType: stage.destinationPointType,
|
||||
destinationLocationName: stage.destinationLocationName,
|
||||
destinationLatitude: stage.destinationLatitude,
|
||||
destinationLongitude: stage.destinationLongitude,
|
||||
distanceKm: stage.distanceKm,
|
||||
travelTimeSeconds: stage.travelTimeSeconds,
|
||||
})),
|
||||
},
|
||||
},
|
||||
include: orderInclude,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user