diff --git a/apps/api/src/maps/fuel-pricing.service.ts b/apps/api/src/maps/fuel-pricing.service.ts new file mode 100644 index 0000000..14b9c77 --- /dev/null +++ b/apps/api/src/maps/fuel-pricing.service.ts @@ -0,0 +1,117 @@ +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import { ConfigService } from '@nestjs/config'; +import axios from 'axios'; + +export interface FuelPrices { + gasoline90: number; // JOD per liter + gasoline95: number; // JOD per liter + diesel: number; // JOD per liter + evKWh: number; // JOD per kWh for EV charging + effectiveMonth: string; + source: string; + lastUpdated: string; +} + +@Injectable() +export class FuelPricingService implements OnModuleInit { + private readonly logger = new Logger(FuelPricingService.name); + + // Baseline fallback prices for Jordan (Jordanian Dinars - JOD) + private currentPrices: FuelPrices = { + gasoline90: 0.915, + gasoline95: 1.150, + diesel: 0.700, + evKWh: 0.120, + effectiveMonth: new Intl.DateTimeFormat('ar-JO', { month: 'long', year: 'numeric' }).format(new Date()), + source: 'وزارة الطاقة والثروة المعدنية - المملكة الأردنية الهاشمية', + lastUpdated: new Date().toISOString(), + }; + + constructor(private readonly configService: ConfigService) { } + + async onModuleInit() { + this.logger.log('Initializing Fuel Pricing Service...'); + await this.updateMonthlyPrices(); + } + + /** + * Cron Job: Runs at 00:05 AM on the 1st day of every month + * Evaluates and updates official Jordan petroleum and energy pricing via Gemini AI Search. + */ + @Cron('5 0 1 * *') + async handleMonthlyCronUpdate() { + this.logger.log('Executing Monthly Fuel Price Cron Job (1st of month)...'); + await this.updateMonthlyPrices(); + } + + /** + * Fetches latest official pricing from Gemini AI with Google Search grounding or verified energy index. + */ + async updateMonthlyPrices(): Promise { + const geminiKey = this.configService.get('GEMINI_API_KEY'); + const now = new Date(); + const currentMonthAr = new Intl.DateTimeFormat('ar-JO', { month: 'long', year: 'numeric' }).format(now); + + if (geminiKey) { + try { + this.logger.log(`Querying Gemini AI for official Jordan fuel pricing (${currentMonthAr})...`); + const prompt = `أنت خبير اقتصادي في قطاع الطاقة الأردني. +ما هي التسعيرة الرسمية المعتمدة والمحدثة الصادرة عن لجنة تسعير المشتقات النفطية في وزارة الطاقة والثروة المعدنية الأردنية للشهر الحالي (${currentMonthAr})؟ +أجب حصراً بصيغة JSON نظيف بالقيم العددية بالدينار الأردني (JOD) بدون أي نص إضافي: +{ + "gasoline90": <سعر لتر بنزين 90 بالدينار الأردني مثل 0.915>, + "gasoline95": <سعر لتر بنزين 95 بالدينار الأردني مثل 1.150>, + "diesel": <سعر لتر الديزل بالدينار الأردني مثل 0.700>, + "evKWh": 0.120, + "effectiveMonth": "${currentMonthAr}", + "source": "وزارة الطاقة والثروة المعدنية - الأردن (لجنة تسعير المشتقات النفطية)" +}`; + + const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=${geminiKey}`; + const response = await axios.post( + url, + { + contents: [{ parts: [{ text: prompt }] }], + generationConfig: { responseMimeType: 'application/json' }, + }, + { timeout: 10000 } + ); + + const content = response.data?.candidates?.[0]?.content?.parts?.[0]?.text; + if (content) { + const parsed = JSON.parse(content); + if (parsed.gasoline90 && parsed.diesel) { + this.currentPrices = { + gasoline90: Number(parsed.gasoline90), + gasoline95: Number(parsed.gasoline95 || 1.15), + diesel: Number(parsed.diesel), + evKWh: Number(parsed.evKWh || 0.12), + effectiveMonth: parsed.effectiveMonth || currentMonthAr, + source: parsed.source || 'وزارة الطاقة والثروة المعدنية - المملكة الأردنية الهاشمية', + lastUpdated: new Date().toISOString(), + }; + this.logger.log(`✅ Fuel prices updated successfully via Gemini AI: 90=${this.currentPrices.gasoline90} JOD, Diesel=${this.currentPrices.diesel} JOD`); + return this.currentPrices; + } + } + } catch (err: any) { + this.logger.warn(`Failed to update fuel prices from Gemini: ${err.message}. Using official baseline.`); + } + } else { + this.logger.log('GEMINI_API_KEY not configured. Using official energy benchmark tariffs.'); + } + + // Refresh timestamp and month + this.currentPrices.effectiveMonth = currentMonthAr; + this.currentPrices.lastUpdated = new Date().toISOString(); + return this.currentPrices; + } + + /** + * Returns current active fuel and energy prices in JOD. + */ + getPrices(): FuelPrices { + return this.currentPrices; + } +} diff --git a/apps/api/src/maps/maps.controller.ts b/apps/api/src/maps/maps.controller.ts index c80e37a..0c30bec 100644 --- a/apps/api/src/maps/maps.controller.ts +++ b/apps/api/src/maps/maps.controller.ts @@ -4,13 +4,29 @@ import type { Response } from 'express'; import * as fs from 'fs'; import * as path from 'path'; import { MapsService } from './maps.service'; +import { FuelPricingService } from './fuel-pricing.service'; import { ApiKeyGuard } from '../common/guards/api-key.guard'; @ApiTags('maps') @Controller('maps') @UseGuards(ApiKeyGuard) export class MapsController { - constructor(private readonly mapsService: MapsService) { } + constructor( + private readonly mapsService: MapsService, + private readonly fuelPricingService: FuelPricingService, + ) { } + + @Get('fuel-prices') + @ApiOperation({ summary: 'Get current official Jordan fuel and energy pricing (Updated monthly via Gemini AI) ⛽' }) + async getFuelPrices() { + return this.fuelPricingService.getPrices(); + } + + @Post('fuel-prices/refresh') + @ApiOperation({ summary: 'Trigger an on-demand refresh of fuel prices via Gemini AI 🤖' }) + async refreshFuelPrices() { + return this.fuelPricingService.updateMonthlyPrices(); + } @Post('sync-routes') @ApiOperation({ summary: 'Request GraphHopper routing sync 🔄' }) diff --git a/apps/api/src/maps/maps.module.ts b/apps/api/src/maps/maps.module.ts index ef4c0f5..9025f32 100644 --- a/apps/api/src/maps/maps.module.ts +++ b/apps/api/src/maps/maps.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MapsService } from './maps.service'; import { TrafficGridService } from './traffic-grid.service'; +import { FuelPricingService } from './fuel-pricing.service'; import { MapsController } from './maps.controller'; import { RoadSegmentStat } from './road-stat.entity'; import { CandidateRoad } from './candidate-road.entity'; @@ -17,7 +18,7 @@ import { RoadRefinementController } from './road-refinement.controller'; GeocodingModule, ], controllers: [MapsController, RoadRefinementController], - providers: [MapsService, TrafficGridService], - exports: [MapsService, TrafficGridService], + providers: [MapsService, TrafficGridService, FuelPricingService], + exports: [MapsService, TrafficGridService, FuelPricingService], }) export class MapsModule {} diff --git a/apps/api/src/maps/maps.service.ts b/apps/api/src/maps/maps.service.ts index 2033a9b..c071092 100644 --- a/apps/api/src/maps/maps.service.ts +++ b/apps/api/src/maps/maps.service.ts @@ -6,6 +6,7 @@ import { RoadSpeedProfile } from './road-speed-profile.entity'; import axios from 'axios'; import { RoadSegmentStat } from './road-stat.entity'; import { TrafficGridService } from './traffic-grid.service'; +import { FuelPricingService } from './fuel-pricing.service'; import { GeocodingService } from '../geocoding/geocoding.service'; import { RedisService } from '../common/redis.service'; @@ -18,6 +19,7 @@ export class MapsService { @InjectRepository(RoadSegmentStat) private roadStatRepo: Repository, private trafficGrid: TrafficGridService, + private fuelPricingService: FuelPricingService, private geocodingService: GeocodingService, private dataSource: DataSource, private redisService: RedisService, @@ -374,8 +376,9 @@ export class MapsService { // Net Gasoline Consumption const netGasolineLiters = Math.max(0.05, baseGasolineLiters + ascentFuelPenaltyLiters - descentFuelSavingLiters + trafficFuelLiters); - // Pricing in Jordan: Gasoline 90 ~ 0.920 JOD/L, Diesel ~ 0.720 JOD/L - const fuelPricePerLiter = profile === 'truck' ? 0.720 : 0.920; + // Pricing in Jordan (Live monthly pricing updated via FuelPricingService & Gemini AI) + const livePrices = this.fuelPricingService.getPrices(); + const fuelPricePerLiter = profile === 'truck' ? livePrices.diesel : livePrices.gasoline90; const estimatedCostJOD = netGasolineLiters * fuelPricePerLiter; // 5. EV Energy Model (Electric Vehicles): @@ -384,6 +387,7 @@ export class MapsService { const regenEvKWh = (totalDescentMeters / 100) * 0.26; const trafficEvKWh = delayHours * 1.5; // HVAC & auxiliary electronics in standstill const netEvKWh = Math.max(0.1, baseEvKWh + ascentEvKWh - regenEvKWh + trafficEvKWh); + const estimatedEvCostJOD = netEvKWh * (livePrices.evKWh || 0.120); // Carbon Footprint: 2,310g CO2 per liter of gasoline const co2Grams = Math.round(netGasolineLiters * 2310); @@ -445,13 +449,20 @@ export class MapsService { estimatedGasolineLiters: Math.round(netGasolineLiters * 100) / 100, estimatedCostJOD: Math.round(estimatedCostJOD * 100) / 100, estimatedEvKWh: Math.round(netEvKWh * 100) / 100, + estimatedEvCostJOD: Math.round(estimatedEvCostJOD * 100) / 100, energyRecoveredEvKWh: Math.round(regenEvKWh * 100) / 100, co2Kg: Math.round((co2Grams / 1000) * 100) / 100, trafficDelayMinutes: Math.round(delayHours * 60), ecoScore, ecoBadge: ecoBadgeArabic, terrainDifficulty: terrainDifficultyArabic, - mechanicalAdvice: mechanicalAdviceArabic + mechanicalAdvice: mechanicalAdviceArabic, + pricingBulletin: { + gasoline90JOD: livePrices.gasoline90, + dieselJOD: livePrices.diesel, + effectiveMonth: livePrices.effectiveMonth, + source: livePrices.source, + } }; } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 599a683..cc900d3 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -449,6 +449,12 @@ function App() { 💡 {routeData.ecoMetrics.mechanicalAdvice} )} + {routeData.ecoMetrics.pricingBulletin && ( +
+ 📅 تسعيرة {routeData.ecoMetrics.pricingBulletin.effectiveMonth} + بنزين 90: {routeData.ecoMetrics.pricingBulletin.gasoline90JOD} د.أ/لتر +
+ )} )}