From 2502308bcb3192bf6a0f3082fe527bbd1068e71d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 18:31:21 +0000 Subject: [PATCH 1/2] Add complete API routes for transport, demand, and vertical farming systems Transport API (11 endpoints): - POST /api/transport/seed-acquisition - Record seed acquisition events - POST /api/transport/planting - Record planting events - POST /api/transport/growing - Record growing transport events - POST /api/transport/harvest - Record harvest events - POST /api/transport/distribution - Record distribution events - POST /api/transport/seed-saving - Record seed saving events - POST /api/transport/seed-sharing - Record seed sharing events - GET /api/transport/journey/[plantId] - Get plant journey - GET /api/transport/footprint/[userId] - Get environmental impact - GET /api/transport/verify/[blockHash] - Verify block integrity - GET /api/transport/qr/[id] - Generate QR code data Demand API (6 endpoints): - POST/GET /api/demand/preferences - Consumer preferences - POST /api/demand/signal - Generate demand signal - GET /api/demand/recommendations - Get planting recommendations - GET /api/demand/forecast - Get demand forecast - POST /api/demand/supply - Register supply commitment - POST /api/demand/match - Create market match Vertical Farm API (9 endpoints): - POST /api/vertical-farm/register - Register new farm - GET /api/vertical-farm/[farmId] - Get farm details - GET/POST /api/vertical-farm/[farmId]/zones - Manage zones - GET /api/vertical-farm/[farmId]/analytics - Get farm analytics - POST /api/vertical-farm/batch/start - Start crop batch - GET /api/vertical-farm/batch/[batchId] - Get batch details - PUT /api/vertical-farm/batch/[batchId]/environment - Record environment - POST /api/vertical-farm/batch/[batchId]/harvest - Complete harvest - GET /api/vertical-farm/recipes - List growing recipes --- pages/api/demand/forecast.ts | 48 +++++++ pages/api/demand/match.ts | 71 ++++++++++ pages/api/demand/preferences.ts | 94 +++++++++++++ pages/api/demand/recommendations.ts | 67 ++++++++++ pages/api/demand/signal.ts | 60 +++++++++ pages/api/demand/supply.ts | 78 +++++++++++ pages/api/transport/distribution.ts | 93 +++++++++++++ pages/api/transport/footprint/[userId].ts | 38 ++++++ pages/api/transport/growing.ts | 89 +++++++++++++ pages/api/transport/harvest.ts | 103 ++++++++++++++ pages/api/transport/journey/[plantId].ts | 45 +++++++ pages/api/transport/planting.ts | 91 +++++++++++++ pages/api/transport/qr/[id].ts | 48 +++++++ pages/api/transport/seed-acquisition.ts | 101 ++++++++++++++ pages/api/transport/seed-saving.ts | 101 ++++++++++++++ pages/api/transport/seed-sharing.ts | 95 +++++++++++++ pages/api/transport/verify/[blockHash].ts | 70 ++++++++++ pages/api/vertical-farm/[farmId]/analytics.ts | 56 ++++++++ pages/api/vertical-farm/[farmId]/index.ts | 45 +++++++ pages/api/vertical-farm/[farmId]/zones.ts | 126 ++++++++++++++++++ .../batch/[batchId]/environment.ts | 106 +++++++++++++++ .../vertical-farm/batch/[batchId]/harvest.ts | 81 +++++++++++ .../vertical-farm/batch/[batchId]/index.ts | 52 ++++++++ pages/api/vertical-farm/batch/start.ts | 81 +++++++++++ pages/api/vertical-farm/recipes.ts | 70 ++++++++++ pages/api/vertical-farm/register.ts | 122 +++++++++++++++++ 26 files changed, 2031 insertions(+) create mode 100644 pages/api/demand/forecast.ts create mode 100644 pages/api/demand/match.ts create mode 100644 pages/api/demand/preferences.ts create mode 100644 pages/api/demand/recommendations.ts create mode 100644 pages/api/demand/signal.ts create mode 100644 pages/api/demand/supply.ts create mode 100644 pages/api/transport/distribution.ts create mode 100644 pages/api/transport/footprint/[userId].ts create mode 100644 pages/api/transport/growing.ts create mode 100644 pages/api/transport/harvest.ts create mode 100644 pages/api/transport/journey/[plantId].ts create mode 100644 pages/api/transport/planting.ts create mode 100644 pages/api/transport/qr/[id].ts create mode 100644 pages/api/transport/seed-acquisition.ts create mode 100644 pages/api/transport/seed-saving.ts create mode 100644 pages/api/transport/seed-sharing.ts create mode 100644 pages/api/transport/verify/[blockHash].ts create mode 100644 pages/api/vertical-farm/[farmId]/analytics.ts create mode 100644 pages/api/vertical-farm/[farmId]/index.ts create mode 100644 pages/api/vertical-farm/[farmId]/zones.ts create mode 100644 pages/api/vertical-farm/batch/[batchId]/environment.ts create mode 100644 pages/api/vertical-farm/batch/[batchId]/harvest.ts create mode 100644 pages/api/vertical-farm/batch/[batchId]/index.ts create mode 100644 pages/api/vertical-farm/batch/start.ts create mode 100644 pages/api/vertical-farm/recipes.ts create mode 100644 pages/api/vertical-farm/register.ts diff --git a/pages/api/demand/forecast.ts b/pages/api/demand/forecast.ts new file mode 100644 index 0000000..7fe848d --- /dev/null +++ b/pages/api/demand/forecast.ts @@ -0,0 +1,48 @@ +/** + * API Route: Get demand forecast + * GET /api/demand/forecast + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { regionName, forecastWeeks } = req.query; + + // Validate required fields + if (!regionName) { + return res.status(400).json({ + success: false, + error: 'Missing required query parameter: regionName' + }); + } + + const weeks = forecastWeeks ? parseInt(forecastWeeks as string, 10) : 12; + + if (isNaN(weeks) || weeks < 1 || weeks > 52) { + return res.status(400).json({ + success: false, + error: 'forecastWeeks must be a number between 1 and 52' + }); + } + + const forecaster = getDemandForecaster(); + const forecast = forecaster.generateForecast(regionName as string, weeks); + + res.status(200).json({ + success: true, + data: forecast + }); + } catch (error: any) { + console.error('Error generating forecast:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/demand/match.ts b/pages/api/demand/match.ts new file mode 100644 index 0000000..d8f78ba --- /dev/null +++ b/pages/api/demand/match.ts @@ -0,0 +1,71 @@ +/** + * API Route: Create market match + * POST /api/demand/match + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; +import { MarketMatch } from '../../../lib/demand/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + demandSignalId, + supplyCommitmentId, + consumerId, + growerId, + produceType, + matchedQuantityKg, + agreedPricePerKg, + currency, + deliveryDate, + deliveryMethod, + deliveryLocation + } = req.body; + + // Validate required fields + if (!demandSignalId || !supplyCommitmentId || !consumerId || !growerId || !produceType || !matchedQuantityKg || !agreedPricePerKg || !deliveryDate || !deliveryMethod || !deliveryLocation) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: demandSignalId, supplyCommitmentId, consumerId, growerId, produceType, matchedQuantityKg, agreedPricePerKg, deliveryDate, deliveryMethod, deliveryLocation' + }); + } + + const match: MarketMatch = { + id: `match-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + demandSignalId, + supplyCommitmentId, + consumerId, + growerId, + produceType, + matchedQuantityKg, + agreedPricePerKg, + totalPrice: matchedQuantityKg * agreedPricePerKg, + currency: currency || 'USD', + deliveryDate, + deliveryMethod, + deliveryLocation, + status: 'pending' + }; + + // Store the match (in a real implementation, this would update the forecaster's internal state) + // For now, we'll just return the match + // The forecaster would track this to update supply commitment remaining quantities + + res.status(201).json({ + success: true, + data: match + }); + } catch (error: any) { + console.error('Error creating market match:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/demand/preferences.ts b/pages/api/demand/preferences.ts new file mode 100644 index 0000000..9d9b662 --- /dev/null +++ b/pages/api/demand/preferences.ts @@ -0,0 +1,94 @@ +/** + * API Route: Consumer preferences + * POST - Register/update consumer preferences + * GET - Get consumer preferences + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; +import { ConsumerPreference } from '../../../lib/demand/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const forecaster = getDemandForecaster(); + + if (req.method === 'POST') { + try { + const preference = req.body as ConsumerPreference; + + // Validate required fields + if (!preference.consumerId || !preference.location || !preference.preferredItems) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: consumerId, location, preferredItems' + }); + } + + // Add timestamps + const now = new Date().toISOString(); + preference.createdAt = preference.createdAt || now; + preference.updatedAt = now; + + // Set defaults + preference.householdSize = preference.householdSize || 1; + preference.dietaryType = preference.dietaryType || ['omnivore']; + preference.allergies = preference.allergies || []; + preference.dislikes = preference.dislikes || []; + preference.preferredCategories = preference.preferredCategories || []; + preference.certificationPreferences = preference.certificationPreferences || []; + preference.freshnessImportance = preference.freshnessImportance || 3; + preference.priceImportance = preference.priceImportance || 3; + preference.sustainabilityImportance = preference.sustainabilityImportance || 3; + preference.deliveryPreferences = preference.deliveryPreferences || { + method: ['home_delivery'], + frequency: 'weekly', + preferredDays: ['saturday'] + }; + + forecaster.registerPreference(preference); + + res.status(201).json({ + success: true, + data: preference + }); + } catch (error: any) { + console.error('Error registering preference:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } + } else if (req.method === 'GET') { + try { + const { consumerId } = req.query; + + if (!consumerId || typeof consumerId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Consumer ID is required' + }); + } + + // Access internal state for lookup + const state = forecaster.toJSON() as any; + const preferences = state.preferences as [string, ConsumerPreference][]; + const preference = preferences.find(([id]) => id === consumerId); + + if (!preference) { + return res.status(404).json({ + success: false, + error: `Preference not found for consumer: ${consumerId}` + }); + } + + res.status(200).json({ + success: true, + data: preference[1] + }); + } catch (error: any) { + console.error('Error fetching preference:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } + } else { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } +} diff --git a/pages/api/demand/recommendations.ts b/pages/api/demand/recommendations.ts new file mode 100644 index 0000000..e25140d --- /dev/null +++ b/pages/api/demand/recommendations.ts @@ -0,0 +1,67 @@ +/** + * API Route: Get planting recommendations + * GET /api/demand/recommendations + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + growerId, + latitude, + longitude, + radiusKm, + availableSpaceSqm, + season + } = req.query; + + // Validate required fields + if (!growerId || !latitude || !longitude || !radiusKm || !availableSpaceSqm || !season) { + return res.status(400).json({ + success: false, + error: 'Missing required query parameters: growerId, latitude, longitude, radiusKm, availableSpaceSqm, season' + }); + } + + // Validate season + const validSeasons = ['spring', 'summer', 'fall', 'winter']; + if (!validSeasons.includes(season as string)) { + return res.status(400).json({ + success: false, + error: 'Invalid season. Must be one of: spring, summer, fall, winter' + }); + } + + const forecaster = getDemandForecaster(); + const recommendations = forecaster.generatePlantingRecommendations( + growerId as string, + parseFloat(latitude as string), + parseFloat(longitude as string), + parseFloat(radiusKm as string), + parseFloat(availableSpaceSqm as string), + season as 'spring' | 'summer' | 'fall' | 'winter' + ); + + res.status(200).json({ + success: true, + data: { + growerId, + season, + totalRecommendations: recommendations.length, + recommendations + } + }); + } catch (error: any) { + console.error('Error generating recommendations:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/demand/signal.ts b/pages/api/demand/signal.ts new file mode 100644 index 0000000..ab7fe46 --- /dev/null +++ b/pages/api/demand/signal.ts @@ -0,0 +1,60 @@ +/** + * API Route: Generate demand signal + * POST /api/demand/signal + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + centerLat, + centerLon, + radiusKm, + regionName, + season + } = req.body; + + // Validate required fields + if (centerLat === undefined || centerLon === undefined || !radiusKm || !regionName || !season) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: centerLat, centerLon, radiusKm, regionName, season' + }); + } + + // Validate season + const validSeasons = ['spring', 'summer', 'fall', 'winter']; + if (!validSeasons.includes(season)) { + return res.status(400).json({ + success: false, + error: 'Invalid season. Must be one of: spring, summer, fall, winter' + }); + } + + const forecaster = getDemandForecaster(); + const signal = forecaster.generateDemandSignal( + centerLat, + centerLon, + radiusKm, + regionName, + season + ); + + res.status(201).json({ + success: true, + data: signal + }); + } catch (error: any) { + console.error('Error generating demand signal:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/demand/supply.ts b/pages/api/demand/supply.ts new file mode 100644 index 0000000..b5a49dd --- /dev/null +++ b/pages/api/demand/supply.ts @@ -0,0 +1,78 @@ +/** + * API Route: Register supply commitment + * POST /api/demand/supply + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getDemandForecaster } from '../../../lib/demand/forecaster'; +import { SupplyCommitment } from '../../../lib/demand/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + growerId, + produceType, + variety, + committedQuantityKg, + availableFrom, + availableUntil, + pricePerKg, + currency, + minimumOrderKg, + certifications, + freshnessGuaranteeHours, + deliveryRadiusKm, + deliveryMethods, + bulkDiscountThreshold, + bulkDiscountPercent + } = req.body; + + // Validate required fields + if (!growerId || !produceType || !committedQuantityKg || !availableFrom || !availableUntil || !pricePerKg) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: growerId, produceType, committedQuantityKg, availableFrom, availableUntil, pricePerKg' + }); + } + + const commitment: SupplyCommitment = { + id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + growerId, + timestamp: new Date().toISOString(), + produceType, + variety, + committedQuantityKg, + availableFrom, + availableUntil, + pricePerKg, + currency: currency || 'USD', + minimumOrderKg: minimumOrderKg || 0.5, + bulkDiscountThreshold, + bulkDiscountPercent, + certifications: certifications || [], + freshnessGuaranteeHours: freshnessGuaranteeHours || 24, + deliveryRadiusKm: deliveryRadiusKm || 50, + deliveryMethods: deliveryMethods || ['customer_pickup', 'grower_delivery'], + status: 'available', + remainingKg: committedQuantityKg + }; + + const forecaster = getDemandForecaster(); + forecaster.registerSupply(commitment); + + res.status(201).json({ + success: true, + data: commitment + }); + } catch (error: any) { + console.error('Error registering supply commitment:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/distribution.ts b/pages/api/transport/distribution.ts new file mode 100644 index 0000000..fe316b0 --- /dev/null +++ b/pages/api/transport/distribution.ts @@ -0,0 +1,93 @@ +/** + * API Route: Record distribution event + * POST /api/transport/distribution + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { DistributionEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + batchIds, + destinationType, + orderId, + customerType, + deliveryWindow, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + actualDeliveryTime, + deliveryAttempts, + handoffVerified, + recipientName, + notes + } = req.body; + + // Validate required fields + if (!batchIds || !destinationType || !customerType || !deliveryWindow || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: batchIds, destinationType, customerType, deliveryWindow, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: DistributionEvent = { + id: `dist-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'distribution', + batchIds: Array.isArray(batchIds) ? batchIds : [batchIds], + destinationType, + orderId, + customerType, + deliveryWindow, + actualDeliveryTime, + deliveryAttempts: deliveryAttempts || 1, + handoffVerified: handoffVerified || false, + recipientName, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'local_delivery', + carbonFootprintKg: 0, + senderId, + receiverId, + status: actualDeliveryTime ? 'delivered' : 'in_transit', + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording distribution event:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/footprint/[userId].ts b/pages/api/transport/footprint/[userId].ts new file mode 100644 index 0000000..a2648d4 --- /dev/null +++ b/pages/api/transport/footprint/[userId].ts @@ -0,0 +1,38 @@ +/** + * API Route: Get environmental impact for a user + * GET /api/transport/footprint/[userId] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../../lib/transport/tracker'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { userId } = req.query; + + if (!userId || typeof userId !== 'string') { + return res.status(400).json({ + success: false, + error: 'User ID is required' + }); + } + + const transportChain = getTransportChain(); + const impact = transportChain.getEnvironmentalImpact(userId); + + res.status(200).json({ + success: true, + data: impact + }); + } catch (error: any) { + console.error('Error fetching environmental impact:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/growing.ts b/pages/api/transport/growing.ts new file mode 100644 index 0000000..e2d2958 --- /dev/null +++ b/pages/api/transport/growing.ts @@ -0,0 +1,89 @@ +/** + * API Route: Record growing transport event + * POST /api/transport/growing + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { GrowingTransportEvent, TransportLocation, TransportMethod, PlantStage } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + plantIds, + reason, + plantStage, + handlingMethod, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + rootDisturbance, + acclimatizationRequired, + acclimatizationDays, + notes + } = req.body; + + // Validate required fields + if (!plantIds || !reason || !plantStage || !handlingMethod || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: plantIds, reason, plantStage, handlingMethod, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: GrowingTransportEvent = { + id: `growing-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'growing_transport', + plantIds: Array.isArray(plantIds) ? plantIds : [plantIds], + reason, + plantStage: plantStage as PlantStage, + handlingMethod, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'walking', + carbonFootprintKg: 0, + senderId, + receiverId, + status: 'verified', + rootDisturbance: rootDisturbance || 'minimal', + acclimatizationRequired: acclimatizationRequired || false, + acclimatizationDays, + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording growing transport:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/harvest.ts b/pages/api/transport/harvest.ts new file mode 100644 index 0000000..ca654e7 --- /dev/null +++ b/pages/api/transport/harvest.ts @@ -0,0 +1,103 @@ +/** + * API Route: Record harvest event + * POST /api/transport/harvest + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { HarvestEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + plantIds, + harvestBatchId, + harvestType, + produceType, + grossWeight, + netWeight, + weightUnit, + itemCount, + qualityGrade, + packagingType, + temperatureRequired, + shelfLifeHours, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + seedsSaved, + seedBatchIdCreated, + notes + } = req.body; + + // Validate required fields + if (!plantIds || !harvestBatchId || !harvestType || !produceType || !grossWeight || !netWeight || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: plantIds, harvestBatchId, harvestType, produceType, grossWeight, netWeight, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: HarvestEvent = { + id: `harvest-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'harvest', + plantIds: Array.isArray(plantIds) ? plantIds : [plantIds], + harvestBatchId, + harvestType, + produceType, + grossWeight, + netWeight, + weightUnit: weightUnit || 'kg', + itemCount, + qualityGrade, + packagingType: packagingType || 'bulk', + temperatureRequired: temperatureRequired || { min: 2, max: 8, optimal: 4, unit: 'celsius' }, + shelfLifeHours: shelfLifeHours || 168, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'walking', + carbonFootprintKg: 0, + senderId, + receiverId, + status: 'verified', + seedsSaved: seedsSaved || false, + seedBatchIdCreated, + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording harvest event:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/journey/[plantId].ts b/pages/api/transport/journey/[plantId].ts new file mode 100644 index 0000000..e8a1ae5 --- /dev/null +++ b/pages/api/transport/journey/[plantId].ts @@ -0,0 +1,45 @@ +/** + * API Route: Get plant journey + * GET /api/transport/journey/[plantId] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../../lib/transport/tracker'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { plantId } = req.query; + + if (!plantId || typeof plantId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Plant ID is required' + }); + } + + const transportChain = getTransportChain(); + const journey = transportChain.getPlantJourney(plantId); + + if (!journey) { + return res.status(404).json({ + success: false, + error: `No journey found for plant: ${plantId}` + }); + } + + res.status(200).json({ + success: true, + data: journey + }); + } catch (error: any) { + console.error('Error fetching plant journey:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/planting.ts b/pages/api/transport/planting.ts new file mode 100644 index 0000000..97ca666 --- /dev/null +++ b/pages/api/transport/planting.ts @@ -0,0 +1,91 @@ +/** + * API Route: Record planting event + * POST /api/transport/planting + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { PlantingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + seedBatchId, + plantIds, + plantingMethod, + quantityPlanted, + growingEnvironment, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + sowingDepth, + spacing, + expectedHarvestDate, + notes + } = req.body; + + // Validate required fields + if (!seedBatchId || !plantIds || !plantingMethod || !quantityPlanted || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: seedBatchId, plantIds, plantingMethod, quantityPlanted, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: PlantingEvent = { + id: `planting-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'planting', + seedBatchId, + plantIds: Array.isArray(plantIds) ? plantIds : [plantIds], + plantingMethod, + quantityPlanted, + growingEnvironment: growingEnvironment || 'outdoor', + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'walking', + carbonFootprintKg: 0, + senderId, + receiverId, + status: 'verified', + sowingDepth, + spacing, + expectedHarvestDate, + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording planting event:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/qr/[id].ts b/pages/api/transport/qr/[id].ts new file mode 100644 index 0000000..6f51970 --- /dev/null +++ b/pages/api/transport/qr/[id].ts @@ -0,0 +1,48 @@ +/** + * API Route: Generate QR code data + * GET /api/transport/qr/[id] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../../lib/transport/tracker'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { id, type } = req.query; + + if (!id || typeof id !== 'string') { + return res.status(400).json({ + success: false, + error: 'ID is required' + }); + } + + const transportChain = getTransportChain(); + + // Determine if this is a plant ID or batch ID + const idType = type === 'batch' ? 'batch' : 'plant'; + + const qrData = idType === 'batch' + ? transportChain.generateQRData(undefined, id) + : transportChain.generateQRData(id, undefined); + + res.status(200).json({ + success: true, + data: { + ...qrData, + qrContent: JSON.stringify(qrData), + scanUrl: qrData.quickLookupUrl + } + }); + } catch (error: any) { + console.error('Error generating QR data:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/seed-acquisition.ts b/pages/api/transport/seed-acquisition.ts new file mode 100644 index 0000000..2c05467 --- /dev/null +++ b/pages/api/transport/seed-acquisition.ts @@ -0,0 +1,101 @@ +/** + * API Route: Record seed acquisition event + * POST /api/transport/seed-acquisition + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { SeedAcquisitionEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + seedBatchId, + sourceType, + species, + variety, + quantity, + quantityUnit, + generation, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + germinationRate, + certifications, + notes + } = req.body; + + // Validate required fields + if (!seedBatchId || !sourceType || !species || !quantity || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: seedBatchId, sourceType, species, quantity, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + // Calculate distance if not provided + const calculatedDistance = distanceKm ?? + (fromLocation && toLocation ? + Math.sqrt( + Math.pow((toLocation.latitude - fromLocation.latitude) * 111, 2) + + Math.pow((toLocation.longitude - fromLocation.longitude) * 111 * Math.cos(fromLocation.latitude * Math.PI / 180), 2) + ) : 0); + + const event: SeedAcquisitionEvent = { + id: `seed-acq-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'seed_acquisition', + seedBatchId, + sourceType, + species, + variety, + quantity, + quantityUnit: quantityUnit || 'seeds', + generation: generation || 0, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: calculatedDistance, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'local_delivery', + carbonFootprintKg: 0, // Will be calculated by tracker + senderId, + receiverId, + status: 'verified', + germinationRate, + certifications, + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording seed acquisition:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/seed-saving.ts b/pages/api/transport/seed-saving.ts new file mode 100644 index 0000000..885dd13 --- /dev/null +++ b/pages/api/transport/seed-saving.ts @@ -0,0 +1,101 @@ +/** + * API Route: Record seed saving event + * POST /api/transport/seed-saving + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { SeedSavingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + parentPlantIds, + newSeedBatchId, + collectionMethod, + seedCount, + seedWeight, + seedWeightUnit, + storageConditions, + storageLocationId, + newGenerationNumber, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + viabilityTestDate, + germinationRate, + availableForSharing, + sharingTerms, + notes + } = req.body; + + // Validate required fields + if (!parentPlantIds || !newSeedBatchId || !collectionMethod || !storageConditions || !storageLocationId || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: parentPlantIds, newSeedBatchId, collectionMethod, storageConditions, storageLocationId, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: SeedSavingEvent = { + id: `seed-save-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'seed_saving', + parentPlantIds: Array.isArray(parentPlantIds) ? parentPlantIds : [parentPlantIds], + newSeedBatchId, + collectionMethod, + seedCount, + seedWeight, + seedWeightUnit, + storageConditions, + storageLocationId, + newGenerationNumber: newGenerationNumber || 1, + viabilityTestDate, + germinationRate, + availableForSharing: availableForSharing || false, + sharingTerms, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'walking', + carbonFootprintKg: 0, + senderId, + receiverId, + status: 'verified', + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording seed saving event:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/seed-sharing.ts b/pages/api/transport/seed-sharing.ts new file mode 100644 index 0000000..9bb5909 --- /dev/null +++ b/pages/api/transport/seed-sharing.ts @@ -0,0 +1,95 @@ +/** + * API Route: Record seed sharing event + * POST /api/transport/seed-sharing + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../lib/transport/tracker'; +import { SeedSharingEvent, TransportLocation, TransportMethod } from '../../../lib/transport/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + seedBatchId, + quantityShared, + quantityUnit, + sharingType, + fromLocation, + toLocation, + transportMethod, + senderId, + receiverId, + distanceKm, + durationMinutes, + tradeDetails, + saleAmount, + saleCurrency, + recipientAgreement, + growingCommitment, + reportBackRequired, + notes + } = req.body; + + // Validate required fields + if (!seedBatchId || !quantityShared || !sharingType || !fromLocation || !toLocation || !senderId || !receiverId) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: seedBatchId, quantityShared, sharingType, fromLocation, toLocation, senderId, receiverId' + }); + } + + const transportChain = getTransportChain(); + + const event: SeedSharingEvent = { + id: `seed-share-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date().toISOString(), + eventType: 'seed_sharing', + seedBatchId, + quantityShared, + quantityUnit: quantityUnit || 'seeds', + sharingType, + tradeDetails, + saleAmount, + saleCurrency, + recipientAgreement: recipientAgreement || false, + growingCommitment, + reportBackRequired: reportBackRequired || false, + fromLocation: fromLocation as TransportLocation, + toLocation: toLocation as TransportLocation, + distanceKm: distanceKm || 0, + durationMinutes: durationMinutes || 0, + transportMethod: (transportMethod as TransportMethod) || 'local_delivery', + carbonFootprintKg: 0, + senderId, + receiverId, + status: 'verified', + notes + }; + + const block = transportChain.recordEvent(event); + + res.status(201).json({ + success: true, + data: { + event, + block: { + index: block.index, + hash: block.hash, + timestamp: block.timestamp, + cumulativeCarbonKg: block.cumulativeCarbonKg, + cumulativeFoodMiles: block.cumulativeFoodMiles + } + } + }); + } catch (error: any) { + console.error('Error recording seed sharing event:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/transport/verify/[blockHash].ts b/pages/api/transport/verify/[blockHash].ts new file mode 100644 index 0000000..bac3d0b --- /dev/null +++ b/pages/api/transport/verify/[blockHash].ts @@ -0,0 +1,70 @@ +/** + * API Route: Verify block integrity + * GET /api/transport/verify/[blockHash] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getTransportChain } from '../../../../lib/transport/tracker'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { blockHash } = req.query; + + if (!blockHash || typeof blockHash !== 'string') { + return res.status(400).json({ + success: false, + error: 'Block hash is required' + }); + } + + const transportChain = getTransportChain(); + + // Find the block with the given hash + const block = transportChain.chain.find(b => b.hash === blockHash); + + if (!block) { + return res.status(404).json({ + success: false, + error: `Block not found with hash: ${blockHash}` + }); + } + + // Verify chain integrity + const isChainValid = transportChain.isChainValid(); + + // Verify this specific block's position in chain + const blockIndex = transportChain.chain.findIndex(b => b.hash === blockHash); + const previousBlock = blockIndex > 0 ? transportChain.chain[blockIndex - 1] : null; + const isBlockValid = previousBlock ? block.previousHash === previousBlock.hash : block.index === 0; + + res.status(200).json({ + success: true, + data: { + block: { + index: block.index, + hash: block.hash, + previousHash: block.previousHash, + timestamp: block.timestamp, + eventType: block.transportEvent.eventType, + eventId: block.transportEvent.id + }, + verification: { + isChainValid, + isBlockValid, + blockPosition: blockIndex, + totalBlocks: transportChain.chain.length + } + } + }); + } catch (error: any) { + console.error('Error verifying block:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/[farmId]/analytics.ts b/pages/api/vertical-farm/[farmId]/analytics.ts new file mode 100644 index 0000000..604f8c3 --- /dev/null +++ b/pages/api/vertical-farm/[farmId]/analytics.ts @@ -0,0 +1,56 @@ +/** + * API Route: Get farm analytics + * GET /api/vertical-farm/[farmId]/analytics + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { farmId, periodDays } = req.query; + + if (!farmId || typeof farmId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Farm ID is required' + }); + } + + const days = periodDays ? parseInt(periodDays as string, 10) : 30; + + if (isNaN(days) || days < 1 || days > 365) { + return res.status(400).json({ + success: false, + error: 'periodDays must be a number between 1 and 365' + }); + } + + const controller = getVerticalFarmController(); + const farm = controller.getFarm(farmId); + + if (!farm) { + return res.status(404).json({ + success: false, + error: `Farm not found: ${farmId}` + }); + } + + const analytics = controller.generateAnalytics(farmId, days); + + res.status(200).json({ + success: true, + data: analytics + }); + } catch (error: any) { + console.error('Error generating analytics:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/[farmId]/index.ts b/pages/api/vertical-farm/[farmId]/index.ts new file mode 100644 index 0000000..b6716ad --- /dev/null +++ b/pages/api/vertical-farm/[farmId]/index.ts @@ -0,0 +1,45 @@ +/** + * API Route: Get farm details + * GET /api/vertical-farm/[farmId] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { farmId } = req.query; + + if (!farmId || typeof farmId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Farm ID is required' + }); + } + + const controller = getVerticalFarmController(); + const farm = controller.getFarm(farmId); + + if (!farm) { + return res.status(404).json({ + success: false, + error: `Farm not found: ${farmId}` + }); + } + + res.status(200).json({ + success: true, + data: farm + }); + } catch (error: any) { + console.error('Error fetching farm:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/[farmId]/zones.ts b/pages/api/vertical-farm/[farmId]/zones.ts new file mode 100644 index 0000000..5568cd5 --- /dev/null +++ b/pages/api/vertical-farm/[farmId]/zones.ts @@ -0,0 +1,126 @@ +/** + * API Route: Manage zones + * GET - List zones for a farm + * POST - Add a new zone + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller'; +import { GrowingZone, ZoneEnvironmentTargets, ZoneEnvironmentReadings } from '../../../../lib/vertical-farming/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { farmId } = req.query; + + if (!farmId || typeof farmId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Farm ID is required' + }); + } + + const controller = getVerticalFarmController(); + const farm = controller.getFarm(farmId); + + if (!farm) { + return res.status(404).json({ + success: false, + error: `Farm not found: ${farmId}` + }); + } + + if (req.method === 'GET') { + try { + res.status(200).json({ + success: true, + data: { + farmId, + totalZones: farm.zones.length, + zones: farm.zones + } + }); + } catch (error: any) { + console.error('Error fetching zones:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } + } else if (req.method === 'POST') { + try { + const { + name, + level, + areaSqm, + lengthM, + widthM, + growingMethod, + plantPositions + } = req.body; + + // Validate required fields + if (!name || level === undefined || !areaSqm || !growingMethod || !plantPositions) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: name, level, areaSqm, growingMethod, plantPositions' + }); + } + + const defaultTargets: ZoneEnvironmentTargets = { + temperatureC: { min: 18, max: 26, target: 22 }, + humidityPercent: { min: 50, max: 80, target: 65 }, + co2Ppm: { min: 400, max: 1500, target: 1000 }, + lightPpfd: { min: 100, max: 500, target: 300 }, + lightHours: 16, + nutrientEc: { min: 1.0, max: 2.0, target: 1.5 }, + nutrientPh: { min: 5.5, max: 6.5, target: 6.0 }, + waterTempC: { min: 18, max: 24, target: 20 } + }; + + const defaultReadings: ZoneEnvironmentReadings = { + timestamp: new Date().toISOString(), + temperatureC: 22, + humidityPercent: 65, + co2Ppm: 800, + vpd: 1.0, + ppfd: 300, + dli: 17, + waterTempC: 20, + ec: 1.5, + ph: 6.0, + dissolvedOxygenPpm: 8, + airflowMs: 0.5, + alerts: [] + }; + + const zone: GrowingZone = { + id: `zone-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + name, + level, + areaSqm, + lengthM: lengthM || Math.sqrt(areaSqm), + widthM: widthM || Math.sqrt(areaSqm), + growingMethod, + plantPositions, + currentCrop: '', + plantIds: [], + plantingDate: '', + expectedHarvestDate: '', + environmentTargets: defaultTargets, + currentEnvironment: defaultReadings, + status: 'empty' + }; + + farm.zones.push(zone); + + res.status(201).json({ + success: true, + data: zone + }); + } catch (error: any) { + console.error('Error adding zone:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } + } else { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } +} diff --git a/pages/api/vertical-farm/batch/[batchId]/environment.ts b/pages/api/vertical-farm/batch/[batchId]/environment.ts new file mode 100644 index 0000000..e1814cd --- /dev/null +++ b/pages/api/vertical-farm/batch/[batchId]/environment.ts @@ -0,0 +1,106 @@ +/** + * API Route: Record environment reading + * PUT /api/vertical-farm/batch/[batchId]/environment + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller'; +import { ZoneEnvironmentReadings } from '../../../../../lib/vertical-farming/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'PUT') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { batchId } = req.query; + const { + temperatureC, + humidityPercent, + co2Ppm, + ppfd, + waterTempC, + ec, + ph, + dissolvedOxygenPpm, + airflowMs + } = req.body; + + if (!batchId || typeof batchId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Batch ID is required' + }); + } + + // Validate required readings + if (temperatureC === undefined || humidityPercent === undefined || ec === undefined || ph === undefined) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: temperatureC, humidityPercent, ec, ph' + }); + } + + const controller = getVerticalFarmController(); + + // Access internal state to find the batch + const state = controller.toJSON() as any; + const batches = state.batches as [string, any][]; + const batchEntry = batches.find(([id]) => id === batchId); + + if (!batchEntry) { + return res.status(404).json({ + success: false, + error: `Batch not found: ${batchId}` + }); + } + + const batch = batchEntry[1]; + + const readings: ZoneEnvironmentReadings = { + timestamp: new Date().toISOString(), + temperatureC, + humidityPercent, + co2Ppm: co2Ppm || 800, + vpd: calculateVpd(temperatureC, humidityPercent), + ppfd: ppfd || 300, + dli: (ppfd || 300) * 16 * 3600 / 1000000, // Approximate DLI + waterTempC: waterTempC || 20, + ec, + ph, + dissolvedOxygenPpm: dissolvedOxygenPpm || 8, + airflowMs: airflowMs || 0.5, + alerts: [] + }; + + const alerts = controller.recordEnvironment(batch.zoneId, readings); + + res.status(200).json({ + success: true, + data: { + readings, + alerts, + batchId, + zoneId: batch.zoneId + } + }); + } catch (error: any) { + console.error('Error recording environment:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} + +/** + * Calculate Vapor Pressure Deficit + */ +function calculateVpd(tempC: number, humidityPercent: number): number { + // Saturation vapor pressure (kPa) + const svp = 0.6108 * Math.exp((17.27 * tempC) / (tempC + 237.3)); + // Actual vapor pressure + const avp = svp * (humidityPercent / 100); + // VPD + return Math.round((svp - avp) * 100) / 100; +} diff --git a/pages/api/vertical-farm/batch/[batchId]/harvest.ts b/pages/api/vertical-farm/batch/[batchId]/harvest.ts new file mode 100644 index 0000000..f1fcf4c --- /dev/null +++ b/pages/api/vertical-farm/batch/[batchId]/harvest.ts @@ -0,0 +1,81 @@ +/** + * API Route: Complete harvest + * POST /api/vertical-farm/batch/[batchId]/harvest + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { batchId } = req.query; + const { actualYieldKg, qualityGrade } = req.body; + + if (!batchId || typeof batchId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Batch ID is required' + }); + } + + // Validate required fields + if (actualYieldKg === undefined || !qualityGrade) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: actualYieldKg, qualityGrade' + }); + } + + // Validate quality grade + const validGrades = ['A', 'B', 'C', 'processing']; + if (!validGrades.includes(qualityGrade)) { + return res.status(400).json({ + success: false, + error: 'Invalid qualityGrade. Must be one of: A, B, C, processing' + }); + } + + if (actualYieldKg < 0) { + return res.status(400).json({ + success: false, + error: 'actualYieldKg must be a positive number' + }); + } + + const controller = getVerticalFarmController(); + + // Verify batch exists + const state = controller.toJSON() as any; + const batches = state.batches as [string, any][]; + const batchEntry = batches.find(([id]) => id === batchId); + + if (!batchEntry) { + return res.status(404).json({ + success: false, + error: `Batch not found: ${batchId}` + }); + } + + const completedBatch = controller.completeHarvest(batchId, actualYieldKg, qualityGrade); + + res.status(200).json({ + success: true, + data: { + batch: completedBatch, + yieldEfficiency: completedBatch.expectedYieldKg > 0 + ? Math.round((actualYieldKg / completedBatch.expectedYieldKg) * 100) + : 0 + } + }); + } catch (error: any) { + console.error('Error completing harvest:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/batch/[batchId]/index.ts b/pages/api/vertical-farm/batch/[batchId]/index.ts new file mode 100644 index 0000000..0fe19f5 --- /dev/null +++ b/pages/api/vertical-farm/batch/[batchId]/index.ts @@ -0,0 +1,52 @@ +/** + * API Route: Get batch details + * GET /api/vertical-farm/batch/[batchId] + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { batchId } = req.query; + + if (!batchId || typeof batchId !== 'string') { + return res.status(400).json({ + success: false, + error: 'Batch ID is required' + }); + } + + const controller = getVerticalFarmController(); + + // Access internal state to find the batch + const state = controller.toJSON() as any; + const batches = state.batches as [string, any][]; + const batchEntry = batches.find(([id]) => id === batchId); + + if (!batchEntry) { + return res.status(404).json({ + success: false, + error: `Batch not found: ${batchId}` + }); + } + + // Update batch progress + const batch = controller.updateBatchProgress(batchId); + + res.status(200).json({ + success: true, + data: batch + }); + } catch (error: any) { + console.error('Error fetching batch:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/batch/start.ts b/pages/api/vertical-farm/batch/start.ts new file mode 100644 index 0000000..831632b --- /dev/null +++ b/pages/api/vertical-farm/batch/start.ts @@ -0,0 +1,81 @@ +/** + * API Route: Start crop batch + * POST /api/vertical-farm/batch/start + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + farmId, + zoneId, + recipeId, + seedBatchId, + plantCount + } = req.body; + + // Validate required fields + if (!farmId || !zoneId || !recipeId || !seedBatchId || !plantCount) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: farmId, zoneId, recipeId, seedBatchId, plantCount' + }); + } + + if (plantCount < 1) { + return res.status(400).json({ + success: false, + error: 'plantCount must be at least 1' + }); + } + + const controller = getVerticalFarmController(); + + // Verify farm exists + const farm = controller.getFarm(farmId); + if (!farm) { + return res.status(404).json({ + success: false, + error: `Farm not found: ${farmId}` + }); + } + + // Verify zone exists + const zone = farm.zones.find(z => z.id === zoneId); + if (!zone) { + return res.status(404).json({ + success: false, + error: `Zone not found: ${zoneId}` + }); + } + + // Verify recipe exists + const recipes = controller.getRecipes(); + const recipe = recipes.find(r => r.id === recipeId); + if (!recipe) { + return res.status(404).json({ + success: false, + error: `Recipe not found: ${recipeId}` + }); + } + + const batch = controller.startCropBatch(farmId, zoneId, recipeId, seedBatchId, plantCount); + + res.status(201).json({ + success: true, + data: batch + }); + } catch (error: any) { + console.error('Error starting crop batch:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/recipes.ts b/pages/api/vertical-farm/recipes.ts new file mode 100644 index 0000000..3b5da4e --- /dev/null +++ b/pages/api/vertical-farm/recipes.ts @@ -0,0 +1,70 @@ +/** + * API Route: List growing recipes + * GET /api/vertical-farm/recipes + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../lib/vertical-farming/controller'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'GET') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { cropType, source } = req.query; + + const controller = getVerticalFarmController(); + let recipes = controller.getRecipes(); + + // Filter by crop type if provided + if (cropType && typeof cropType === 'string') { + recipes = recipes.filter(r => r.cropType.toLowerCase() === cropType.toLowerCase()); + } + + // Filter by source if provided + if (source && typeof source === 'string') { + recipes = recipes.filter(r => r.source === source); + } + + // Sort by rating (highest first) then by times used + recipes.sort((a, b) => { + if ((b.rating || 0) !== (a.rating || 0)) { + return (b.rating || 0) - (a.rating || 0); + } + return b.timesUsed - a.timesUsed; + }); + + res.status(200).json({ + success: true, + data: { + totalRecipes: recipes.length, + recipes: recipes.map(recipe => ({ + id: recipe.id, + name: recipe.name, + cropType: recipe.cropType, + variety: recipe.variety, + version: recipe.version, + expectedDays: recipe.expectedDays, + expectedYieldGrams: recipe.expectedYieldGrams, + expectedYieldPerSqm: recipe.expectedYieldPerSqm, + requirements: recipe.requirements, + source: recipe.source, + rating: recipe.rating, + timesUsed: recipe.timesUsed, + stages: recipe.stages.map(s => ({ + name: s.name, + daysStart: s.daysStart, + daysEnd: s.daysEnd + })) + })) + } + }); + } catch (error: any) { + console.error('Error fetching recipes:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} diff --git a/pages/api/vertical-farm/register.ts b/pages/api/vertical-farm/register.ts new file mode 100644 index 0000000..7bfd9b9 --- /dev/null +++ b/pages/api/vertical-farm/register.ts @@ -0,0 +1,122 @@ +/** + * API Route: Register new vertical farm + * POST /api/vertical-farm/register + */ + +import type { NextApiRequest, NextApiResponse } from 'next'; +import { getVerticalFarmController } from '../../../lib/vertical-farming/controller'; +import { VerticalFarm } from '../../../lib/vertical-farming/types'; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method !== 'POST') { + return res.status(405).json({ success: false, error: 'Method not allowed' }); + } + + try { + const { + name, + ownerId, + location, + specs, + zones, + environmentalControl, + irrigationSystem, + lightingSystem, + nutrientSystem, + automationLevel, + automationSystems + } = req.body; + + // Validate required fields + if (!name || !ownerId || !location || !specs) { + return res.status(400).json({ + success: false, + error: 'Missing required fields: name, ownerId, location, specs' + }); + } + + const controller = getVerticalFarmController(); + + const farm: VerticalFarm = { + id: `farm-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, + name, + ownerId, + location, + specs: { + ...specs, + certifications: specs.certifications || [], + currentActivePlants: specs.currentActivePlants || 0 + }, + zones: zones || [], + environmentalControl: environmentalControl || { + hvacUnits: [], + co2Injection: { type: 'tank', capacityKg: 0, currentLevelKg: 0, injectionRateKgPerHour: 0, status: 'off' }, + humidification: { type: 'ultrasonic', capacityLPerHour: 0, status: 'off', currentOutput: 0 }, + airCirculation: { fans: [] }, + controlMode: 'manual' + }, + irrigationSystem: irrigationSystem || { + type: 'recirculating', + freshWaterTankL: 1000, + freshWaterLevelL: 500, + nutrientTankL: 500, + nutrientLevelL: 250, + wasteTankL: 200, + wasteLevelL: 0, + waterTreatment: { ro: false, uv: false, ozone: false, filtration: 'basic' }, + pumps: [], + irrigationSchedule: [] + }, + lightingSystem: lightingSystem || { + type: 'LED', + fixtures: [], + lightSchedules: [], + totalWattage: 0, + currentWattage: 0, + efficacyUmolJ: 2.5 + }, + nutrientSystem: nutrientSystem || { + mixingMethod: 'manual', + stockSolutions: [], + dosingPumps: [], + currentRecipe: { + id: 'default', + name: 'Default Recipe', + cropType: 'general', + growthStage: 'vegetative', + targetEc: 1.5, + targetPh: 6.0, + ratios: { n: 150, p: 50, k: 200, ca: 200, mg: 50, s: 60, fe: 3, mn: 0.5, zn: 0.05, cu: 0.02, b: 0.5, mo: 0.01 }, + dosingRatiosMlPerL: [] + }, + monitoring: { + ec: 0, + ph: 0, + lastCalibration: new Date().toISOString(), + calibrationDue: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() + } + }, + automationLevel: automationLevel || 'manual', + automationSystems: automationSystems || [], + status: 'operational', + operationalSince: new Date().toISOString(), + lastMaintenanceDate: new Date().toISOString(), + currentCapacityUtilization: 0, + averageYieldEfficiency: 0, + energyEfficiencyScore: 50 + }; + + controller.registerFarm(farm); + + res.status(201).json({ + success: true, + data: farm + }); + } catch (error: any) { + console.error('Error registering farm:', error); + res.status(500).json({ success: false, error: error.message || 'Internal server error' }); + } +} From 71a9dc323f76d3c78f3123dba87f36fec5db2e30 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Nov 2025 18:32:48 +0000 Subject: [PATCH 2/2] Add tsconfig.tsbuildinfo to gitignore Build artifacts should not be tracked in version control. --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index a0db81a..52037dc 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,5 @@ bun-debug.log* /certificates/* cypress/screenshots -cypress/videos \ No newline at end of file +cypress/videos +tsconfig.tsbuildinfo