/** * Transport Event Database Service * CRUD operations for transport events and supply chain tracking */ import prisma from './prisma'; import type { TransportEvent, TransportEventType, TransportMethod, TransportStatus, Prisma, } from '@prisma/client'; import type { PaginationOptions, PaginatedResult, DateRangeFilter } from './types'; import { createPaginatedResult, calculateDistanceKm } from './types'; // Carbon emission factors (kg CO2 per km per kg of cargo) const CARBON_FACTORS: Record = { WALKING: 0, BICYCLE: 0, ELECTRIC_VEHICLE: 0.02, HYBRID_VEHICLE: 0.08, GASOLINE_VEHICLE: 0.12, DIESEL_TRUCK: 0.15, ELECTRIC_TRUCK: 0.03, REFRIGERATED_TRUCK: 0.25, RAIL: 0.01, SHIP: 0.008, AIR: 0.5, DRONE: 0.01, LOCAL_DELIVERY: 0.05, CUSTOMER_PICKUP: 0.1, }; // Create a transport event export async function createTransportEvent(data: { eventType: TransportEventType; fromLatitude: number; fromLongitude: number; fromAddress?: string; fromCity?: string; fromCountry?: string; fromLocationType: 'FARM' | 'GREENHOUSE' | 'VERTICAL_FARM' | 'WAREHOUSE' | 'HUB' | 'MARKET' | 'CONSUMER' | 'SEED_BANK' | 'OTHER'; fromFacilityId?: string; fromFacilityName?: string; toLatitude: number; toLongitude: number; toAddress?: string; toCity?: string; toCountry?: string; toLocationType: 'FARM' | 'GREENHOUSE' | 'VERTICAL_FARM' | 'WAREHOUSE' | 'HUB' | 'MARKET' | 'CONSUMER' | 'SEED_BANK' | 'OTHER'; toFacilityId?: string; toFacilityName?: string; durationMinutes: number; transportMethod: TransportMethod; senderId: string; receiverId: string; notes?: string; photos?: string[]; documents?: string[]; eventData?: Record; plantIds?: string[]; seedBatchId?: string; harvestBatchId?: string; cargoWeightKg?: number; }): Promise { // Calculate distance const distanceKm = calculateDistanceKm( data.fromLatitude, data.fromLongitude, data.toLatitude, data.toLongitude ); // Calculate carbon footprint const cargoWeight = data.cargoWeightKg || 1; const carbonFootprintKg = distanceKm * CARBON_FACTORS[data.transportMethod] * cargoWeight; return prisma.transportEvent.create({ data: { eventType: data.eventType, fromLatitude: data.fromLatitude, fromLongitude: data.fromLongitude, fromAddress: data.fromAddress, fromCity: data.fromCity, fromCountry: data.fromCountry, fromLocationType: data.fromLocationType, fromFacilityId: data.fromFacilityId, fromFacilityName: data.fromFacilityName, toLatitude: data.toLatitude, toLongitude: data.toLongitude, toAddress: data.toAddress, toCity: data.toCity, toCountry: data.toCountry, toLocationType: data.toLocationType, toFacilityId: data.toFacilityId, toFacilityName: data.toFacilityName, distanceKm, durationMinutes: data.durationMinutes, transportMethod: data.transportMethod, carbonFootprintKg, senderId: data.senderId, receiverId: data.receiverId, status: 'PENDING', notes: data.notes, photos: data.photos || [], documents: data.documents || [], eventData: data.eventData, seedBatchId: data.seedBatchId, harvestBatchId: data.harvestBatchId, plants: data.plantIds ? { connect: data.plantIds.map(id => ({ id })) } : undefined, }, }); } // Get transport event by ID export async function getTransportEventById(id: string): Promise { return prisma.transportEvent.findUnique({ where: { id }, }); } // Get transport event with related data export async function getTransportEventWithDetails(id: string) { return prisma.transportEvent.findUnique({ where: { id }, include: { sender: true, receiver: true, plants: true, seedBatch: true, harvestBatch: true, }, }); } // Update transport event export async function updateTransportEvent( id: string, data: Prisma.TransportEventUpdateInput ): Promise { return prisma.transportEvent.update({ where: { id }, data, }); } // Update transport event status export async function updateTransportEventStatus( id: string, status: TransportStatus, signature?: { type: 'sender' | 'receiver' | 'verifier'; signature: string } ): Promise { const updateData: Prisma.TransportEventUpdateInput = { status }; if (signature) { if (signature.type === 'sender') updateData.senderSignature = signature.signature; if (signature.type === 'receiver') updateData.receiverSignature = signature.signature; if (signature.type === 'verifier') updateData.verifierSignature = signature.signature; } return prisma.transportEvent.update({ where: { id }, data: updateData, }); } // Delete transport event export async function deleteTransportEvent(id: string): Promise { return prisma.transportEvent.delete({ where: { id }, }); } // Get transport events with pagination export async function getTransportEvents( options: PaginationOptions = {}, filters?: { eventType?: TransportEventType; senderId?: string; receiverId?: string; status?: TransportStatus; dateRange?: DateRangeFilter; } ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.TransportEventWhereInput = {}; if (filters?.eventType) where.eventType = filters.eventType; if (filters?.senderId) where.senderId = filters.senderId; if (filters?.receiverId) where.receiverId = filters.receiverId; if (filters?.status) where.status = filters.status; if (filters?.dateRange) { where.timestamp = { gte: filters.dateRange.start, lte: filters.dateRange.end, }; } const [events, total] = await Promise.all([ prisma.transportEvent.findMany({ where, skip, take: limit, orderBy: { timestamp: 'desc' }, include: { sender: true, receiver: true, }, }), prisma.transportEvent.count({ where }), ]); return createPaginatedResult(events, total, page, limit); } // Get transport events by plant export async function getTransportEventsByPlant(plantId: string): Promise { return prisma.transportEvent.findMany({ where: { plants: { some: { id: plantId }, }, }, orderBy: { timestamp: 'asc' }, include: { sender: true, receiver: true, }, }); } // Get plant journey (complete transport history) export async function getPlantJourney(plantId: string) { const plant = await prisma.plant.findUnique({ where: { id: plantId }, include: { owner: true }, }); if (!plant) return null; const events = await prisma.transportEvent.findMany({ where: { plants: { some: { id: plantId }, }, }, orderBy: { timestamp: 'asc' }, include: { sender: true, receiver: true, }, }); // Calculate totals const totalFoodMiles = events.reduce((sum, e) => sum + e.distanceKm, 0); const totalCarbonKg = events.reduce((sum, e) => sum + e.carbonFootprintKg, 0); const daysInTransit = events.reduce((sum, e) => sum + e.durationMinutes / 1440, 0); return { plantId, plant, events, totalFoodMiles, totalCarbonKg, daysInTransit, generation: plant.generation, }; } // Get environmental impact summary export async function getEnvironmentalImpact(filters?: { userId?: string; dateRange?: DateRangeFilter; }) { const where: Prisma.TransportEventWhereInput = {}; if (filters?.userId) { where.OR = [ { senderId: filters.userId }, { receiverId: filters.userId }, ]; } if (filters?.dateRange) { where.timestamp = { gte: filters.dateRange.start, lte: filters.dateRange.end, }; } const events = await prisma.transportEvent.findMany({ where }); const totalCarbonKg = events.reduce((sum, e) => sum + e.carbonFootprintKg, 0); const totalFoodMiles = events.reduce((sum, e) => sum + e.distanceKm, 0); // Breakdown by method const breakdownByMethod: Record = {}; events.forEach(e => { if (!breakdownByMethod[e.transportMethod]) { breakdownByMethod[e.transportMethod] = { distance: 0, carbon: 0 }; } breakdownByMethod[e.transportMethod].distance += e.distanceKm; breakdownByMethod[e.transportMethod].carbon += e.carbonFootprintKg; }); // Breakdown by event type const breakdownByEventType: Record = {}; events.forEach(e => { if (!breakdownByEventType[e.eventType]) { breakdownByEventType[e.eventType] = { count: 0, carbon: 0 }; } breakdownByEventType[e.eventType].count += 1; breakdownByEventType[e.eventType].carbon += e.carbonFootprintKg; }); return { totalCarbonKg, totalFoodMiles, eventCount: events.length, breakdownByMethod, breakdownByEventType, }; } // Get user's carbon footprint export async function getUserCarbonFootprint(userId: string) { const events = await prisma.transportEvent.findMany({ where: { OR: [ { senderId: userId }, { receiverId: userId }, ], }, }); const sent = events.filter(e => e.senderId === userId); const received = events.filter(e => e.receiverId === userId); return { totalCarbonKg: events.reduce((sum, e) => sum + e.carbonFootprintKg, 0), totalDistanceKm: events.reduce((sum, e) => sum + e.distanceKm, 0), sentEvents: sent.length, receivedEvents: received.length, sentCarbonKg: sent.reduce((sum, e) => sum + e.carbonFootprintKg, 0), receivedCarbonKg: received.reduce((sum, e) => sum + e.carbonFootprintKg, 0), }; } // Seed Batch operations export async function createSeedBatch(data: { species: string; variety?: string; quantity: number; quantityUnit?: string; generation?: number; germinationRate?: number; purityPercentage?: number; harvestDate?: Date; expirationDate?: Date; certifications?: string[]; }) { return prisma.seedBatch.create({ data: { ...data, quantityUnit: data.quantityUnit || 'seeds', generation: data.generation || 0, status: 'AVAILABLE', }, }); } export async function getSeedBatchById(id: string) { return prisma.seedBatch.findUnique({ where: { id }, }); } // Harvest Batch operations export async function createHarvestBatch(data: { produceType: string; harvestType?: string; grossWeight: number; netWeight: number; weightUnit?: string; itemCount?: number; qualityGrade?: string; qualityNotes?: string; packagingType?: string; shelfLifeHours?: number; cropBatchId?: string; }) { return prisma.harvestBatch.create({ data: { ...data, harvestType: data.harvestType || 'full', weightUnit: data.weightUnit || 'kg', }, }); } export async function getHarvestBatchById(id: string) { return prisma.harvestBatch.findUnique({ where: { id }, include: { transportEvents: true, cropBatch: true, }, }); }