- 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
407 lines
14 KiB
TypeScript
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',
|
|
};
|
|
}
|