exchange-rate-services
4 weeks ago
geolocation
4 weeks ago
payment-gateways
4 weeks ago
class-wcml-exchange-rates.php
4 weeks ago
class-wcml-exchange-rates.php
379 lines
| 1 | <?php |
| 2 | |
| 3 | use WCML\MultiCurrency\ExchangeRateServices\Service; |
| 4 | use WPML\FP\Obj; |
| 5 | |
| 6 | class WCML_Exchange_Rates { |
| 7 | |
| 8 | private $woocommerce_wpml; |
| 9 | private $services = []; |
| 10 | private $settings; |
| 11 | private $wp_locale; |
| 12 | |
| 13 | const CRONJOB_EVENT = 'wcml_exchange_rates_update'; |
| 14 | const DIGITS_AFTER_DECIMAL_POINT = 6; |
| 15 | const KEY_RATES_UPDATED_FLAG = 'wcml_exchange_rates_manually_updated'; |
| 16 | |
| 17 | public function __construct( woocommerce_wpml $woocommerce_wpml, $wp_locale ) { |
| 18 | $this->woocommerce_wpml = $woocommerce_wpml; |
| 19 | $this->wp_locale = $wp_locale; |
| 20 | } |
| 21 | |
| 22 | public static function create() { |
| 23 | global $woocommerce_wpml, $wp_locale; |
| 24 | |
| 25 | return new self( $woocommerce_wpml, $wp_locale ); |
| 26 | } |
| 27 | |
| 28 | public function add_actions() { |
| 29 | if ( is_admin() ) { |
| 30 | add_action( 'wcml_saved_mc_options', [ $this, 'update_exchange_rate_options' ] ); |
| 31 | } |
| 32 | add_action( 'init', [ $this, 'init' ] ); |
| 33 | } |
| 34 | |
| 35 | public function init() { |
| 36 | if ( $this->woocommerce_wpml->multi_currency->get_currencies() ) { |
| 37 | if ( is_admin() ) { |
| 38 | add_action( 'wp_ajax_wcml_update_exchange_rates', [ $this, 'update_exchange_rates_ajax' ] ); |
| 39 | } |
| 40 | add_filter( 'cron_schedules', [ $this, 'cron_schedules' ] ); |
| 41 | add_action( self::CRONJOB_EVENT, [ $this, 'action_update_exchange_rates' ] ); |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | public function initialize_settings() { |
| 46 | if ( ! isset( $this->woocommerce_wpml->settings['multi_currency']['exchange_rates'] ) ) { |
| 47 | $this->settings = [ |
| 48 | 'automatic' => 0, |
| 49 | 'service' => 'currencylayer', |
| 50 | 'lifting_charge' => 0, |
| 51 | 'schedule' => 'manual', |
| 52 | 'week_day' => 1, |
| 53 | 'month_day' => 1, |
| 54 | ]; |
| 55 | $this->save_settings(); |
| 56 | } else { |
| 57 | $this->settings =& $this->woocommerce_wpml->settings['multi_currency']['exchange_rates']; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | public function get_services() { |
| 62 | return $this->services; |
| 63 | } |
| 64 | |
| 65 | public function add_service( $service_id, $service ) { |
| 66 | $this->services[ $service_id ] = $service; |
| 67 | } |
| 68 | |
| 69 | public function get_settings() { |
| 70 | return $this->settings; |
| 71 | } |
| 72 | |
| 73 | public function get_setting( $key ) { |
| 74 | return $this->settings[ $key ] ?? null; |
| 75 | } |
| 76 | |
| 77 | public function save_settings() { |
| 78 | $this->woocommerce_wpml->settings['multi_currency']['exchange_rates'] = $this->settings; |
| 79 | $this->woocommerce_wpml->update_settings(); |
| 80 | } |
| 81 | |
| 82 | public function save_setting( $key, $value ) { |
| 83 | $this->settings[ $key ] = $value; |
| 84 | $this->save_settings(); |
| 85 | } |
| 86 | |
| 87 | public function update_exchange_rates_ajax() { |
| 88 | $response = []; |
| 89 | if ( wp_create_nonce( 'update-exchange-rates' ) === $_POST['wcml_nonce'] ) { |
| 90 | try { |
| 91 | $rates = $this->update_exchange_rates(); |
| 92 | $response['success'] = 1; |
| 93 | $response['last_updated'] = date_i18n( 'F j, Y g:i a', $this->settings['last_updated'] ); |
| 94 | $response['rates'] = $rates; |
| 95 | } catch ( Exception $e ) { |
| 96 | $response['success'] = 0; |
| 97 | $response['error'] = $e->getMessage(); |
| 98 | $response['service'] = $this->settings['service']; |
| 99 | } |
| 100 | } else { |
| 101 | $response['success'] = 0; |
| 102 | $response['error'] = 'Invalid nonce'; |
| 103 | } |
| 104 | wp_send_json( $response ); |
| 105 | } |
| 106 | |
| 107 | public function update_exchange_rates() { |
| 108 | $currencies = $this->woocommerce_wpml->multi_currency->get_currency_codes(); |
| 109 | $rates = $this->fetch_exchange_rates_from_active_service( $currencies ); |
| 110 | |
| 111 | foreach ( $rates as $to => $rate ) { |
| 112 | if ( $rate && is_numeric( $rate ) ) { |
| 113 | $this->save_exchage_rate( $to, $rate ); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | $this->settings['last_updated'] = current_time( 'timestamp' ); |
| 118 | $this->save_settings(); |
| 119 | |
| 120 | return $rates; |
| 121 | } |
| 122 | |
| 123 | public function action_update_exchange_rates() { |
| 124 | $this->update_exchange_rates(); |
| 125 | } |
| 126 | |
| 127 | public function fetch_exchange_rates_from_active_service( $currencies ) { |
| 128 | if ( ! isset( $this->services[ $this->settings['service'] ] ) ) { |
| 129 | throw new Exception( 'The exchange rate service "' . $this->settings['service'] . '" is not defined.' ); |
| 130 | } |
| 131 | |
| 132 | $service = $this->get_current_service(); |
| 133 | |
| 134 | $default_currency = wcml_get_woocommerce_currency_option(); |
| 135 | $secondary_currencies = array_diff( $currencies, [ $default_currency ] ); |
| 136 | |
| 137 | try { |
| 138 | $rates = $service->getRates( $default_currency, $secondary_currencies ); |
| 139 | } catch ( Exception $e ) { |
| 140 | if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { |
| 141 | error_log( 'Exchange rates update error (' . $this->settings['service'] . '): ' . $e->getMessage() ); |
| 142 | } |
| 143 | throw new Exception( $e->getMessage() ); |
| 144 | } |
| 145 | |
| 146 | $this->apply_lifting_charge( $rates ); |
| 147 | |
| 148 | return $rates; |
| 149 | } |
| 150 | |
| 151 | public function apply_lifting_charge( &$rates ) { |
| 152 | foreach ( $rates as $k => $rate ) { |
| 153 | $rates[ $k ] = round( $rate * ( 1 + $this->settings['lifting_charge'] / 100 ), self::DIGITS_AFTER_DECIMAL_POINT ); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | private function save_exchage_rate( $currency, $rate ) { |
| 158 | $this->woocommerce_wpml->settings['currency_options'][ $currency ]['previous_rate'] = |
| 159 | $this->woocommerce_wpml->settings['currency_options'][ $currency ]['rate']; |
| 160 | $this->woocommerce_wpml->settings['currency_options'][ $currency ]['rate'] = $rate; |
| 161 | $this->woocommerce_wpml->update_settings(); |
| 162 | } |
| 163 | |
| 164 | public function get_currency_rate( $currency ) { |
| 165 | return $this->woocommerce_wpml->settings['currency_options'][ $currency ]['rate']; |
| 166 | } |
| 167 | |
| 168 | public function update_exchange_rate_options( $post_data ) { |
| 169 | |
| 170 | if ( isset( $post_data['exchange-rates-automatic'] ) && $post_data['exchange-rates-automatic'] ) { |
| 171 | $active_service_changed = false; |
| 172 | $active_key_changed = false; |
| 173 | $active_service_id = Obj::prop( 'service', $this->settings ); |
| 174 | $active_service = Obj::prop( $active_service_id, $this->services ); |
| 175 | |
| 176 | $this->settings['automatic'] = (int) $post_data['exchange-rates-automatic']; |
| 177 | |
| 178 | if ( isset( $post_data['exchange-rates-service'] ) ) { |
| 179 | |
| 180 | if ( isset( $this->services[ $this->settings['service'] ] ) && $post_data['exchange-rates-service'] !== $this->settings['service'] ) { |
| 181 | $this->services[ $this->settings['service'] ]->clearLastError(); |
| 182 | } |
| 183 | |
| 184 | $this->settings['service'] = sanitize_text_field( $post_data['exchange-rates-service'] ); |
| 185 | $active_service_changed = $active_service_id !== $this->settings['service']; |
| 186 | } |
| 187 | |
| 188 | if ( isset( $post_data['services'] ) ) { |
| 189 | $active_service_key = $active_service ? $active_service->getSetting( 'api-key' ) : ''; |
| 190 | |
| 191 | foreach ( $post_data['services'] as $service_id => $service_data ) { |
| 192 | if ( isset( $service_data['api-key'] ) ) { |
| 193 | $this->services[ $service_id ]->saveSetting( 'api-key', $service_data['api-key'] ); |
| 194 | |
| 195 | if ( $service_id === $active_service_id ) { |
| 196 | $active_key_changed = $active_service_key !== $service_data['api-key']; |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | $this->settings['lifting_charge'] = is_numeric( $post_data['lifting_charge'] ) ? $post_data['lifting_charge'] : 0; |
| 203 | |
| 204 | if ( isset( $post_data['update-schedule'] ) ) { |
| 205 | $this->settings['schedule'] = sanitize_text_field( $post_data['update-schedule'] ); |
| 206 | } |
| 207 | |
| 208 | if ( isset( $post_data['update-time'] ) ) { |
| 209 | $this->settings['time'] = sanitize_text_field( $post_data['update-time'] ); |
| 210 | } |
| 211 | |
| 212 | if ( isset( $post_data['update-weekly-day'] ) ) { |
| 213 | $this->settings['week_day'] = sanitize_text_field( $post_data['update-weekly-day'] ); |
| 214 | } |
| 215 | |
| 216 | if ( isset( $post_data['update-monthly-day'] ) ) { |
| 217 | $this->settings['month_day'] = sanitize_text_field( $post_data['update-monthly-day'] ); |
| 218 | } |
| 219 | |
| 220 | if ( $this->settings['schedule'] === 'manual' ) { |
| 221 | $this->delete_update_cronjob(); |
| 222 | } else { |
| 223 | $this->enable_update_cronjob(); |
| 224 | } |
| 225 | |
| 226 | if ( $active_key_changed || $active_service_changed ) { |
| 227 | $currentService = $this->get_current_service(); |
| 228 | |
| 229 | if ( $currentService ) { |
| 230 | $currentService->resetConnectionCache(); |
| 231 | } |
| 232 | |
| 233 | add_action( 'init', [ $this, 'update_rates_on_service_or_key_changed' ], 5 ); |
| 234 | } |
| 235 | } else { |
| 236 | $this->settings['automatic'] = 0; |
| 237 | $this->delete_update_cronjob(); |
| 238 | } |
| 239 | |
| 240 | $this->save_settings(); |
| 241 | } |
| 242 | |
| 243 | public function update_rates_on_service_or_key_changed() { |
| 244 | try { |
| 245 | $this->update_exchange_rates(); |
| 246 | $this->woocommerce_wpml->get_multi_currency()->init_currencies(); |
| 247 | wp_cache_add( self::KEY_RATES_UPDATED_FLAG, true ); |
| 248 | add_action( 'shutdown', function() { |
| 249 | wp_cache_delete( WCML_Exchange_Rates::KEY_RATES_UPDATED_FLAG ); |
| 250 | } ); |
| 251 | } catch ( \Exception $e ) {} |
| 252 | } |
| 253 | |
| 254 | public function enable_update_cronjob() { |
| 255 | $schedule = wp_get_schedule( self::CRONJOB_EVENT ); |
| 256 | |
| 257 | if ( $schedule !== $this->settings['schedule'] ) { |
| 258 | $this->delete_update_cronjob(); |
| 259 | } |
| 260 | |
| 261 | if ( 'monthly' === $this->settings['schedule'] ) { |
| 262 | $time_offset = $this->get_monthly_schedule_time_offset(); |
| 263 | $schedule = 'wcml_' . $this->settings['schedule'] . '_on_' . $this->settings['month_day']; |
| 264 | } elseif ( 'weekly' === $this->settings['schedule'] ) { |
| 265 | $time_offset = $this->get_weekly_schedule_time_offset(); |
| 266 | $schedule = 'wcml_' . $this->settings['schedule'] . '_on_' . $this->settings['week_day']; |
| 267 | |
| 268 | } else { |
| 269 | $time_offset = time(); |
| 270 | $schedule = $this->settings['schedule']; |
| 271 | } |
| 272 | |
| 273 | if ( ! wp_next_scheduled( self::CRONJOB_EVENT ) ) { |
| 274 | wp_schedule_event( $time_offset, $schedule, self::CRONJOB_EVENT ); |
| 275 | } |
| 276 | |
| 277 | } |
| 278 | |
| 279 | private function get_monthly_schedule_time_offset() { |
| 280 | $current_day = date( 'j' ); |
| 281 | $days_in_current_month = cal_days_in_month( CAL_GREGORIAN, (int) date( 'n' ), (int) date( 'Y' ) ); |
| 282 | |
| 283 | if ( $this->settings['month_day'] >= $current_day && $this->settings['month_day'] <= $days_in_current_month ) { |
| 284 | $days = $this->settings['month_day'] - $current_day; |
| 285 | } else { |
| 286 | $days = $days_in_current_month - $current_day + $this->settings['month_day']; |
| 287 | } |
| 288 | |
| 289 | $time_offset = time() + $days * DAY_IN_SECONDS; |
| 290 | |
| 291 | return $time_offset; |
| 292 | } |
| 293 | |
| 294 | private function get_weekly_schedule_time_offset() { |
| 295 | $current_day = date( 'w' ); |
| 296 | if ( $this->settings['week_day'] >= $current_day ) { |
| 297 | $days = $this->settings['week_day'] - $current_day; |
| 298 | } else { |
| 299 | $days = 7 - $current_day + $this->settings['week_day']; |
| 300 | } |
| 301 | |
| 302 | $time_offset = time() + $days * DAY_IN_SECONDS; |
| 303 | |
| 304 | return $time_offset; |
| 305 | } |
| 306 | |
| 307 | public function delete_update_cronjob() { |
| 308 | wp_clear_scheduled_hook( self::CRONJOB_EVENT ); |
| 309 | } |
| 310 | |
| 311 | public function cron_schedules( $schedules ) { |
| 312 | |
| 313 | if ( 'monthly' === $this->settings['schedule'] ) { |
| 314 | |
| 315 | $month_day = $this->get_month_day_formatted(); |
| 316 | $current_month = date( 'n' ); |
| 317 | $days_in_current_month = cal_days_in_month( CAL_GREGORIAN, $current_month, date( 'Y' ) ); |
| 318 | if ( $this->settings['month_day'] <= $days_in_current_month && $this->settings['month_day'] >= date( 'j' ) ) { |
| 319 | $interval = DAY_IN_SECONDS * $days_in_current_month; |
| 320 | } else { |
| 321 | $month_number = 12 === (int) $current_month ? 1 : $current_month + 1; |
| 322 | $year_number = 12 === (int) $current_month ? date( 'Y' ) + 1 : date( 'Y' ); |
| 323 | $interval = DAY_IN_SECONDS * cal_days_in_month( CAL_GREGORIAN, $month_number, $year_number ); |
| 324 | } |
| 325 | |
| 326 | $schedules[ 'wcml_monthly_on_' . $this->settings['month_day'] ] = [ |
| 327 | 'interval' => $interval, |
| 328 | /* translators: %s is the month day */ |
| 329 | 'display' => sprintf( __( 'Monthly on the %s', 'woocommerce-multilingual' ), $month_day ), |
| 330 | ]; |
| 331 | |
| 332 | } elseif ( 'weekly' === $this->settings['schedule'] ) { |
| 333 | |
| 334 | $week_day = $this->wp_locale->get_weekday( $this->settings['week_day'] ); |
| 335 | $schedules[ 'wcml_weekly_on_' . $this->settings['week_day'] ] = [ |
| 336 | 'interval' => WEEK_IN_SECONDS, |
| 337 | /* translators: %s is the week day */ |
| 338 | 'display' => sprintf( __( 'Weekly on %s', 'woocommerce-multilingual' ), $week_day ), |
| 339 | ]; |
| 340 | |
| 341 | } |
| 342 | |
| 343 | return $schedules; |
| 344 | } |
| 345 | |
| 346 | private function get_month_day_formatted() { |
| 347 | $month_day = $this->settings['month_day']; |
| 348 | switch ( $month_day ) { |
| 349 | case 1: |
| 350 | $month_day .= 'st'; |
| 351 | break; |
| 352 | case 2: |
| 353 | $month_day .= 'nd'; |
| 354 | break; |
| 355 | case 3: |
| 356 | $month_day .= 'rd'; |
| 357 | break; |
| 358 | default: |
| 359 | $month_day .= 'th'; |
| 360 | break; |
| 361 | } |
| 362 | return $month_day; |
| 363 | } |
| 364 | |
| 365 | private function get_current_service() { |
| 366 | return Obj::prop( Obj::prop( 'service', (array) $this->settings ), (array) $this->services ); |
| 367 | } |
| 368 | |
| 369 | public function is_current_service_actionable() { |
| 370 | $current_service = $this->get_current_service(); |
| 371 | |
| 372 | return $current_service |
| 373 | && ( |
| 374 | ! $current_service->isKeyRequired() |
| 375 | || $current_service->getSetting( 'api-key' ) |
| 376 | ); |
| 377 | } |
| 378 | } |
| 379 |