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

374 lines
13 KiB
TypeScript

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