localgreenchain/lib/db/transport.ts
Claude 3d2ccdc29a
feat(db): implement PostgreSQL database integration with Prisma ORM
Agent 2 - Database Integration (P0 Critical):

- Add Prisma ORM with PostgreSQL for persistent data storage
- Create comprehensive database schema with 20+ models:
  - User & authentication models
  - Plant & lineage tracking
  - Transport events & supply chain
  - Vertical farming (farms, zones, batches, recipes)
  - Demand & market matching
  - Audit logging & blockchain storage

- Implement complete database service layer (lib/db/):
  - users.ts: User CRUD with search and stats
  - plants.ts: Plant operations with lineage tracking
  - transport.ts: Transport events and carbon tracking
  - farms.ts: Vertical farm and crop batch management
  - demand.ts: Consumer preferences and market matching
  - audit.ts: Audit logging and blockchain integrity

- Add PlantChainDB for database-backed blockchain
- Create development seed script with sample data
- Add database documentation (docs/DATABASE.md)
- Update package.json with Prisma dependencies and scripts

This provides the foundation for all other agents to build upon
with persistent, scalable data storage.
2025-11-23 03:56:40 +00:00

410 lines
11 KiB
TypeScript

/**
* 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<TransportMethod, number> = {
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<string, unknown>;
plantIds?: string[];
seedBatchId?: string;
harvestBatchId?: string;
cargoWeightKg?: number;
}): Promise<TransportEvent> {
// 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<TransportEvent | null> {
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<TransportEvent> {
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<TransportEvent> {
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<TransportEvent> {
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<PaginatedResult<TransportEvent>> {
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<TransportEvent[]> {
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<string, { distance: number; carbon: number }> = {};
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<string, { count: number; carbon: number }> = {};
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,
},
});
}