/** * DemandForecaster Tests * Tests for the demand forecasting system */ import { DemandForecaster, getDemandForecaster, } from '../../../lib/demand/forecaster'; import { ConsumerPreference, SupplyCommitment, DemandSignal, PlantingRecommendation, } from '../../../lib/demand/types'; describe('DemandForecaster', () => { let forecaster: DemandForecaster; beforeEach(() => { forecaster = new DemandForecaster(); }); describe('Initialization', () => { it('should create empty forecaster', () => { const json = forecaster.toJSON() as any; expect(json.preferences).toEqual([]); expect(json.supplyCommitments).toEqual([]); expect(json.demandSignals).toEqual([]); }); }); describe('Consumer Preferences', () => { it('should register consumer preferences', () => { const preference = createConsumerPreference('consumer-1'); forecaster.registerPreference(preference); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(1); }); it('should update existing preference', () => { const preference1 = createConsumerPreference('consumer-1'); preference1.householdSize = 2; forecaster.registerPreference(preference1); const preference2 = createConsumerPreference('consumer-1'); preference2.householdSize = 4; forecaster.registerPreference(preference2); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(1); expect(json.preferences[0][1].householdSize).toBe(4); }); it('should register multiple consumers', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); forecaster.registerPreference(createConsumerPreference('consumer-2')); forecaster.registerPreference(createConsumerPreference('consumer-3')); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(3); }); }); describe('Supply Commitments', () => { it('should register supply commitment', () => { const commitment = createSupplyCommitment('grower-1', 'lettuce', 50); forecaster.registerSupply(commitment); const json = forecaster.toJSON() as any; expect(json.supplyCommitments.length).toBe(1); }); it('should track multiple supply commitments', () => { forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50)); forecaster.registerSupply(createSupplyCommitment('grower-2', 'tomato', 100)); const json = forecaster.toJSON() as any; expect(json.supplyCommitments.length).toBe(2); }); }); describe('Demand Signal Generation', () => { it('should generate demand signal for region', () => { // Add consumers forecaster.registerPreference(createConsumerPreference('consumer-1', 40.7, -74.0)); forecaster.registerPreference(createConsumerPreference('consumer-2', 40.71, -74.01)); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'New York Metro', 'summer' ); expect(signal.id).toBeDefined(); expect(signal.region.name).toBe('New York Metro'); expect(signal.totalConsumers).toBe(2); }); it('should filter consumers by region radius', () => { // Consumer inside radius forecaster.registerPreference(createConsumerPreference('inside', 40.7, -74.0)); // Consumer outside radius (far away) forecaster.registerPreference(createConsumerPreference('outside', 35.0, -80.0)); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 10, 'Small Region', 'summer' ); expect(signal.totalConsumers).toBe(1); }); it('should aggregate demand items', () => { const pref1 = createConsumerPreference('consumer-1'); pref1.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, { produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false }, ]; forecaster.registerPreference(pref1); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test Region', 'summer' ); expect(signal.demandItems.length).toBeGreaterThan(0); }); it('should calculate supply status', () => { // Add consumer demand forecaster.registerPreference(createConsumerPreference('consumer-1')); // No supply - should show shortage let signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); expect(['shortage', 'critical', 'balanced']).toContain(signal.supplyStatus); }); it('should include seasonal period', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'winter' ); expect(signal.seasonalPeriod).toBe('winter'); }); it('should calculate weekly demand correctly', () => { const pref = createConsumerPreference('consumer-1'); pref.householdSize = 4; pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, ]; forecaster.registerPreference(pref); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); // Weekly demand = weeklyQuantity * householdSize = 2 * 4 = 8 expect(lettuceItem?.weeklyDemandKg).toBe(8); }); }); describe('Demand Forecasting', () => { it('should generate forecast for region', () => { // Setup some historical data forecaster.registerPreference(createConsumerPreference('consumer-1')); forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test Region', 'summer'); const forecast = forecaster.generateForecast('Test Region', 12); expect(forecast.id).toBeDefined(); expect(forecast.region).toBe('Test Region'); expect(forecast.forecasts).toBeDefined(); }); it('should include confidence intervals', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer'); const forecast = forecaster.generateForecast('Test', 12); forecast.forecasts.forEach(f => { expect(f.confidenceInterval.low).toBeLessThanOrEqual(f.predictedDemandKg); expect(f.confidenceInterval.high).toBeGreaterThanOrEqual(f.predictedDemandKg); }); }); it('should identify trends', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); forecaster.generateDemandSignal(40.7, -74.0, 100, 'Test', 'summer'); const forecast = forecaster.generateForecast('Test', 12); forecast.forecasts.forEach(f => { expect(['increasing', 'stable', 'decreasing']).toContain(f.trend); }); }); }); describe('Singleton', () => { it('should return same instance from getDemandForecaster', () => { const forecaster1 = getDemandForecaster(); const forecaster2 = getDemandForecaster(); expect(forecaster1).toBe(forecaster2); }); }); describe('Serialization', () => { it('should export to JSON', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); const json = forecaster.toJSON(); expect(json).toHaveProperty('preferences'); expect(json).toHaveProperty('supplyCommitments'); expect(json).toHaveProperty('demandSignals'); }); it('should import from JSON', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); forecaster.registerSupply(createSupplyCommitment('grower-1', 'lettuce', 50)); const json = forecaster.toJSON(); const restored = DemandForecaster.fromJSON(json); const restoredJson = restored.toJSON() as any; expect(restoredJson.preferences.length).toBe(1); expect(restoredJson.supplyCommitments.length).toBe(1); }); }); }); // Helper functions function createConsumerPreference( consumerId: string, lat: number = 40.7128, lon: number = -74.006 ): ConsumerPreference { return { consumerId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), location: { latitude: lat, longitude: lon, maxDeliveryRadiusKm: 25, city: 'New York', }, dietaryType: ['omnivore'], allergies: [], dislikes: [], preferredCategories: ['leafy_greens', 'nightshades'], preferredItems: [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, { produceType: 'tomato', category: 'nightshades', priority: 'preferred', weeklyQuantity: 2, seasonalOnly: false }, ], certificationPreferences: ['organic', 'local'], freshnessImportance: 5, priceImportance: 3, sustainabilityImportance: 4, deliveryPreferences: { method: ['home_delivery'], frequency: 'weekly', preferredDays: ['saturday'], }, householdSize: 3, weeklyBudget: 100, currency: 'USD', }; } function createSupplyCommitment( growerId: string, produceType: string, quantity: number ): SupplyCommitment { return { id: `supply-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, growerId, timestamp: new Date().toISOString(), produceType, committedQuantityKg: quantity, 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', 'customer_pickup'], status: 'available', remainingKg: quantity, }; }