localgreenchain/lib/db/farms.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

568 lines
14 KiB
TypeScript

/**
* Vertical Farm Database Service
* CRUD operations for vertical farms, zones, and crop batches
*/
import prisma from './prisma';
import type {
VerticalFarm,
GrowingZone,
CropBatch,
GrowingRecipe,
FarmStatus,
ZoneStatus,
CropBatchStatus,
Prisma,
} from '@prisma/client';
import type { PaginationOptions, PaginatedResult } from './types';
import { createPaginatedResult } from './types';
// ============================================
// VERTICAL FARM OPERATIONS
// ============================================
// Create a vertical farm
export async function createVerticalFarm(data: {
name: string;
ownerId: string;
latitude: number;
longitude: number;
address: string;
city: string;
country: string;
timezone?: string;
specs: Record<string, unknown>;
environmentalControl?: Record<string, unknown>;
irrigationSystem?: Record<string, unknown>;
lightingSystem?: Record<string, unknown>;
nutrientSystem?: Record<string, unknown>;
automationLevel?: 'MANUAL' | 'SEMI_AUTOMATED' | 'FULLY_AUTOMATED';
automationSystems?: Record<string, unknown>;
}): Promise<VerticalFarm> {
return prisma.verticalFarm.create({
data: {
...data,
timezone: data.timezone || 'UTC',
automationLevel: data.automationLevel || 'MANUAL',
status: 'OFFLINE',
},
});
}
// Get vertical farm by ID
export async function getVerticalFarmById(id: string): Promise<VerticalFarm | null> {
return prisma.verticalFarm.findUnique({
where: { id },
});
}
// Get vertical farm with zones
export async function getVerticalFarmWithZones(id: string) {
return prisma.verticalFarm.findUnique({
where: { id },
include: {
owner: true,
zones: true,
cropBatches: {
where: { status: { not: 'COMPLETED' } },
},
},
});
}
// Update vertical farm
export async function updateVerticalFarm(
id: string,
data: Prisma.VerticalFarmUpdateInput
): Promise<VerticalFarm> {
return prisma.verticalFarm.update({
where: { id },
data,
});
}
// Update farm status
export async function updateFarmStatus(
id: string,
status: FarmStatus
): Promise<VerticalFarm> {
return prisma.verticalFarm.update({
where: { id },
data: { status },
});
}
// Delete vertical farm
export async function deleteVerticalFarm(id: string): Promise<VerticalFarm> {
return prisma.verticalFarm.delete({
where: { id },
});
}
// Get farms with pagination
export async function getVerticalFarms(
options: PaginationOptions = {},
filters?: {
ownerId?: string;
status?: FarmStatus;
city?: string;
country?: string;
}
): Promise<PaginatedResult<VerticalFarm>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.VerticalFarmWhereInput = {};
if (filters?.ownerId) where.ownerId = filters.ownerId;
if (filters?.status) where.status = filters.status;
if (filters?.city) where.city = { contains: filters.city, mode: 'insensitive' };
if (filters?.country) where.country = { contains: filters.country, mode: 'insensitive' };
const [farms, total] = await Promise.all([
prisma.verticalFarm.findMany({
where,
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
owner: true,
zones: true,
},
}),
prisma.verticalFarm.count({ where }),
]);
return createPaginatedResult(farms, total, page, limit);
}
// Get farms by owner
export async function getVerticalFarmsByOwner(ownerId: string): Promise<VerticalFarm[]> {
return prisma.verticalFarm.findMany({
where: { ownerId },
include: { zones: true },
orderBy: { name: 'asc' },
});
}
// ============================================
// GROWING ZONE OPERATIONS
// ============================================
// Create a growing zone
export async function createGrowingZone(data: {
name: string;
farmId: string;
level: number;
areaSqm: number;
lengthM?: number;
widthM?: number;
growingMethod: 'NFT' | 'DWC' | 'EBB_FLOW' | 'AEROPONICS' | 'VERTICAL_TOWERS' | 'RACK_SYSTEM';
plantPositions: number;
environmentTargets?: Record<string, unknown>;
}): Promise<GrowingZone> {
return prisma.growingZone.create({
data: {
...data,
status: 'EMPTY',
},
});
}
// Get growing zone by ID
export async function getGrowingZoneById(id: string): Promise<GrowingZone | null> {
return prisma.growingZone.findUnique({
where: { id },
});
}
// Get growing zone with current batch
export async function getGrowingZoneWithBatch(id: string) {
return prisma.growingZone.findUnique({
where: { id },
include: {
farm: true,
cropBatches: {
where: { status: { not: 'COMPLETED' } },
take: 1,
},
},
});
}
// Update growing zone
export async function updateGrowingZone(
id: string,
data: Prisma.GrowingZoneUpdateInput
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data,
});
}
// Update zone status
export async function updateZoneStatus(
id: string,
status: ZoneStatus
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data: { status },
});
}
// Delete growing zone
export async function deleteGrowingZone(id: string): Promise<GrowingZone> {
return prisma.growingZone.delete({
where: { id },
});
}
// Get zones by farm
export async function getGrowingZonesByFarm(farmId: string): Promise<GrowingZone[]> {
return prisma.growingZone.findMany({
where: { farmId },
orderBy: [{ level: 'asc' }, { name: 'asc' }],
});
}
// Update zone environment readings
export async function updateZoneEnvironment(
id: string,
readings: Record<string, unknown>
): Promise<GrowingZone> {
return prisma.growingZone.update({
where: { id },
data: { currentEnvironment: readings },
});
}
// ============================================
// CROP BATCH OPERATIONS
// ============================================
// Create a crop batch
export async function createCropBatch(data: {
farmId: string;
zoneId: string;
cropType: string;
variety?: string;
recipeId?: string;
seedBatchId?: string;
plantCount: number;
plantingDate: Date;
expectedHarvestDate: Date;
expectedYieldKg: number;
}): Promise<CropBatch> {
// Update zone status
await prisma.growingZone.update({
where: { id: data.zoneId },
data: {
status: 'PLANTED',
currentCrop: data.cropType,
plantingDate: data.plantingDate,
expectedHarvestDate: data.expectedHarvestDate,
},
});
return prisma.cropBatch.create({
data: {
...data,
currentStage: 'germinating',
currentDay: 0,
status: 'GERMINATING',
},
});
}
// Get crop batch by ID
export async function getCropBatchById(id: string): Promise<CropBatch | null> {
return prisma.cropBatch.findUnique({
where: { id },
});
}
// Get crop batch with details
export async function getCropBatchWithDetails(id: string) {
return prisma.cropBatch.findUnique({
where: { id },
include: {
farm: true,
zone: true,
recipe: true,
seedBatch: true,
plants: true,
harvestBatches: true,
},
});
}
// Update crop batch
export async function updateCropBatch(
id: string,
data: Prisma.CropBatchUpdateInput
): Promise<CropBatch> {
return prisma.cropBatch.update({
where: { id },
data,
});
}
// Update crop batch status
export async function updateCropBatchStatus(
id: string,
status: CropBatchStatus,
extras?: {
currentStage?: string;
currentDay?: number;
healthScore?: number;
actualHarvestDate?: Date;
actualYieldKg?: number;
qualityGrade?: string;
}
): Promise<CropBatch> {
const batch = await prisma.cropBatch.update({
where: { id },
data: { status, ...extras },
});
// Update zone status based on batch status
if (status === 'COMPLETED' || status === 'FAILED') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: {
status: 'CLEANING',
currentCrop: null,
plantingDate: null,
expectedHarvestDate: null,
},
});
} else if (status === 'HARVESTING') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: { status: 'HARVESTING' },
});
} else if (status === 'GROWING') {
await prisma.growingZone.update({
where: { id: batch.zoneId },
data: { status: 'GROWING' },
});
}
return batch;
}
// Delete crop batch
export async function deleteCropBatch(id: string): Promise<CropBatch> {
return prisma.cropBatch.delete({
where: { id },
});
}
// Get crop batches with pagination
export async function getCropBatches(
options: PaginationOptions = {},
filters?: {
farmId?: string;
zoneId?: string;
status?: CropBatchStatus;
cropType?: string;
}
): Promise<PaginatedResult<CropBatch>> {
const page = options.page || 1;
const limit = options.limit || 20;
const skip = (page - 1) * limit;
const where: Prisma.CropBatchWhereInput = {};
if (filters?.farmId) where.farmId = filters.farmId;
if (filters?.zoneId) where.zoneId = filters.zoneId;
if (filters?.status) where.status = filters.status;
if (filters?.cropType) where.cropType = { contains: filters.cropType, mode: 'insensitive' };
const [batches, total] = await Promise.all([
prisma.cropBatch.findMany({
where,
skip,
take: limit,
orderBy: { plantingDate: 'desc' },
include: {
zone: true,
recipe: true,
},
}),
prisma.cropBatch.count({ where }),
]);
return createPaginatedResult(batches, total, page, limit);
}
// Get active batches by farm
export async function getActiveCropBatchesByFarm(farmId: string): Promise<CropBatch[]> {
return prisma.cropBatch.findMany({
where: {
farmId,
status: { notIn: ['COMPLETED', 'FAILED'] },
},
include: {
zone: true,
recipe: true,
},
orderBy: { expectedHarvestDate: 'asc' },
});
}
// Add issue to crop batch
export async function addCropBatchIssue(
id: string,
issue: {
type: string;
severity: string;
description: string;
affectedPlants: number;
}
) {
const batch = await prisma.cropBatch.findUnique({
where: { id },
select: { issues: true },
});
const issues = (batch?.issues as unknown[] || []) as Record<string, unknown>[];
issues.push({
id: `issue_${Date.now()}`,
timestamp: new Date().toISOString(),
...issue,
});
return prisma.cropBatch.update({
where: { id },
data: { issues },
});
}
// ============================================
// GROWING RECIPE OPERATIONS
// ============================================
// Create a growing recipe
export async function createGrowingRecipe(data: {
name: string;
cropType: string;
variety?: string;
version?: string;
stages: Record<string, unknown>[];
expectedDays: number;
expectedYieldGrams: number;
expectedYieldPerSqm?: number;
requirements?: Record<string, unknown>;
source?: 'INTERNAL' | 'COMMUNITY' | 'COMMERCIAL';
author?: string;
}): Promise<GrowingRecipe> {
return prisma.growingRecipe.create({
data: {
...data,
version: data.version || '1.0',
source: data.source || 'INTERNAL',
timesUsed: 0,
},
});
}
// Get growing recipe by ID
export async function getGrowingRecipeById(id: string): Promise<GrowingRecipe | null> {
return prisma.growingRecipe.findUnique({
where: { id },
});
}
// Get recipes by crop type
export async function getGrowingRecipesByCrop(cropType: string): Promise<GrowingRecipe[]> {
return prisma.growingRecipe.findMany({
where: { cropType: { contains: cropType, mode: 'insensitive' } },
orderBy: { rating: 'desc' },
});
}
// Increment recipe usage
export async function incrementRecipeUsage(id: string): Promise<GrowingRecipe> {
return prisma.growingRecipe.update({
where: { id },
data: { timesUsed: { increment: 1 } },
});
}
// ============================================
// RESOURCE & ANALYTICS OPERATIONS
// ============================================
// Record resource usage
export async function recordResourceUsage(data: {
farmId: string;
periodStart: Date;
periodEnd: Date;
electricityKwh: number;
electricityCostUsd?: number;
renewablePercent?: number;
peakDemandKw?: number;
waterUsageL: number;
waterCostUsd?: number;
waterRecycledPercent?: number;
co2UsedKg?: number;
co2CostUsd?: number;
nutrientsUsedL?: number;
nutrientCostUsd?: number;
}) {
return prisma.resourceUsage.create({ data });
}
// Get resource usage for farm
export async function getResourceUsage(farmId: string, periodStart: Date, periodEnd: Date) {
return prisma.resourceUsage.findMany({
where: {
farmId,
periodStart: { gte: periodStart },
periodEnd: { lte: periodEnd },
},
orderBy: { periodStart: 'asc' },
});
}
// Record farm analytics
export async function recordFarmAnalytics(data: {
farmId: string;
period: string;
totalYieldKg: number;
yieldPerSqmPerYear?: number;
cropCyclesCompleted: number;
averageCyclesDays?: number;
averageQualityScore?: number;
gradeAPercent?: number;
wastagePercent?: number;
cropSuccessRate?: number;
spaceUtilization?: number;
laborHoursPerKg?: number;
revenueUsd?: number;
costUsd?: number;
profitMarginPercent?: number;
revenuePerSqm?: number;
carbonFootprintKgPerKg?: number;
waterUseLPerKg?: number;
energyUseKwhPerKg?: number;
topCropsByYield?: Record<string, unknown>[];
topCropsByRevenue?: Record<string, unknown>[];
topCropsByEfficiency?: Record<string, unknown>[];
}) {
return prisma.farmAnalytics.create({ data });
}
// Get farm analytics
export async function getFarmAnalytics(farmId: string, period?: string) {
const where: Prisma.FarmAnalyticsWhereInput = { farmId };
if (period) where.period = period;
return prisma.farmAnalytics.findMany({
where,
orderBy: { generatedAt: 'desc' },
take: period ? 1 : 12,
});
}