/** * Demand Aggregation Tests * Tests for demand aggregation logic */ import { DemandForecaster, } from '../../../lib/demand/forecaster'; import { ConsumerPreference, DemandSignal, DemandItem, } from '../../../lib/demand/types'; describe('Demand Aggregation', () => { let forecaster: DemandForecaster; beforeEach(() => { forecaster = new DemandForecaster(); }); describe('Consumer Aggregation', () => { it('should aggregate demand from multiple consumers', () => { // Add 5 consumers all wanting lettuce for (let i = 0; i < 5; i++) { const pref = createConsumerPreference(`consumer-${i}`); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, ]; pref.householdSize = 2; forecaster.registerPreference(pref); } const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test Region', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.consumerCount).toBe(5); // Total demand = 5 consumers * 1kg/week * 2 household = 10 kg expect(lettuceItem?.weeklyDemandKg).toBe(10); }); it('should aggregate different produce types separately', () => { const pref1 = createConsumerPreference('consumer-1'); pref1.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, ]; forecaster.registerPreference(pref1); const pref2 = createConsumerPreference('consumer-2'); pref2.preferredItems = [ { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, ]; forecaster.registerPreference(pref2); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); const tomatoItem = signal.demandItems.find(i => i.produceType === 'tomato'); expect(lettuceItem).toBeDefined(); expect(tomatoItem).toBeDefined(); expect(lettuceItem?.consumerCount).toBe(1); expect(tomatoItem?.consumerCount).toBe(1); }); it('should calculate monthly demand from weekly', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, ]; pref.householdSize = 1; forecaster.registerPreference(pref); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.monthlyDemandKg).toBe(lettuceItem!.weeklyDemandKg * 4); }); }); describe('Priority Aggregation', () => { it('should calculate aggregate priority from consumer priorities', () => { const pref1 = createConsumerPreference('consumer-1'); pref1.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, ]; forecaster.registerPreference(pref1); const pref2 = createConsumerPreference('consumer-2'); pref2.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 1, seasonalOnly: false }, ]; forecaster.registerPreference(pref2); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); // must_have=10, occasional=2, average = 6 expect(lettuceItem?.aggregatePriority).toBe(6); }); it('should determine urgency from aggregate priority', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, 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'); expect(lettuceItem?.urgency).toBe('immediate'); }); it('should sort demand items by priority', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'occasional', weeklyQuantity: 1, seasonalOnly: false }, { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, ]; forecaster.registerPreference(pref); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); // Tomato (must_have) should come before lettuce (occasional) expect(signal.demandItems[0].produceType).toBe('tomato'); }); }); describe('Certification Aggregation', () => { it('should aggregate certification preferences', () => { const pref1 = createConsumerPreference('consumer-1'); pref1.certificationPreferences = ['organic']; forecaster.registerPreference(pref1); const pref2 = createConsumerPreference('consumer-2'); pref2.certificationPreferences = ['organic', 'local']; forecaster.registerPreference(pref2); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); // Check that certification preferences are aggregated signal.demandItems.forEach(item => { expect(item.preferredCertifications).toContain('organic'); }); }); }); describe('Price Aggregation', () => { it('should calculate average willing price', () => { const pref1 = createConsumerPreference('consumer-1'); pref1.weeklyBudget = 100; pref1.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 2, seasonalOnly: false }, ]; pref1.householdSize = 1; forecaster.registerPreference(pref1); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.averageWillingPrice).toBeGreaterThan(0); expect(lettuceItem?.priceUnit).toBe('per_kg'); }); }); describe('Supply Gap Calculation', () => { it('should calculate supply gap when no supply', () => { 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); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.gapKg).toBe(10); expect(lettuceItem?.matchedSupply).toBe(0); }); it('should reduce gap when supply is available', () => { 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); // Add partial supply forecaster.registerSupply({ id: 'supply-1', growerId: 'grower-1', timestamp: new Date().toISOString(), produceType: 'lettuce', committedQuantityKg: 5, availableFrom: new Date().toISOString(), availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), pricePerKg: 5, currency: 'USD', minimumOrderKg: 1, certifications: [], freshnessGuaranteeHours: 48, deliveryRadiusKm: 50, deliveryMethods: ['grower_delivery'], status: 'available', remainingKg: 5, }); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.matchedSupply).toBe(5); expect(lettuceItem?.gapKg).toBe(5); }); it('should show surplus when supply exceeds demand', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, ]; pref.householdSize = 1; forecaster.registerPreference(pref); // Add excess supply forecaster.registerSupply({ id: 'supply-1', growerId: 'grower-1', timestamp: new Date().toISOString(), produceType: 'lettuce', committedQuantityKg: 20, availableFrom: new Date().toISOString(), availableUntil: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), pricePerKg: 5, currency: 'USD', minimumOrderKg: 1, certifications: [], freshnessGuaranteeHours: 48, deliveryRadiusKm: 50, deliveryMethods: ['grower_delivery'], status: 'available', remainingKg: 20, }); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); expect(signal.supplyStatus).toBe('surplus'); }); }); describe('Seasonal Availability', () => { it('should mark items as in-season correctly', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, { produceType: 'tomato', category: 'nightshades', priority: 'must_have', weeklyQuantity: 1, seasonalOnly: false }, ]; forecaster.registerPreference(pref); // Summer - tomato in season, lettuce depends const summerSignal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); const tomatoItem = summerSignal.demandItems.find(i => i.produceType === 'tomato'); expect(tomatoItem?.inSeason).toBe(true); }); it('should include seasonal availability info', () => { const pref = createConsumerPreference('consumer-1'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 1, 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'); expect(lettuceItem?.seasonalAvailability).toBeDefined(); expect(lettuceItem?.seasonalAvailability.spring).toBeDefined(); expect(lettuceItem?.seasonalAvailability.summer).toBeDefined(); expect(lettuceItem?.seasonalAvailability.fall).toBeDefined(); expect(lettuceItem?.seasonalAvailability.winter).toBeDefined(); }); }); describe('Confidence Level', () => { it('should increase confidence with more consumers', () => { forecaster.registerPreference(createConsumerPreference('consumer-1')); const signal1 = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); forecaster.registerPreference(createConsumerPreference('consumer-2')); forecaster.registerPreference(createConsumerPreference('consumer-3')); const signal2 = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); expect(signal2.confidenceLevel).toBeGreaterThan(signal1.confidenceLevel); }); it('should cap confidence at 100', () => { // Add many consumers for (let i = 0; i < 100; i++) { forecaster.registerPreference(createConsumerPreference(`consumer-${i}`)); } const signal = forecaster.generateDemandSignal( 40.7, -74.0, 100, 'Test', 'summer' ); expect(signal.confidenceLevel).toBeLessThanOrEqual(100); }); }); }); // 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', }; }