localgreenchain/__tests__/integration/demand-to-harvest.test.ts
Claude b8d2d5be5f
Add comprehensive testing suite for transport, demand, and vertical farming systems
- Set up Jest testing framework with TypeScript support
- Add unit tests for TransportChain blockchain (tracker, carbon, types)
- Add unit tests for DemandForecaster (forecaster, aggregation, recommendations)
- Add unit tests for VerticalFarmController (controller, recipes, environment)
- Add API tests for transport, demand, and vertical-farm endpoints
- Add integration tests for full lifecycle workflows:
  - Seed-to-seed lifecycle
  - Demand-to-harvest flow
  - VF batch lifecycle
2025-11-22 18:47:04 +00:00

407 lines
14 KiB
TypeScript

/**
* Demand to Harvest Integration Tests
* Tests the complete flow from demand signal to plant to harvest
*/
import { TransportChain, setTransportChain } from '../../lib/transport/tracker';
import { DemandForecaster } from '../../lib/demand/forecaster';
import {
ConsumerPreference,
PlantingRecommendation,
} from '../../lib/demand/types';
import {
SeedAcquisitionEvent,
PlantingEvent,
HarvestEvent,
DistributionEvent,
ConsumerDeliveryEvent,
TransportLocation,
} from '../../lib/transport/types';
describe('Demand to Harvest Integration', () => {
let chain: TransportChain;
let forecaster: DemandForecaster;
beforeEach(() => {
chain = new TransportChain(1);
setTransportChain(chain);
forecaster = new DemandForecaster();
});
describe('Complete Demand-Driven Flow', () => {
it('should complete full flow from demand to consumer delivery', () => {
// Step 1: Register consumer demand
const consumerId = 'consumer-integration-001';
const pref: ConsumerPreference = {
consumerId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
location: {
latitude: 40.7128,
longitude: -74.006,
maxDeliveryRadiusKm: 25,
city: 'New York',
},
dietaryType: ['omnivore'],
allergies: [],
dislikes: [],
preferredCategories: ['leafy_greens'],
preferredItems: [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false },
],
certificationPreferences: ['organic', 'local'],
freshnessImportance: 5,
priceImportance: 3,
sustainabilityImportance: 5,
deliveryPreferences: {
method: ['home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday'],
},
householdSize: 4,
weeklyBudget: 100,
currency: 'USD',
};
forecaster.registerPreference(pref);
// Step 2: Generate demand signal
const signal = forecaster.generateDemandSignal(
40.7128, -74.006, 50, 'NYC Metro', 'spring'
);
expect(signal.totalConsumers).toBe(1);
expect(signal.demandItems.length).toBeGreaterThan(0);
const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce');
expect(lettuceItem).toBeDefined();
expect(lettuceItem!.weeklyDemandKg).toBe(20); // 5 kg * 4 household size
// Step 3: Generate planting recommendation for grower
const recommendations = forecaster.generatePlantingRecommendations(
'grower-integration-001',
40.72, -74.01, // Near NYC
50, // 50km delivery radius
100, // 100 sqm available
'spring'
);
expect(recommendations.length).toBeGreaterThan(0);
const lettuceRec = recommendations.find(r => r.produceType === 'lettuce');
expect(lettuceRec).toBeDefined();
// Step 4: Grower follows recommendation and plants
const seedBatchId = 'demand-driven-batch-001';
const plantIds = ['demand-plant-001', 'demand-plant-002'];
const seedEvent: SeedAcquisitionEvent = {
id: 'demand-seed-001',
timestamp: new Date().toISOString(),
eventType: 'seed_acquisition',
fromLocation: createLocation('seed_bank', 'Local Organic Seeds'),
toLocation: createLocation('greenhouse', 'Grower Greenhouse'),
distanceKm: 10,
durationMinutes: 20,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'seed-supplier',
receiverId: 'grower-integration-001',
status: 'verified',
seedBatchId,
sourceType: 'purchase',
species: 'Lactuca sativa',
variety: 'Butterhead',
quantity: 100,
quantityUnit: 'seeds',
generation: 1,
certifications: ['organic'],
};
chain.recordEvent(seedEvent);
const plantingEvent: PlantingEvent = {
id: 'demand-planting-001',
timestamp: new Date(Date.now() + 1000).toISOString(),
eventType: 'planting',
fromLocation: createLocation('greenhouse', 'Grower Greenhouse'),
toLocation: createLocation('greenhouse', 'Grower Greenhouse'),
distanceKm: 0,
durationMinutes: 30,
transportMethod: 'walking',
carbonFootprintKg: 0,
senderId: 'grower-integration-001',
receiverId: 'grower-integration-001',
status: 'verified',
seedBatchId,
plantIds,
plantingMethod: 'indoor_start',
quantityPlanted: plantIds.length,
growingEnvironment: 'greenhouse',
expectedHarvestDate: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString(),
};
chain.recordEvent(plantingEvent);
// Step 5: Harvest
const harvestBatchId = 'demand-harvest-batch-001';
const harvestEvent: HarvestEvent = {
id: 'demand-harvest-001',
timestamp: new Date(Date.now() + 45 * 24 * 60 * 60 * 1000).toISOString(),
eventType: 'harvest',
fromLocation: createLocation('greenhouse', 'Grower Greenhouse'),
toLocation: createLocation('hub', 'Distribution Hub'),
distanceKm: 5,
durationMinutes: 15,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'grower-integration-001',
receiverId: 'hub-distribution',
status: 'verified',
plantIds,
harvestBatchId,
harvestType: 'full',
produceType: 'Butterhead Lettuce',
grossWeight: 2,
netWeight: 1.8,
weightUnit: 'kg',
qualityGrade: 'A',
packagingType: 'sustainable_boxes',
temperatureRequired: { min: 2, max: 8, optimal: 4, unit: 'celsius' },
shelfLifeHours: 168,
seedsSaved: false,
};
chain.recordEvent(harvestEvent);
// Step 6: Distribution
const distributionEvent: DistributionEvent = {
id: 'demand-distribution-001',
timestamp: new Date(Date.now() + 46 * 24 * 60 * 60 * 1000).toISOString(),
eventType: 'distribution',
fromLocation: createLocation('hub', 'Distribution Hub'),
toLocation: createLocation('market', 'Local Delivery Zone'),
distanceKm: 10,
durationMinutes: 20,
transportMethod: 'electric_truck',
carbonFootprintKg: 0,
senderId: 'hub-distribution',
receiverId: 'delivery-service',
status: 'verified',
batchIds: [harvestBatchId],
destinationType: 'consumer',
customerType: 'individual',
deliveryWindow: {
start: new Date(Date.now() + 46 * 24 * 60 * 60 * 1000).toISOString(),
end: new Date(Date.now() + 46.5 * 24 * 60 * 60 * 1000).toISOString(),
},
deliveryAttempts: 1,
handoffVerified: true,
};
chain.recordEvent(distributionEvent);
// Step 7: Consumer Delivery
const deliveryEvent: ConsumerDeliveryEvent = {
id: 'demand-delivery-001',
timestamp: new Date(Date.now() + 46.25 * 24 * 60 * 60 * 1000).toISOString(),
eventType: 'consumer_delivery',
fromLocation: createLocation('market', 'Local Delivery Zone'),
toLocation: createLocation('consumer', 'Consumer Home'),
distanceKm: 3,
durationMinutes: 10,
transportMethod: 'electric_vehicle',
carbonFootprintKg: 0,
senderId: 'delivery-service',
receiverId: consumerId,
status: 'verified',
orderId: 'order-integration-001',
batchIds: [harvestBatchId],
deliveryMethod: 'home_delivery',
finalMileMethod: 'electric_vehicle',
packagingReturned: true,
feedbackReceived: true,
feedbackRating: 5,
feedbackNotes: 'Fresh and delicious!',
};
chain.recordEvent(deliveryEvent);
// Verify complete chain
expect(chain.isChainValid()).toBe(true);
expect(chain.chain.length).toBe(7); // Genesis + 6 events
// Verify plant journey
const journey = chain.getPlantJourney(plantIds[0]);
expect(journey).not.toBeNull();
expect(journey!.events.length).toBe(3); // planting, harvest, and possibly more
// Verify environmental impact
const impact = chain.getEnvironmentalImpact('grower-integration-001');
expect(impact.totalFoodMiles).toBeLessThan(50); // Local delivery
expect(impact.comparisonToConventional.percentageReduction).toBeGreaterThan(0);
});
});
describe('Supply Registration Affects Recommendations', () => {
it('should adjust recommendations based on existing supply', () => {
// Register high demand
const pref = createConsumerPreference('consumer-001');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false },
];
pref.householdSize = 1;
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
// Get recommendations without supply
const recsWithoutSupply = forecaster.generatePlantingRecommendations(
'grower-001', 40.7, -74.0, 50, 100, 'spring'
);
// Register significant supply
forecaster.registerSupply({
id: 'supply-001',
growerId: 'other-grower',
timestamp: new Date().toISOString(),
produceType: 'lettuce',
committedQuantityKg: 80,
availableFrom: new Date().toISOString(),
availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
pricePerKg: 5,
currency: 'USD',
minimumOrderKg: 1,
certifications: ['organic'],
freshnessGuaranteeHours: 48,
deliveryRadiusKm: 50,
deliveryMethods: ['grower_delivery'],
status: 'available',
remainingKg: 80,
});
// Generate new demand signal (with supply factored in)
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
// Get recommendations with supply
const recsWithSupply = forecaster.generatePlantingRecommendations(
'grower-001', 40.7, -74.0, 50, 100, 'spring'
);
// Recommendations should be different (less space recommended for lettuce)
if (recsWithoutSupply.length > 0 && recsWithSupply.length > 0) {
const lettuceWithout = recsWithoutSupply.find(r => r.produceType === 'lettuce');
const lettuceWith = recsWithSupply.find(r => r.produceType === 'lettuce');
if (lettuceWithout && lettuceWith) {
expect(lettuceWith.recommendedQuantity).toBeLessThanOrEqual(lettuceWithout.recommendedQuantity);
}
}
});
});
describe('Regional Demand Matching', () => {
it('should match growers with regional demand', () => {
// Create consumers in different regions
const nycConsumer = createConsumerPreference('nyc-consumer');
nycConsumer.location = { latitude: 40.7128, longitude: -74.006, maxDeliveryRadiusKm: 10 };
forecaster.registerPreference(nycConsumer);
const laConsumer = createConsumerPreference('la-consumer');
laConsumer.location = { latitude: 34.0522, longitude: -118.2437, maxDeliveryRadiusKm: 10 };
forecaster.registerPreference(laConsumer);
// Generate signals for each region
const nycSignal = forecaster.generateDemandSignal(40.7128, -74.006, 20, 'NYC', 'spring');
const laSignal = forecaster.generateDemandSignal(34.0522, -118.2437, 20, 'LA', 'spring');
expect(nycSignal.totalConsumers).toBe(1);
expect(laSignal.totalConsumers).toBe(1);
// NYC grower should only see NYC demand
const nycGrowerRecs = forecaster.generatePlantingRecommendations(
'nyc-grower', 40.72, -74.01, 25, 100, 'spring'
);
// LA grower should only see LA demand
const laGrowerRecs = forecaster.generatePlantingRecommendations(
'la-grower', 34.06, -118.25, 25, 100, 'spring'
);
// Both should have recommendations from their respective regions
expect(nycGrowerRecs.length + laGrowerRecs.length).toBeGreaterThanOrEqual(0);
});
});
describe('Seasonal Demand Flow', () => {
it('should respect seasonal availability in recommendations', () => {
const pref = createConsumerPreference('seasonal-consumer');
pref.preferredItems = [
{ produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false },
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false },
];
forecaster.registerPreference(pref);
// Winter recommendations
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'winter');
const winterRecs = forecaster.generatePlantingRecommendations(
'grower-001', 40.7, -74.0, 50, 100, 'winter'
);
// Tomato should not be recommended in winter (not in season)
const tomatoWinter = winterRecs.find(r => r.produceType === 'tomato');
expect(tomatoWinter).toBeUndefined();
// Summer recommendations
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer');
const summerRecs = forecaster.generatePlantingRecommendations(
'grower-001', 40.7, -74.0, 50, 100, 'summer'
);
// Tomato should be recommended in summer
const tomatoSummer = summerRecs.find(r => r.produceType === 'tomato');
expect(tomatoSummer).toBeDefined();
});
});
});
// Helper functions
function createLocation(type: string, name: string): TransportLocation {
return {
latitude: 40.7 + Math.random() * 0.1,
longitude: -74.0 + Math.random() * 0.1,
locationType: type as any,
facilityName: name,
};
}
function createConsumerPreference(consumerId: string): ConsumerPreference {
return {
consumerId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
location: {
latitude: 40.7128,
longitude: -74.006,
maxDeliveryRadiusKm: 25,
},
dietaryType: ['omnivore'],
allergies: [],
dislikes: [],
preferredCategories: ['leafy_greens'],
preferredItems: [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false },
],
certificationPreferences: ['organic'],
freshnessImportance: 4,
priceImportance: 3,
sustainabilityImportance: 4,
deliveryPreferences: {
method: ['home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday'],
},
householdSize: 2,
weeklyBudget: 100,
currency: 'USD',
};
}