feat(pricing): Automated Monthly Jordan Fuel Pricing Service via Gemini AI and Cron Schedule

This commit is contained in:
Hamza-Ayed
2026-08-21 02:19:08 +03:00
parent f14d40dac7
commit 0849b7b53c
5 changed files with 157 additions and 6 deletions
+117
View File
@@ -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<FuelPrices> {
const geminiKey = this.configService.get<string>('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;
}
}
+17 -1
View File
@@ -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 🔄' })
+3 -2
View File
@@ -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 {}
+14 -3
View File
@@ -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<RoadSegmentStat>,
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,
}
};
}
+6
View File
@@ -449,6 +449,12 @@ function App() {
💡 {routeData.ecoMetrics.mechanicalAdvice}
</div>
)}
{routeData.ecoMetrics.pricingBulletin && (
<div style={{ fontSize: '0.68rem', color: '#64748b', marginTop: 5, display: 'flex', justifyContent: 'space-between' }}>
<span>📅 تسعيرة {routeData.ecoMetrics.pricingBulletin.effectiveMonth}</span>
<span>بنزين 90: {routeData.ecoMetrics.pricingBulletin.gasoline90JOD} د.أ/لتر</span>
</div>
)}
</div>
)}