*/ public static function defaults(): array { return array( 'search_radius_km' => 15, 'max_stations' => 10, 'fuel_types' => Fontdelbosc_Combustible_Fuels::DEFAULT_IDS, 'show_only_public' => true, 'cache_ttl' => 3600, 'show_distance' => true, 'show_schedule' => true, 'sort_by' => 'distance', 'delete_data_on_uninstall' => false, ); } /** * Returns the merged settings. * * @return array */ public static function options(): array { $clean = array(); foreach ( Fontdelbosc_Combustible_Payload::arr( get_option( self::OPTIONS_OPTION, array() ) ) as $key => $value ) { if ( is_string( $key ) ) { $clean[ $key ] = $value; } } return array_merge( self::defaults(), $clean ); } /** * Returns the cached province payload, refreshing it when stale. * * @param bool $force_refresh Skip the live cache. * @return array{fecha:string,stations:array>,fetched_at:int,duration:float,stale?:bool}|null */ public static function get_payload( bool $force_refresh = false ): ?array { if ( ! $force_refresh ) { $cached = get_transient( self::PROVINCE_TRANSIENT ); if ( is_array( $cached ) ) { return self::shape( $cached ); } } $payload = Fontdelbosc_Combustible_Api::fetch_province(); if ( null === $payload ) { $stale = get_transient( self::STALE_TRANSIENT ); if ( is_array( $stale ) ) { $shaped = self::shape( $stale ); $shaped['stale'] = true; return $shaped; } return null; } set_transient( self::PROVINCE_TRANSIENT, $payload, max( 60, Fontdelbosc_Combustible_Payload::int( self::options()['cache_ttl'] ?? null ) ?? 3600 ) ); set_transient( self::STALE_TRANSIENT, $payload, self::STALE_TTL ); self::record_status( $payload ); return $payload; } /** * Returns the cached product catalog, possibly empty. * * @return array */ public static function get_products(): array { $cached = get_transient( self::PRODUCTS_TRANSIENT ); if ( is_array( $cached ) ) { return self::shape_products( $cached ); } $products = Fontdelbosc_Combustible_Api::fetch_products(); if ( null === $products ) { return array(); } set_transient( self::PRODUCTS_TRANSIENT, $products, self::PRODUCTS_TTL ); return $products; } /** * Builds the final station selection. * * @param array $args Overrides: radius, max, fuel, sort, only_public. * @return array{stations:array>,fecha:string,stale:bool,radius:int,fuels:array,fields:array} */ public static function get_stations( array $args = array() ): array { $options = self::options(); $opt_radius = Fontdelbosc_Combustible_Payload::int( $options['search_radius_km'] ?? null ) ?? 15; $opt_max = Fontdelbosc_Combustible_Payload::int( $options['max_stations'] ?? null ) ?? 10; $opt_sort = Fontdelbosc_Combustible_Payload::str( $options['sort_by'] ?? null ) ?? 'distance'; $opt_public = ! empty( $options['show_only_public'] ); $only_public = $opt_public && ( $args['only_public'] ?? true ); $radius = max( 5, min( 50, Fontdelbosc_Combustible_Payload::int( $args['radius'] ?? null ) ?? $opt_radius ) ); $max = max( 1, min( 20, Fontdelbosc_Combustible_Payload::int( $args['max'] ?? null ) ?? $opt_max ) ); $fuels = self::fuel_ids( Fontdelbosc_Combustible_Payload::str( $args['fuel'] ?? null ) ?? '', $options ); $sort_att = Fontdelbosc_Combustible_Payload::str( $args['sort'] ?? null ); $sort = ( $sort_att ?? $opt_sort ) === 'price_asc' ? 'price_asc' : 'distance'; $stations = array(); $fields = array(); $stale = false; $fecha = ''; $payload = self::get_payload(); if ( null !== $payload ) { $stale = ! empty( $payload['stale'] ); $fecha = $payload['fecha']; $selected = self::select( $payload['stations'], $radius, $fuels, $sort, $only_public ); $stations = $selected['stations']; $fields = $selected['fields']; } return array( 'stations' => array_slice( $stations, 0, $max ), 'fecha' => $fecha, 'stale' => $stale, 'radius' => $radius, 'fuels' => $fuels, 'fields' => $fields, ); } /** * Deletes every cache entry. */ public static function flush_cache(): void { delete_transient( self::PROVINCE_TRANSIENT ); delete_transient( self::STALE_TRANSIENT ); delete_transient( self::PRODUCTS_TRANSIENT ); } /** * Filters, sorts and dedupes the normalized station list. * * @param array> $stations Stations. * @param int $radius Radius in km. * @param array $fuels Product IDs. * @param string $sort distance|price_asc. * @param bool $only_public Keep Tipo Venta "P" only. * @return array{stations:array>,fields:array} */ private static function select( array $stations, int $radius, array $fuels, string $sort, bool $only_public ): array { $labels = array(); foreach ( $stations as $station ) { foreach ( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $station, 'prices' ) ) as $label => $_unused ) { if ( is_string( $label ) ) { $labels[ $label ] = true; } } } $fields = self::fuel_fields( $fuels, array_keys( $labels ) ); $keep = array(); foreach ( $stations as $station ) { $tipo = Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $station, 'tipo_venta' ) ) ?? ''; if ( $only_public && 'P' !== $tipo ) { continue; } $distance = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $station, 'distance' ) ); if ( null === $distance || $distance > (float) $radius ) { continue; } $prices = array(); foreach ( $fields as $field ) { $price = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $station, 'prices' ) ), $field ) ); if ( null !== $price ) { $prices[ $field ] = $price; } } if ( array() === $prices ) { continue; } $station['selected_prices'] = $prices; $station['best_price'] = min( $prices ); $keep[] = $station; } usort( $keep, static function ( array $a, array $b ) use ( $sort ): int { $a_price = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $a, 'best_price' ) ); $b_price = Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $b, 'best_price' ) ); $a_key = 'price_asc' === $sort ? ( $a_price ?? INF ) : Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $a, 'distance' ) ) ?? INF; $b_key = 'price_asc' === $sort ? ( $b_price ?? INF ) : Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $b, 'distance' ) ) ?? INF; return $a_key <=> $b_key; } ); return array( 'stations' => $keep, 'fields' => $fields, ); } /** * Resolves the fuel selection from a shortcode attribute or the options. * * @param string $att Raw "1,4" style attribute. * @param array $options Settings. * @return array */ private static function fuel_ids( string $att, array $options ): array { if ( '' !== $att ) { $ids = array(); foreach ( explode( ',', $att ) as $piece ) { $id = Fontdelbosc_Combustible_Payload::int( trim( $piece ) ); if ( null !== $id && $id > 0 ) { $ids[] = $id; } } if ( array() !== $ids ) { return array_values( array_unique( $ids ) ); } } $saved = array(); foreach ( Fontdelbosc_Combustible_Payload::arr( $options['fuel_types'] ?? null ) as $id ) { $int = Fontdelbosc_Combustible_Payload::int( $id ); if ( null !== $int && $int > 0 ) { $saved[] = $int; } } return array() === $saved ? Fontdelbosc_Combustible_Fuels::DEFAULT_IDS : array_values( array_unique( $saved ) ); } /** * Maps product IDs to station price fields using the catalog. * * @param array $ids Product IDs. * @param array $labels Price field labels present in the payload. * @return array */ private static function fuel_fields( array $ids, array $labels ): array { $names = array(); foreach ( self::get_products() as $product ) { $names[ $product['id'] ] = $product['name']; } $fields = array(); foreach ( $ids as $id ) { $name = $names[ (int) $id ] ?? null; $field = null === $name ? null : Fontdelbosc_Combustible_Fuels::station_field( $name, $labels ); if ( null !== $field ) { $fields[ (int) $id ] = $field; } } return $fields; } /** * Coerces a cached payload back into its typed shape. * * @param array $raw Cached value. * @return array{fecha:string,stations:array>,fetched_at:int,duration:float,stale?:bool} */ private static function shape( array $raw ): array { $stations = array(); foreach ( Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $raw, 'stations' ) ) as $station ) { if ( is_array( $station ) ) { $stations[] = $station; } } return array( 'fecha' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'fecha' ) ) ?? '', 'stations' => $stations, 'fetched_at' => Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $raw, 'fetched_at' ) ) ?? time(), 'duration' => Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $raw, 'duration' ) ) ?? 0.0, ); } /** * Coerces a cached product catalog back into its typed shape. * * @param array $raw Cached value. * @return array */ private static function shape_products( array $raw ): array { $products = array(); foreach ( $raw as $entry ) { $id = Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $entry, 'IDProducto' ) ) ?? Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $entry, 'id' ) ); if ( null === $id ) { continue; } $products[] = array( 'id' => $id, 'name' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'name' ) ) ?? (string) $id, 'abbr' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'abbr' ) ) ?? '', ); } return $products; } /** * Persists fetch status for the admin screen. * * @param array $payload Fresh payload. */ private static function record_status( array $payload ): void { update_option( self::STATUS_OPTION, array( 'last_success' => Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $payload, 'fetched_at' ) ) ?? time(), 'last_duration' => Fontdelbosc_Combustible_Payload::float( Fontdelbosc_Combustible_Payload::get( $payload, 'duration' ) ) ?? 0.0, 'last_data_date' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $payload, 'fecha' ) ) ?? '', ), false ); } }