/** * 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', }; }