feat(pricing): Automated Monthly Jordan Fuel Pricing Service via Gemini AI and Cron Schedule
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 🔄' })
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user