1+ /**
2+ * Dynamic pricing analysis based on current market data
3+ * Calculates price thresholds from actual fuel station prices
4+ */
5+ import { FuelCategorizer } from './fuel-categorizer'
6+ import { DYNAMIC_PRICING , STATIC_PRICE_FALLBACK } from './constants'
7+
8+ export interface PriceThresholds {
9+ low : number ; // Good deal threshold (green)
10+ high : number ; // High price threshold (red)
11+ average : number ; // Market average
12+ sampleSize : number ; // Number of stations used in calculation
13+ }
14+
15+ export interface FuelPriceAnalysis {
16+ unleaded ?: PriceThresholds ;
17+ diesel ?: PriceThresholds ;
18+ premium ?: PriceThresholds ;
19+ }
20+
21+ export class DynamicPricing {
22+
23+ /**
24+ * Analyze all fuel prices and calculate dynamic thresholds for each fuel type
25+ */
26+ static analyzePrices ( fuelData : any ) : FuelPriceAnalysis {
27+ const pricesByFuelType = this . collectPricesByFuelType ( fuelData ) ;
28+ const analysis : FuelPriceAnalysis = { } ;
29+
30+ for ( const [ fuelType , prices ] of Object . entries ( pricesByFuelType ) ) {
31+ if ( prices . length >= DYNAMIC_PRICING . MIN_SAMPLE_SIZE ) {
32+ const thresholds = this . calculateThresholds ( prices ) ;
33+ analysis [ fuelType as keyof FuelPriceAnalysis ] = thresholds ;
34+
35+ console . log ( `${ fuelType } analysis: avg £${ thresholds . average . toFixed ( 2 ) } , ` +
36+ `good ≤£${ thresholds . low . toFixed ( 2 ) } , high ≥£${ thresholds . high . toFixed ( 2 ) } ` +
37+ `(${ thresholds . sampleSize } stations)` ) ;
38+ } else {
39+ console . log ( `Insufficient data for ${ fuelType } : only ${ prices . length } stations` ) ;
40+ }
41+ }
42+
43+ return analysis ;
44+ }
45+
46+ /**
47+ * Collect and normalize all prices by fuel type from the raw data
48+ */
49+ private static collectPricesByFuelType ( fuelData : any ) : Record < string , number [ ] > {
50+ const pricesByType : Record < string , number [ ] > = { } ;
51+
52+ for ( const brand of Object . values ( fuelData ) ) {
53+ for ( const station of Object . values ( brand as any ) ) {
54+ const stationData = station as any ;
55+ if ( ! stationData . prices ) continue ;
56+
57+ for ( const [ fuelName , price ] of Object . entries ( stationData . prices ) ) {
58+ if ( typeof price !== 'number' ) continue ;
59+
60+ // Convert to pounds and validate
61+ const priceInPounds = price > DYNAMIC_PRICING . PENCE_TO_POUNDS_THRESHOLD ? price / 100 : price ;
62+ if ( priceInPounds < DYNAMIC_PRICING . MIN_PRICE || priceInPounds > DYNAMIC_PRICING . MAX_PRICE ) continue ;
63+
64+ // Categorize and store
65+ const category = FuelCategorizer . categorizeFuelType ( fuelName ) ;
66+ if ( category ) {
67+ if ( ! pricesByType [ category . name ] ) {
68+ pricesByType [ category . name ] = [ ] ;
69+ }
70+ pricesByType [ category . name ] . push ( priceInPounds ) ;
71+ }
72+ }
73+ }
74+ }
75+
76+ return pricesByType ;
77+ }
78+
79+ /**
80+ * Calculate price thresholds for a given set of prices
81+ */
82+ private static calculateThresholds ( prices : number [ ] ) : PriceThresholds {
83+ const cleanPrices = this . removeOutliers ( prices ) ;
84+ const average = cleanPrices . reduce ( ( a , b ) => a + b , 0 ) / cleanPrices . length ;
85+
86+ return {
87+ low : average - DYNAMIC_PRICING . THRESHOLD_MARGIN ,
88+ high : average + DYNAMIC_PRICING . THRESHOLD_MARGIN ,
89+ average,
90+ sampleSize : cleanPrices . length
91+ } ;
92+ }
93+
94+ /**
95+ * Remove outliers by excluding extreme values (top and bottom 10%)
96+ */
97+ private static removeOutliers ( prices : number [ ] ) : number [ ] {
98+ if ( prices . length < DYNAMIC_PRICING . MIN_SAMPLE_SIZE ) return prices ;
99+
100+ const sorted = [ ...prices ] . sort ( ( a , b ) => a - b ) ;
101+ const removeCount = Math . floor ( sorted . length * DYNAMIC_PRICING . OUTLIER_PERCENTILE ) ;
102+
103+ return sorted . slice ( removeCount , - removeCount || undefined ) ;
104+ }
105+
106+
107+ /**
108+ * Get price category for a given fuel type and price
109+ */
110+ static getPriceCategory ( analysis : FuelPriceAnalysis , fuelType : string , price : number ) : 'low' | 'medium' | 'high' {
111+ const thresholds = analysis [ fuelType as keyof FuelPriceAnalysis ] ;
112+
113+ if ( ! thresholds ) {
114+ // Fallback to static thresholds
115+ return price < STATIC_PRICE_FALLBACK . LOW ? 'low' : price < STATIC_PRICE_FALLBACK . MEDIUM ? 'medium' : 'high' ;
116+ }
117+
118+ return price <= thresholds . low ? 'low' : price >= thresholds . high ? 'high' : 'medium' ;
119+ }
120+
121+ /**
122+ * Get price color for display based on dynamic analysis
123+ */
124+ static getPriceColor ( analysis : FuelPriceAnalysis , fuelType : string , price : number ) : string {
125+ const colors = { low : '#00C851' , medium : '#ffbb33' , high : '#FF4444' } ;
126+ return colors [ this . getPriceCategory ( analysis , fuelType , price ) ] || '#666666' ;
127+ }
128+ }
0 commit comments