308 lines
11 KiB
TypeScript
308 lines
11 KiB
TypeScript
import { createHash } from 'node:crypto'
|
|
import { Prisma } from '@prisma/client'
|
|
import { prisma } from './db.js'
|
|
import { ensureDemoKycPricingContext, lockCurrencyPrice } from './pricing-context.js'
|
|
|
|
interface DemoWorkspaceAccess {
|
|
teamUuid: string
|
|
userId: string
|
|
}
|
|
|
|
interface DemoRoute {
|
|
title: string
|
|
sourceCountryCode: string
|
|
sourceLocationName: string
|
|
sourceLatitude: number
|
|
sourceLongitude: number
|
|
destinationCountryCode: string
|
|
destinationLocationName: string
|
|
destinationLatitude: number
|
|
destinationLongitude: number
|
|
transportTypeCode: string
|
|
amountUsd: number
|
|
etaDays: number
|
|
grossWeightKg: number
|
|
volumeCbm: number
|
|
routeDistanceKm: number
|
|
}
|
|
|
|
const demoRoutes: DemoRoute[] = [
|
|
{
|
|
title: 'Mombasa → Almaty',
|
|
sourceCountryCode: 'KE',
|
|
sourceLocationName: 'Mombasa',
|
|
sourceLatitude: -4.0435,
|
|
sourceLongitude: 39.6682,
|
|
destinationCountryCode: 'KZ',
|
|
destinationLocationName: 'Almaty',
|
|
destinationLatitude: 43.2389,
|
|
destinationLongitude: 76.8897,
|
|
transportTypeCode: 'MULTIMODAL',
|
|
amountUsd: 2450,
|
|
etaDays: 16,
|
|
grossWeightKg: 18400,
|
|
volumeCbm: 68,
|
|
routeDistanceKm: 8240,
|
|
},
|
|
{
|
|
title: 'Casablanca → Moscow',
|
|
sourceCountryCode: 'MA',
|
|
sourceLocationName: 'Casablanca',
|
|
sourceLatitude: 33.5731,
|
|
sourceLongitude: -7.5898,
|
|
destinationCountryCode: 'RU',
|
|
destinationLocationName: 'Moscow',
|
|
destinationLatitude: 55.7558,
|
|
destinationLongitude: 37.6173,
|
|
transportTypeCode: 'MULTIMODAL',
|
|
amountUsd: 3100,
|
|
etaDays: 12,
|
|
grossWeightKg: 12600,
|
|
volumeCbm: 52,
|
|
routeDistanceKm: 5060,
|
|
},
|
|
{
|
|
title: 'Nairobi → Dubai',
|
|
sourceCountryCode: 'KE',
|
|
sourceLocationName: 'Nairobi',
|
|
sourceLatitude: -1.2921,
|
|
sourceLongitude: 36.8219,
|
|
destinationCountryCode: 'AE',
|
|
destinationLocationName: 'Dubai',
|
|
destinationLatitude: 25.2048,
|
|
destinationLongitude: 55.2708,
|
|
transportTypeCode: 'AIR',
|
|
amountUsd: 1850,
|
|
etaDays: 4,
|
|
grossWeightKg: 4200,
|
|
volumeCbm: 18,
|
|
routeDistanceKm: 3560,
|
|
},
|
|
{
|
|
title: 'Johannesburg → Warsaw',
|
|
sourceCountryCode: 'ZA',
|
|
sourceLocationName: 'Johannesburg',
|
|
sourceLatitude: -26.2041,
|
|
sourceLongitude: 28.0473,
|
|
destinationCountryCode: 'PL',
|
|
destinationLocationName: 'Warsaw',
|
|
destinationLatitude: 52.2297,
|
|
destinationLongitude: 21.0122,
|
|
transportTypeCode: 'AIR',
|
|
amountUsd: 4200,
|
|
etaDays: 7,
|
|
grossWeightKg: 7600,
|
|
volumeCbm: 31,
|
|
routeDistanceKm: 8690,
|
|
},
|
|
]
|
|
|
|
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 {
|
|
const hex = createHash('sha256')
|
|
.update(`${teamUuid}:${entity}:${index}`)
|
|
.digest('hex')
|
|
.slice(0, 32)
|
|
.split('')
|
|
hex[12] = '4'
|
|
hex[16] = '8'
|
|
const value = hex.join('')
|
|
return `${value.slice(0, 8)}-${value.slice(8, 12)}-${value.slice(12, 16)}-${value.slice(16, 20)}-${value.slice(20)}`
|
|
}
|
|
|
|
export async function ensureDemoWorkspaceData(access: DemoWorkspaceAccess): Promise<void> {
|
|
const [quotationCount, orderCount, demoQuotationPricingCount, demoOrderPricingCount] = await Promise.all([
|
|
prisma.quotation.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 },
|
|
}),
|
|
])
|
|
const needsCalculations = quotationCount === 0
|
|
const needsOrders = orderCount === 0
|
|
const needsPricingBackfill = demoQuotationPricingCount > 0 || demoOrderPricingCount > 0
|
|
const kyc = needsCalculations || needsOrders || needsPricingBackfill
|
|
? await ensureDemoKycPricingContext({
|
|
teamUuid: access.teamUuid,
|
|
userId: access.userId,
|
|
countryCode: demoPricingCountryCode,
|
|
})
|
|
: null
|
|
|
|
for (const [index, route] of demoRoutes.entries()) {
|
|
const createsOrder = index < 2
|
|
|
|
const tariffUuid = stableUuid(access.teamUuid, 'demo-tariff', index)
|
|
const tariff = await prisma.tariffReference.upsert({
|
|
where: { uuid: tariffUuid },
|
|
create: {
|
|
uuid: tariffUuid,
|
|
teamUuid: access.teamUuid,
|
|
createdByUserId: access.userId,
|
|
name: route.title,
|
|
status: 'active',
|
|
sourceCountryCode: route.sourceCountryCode,
|
|
destinationCountryCode: route.destinationCountryCode,
|
|
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
|
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
|
etaDays: route.etaDays,
|
|
priority: 50 + index,
|
|
sourceCurrency: 'USD',
|
|
notes: demoNote,
|
|
},
|
|
update: {
|
|
teamUuid: access.teamUuid,
|
|
createdByUserId: access.userId,
|
|
name: route.title,
|
|
status: 'active',
|
|
operationCode: null,
|
|
incotermsCode: null,
|
|
transportTypeCode: null,
|
|
tareTypeCode: null,
|
|
sourceCountryCode: route.sourceCountryCode,
|
|
destinationCountryCode: route.destinationCountryCode,
|
|
sourceAmount: new Prisma.Decimal(route.amountUsd),
|
|
minSourcePrice: new Prisma.Decimal(route.amountUsd),
|
|
etaDays: route.etaDays,
|
|
priority: 50 + index,
|
|
sourceCurrency: 'USD',
|
|
notes: demoNote,
|
|
},
|
|
})
|
|
|
|
if (!needsCalculations && !(needsOrders && createsOrder) && !needsPricingBackfill) 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 quotation = await prisma.quotation.upsert({
|
|
where: { uuid: quotationUuid },
|
|
create: {
|
|
uuid: quotationUuid,
|
|
teamUuid: access.teamUuid,
|
|
createdByUserId: access.userId,
|
|
title: route.title,
|
|
status: createsOrder && needsOrders ? 'converted' : 'priced',
|
|
operationCode: 'IMPORT',
|
|
incotermsCode: 'DAP',
|
|
transportTypeCode: route.transportTypeCode,
|
|
sourceCountryCode: route.sourceCountryCode,
|
|
sourceLocationName: route.sourceLocationName,
|
|
sourceLatitude: route.sourceLatitude,
|
|
sourceLongitude: route.sourceLongitude,
|
|
destinationCountryCode: route.destinationCountryCode,
|
|
destinationLocationName: route.destinationLocationName,
|
|
destinationLatitude: route.destinationLatitude,
|
|
destinationLongitude: route.destinationLongitude,
|
|
grossWeightKg: new Prisma.Decimal(route.grossWeightKg),
|
|
chargeableWeightKg: new Prisma.Decimal(route.grossWeightKg),
|
|
volumeCbm: new Prisma.Decimal(route.volumeCbm),
|
|
unitsCount: 1 + index,
|
|
routeDistanceKm: route.routeDistanceKm,
|
|
selectedTariffId: tariff.id,
|
|
tariffMatchSummary: `Demo tariff: ${route.title}`,
|
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
|
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,
|
|
notes: demoNote,
|
|
},
|
|
update: {
|
|
teamUuid: access.teamUuid,
|
|
createdByUserId: access.userId,
|
|
selectedTariffId: tariff.id,
|
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
|
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,
|
|
notes: demoNote,
|
|
},
|
|
})
|
|
|
|
if ((!needsOrders && !needsPricingBackfill) || !createsOrder) continue
|
|
await prisma.order.upsert({
|
|
where: { uuid: stableUuid(access.teamUuid, 'demo-order', index) },
|
|
create: {
|
|
uuid: stableUuid(access.teamUuid, 'demo-order', index),
|
|
teamUuid: access.teamUuid,
|
|
quotationId: quotation.id,
|
|
createdByUserId: access.userId,
|
|
name: route.title,
|
|
status: index === 0 ? 'in_transit' : 'processing',
|
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
|
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,
|
|
sourceLocationName: route.sourceLocationName,
|
|
sourceCountryCode: route.sourceCountryCode,
|
|
sourceLatitude: route.sourceLatitude,
|
|
sourceLongitude: route.sourceLongitude,
|
|
destinationLocationName: route.destinationLocationName,
|
|
destinationCountryCode: route.destinationCountryCode,
|
|
destinationLatitude: route.destinationLatitude,
|
|
destinationLongitude: route.destinationLongitude,
|
|
etaDays: route.etaDays,
|
|
notes: demoNote,
|
|
},
|
|
update: {
|
|
teamUuid: access.teamUuid,
|
|
createdByUserId: access.userId,
|
|
status: index === 0 ? 'in_transit' : 'processing',
|
|
sourceAmount: new Prisma.Decimal(price.sourceAmount),
|
|
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,
|
|
},
|
|
})
|
|
}
|
|
}
|