/** * Demand API Tests * Tests for demand-related API endpoints */ import { DemandForecaster, getDemandForecaster } from '../../lib/demand/forecaster'; import { ConsumerPreference, SupplyCommitment, DemandSignal, PlantingRecommendation, } from '../../lib/demand/types'; describe('Demand API', () => { let forecaster: DemandForecaster; beforeEach(() => { forecaster = new DemandForecaster(); }); describe('POST /api/demand/preferences', () => { it('should register consumer preferences', () => { const preference = createConsumerPreference('api-consumer-001'); forecaster.registerPreference(preference); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(1); expect(json.preferences[0][0]).toBe('api-consumer-001'); }); it('should update existing preferences', () => { const pref1 = createConsumerPreference('api-consumer-001'); pref1.householdSize = 2; forecaster.registerPreference(pref1); const pref2 = createConsumerPreference('api-consumer-001'); pref2.householdSize = 5; forecaster.registerPreference(pref2); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(1); expect(json.preferences[0][1].householdSize).toBe(5); }); it('should validate required fields', () => { const preference = createConsumerPreference('api-consumer-001'); // All required fields present forecaster.registerPreference(preference); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(1); }); }); describe('GET /api/demand/preferences', () => { it('should return preferences for consumer', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); forecaster.registerPreference(createConsumerPreference('consumer-002')); const json = forecaster.toJSON() as any; expect(json.preferences.length).toBe(2); }); }); describe('POST /api/demand/signal', () => { it('should generate demand signal for region', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'Test Region', 'summer' ); expect(signal.id).toBeDefined(); expect(signal.region.name).toBe('Test Region'); expect(signal.demandItems).toBeDefined(); }); it('should include supply status', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'Test Region', 'summer' ); expect(['surplus', 'balanced', 'shortage', 'critical']).toContain(signal.supplyStatus); }); it('should calculate confidence level', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'Test Region', 'summer' ); expect(signal.confidenceLevel).toBeGreaterThanOrEqual(0); expect(signal.confidenceLevel).toBeLessThanOrEqual(100); }); }); describe('GET /api/demand/recommendations', () => { it('should return planting recommendations', () => { const pref = createConsumerPreference('consumer-001'); 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-001', 40.7, -74.0, 50, 100, 'spring' ); expect(recommendations.length).toBeGreaterThanOrEqual(0); }); it('should include risk assessment', () => { const pref = createConsumerPreference('consumer-001'); 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-001', 40.7, -74.0, 50, 100, 'spring' ); if (recommendations.length > 0) { expect(['low', 'medium', 'high']).toContain(recommendations[0].overallRisk); } }); }); describe('GET /api/demand/forecast', () => { it('should return demand forecast', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); forecaster.generateDemandSignal(40.7, -74.0, 50, '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 trend analysis', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test Region', 'summer'); const forecast = forecaster.generateForecast('Test Region', 12); forecast.forecasts.forEach(f => { expect(['increasing', 'stable', 'decreasing']).toContain(f.trend); }); }); }); describe('POST /api/demand/supply', () => { it('should register supply commitment', () => { const commitment = createSupplyCommitment('grower-001', 'lettuce', 50); forecaster.registerSupply(commitment); const json = forecaster.toJSON() as any; expect(json.supplyCommitments.length).toBe(1); }); it('should affect supply gap calculation', () => { const pref = createConsumerPreference('consumer-001'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 10, seasonalOnly: false }, ]; pref.householdSize = 1; forecaster.registerPreference(pref); // Generate signal without supply const signal1 = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); const gap1 = signal1.demandItems.find(i => i.produceType === 'lettuce')?.gapKg || 0; // Add supply forecaster.registerSupply(createSupplyCommitment('grower-001', 'lettuce', 5)); // Generate signal with supply const signal2 = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); const gap2 = signal2.demandItems.find(i => i.produceType === 'lettuce')?.gapKg || 0; expect(gap2).toBeLessThan(gap1); }); }); describe('POST /api/demand/match', () => { it('should create market match', () => { // This tests the matching logic between supply and demand const pref = createConsumerPreference('consumer-001'); pref.preferredItems = [ { produceType: 'lettuce', category: 'leafy_greens', priority: 'must_have', weeklyQuantity: 5, seasonalOnly: false }, ]; forecaster.registerPreference(pref); forecaster.registerSupply(createSupplyCommitment('grower-001', 'lettuce', 10)); const signal = forecaster.generateDemandSignal(40.7, -74.0, 50, 'Test', 'spring'); const lettuceItem = signal.demandItems.find(i => i.produceType === 'lettuce'); expect(lettuceItem?.matchedSupply).toBeGreaterThan(0); }); }); describe('Error Handling', () => { it('should handle empty region gracefully', () => { // No consumers registered const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'Empty Region', 'summer' ); expect(signal.totalConsumers).toBe(0); expect(signal.demandItems.length).toBe(0); }); it('should handle invalid coordinates gracefully', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); // Very distant coordinates const signal = forecaster.generateDemandSignal( -90, 0, 1, 'Antarctica', 'winter' ); // Should still return valid signal structure expect(signal.id).toBeDefined(); }); }); describe('Response Format', () => { it('should return consistent signal format', () => { forecaster.registerPreference(createConsumerPreference('consumer-001')); const signal = forecaster.generateDemandSignal( 40.7, -74.0, 50, 'Test', 'summer' ); // Check all required fields expect(signal.id).toBeDefined(); expect(signal.timestamp).toBeDefined(); expect(signal.region).toBeDefined(); expect(signal.periodStart).toBeDefined(); expect(signal.periodEnd).toBeDefined(); expect(signal.seasonalPeriod).toBeDefined(); expect(signal.demandItems).toBeDefined(); expect(signal.totalConsumers).toBeDefined(); expect(signal.totalWeeklyDemandKg).toBeDefined(); expect(signal.confidenceLevel).toBeDefined(); expect(signal.currentSupplyKg).toBeDefined(); expect(signal.supplyGapKg).toBeDefined(); expect(signal.supplyStatus).toBeDefined(); }); }); }); // Helper functions 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: 'preferred', weeklyQuantity: 1, seasonalOnly: false }, ], certificationPreferences: ['organic'], freshnessImportance: 4, priceImportance: 3, sustainabilityImportance: 4, deliveryPreferences: { method: ['home_delivery'], frequency: 'weekly', preferredDays: ['saturday'], }, householdSize: 2, 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'], status: 'available', remainingKg: quantity, }; }