From d0271bc25bca9628ac1f7159a069bc6960d67f1e Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev <572431+veikab@users.noreply.github.com> Date: Mon, 24 Aug 2026 11:35:50 +0700 Subject: [PATCH] Seed empty client workspaces --- src/demo-workspace.ts | 232 ++++++++++++++++++++++++++++++++++++++++++ src/schemas/team.ts | 15 +++ 2 files changed, 247 insertions(+) create mode 100644 src/demo-workspace.ts diff --git a/src/demo-workspace.ts b/src/demo-workspace.ts new file mode 100644 index 0000000..c3cabe7 --- /dev/null +++ b/src/demo-workspace.ts @@ -0,0 +1,232 @@ +import { createHash } from 'node:crypto' +import { Prisma } from '@prisma/client' +import { prisma } from './db.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' }) + +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 { + const [quotationCount, orderCount] = await Promise.all([ + prisma.quotation.count({ where: { teamUuid: access.teamUuid } }), + prisma.order.count({ where: { teamUuid: access.teamUuid } }), + ]) + const needsCalculations = quotationCount === 0 + const needsOrders = orderCount === 0 + if (!needsCalculations && !needsOrders) return + + for (const [index, route] of demoRoutes.entries()) { + const createsOrder = index < 2 + if (!needsCalculations && !(needsOrders && createsOrder)) continue + + 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', + operationCode: 'IMPORT', + transportTypeCode: route.transportTypeCode, + sourceCountryCode: route.sourceCountryCode, + destinationCountryCode: route.destinationCountryCode, + amountUsd: new Prisma.Decimal(route.amountUsd), + minPriceUsd: new Prisma.Decimal(route.amountUsd), + etaDays: route.etaDays, + priority: 50 + index, + currency: 'USD', + notes: demoNote, + }, + update: { + teamUuid: access.teamUuid, + createdByUserId: access.userId, + amountUsd: new Prisma.Decimal(route.amountUsd), + minPriceUsd: new Prisma.Decimal(route.amountUsd), + etaDays: route.etaDays, + notes: demoNote, + }, + }) + + 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}`, + totalAmount: new Prisma.Decimal(route.amountUsd), + currency: 'USD', + etaDays: route.etaDays, + notes: demoNote, + }, + update: { + teamUuid: access.teamUuid, + createdByUserId: access.userId, + selectedTariffId: tariff.id, + totalAmount: new Prisma.Decimal(route.amountUsd), + currency: 'USD', + etaDays: route.etaDays, + notes: demoNote, + }, + }) + + if (!needsOrders || !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', + totalAmount: new Prisma.Decimal(route.amountUsd), + currency: 'USD', + 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', + totalAmount: new Prisma.Decimal(route.amountUsd), + currency: 'USD', + notes: demoNote, + }, + }) + } +} diff --git a/src/schemas/team.ts b/src/schemas/team.ts index 0ce06d9..d77a1ed 100644 --- a/src/schemas/team.ts +++ b/src/schemas/team.ts @@ -8,6 +8,7 @@ import { } from '@prisma/client' import { requireScopes, type AuthContext } from '../auth.js' import { prisma } from '../db.js' +import { ensureDemoWorkspaceData } from '../demo-workspace.js' const quotationInclude = { selectedTariff: true, @@ -440,6 +441,18 @@ function teamAccessWhere(access: { teamUuid?: string }): { teamUuid?: string } { return access.teamUuid ? { teamUuid: access.teamUuid } : {} } +async function ensureClientWorkspaceData(access: { + teamUuid?: string + userId: string + isManager: boolean +}): Promise { + if (access.isManager || !access.teamUuid) return + await ensureDemoWorkspaceData({ + teamUuid: access.teamUuid, + userId: access.userId, + }) +} + function mapTariffReference(item: PrismaTariffReference) { return { uuid: item.uuid, @@ -951,6 +964,7 @@ export const teamResolvers = { Query: { getTeamOrders: async (_: unknown, __: unknown, ctx: AuthContext) => { const access = assertTeamAccess(ctx) + await ensureClientWorkspaceData(access) const localOrders = await prisma.order.findMany({ where: { @@ -981,6 +995,7 @@ export const teamResolvers = { quotations: async (_: unknown, args: { status?: string | null }, ctx: AuthContext) => { const access = assertTeamAccess(ctx) + await ensureClientWorkspaceData(access) const items = await prisma.quotation.findMany({ where: {