localgreenchain/lib/transport/tracker.ts
Claude ac93368e9a
Add seed-to-seed transport tracking, demand forecasting, and vertical farming systems
This comprehensive update implements:

Transport Tracking System:
- Complete seed-to-seed lifecycle tracking with 9 event types
- TransportChain blockchain for immutable transport records
- Carbon footprint calculation per transport method
- Food miles tracking with Haversine distance calculation
- QR code generation for full traceability

Demand Forecasting System:
- Consumer preference registration and aggregation
- Regional demand signal generation
- Supply gap identification and market matching
- Grower planting recommendations with risk assessment
- Seasonal planning integration

Vertical Farming Module:
- Multi-zone facility management
- Environmental control systems (HVAC, CO2, humidity, lighting)
- Growing recipes with stage-based environment targets
- Crop batch tracking with health scoring
- Farm analytics generation

Documentation:
- Complete docs/ folder structure for Turborepo
- Seed-to-seed transport concept documentation
- Demand forecasting and seasonal planning guides
- System architecture and user blockchain design
- Transport API reference
- Vertical farming integration guide

Agent Report:
- AGENT_REPORT.md with 5 parallel agent tasks for continued development
- API routes implementation task
- UI components task
- Vertical farming pages task
- Testing suite task
- Documentation completion task
2025-11-22 18:23:08 +00:00

516 lines
15 KiB
TypeScript

/**
* Transport Tracker for LocalGreenChain
* Manages seed-to-seed transport events and blockchain recording
*/
import crypto from 'crypto';
import {
TransportEvent,
TransportBlock,
TransportLocation,
TransportMethod,
PlantJourney,
EnvironmentalImpact,
TransportQRData,
CARBON_FACTORS,
SeedAcquisitionEvent,
PlantingEvent,
GrowingTransportEvent,
HarvestEvent,
ProcessingEvent,
DistributionEvent,
ConsumerDeliveryEvent,
SeedSavingEvent,
SeedSharingEvent,
TransportEventType
} from './types';
/**
* TransportChain - Blockchain for transport events
*/
export class TransportChain {
public chain: TransportBlock[];
public difficulty: number;
private eventIndex: Map<string, TransportBlock[]>;
private plantEvents: Map<string, TransportEvent[]>;
private batchEvents: Map<string, TransportEvent[]>;
constructor(difficulty: number = 3) {
this.chain = [this.createGenesisBlock()];
this.difficulty = difficulty;
this.eventIndex = new Map();
this.plantEvents = new Map();
this.batchEvents = new Map();
}
private createGenesisBlock(): TransportBlock {
const genesisEvent: SeedAcquisitionEvent = {
id: 'genesis-transport-0',
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
fromLocation: {
latitude: 0,
longitude: 0,
locationType: 'seed_bank',
facilityName: 'LocalGreenChain Genesis'
},
toLocation: {
latitude: 0,
longitude: 0,
locationType: 'seed_bank',
facilityName: 'LocalGreenChain Genesis'
},
distanceKm: 0,
durationMinutes: 0,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'system',
receiverId: 'system',
status: 'verified',
seedBatchId: 'genesis-seed-batch',
sourceType: 'seed_bank',
species: 'Blockchain primordialis',
quantity: 1,
quantityUnit: 'seeds',
generation: 0
};
return {
index: 0,
timestamp: new Date().toISOString(),
transportEvent: genesisEvent,
previousHash: '0',
hash: this.calculateHash(0, new Date().toISOString(), genesisEvent, '0', 0),
nonce: 0,
cumulativeCarbonKg: 0,
cumulativeFoodMiles: 0,
chainLength: 1
};
}
private calculateHash(
index: number,
timestamp: string,
event: TransportEvent,
previousHash: string,
nonce: number
): string {
const data = `${index}${timestamp}${JSON.stringify(event)}${previousHash}${nonce}`;
return crypto.createHash('sha256').update(data).digest('hex');
}
private mineBlock(block: TransportBlock): void {
const target = '0'.repeat(this.difficulty);
while (block.hash.substring(0, this.difficulty) !== target) {
block.nonce++;
block.hash = this.calculateHash(
block.index,
block.timestamp,
block.transportEvent,
block.previousHash,
block.nonce
);
}
}
getLatestBlock(): TransportBlock {
return this.chain[this.chain.length - 1];
}
/**
* Record a new transport event
*/
recordEvent(event: TransportEvent): TransportBlock {
const latestBlock = this.getLatestBlock();
// Calculate carbon footprint if not provided
if (!event.carbonFootprintKg) {
event.carbonFootprintKg = this.calculateCarbon(
event.transportMethod,
event.distanceKm,
this.estimateWeight(event)
);
}
const newBlock: TransportBlock = {
index: this.chain.length,
timestamp: new Date().toISOString(),
transportEvent: event,
previousHash: latestBlock.hash,
hash: '',
nonce: 0,
cumulativeCarbonKg: latestBlock.cumulativeCarbonKg + event.carbonFootprintKg,
cumulativeFoodMiles: latestBlock.cumulativeFoodMiles + event.distanceKm,
chainLength: this.chain.length + 1
};
newBlock.hash = this.calculateHash(
newBlock.index,
newBlock.timestamp,
event,
newBlock.previousHash,
newBlock.nonce
);
this.mineBlock(newBlock);
this.chain.push(newBlock);
this.indexEvent(event, newBlock);
return newBlock;
}
private indexEvent(event: TransportEvent, block: TransportBlock): void {
// Index by event ID
const eventBlocks = this.eventIndex.get(event.id) || [];
eventBlocks.push(block);
this.eventIndex.set(event.id, eventBlocks);
// Index by plant IDs
const plantIds = this.extractPlantIds(event);
for (const plantId of plantIds) {
const events = this.plantEvents.get(plantId) || [];
events.push(event);
this.plantEvents.set(plantId, events);
}
// Index by batch IDs
const batchIds = this.extractBatchIds(event);
for (const batchId of batchIds) {
const events = this.batchEvents.get(batchId) || [];
events.push(event);
this.batchEvents.set(batchId, events);
}
}
private extractPlantIds(event: TransportEvent): string[] {
switch (event.eventType) {
case 'planting':
return (event as PlantingEvent).plantIds;
case 'growing_transport':
return (event as GrowingTransportEvent).plantIds;
case 'harvest':
return (event as HarvestEvent).plantIds;
case 'seed_saving':
return (event as SeedSavingEvent).parentPlantIds;
default:
return [];
}
}
private extractBatchIds(event: TransportEvent): string[] {
const batchIds: string[] = [];
switch (event.eventType) {
case 'seed_acquisition':
batchIds.push((event as SeedAcquisitionEvent).seedBatchId);
break;
case 'planting':
batchIds.push((event as PlantingEvent).seedBatchId);
break;
case 'harvest':
batchIds.push((event as HarvestEvent).harvestBatchId);
if ((event as HarvestEvent).seedBatchIdCreated) {
batchIds.push((event as HarvestEvent).seedBatchIdCreated!);
}
break;
case 'processing':
batchIds.push(...(event as ProcessingEvent).harvestBatchIds);
batchIds.push((event as ProcessingEvent).processingBatchId);
break;
case 'distribution':
batchIds.push(...(event as DistributionEvent).batchIds);
break;
case 'consumer_delivery':
batchIds.push(...(event as ConsumerDeliveryEvent).batchIds);
break;
case 'seed_saving':
batchIds.push((event as SeedSavingEvent).newSeedBatchId);
break;
case 'seed_sharing':
batchIds.push((event as SeedSharingEvent).seedBatchId);
break;
}
return batchIds;
}
private estimateWeight(event: TransportEvent): number {
// Estimate weight in kg based on event type
switch (event.eventType) {
case 'seed_acquisition':
case 'seed_saving':
case 'seed_sharing':
return 0.1; // Seeds are light
case 'planting':
return 0.5;
case 'growing_transport':
return 2;
case 'harvest':
return (event as HarvestEvent).netWeight || 5;
case 'processing':
return (event as ProcessingEvent).outputWeight || 5;
case 'distribution':
case 'consumer_delivery':
return 5;
default:
return 1;
}
}
/**
* Calculate carbon footprint
*/
calculateCarbon(method: TransportMethod, distanceKm: number, weightKg: number): number {
const factor = CARBON_FACTORS[method] || 0.1;
return factor * distanceKm * weightKg;
}
/**
* Calculate distance between two locations using Haversine formula
*/
static calculateDistance(from: TransportLocation, to: TransportLocation): number {
const R = 6371; // Earth's radius in km
const dLat = TransportChain.toRadians(to.latitude - from.latitude);
const dLon = TransportChain.toRadians(to.longitude - from.longitude);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(TransportChain.toRadians(from.latitude)) *
Math.cos(TransportChain.toRadians(to.latitude)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
private static toRadians(degrees: number): number {
return degrees * (Math.PI / 180);
}
/**
* Get complete journey for a plant
*/
getPlantJourney(plantId: string): PlantJourney | null {
const events = this.plantEvents.get(plantId);
if (!events || events.length === 0) return null;
// Sort events by timestamp
const sortedEvents = [...events].sort(
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
const lastEvent = sortedEvents[sortedEvents.length - 1];
// Calculate metrics
let totalFoodMiles = 0;
let totalCarbonKg = 0;
let daysInTransit = 0;
for (const event of sortedEvents) {
totalFoodMiles += event.distanceKm;
totalCarbonKg += event.carbonFootprintKg;
daysInTransit += event.durationMinutes / (60 * 24);
}
// Find seed batch origin
const seedAcquisition = sortedEvents.find(e => e.eventType === 'seed_acquisition') as SeedAcquisitionEvent | undefined;
const planting = sortedEvents.find(e => e.eventType === 'planting') as PlantingEvent | undefined;
// Calculate growing days
const plantingDate = planting ? new Date(planting.timestamp) : null;
const lastDate = new Date(lastEvent.timestamp);
const daysGrowing = plantingDate
? Math.floor((lastDate.getTime() - plantingDate.getTime()) / (1000 * 60 * 60 * 24))
: 0;
// Determine current stage
let currentStage: PlantJourney['currentStage'] = 'seed';
if (sortedEvents.some(e => e.eventType === 'seed_saving')) {
currentStage = 'seed_saving';
} else if (sortedEvents.some(e => e.eventType === 'harvest')) {
currentStage = 'post_harvest';
} else if (sortedEvents.some(e => e.eventType === 'growing_transport')) {
const lastGrowing = sortedEvents.filter(e => e.eventType === 'growing_transport').pop() as GrowingTransportEvent;
currentStage = lastGrowing.plantStage;
} else if (sortedEvents.some(e => e.eventType === 'planting')) {
currentStage = 'seedling';
}
return {
plantId,
seedBatchOrigin: seedAcquisition?.seedBatchId || planting?.seedBatchId || 'unknown',
currentCustodian: lastEvent.receiverId,
currentLocation: lastEvent.toLocation,
currentStage,
events: sortedEvents,
totalFoodMiles,
totalCarbonKg,
daysInTransit: Math.round(daysInTransit),
daysGrowing,
generation: seedAcquisition?.generation || 0,
ancestorPlantIds: seedAcquisition?.parentPlantIds || [],
descendantSeedBatches: sortedEvents
.filter(e => e.eventType === 'seed_saving')
.map(e => (e as SeedSavingEvent).newSeedBatchId)
};
}
/**
* Get environmental impact summary for a user
*/
getEnvironmentalImpact(userId: string): EnvironmentalImpact {
const userEvents = this.chain
.filter(block =>
block.transportEvent.senderId === userId ||
block.transportEvent.receiverId === userId
)
.map(block => block.transportEvent);
let totalCarbonKg = 0;
let totalFoodMiles = 0;
let totalWeight = 0;
const breakdownByMethod: EnvironmentalImpact['breakdownByMethod'] = {} as any;
const breakdownByEventType: EnvironmentalImpact['breakdownByEventType'] = {} as any;
for (const event of userEvents) {
totalCarbonKg += event.carbonFootprintKg;
totalFoodMiles += event.distanceKm;
totalWeight += this.estimateWeight(event);
// Method breakdown
if (!breakdownByMethod[event.transportMethod]) {
breakdownByMethod[event.transportMethod] = { distance: 0, carbon: 0 };
}
breakdownByMethod[event.transportMethod].distance += event.distanceKm;
breakdownByMethod[event.transportMethod].carbon += event.carbonFootprintKg;
// Event type breakdown
if (!breakdownByEventType[event.eventType]) {
breakdownByEventType[event.eventType] = { count: 0, carbon: 0 };
}
breakdownByEventType[event.eventType].count++;
breakdownByEventType[event.eventType].carbon += event.carbonFootprintKg;
}
// Conventional comparison (assume 1500 miles avg, 2.5 kg CO2/lb)
const conventionalMiles = totalWeight * 1500;
const conventionalCarbon = totalWeight * 2.5;
return {
totalCarbonKg,
totalFoodMiles,
carbonPerKgProduce: totalWeight > 0 ? totalCarbonKg / totalWeight : 0,
milesPerKgProduce: totalWeight > 0 ? totalFoodMiles / totalWeight : 0,
breakdownByMethod,
breakdownByEventType,
comparisonToConventional: {
carbonSaved: Math.max(0, conventionalCarbon - totalCarbonKg),
milesSaved: Math.max(0, conventionalMiles - totalFoodMiles),
percentageReduction: conventionalCarbon > 0
? Math.round((1 - totalCarbonKg / conventionalCarbon) * 100)
: 0
}
};
}
/**
* Generate QR code data for a plant or batch
*/
generateQRData(plantId?: string, batchId?: string): TransportQRData {
const events = plantId
? this.plantEvents.get(plantId)
: batchId
? this.batchEvents.get(batchId)
: [];
const lastEvent = events && events.length > 0
? events[events.length - 1]
: null;
const lineageHash = crypto.createHash('sha256')
.update(JSON.stringify(events))
.digest('hex')
.substring(0, 16);
return {
plantId,
batchId,
blockchainAddress: this.getLatestBlock().hash.substring(0, 42),
quickLookupUrl: `https://localgreenchain.org/track/${plantId || batchId}`,
lineageHash,
currentCustodian: lastEvent?.receiverId || 'unknown',
lastEventType: lastEvent?.eventType || 'seed_acquisition',
lastEventTimestamp: lastEvent?.timestamp || new Date().toISOString(),
verificationCode: crypto.randomBytes(4).toString('hex').toUpperCase()
};
}
/**
* Verify chain integrity
*/
isChainValid(): boolean {
for (let i = 1; i < this.chain.length; i++) {
const currentBlock = this.chain[i];
const previousBlock = this.chain[i - 1];
// Verify hash
const expectedHash = this.calculateHash(
currentBlock.index,
currentBlock.timestamp,
currentBlock.transportEvent,
currentBlock.previousHash,
currentBlock.nonce
);
if (currentBlock.hash !== expectedHash) {
return false;
}
// Verify chain link
if (currentBlock.previousHash !== previousBlock.hash) {
return false;
}
}
return true;
}
/**
* Export to JSON
*/
toJSON(): object {
return {
difficulty: this.difficulty,
chain: this.chain
};
}
/**
* Import from JSON
*/
static fromJSON(data: any): TransportChain {
const chain = new TransportChain(data.difficulty);
chain.chain = data.chain;
// Rebuild indexes
for (const block of chain.chain) {
chain.indexEvent(block.transportEvent, block);
}
return chain;
}
}
// Singleton instance
let transportChainInstance: TransportChain | null = null;
export function getTransportChain(): TransportChain {
if (!transportChainInstance) {
transportChainInstance = new TransportChain();
}
return transportChainInstance;
}
export function setTransportChain(chain: TransportChain): void {
transportChainInstance = chain;
}