/** * Demand & Market Matching Database Service * CRUD operations for consumer preferences, demand signals, and market matching */ import prisma from './prisma'; import type { ConsumerPreference, DemandSignal, SupplyCommitment, MarketMatch, SeasonalPlan, DemandForecast, PlantingRecommendation, SupplyStatus, CommitmentStatus, MatchStatus, PlanStatus, Prisma, } from '@prisma/client'; import type { PaginationOptions, PaginatedResult, LocationFilter, DateRangeFilter } from './types'; import { createPaginatedResult, calculateDistanceKm } from './types'; // ============================================ // CONSUMER PREFERENCE OPERATIONS // ============================================ // Create or update consumer preference export async function upsertConsumerPreference(data: { consumerId: string; latitude: number; longitude: number; maxDeliveryRadiusKm?: number; city?: string; region?: string; dietaryType?: string[]; allergies?: string[]; dislikes?: string[]; preferredCategories?: string[]; preferredItems?: Record[]; certificationPreferences?: string[]; freshnessImportance?: number; priceImportance?: number; sustainabilityImportance?: number; deliveryPreferences?: Record; householdSize?: number; weeklyBudget?: number; currency?: string; }): Promise { return prisma.consumerPreference.upsert({ where: { consumerId: data.consumerId }, update: { ...data, updatedAt: new Date(), }, create: { ...data, maxDeliveryRadiusKm: data.maxDeliveryRadiusKm || 50, dietaryType: data.dietaryType || [], allergies: data.allergies || [], dislikes: data.dislikes || [], preferredCategories: data.preferredCategories || [], certificationPreferences: data.certificationPreferences || [], freshnessImportance: data.freshnessImportance || 3, priceImportance: data.priceImportance || 3, sustainabilityImportance: data.sustainabilityImportance || 3, householdSize: data.householdSize || 1, currency: data.currency || 'USD', }, }); } // Get consumer preference by consumer ID export async function getConsumerPreference(consumerId: string): Promise { return prisma.consumerPreference.findUnique({ where: { consumerId }, }); } // Get consumer preferences near location export async function getNearbyConsumerPreferences( location: LocationFilter ): Promise { const preferences = await prisma.consumerPreference.findMany(); return preferences.filter(pref => { const distance = calculateDistanceKm( location.latitude, location.longitude, pref.latitude, pref.longitude ); return distance <= location.radiusKm; }); } // ============================================ // DEMAND SIGNAL OPERATIONS // ============================================ // Create a demand signal export async function createDemandSignal(data: { centerLat: number; centerLon: number; radiusKm: number; regionName: string; periodStart: Date; periodEnd: Date; seasonalPeriod: string; demandItems: Record[]; totalConsumers: number; totalWeeklyDemandKg: number; confidenceLevel: number; currentSupplyKg?: number; supplyGapKg?: number; supplyStatus?: SupplyStatus; }): Promise { return prisma.demandSignal.create({ data: { ...data, supplyStatus: data.supplyStatus || 'BALANCED', }, }); } // Get demand signal by ID export async function getDemandSignalById(id: string): Promise { return prisma.demandSignal.findUnique({ where: { id }, }); } // Get demand signals with pagination export async function getDemandSignals( options: PaginationOptions = {}, filters?: { regionName?: string; supplyStatus?: SupplyStatus; dateRange?: DateRangeFilter; } ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.DemandSignalWhereInput = {}; if (filters?.regionName) where.regionName = { contains: filters.regionName, mode: 'insensitive' }; if (filters?.supplyStatus) where.supplyStatus = filters.supplyStatus; if (filters?.dateRange) { where.periodStart = { gte: filters.dateRange.start }; where.periodEnd = { lte: filters.dateRange.end }; } const [signals, total] = await Promise.all([ prisma.demandSignal.findMany({ where, skip, take: limit, orderBy: { timestamp: 'desc' }, }), prisma.demandSignal.count({ where }), ]); return createPaginatedResult(signals, total, page, limit); } // Get active demand signals for region export async function getActiveDemandSignals( location: LocationFilter ): Promise { const now = new Date(); const signals = await prisma.demandSignal.findMany({ where: { periodEnd: { gte: now }, }, orderBy: { timestamp: 'desc' }, }); return signals.filter(signal => { const distance = calculateDistanceKm( location.latitude, location.longitude, signal.centerLat, signal.centerLon ); return distance <= signal.radiusKm; }); } // ============================================ // SUPPLY COMMITMENT OPERATIONS // ============================================ // Create a supply commitment export async function createSupplyCommitment(data: { growerId: string; produceType: string; variety?: string; committedQuantityKg: number; availableFrom: Date; availableUntil: Date; pricePerKg: number; currency?: string; minimumOrderKg?: number; bulkDiscountThreshold?: number; bulkDiscountPercent?: number; certifications?: string[]; freshnessGuaranteeHours?: number; deliveryRadiusKm: number; deliveryMethods: string[]; }): Promise { return prisma.supplyCommitment.create({ data: { ...data, currency: data.currency || 'USD', minimumOrderKg: data.minimumOrderKg || 0, certifications: data.certifications || [], status: 'AVAILABLE', remainingKg: data.committedQuantityKg, }, }); } // Get supply commitment by ID export async function getSupplyCommitmentById(id: string): Promise { return prisma.supplyCommitment.findUnique({ where: { id }, }); } // Update supply commitment export async function updateSupplyCommitment( id: string, data: Prisma.SupplyCommitmentUpdateInput ): Promise { return prisma.supplyCommitment.update({ where: { id }, data, }); } // Reduce commitment quantity export async function reduceCommitmentQuantity( id: string, quantityKg: number ): Promise { const commitment = await prisma.supplyCommitment.findUnique({ where: { id }, }); if (!commitment) throw new Error('Commitment not found'); const newRemainingKg = commitment.remainingKg - quantityKg; let newStatus: CommitmentStatus = commitment.status; if (newRemainingKg <= 0) { newStatus = 'FULLY_COMMITTED'; } else if (newRemainingKg < commitment.committedQuantityKg) { newStatus = 'PARTIALLY_COMMITTED'; } return prisma.supplyCommitment.update({ where: { id }, data: { remainingKg: Math.max(0, newRemainingKg), status: newStatus, }, }); } // Get supply commitments with pagination export async function getSupplyCommitments( options: PaginationOptions = {}, filters?: { growerId?: string; produceType?: string; status?: CommitmentStatus; dateRange?: DateRangeFilter; } ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.SupplyCommitmentWhereInput = {}; if (filters?.growerId) where.growerId = filters.growerId; if (filters?.produceType) where.produceType = { contains: filters.produceType, mode: 'insensitive' }; if (filters?.status) where.status = filters.status; if (filters?.dateRange) { where.availableFrom = { lte: filters.dateRange.end }; where.availableUntil = { gte: filters.dateRange.start }; } const [commitments, total] = await Promise.all([ prisma.supplyCommitment.findMany({ where, skip, take: limit, orderBy: { timestamp: 'desc' }, include: { grower: true }, }), prisma.supplyCommitment.count({ where }), ]); return createPaginatedResult(commitments, total, page, limit); } // Find matching supply for demand export async function findMatchingSupply( produceType: string, location: LocationFilter, requiredDate: Date ): Promise { const commitments = await prisma.supplyCommitment.findMany({ where: { produceType: { contains: produceType, mode: 'insensitive' }, status: { in: ['AVAILABLE', 'PARTIALLY_COMMITTED'] }, availableFrom: { lte: requiredDate }, availableUntil: { gte: requiredDate }, remainingKg: { gt: 0 }, }, include: { grower: true }, }); // Filter by delivery radius return commitments.filter(c => { if (!c.grower.latitude || !c.grower.longitude) return false; const distance = calculateDistanceKm( location.latitude, location.longitude, c.grower.latitude, c.grower.longitude ); return distance <= c.deliveryRadiusKm; }); } // ============================================ // MARKET MATCH OPERATIONS // ============================================ // Create a market match export async function createMarketMatch(data: { consumerId: string; growerId: string; demandSignalId: string; supplyCommitmentId: string; produceType: string; matchedQuantityKg: number; agreedPricePerKg: number; totalPrice: number; currency?: string; deliveryDate: Date; deliveryMethod: string; deliveryLatitude?: number; deliveryLongitude?: number; deliveryAddress?: string; }): Promise { // Reduce the supply commitment await reduceCommitmentQuantity(data.supplyCommitmentId, data.matchedQuantityKg); return prisma.marketMatch.create({ data: { ...data, currency: data.currency || 'USD', status: 'PENDING', }, }); } // Get market match by ID export async function getMarketMatchById(id: string): Promise { return prisma.marketMatch.findUnique({ where: { id }, }); } // Get market match with details export async function getMarketMatchWithDetails(id: string) { return prisma.marketMatch.findUnique({ where: { id }, include: { demandSignal: true, supplyCommitment: { include: { grower: true }, }, }, }); } // Update market match status export async function updateMarketMatchStatus( id: string, status: MatchStatus, extras?: { consumerRating?: number; growerRating?: number; feedback?: string; } ): Promise { return prisma.marketMatch.update({ where: { id }, data: { status, ...extras }, }); } // Get market matches with pagination export async function getMarketMatches( options: PaginationOptions = {}, filters?: { consumerId?: string; growerId?: string; status?: MatchStatus; dateRange?: DateRangeFilter; } ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const where: Prisma.MarketMatchWhereInput = {}; if (filters?.consumerId) where.consumerId = filters.consumerId; if (filters?.growerId) where.growerId = filters.growerId; if (filters?.status) where.status = filters.status; if (filters?.dateRange) { where.deliveryDate = { gte: filters.dateRange.start, lte: filters.dateRange.end, }; } const [matches, total] = await Promise.all([ prisma.marketMatch.findMany({ where, skip, take: limit, orderBy: { deliveryDate: 'desc' }, include: { demandSignal: true, supplyCommitment: true, }, }), prisma.marketMatch.count({ where }), ]); return createPaginatedResult(matches, total, page, limit); } // ============================================ // SEASONAL PLAN OPERATIONS // ============================================ // Create a seasonal plan export async function createSeasonalPlan(data: { growerId: string; year: number; season: string; location: Record; growingCapacity: Record; plannedCrops: Record[]; expectedTotalYieldKg?: number; expectedRevenue?: number; expectedCarbonFootprintKg?: number; }): Promise { return prisma.seasonalPlan.create({ data: { ...data, status: 'DRAFT', }, }); } // Get seasonal plan by ID export async function getSeasonalPlanById(id: string): Promise { return prisma.seasonalPlan.findUnique({ where: { id }, }); } // Update seasonal plan export async function updateSeasonalPlan( id: string, data: Prisma.SeasonalPlanUpdateInput ): Promise { return prisma.seasonalPlan.update({ where: { id }, data, }); } // Update seasonal plan status export async function updateSeasonalPlanStatus( id: string, status: PlanStatus, completionPercentage?: number ): Promise { return prisma.seasonalPlan.update({ where: { id }, data: { status, completionPercentage }, }); } // Get seasonal plans by grower export async function getSeasonalPlansByGrower( growerId: string, year?: number ): Promise { return prisma.seasonalPlan.findMany({ where: { growerId, ...(year && { year }), }, orderBy: [{ year: 'desc' }, { season: 'asc' }], }); } // ============================================ // DEMAND FORECAST OPERATIONS // ============================================ // Create a demand forecast export async function createDemandForecast(data: { region: string; forecastPeriodStart: Date; forecastPeriodEnd: Date; forecasts: Record[]; modelVersion: string; dataPointsUsed: number; lastTrainingDate?: Date; }): Promise { return prisma.demandForecast.create({ data }); } // Get latest demand forecast for region export async function getLatestDemandForecast(region: string): Promise { return prisma.demandForecast.findFirst({ where: { region }, orderBy: { generatedAt: 'desc' }, }); } // ============================================ // PLANTING RECOMMENDATION OPERATIONS // ============================================ // Create a planting recommendation export async function createPlantingRecommendation(data: { growerId: string; produceType: string; variety?: string; category: string; recommendedQuantity: number; quantityUnit: string; expectedYieldKg: number; yieldConfidence: number; plantByDate: Date; expectedHarvestStart: Date; expectedHarvestEnd: Date; growingDays: number; projectedDemandKg?: number; projectedPricePerKg?: number; projectedRevenue?: number; marketConfidence?: number; riskFactors?: Record[]; overallRisk?: string; demandSignalIds: string[]; explanation?: string; }): Promise { return prisma.plantingRecommendation.create({ data: { ...data, overallRisk: data.overallRisk || 'medium', }, }); } // Get planting recommendations for grower export async function getPlantingRecommendations( growerId: string, options: PaginationOptions = {} ): Promise> { const page = options.page || 1; const limit = options.limit || 20; const skip = (page - 1) * limit; const [recommendations, total] = await Promise.all([ prisma.plantingRecommendation.findMany({ where: { growerId }, skip, take: limit, orderBy: { plantByDate: 'asc' }, }), prisma.plantingRecommendation.count({ where: { growerId } }), ]); return createPaginatedResult(recommendations, total, page, limit); }