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

319 lines
10 KiB
TypeScript

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