localgreenchain/__tests__/lib/demand/recommendations.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

451 lines
16 KiB
TypeScript

/**
* Recommendation Logic Tests
* Tests for planting recommendation generation
*/
import {
DemandForecaster,
} from '../../../lib/demand/forecaster';
import {
ConsumerPreference,
PlantingRecommendation,
RiskFactor,
} from '../../../lib/demand/types';
describe('Planting Recommendations', () => {
let forecaster: DemandForecaster;
beforeEach(() => {
forecaster = new DemandForecaster();
});
describe('Recommendation Generation', () => {
it('should generate recommendations based on demand signals', () => {
// Create demand
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
pref.householdSize = 1;
forecaster.registerPreference(pref);
// Generate demand signal
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test Region', 'spring');
// Generate recommendations
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1',
40.7,
-74.0,
50,
100, // 100 sqm available
'spring'
);
expect(recommendations.length).toBeGreaterThan(0);
});
it('should include recommendation details', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
const rec = recommendations[0];
expect(rec.id).toBeDefined();
expect(rec.growerId).toBe('grower-1');
expect(rec.produceType).toBeDefined();
expect(rec.recommendedQuantity).toBeGreaterThan(0);
expect(rec.expectedYieldKg).toBeGreaterThan(0);
});
it('should not exceed available space', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1000, seasonalOnly: false },
];
pref.householdSize = 1;
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const availableSpace = 50; // Only 50 sqm
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, availableSpace, 'spring'
);
const totalSpace = recommendations.reduce((sum, r) => sum + r.recommendedQuantity, 0);
expect(totalSpace).toBeLessThanOrEqual(availableSpace);
});
it('should prioritize high-demand produce', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 50, seasonalOnly: false },
{ produceType: 'spinach', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 5, seasonalOnly: false },
];
pref.householdSize = 1;
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 30, 'spring' // Limited space
);
// Should prioritize lettuce (high demand) over spinach
if (recommendations.length > 0) {
expect(recommendations[0].produceType).toBe('lettuce');
}
});
});
describe('Timing Calculations', () => {
it('should include planting and harvest dates', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
const rec = recommendations[0];
expect(rec.plantByDate).toBeDefined();
expect(rec.expectedHarvestStart).toBeDefined();
expect(rec.expectedHarvestEnd).toBeDefined();
expect(rec.growingDays).toBeGreaterThan(0);
});
it('should calculate harvest date based on growing days', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
const rec = recommendations[0];
const plantDate = new Date(rec.plantByDate);
const harvestStart = new Date(rec.expectedHarvestStart);
const daysDiff = (harvestStart.getTime() - plantDate.getTime()) / (1000 * 60 * 60 * 24);
expect(daysDiff).toBeCloseTo(rec.growingDays, 0);
});
});
describe('Financial Projections', () => {
it('should calculate projected revenue', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
pref.weeklyBudget = 100;
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
const rec = recommendations[0];
expect(rec.projectedPricePerKg).toBeGreaterThan(0);
expect(rec.projectedRevenue).toBeGreaterThan(0);
expect(rec.projectedRevenue).toBeCloseTo(
rec.expectedYieldKg * rec.projectedPricePerKg,
-1 // Allow some rounding
);
});
it('should include market confidence', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
const rec = recommendations[0];
expect(rec.marketConfidence).toBeGreaterThanOrEqual(0);
expect(rec.marketConfidence).toBeLessThanOrEqual(100);
});
});
describe('Risk Assessment', () => {
it('should include risk factors', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'summer'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
expect(rec.riskFactors).toBeDefined();
expect(Array.isArray(rec.riskFactors)).toBe(true);
}
});
it('should calculate overall risk level', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
expect(['low', 'medium', 'high']).toContain(rec.overallRisk);
}
});
it('should include risk mitigation suggestions', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'summer');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 10, 'summer' // Small space for large demand
);
if (recommendations.length > 0) {
const rec = recommendations[0];
rec.riskFactors.forEach(risk => {
if (risk.mitigationSuggestion) {
expect(typeof risk.mitigationSuggestion).toBe('string');
}
});
}
});
});
describe('Seasonal Filtering', () => {
it('should only recommend in-season produce', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
{ produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
// Winter - tomato is not in season
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'winter');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'winter'
);
// Tomato should not be recommended in winter
const tomatoRec = recommendations.find(r => r.produceType === 'tomato');
expect(tomatoRec).toBeUndefined();
});
});
describe('Demand Signal Matching', () => {
it('should filter signals by delivery radius', () => {
// Consumer far from grower
const pref = createConsumerPreference('consumer-1');
pref.location = { latitude: 35.0, longitude: -80.0, maxDeliveryRadiusKm: 10 };
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 100, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(35.0, -80.0, 10, 'Distant Region', 'spring');
// Grower is far from demand signal
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1',
40.7, // New York area
-74.0,
25, // Only 25km delivery radius
100,
'spring'
);
// Should have no recommendations because demand is too far
expect(recommendations.length).toBe(0);
});
it('should match signals from multiple regions', () => {
// Multiple consumers in nearby regions
const pref1 = createConsumerPreference('consumer-1');
pref1.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref1);
const pref2 = createConsumerPreference('consumer-2');
pref2.location = { latitude: 40.72, longitude: -74.01, maxDeliveryRadiusKm: 25 };
pref2.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref2);
forecaster.generateDemandSignal(40.7, -74.0, 20, 'Region 1', 'spring');
forecaster.generateDemandSignal(40.75, -74.05, 20, 'Region 2', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
// Should aggregate demand from both regions
expect(recommendations.length).toBeGreaterThan(0);
});
});
describe('Yield Calculations', () => {
it('should calculate expected yield based on area', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
// Lettuce yields about 4 kg/sqm according to SEASONAL_DATA
expect(rec.expectedYieldKg).toBeGreaterThan(0);
expect(rec.yieldConfidence).toBeGreaterThan(0);
}
});
it('should include yield confidence', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
expect(rec.yieldConfidence).toBeGreaterThanOrEqual(0);
expect(rec.yieldConfidence).toBeLessThanOrEqual(100);
}
});
});
describe('Explanation', () => {
it('should include human-readable explanation', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
expect(rec.explanation).toBeDefined();
expect(rec.explanation.length).toBeGreaterThan(20);
expect(rec.explanation).toContain('demand signal');
}
});
it('should include demand signal IDs', () => {
const pref = createConsumerPreference('consumer-1');
pref.preferredItems = [
{ produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false },
];
forecaster.registerPreference(pref);
forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring');
const recommendations = forecaster.generatePlantingRecommendations(
'grower-1', 40.7, -74.0, 50, 100, 'spring'
);
if (recommendations.length > 0) {
const rec = recommendations[0];
expect(rec.demandSignalIds).toBeDefined();
expect(rec.demandSignalIds.length).toBeGreaterThan(0);
}
});
});
});
// Helper function
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: [],
certificationPreferences: ['organic'],
freshnessImportance: 4,
priceImportance: 3,
sustainabilityImportance: 4,
deliveryPreferences: {
method: ['home_delivery'],
frequency: 'weekly',
preferredDays: ['saturday'],
},
householdSize: 2,
weeklyBudget: 100,
currency: 'USD',
};
}