nshelper

WP Global Cart - Scheduled currency price sync for WooCommerce products.

Sep 16th, 2026
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 8.44 KB | None | 0 0
  1. <?php
  2.  
  3.  
  4.     /**
  5.      * Scheduled currency price sync for WooCommerce products.
  6.      *
  7.      * Runs on a recurring WP-Cron schedule (default: once per day) and updates
  8.      * the price of every product — simple, variable (per variation), external,
  9.      * grouped, etc. — that carries both the '_origin_product_currency' and
  10.      * '_origin_product_price' meta keys, converting the stored origin price
  11.      * into the shop's current currency via WOOGC_Currency_Converter::convert().
  12.      *
  13.      * Products/variations missing EITHER meta key are skipped and the loop
  14.      * continues to the next one.
  15.      *
  16.      * USAGE:
  17.      * Paste this into a custom/mu-plugin file, or into your theme's
  18.      * functions.php. Requires the plugin that provides WOOGC_Currency_Converter
  19.      * to be active.
  20.      */
  21.  
  22.     if ( ! defined( 'ABSPATH' ) ) {
  23.         exit;
  24.     }
  25.  
  26.     define( 'CUSTOM_PRICE_SYNC_HOOK', 'custom_price_sync_cron_hook' );
  27.    
  28.    
  29.     add_filter( 'woogc/ps/synchronize_product/origin_product/meta_data', 'custom_woogc_add_site_currency', 10, 1 );
  30.     function custom_woogc_add_site_currency( $args )
  31.         {
  32.             $product    =   wc_get_product( $args[ 'origin_product_id' ] );
  33.            
  34.             $args['product_meta']['_origin_product_currency'][] = get_option( 'woocommerce_currency' );
  35.             $args['product_meta']['_origin_product_price'][]    = $product->get_price();
  36.  
  37.             return $args;
  38.         }
  39.        
  40.  
  41.     /**
  42.      *
  43.      * Change the interval via the 'custom_price_sync_interval_seconds' filter,
  44.      * e.g. add_filter( 'custom_price_sync_interval_seconds', fn() => HOUR_IN_SECONDS * 6 );
  45.      *
  46.      * NOTE: if you change this after the event is already scheduled, run
  47.      * custom_price_sync_unschedule_event() once (or deactivate/reactivate)
  48.      * so WordPress picks up the new interval — WP doesn't reschedule an
  49.      * already-queued event automatically.
  50.      */
  51.     add_filter( 'cron_schedules', 'custom_price_sync_register_interval' );
  52.     function custom_price_sync_register_interval( $schedules ) {
  53.         $schedules['custom_price_sync_interval'] = array(
  54.             'interval' => apply_filters( 'custom_price_sync_interval_seconds', DAY_IN_SECONDS ),
  55.             'display'  => __( 'Custom Price Sync Interval', 'custom-price-sync' ),
  56.         );
  57.         return $schedules;
  58.     }
  59.  
  60.     /**
  61.      * Schedule the event if it isn't already scheduled.
  62.      */
  63.     add_action( 'init', 'custom_price_sync_schedule_event' );
  64.     function custom_price_sync_schedule_event() {
  65.         if ( ! wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK ) ) {
  66.             wp_schedule_event( time(), 'custom_price_sync_interval', CUSTOM_PRICE_SYNC_HOOK );
  67.         }
  68.     }
  69.  
  70.     /**
  71.      * Unschedule helper — call this from a plugin deactivation hook if you turn
  72.      * this into its own plugin file:
  73.      *   register_deactivation_hook( __FILE__, 'custom_price_sync_unschedule_event' );
  74.      */
  75.     function custom_price_sync_unschedule_event() {
  76.         $timestamp = wp_next_scheduled( CUSTOM_PRICE_SYNC_HOOK );
  77.         if ( $timestamp ) {
  78.             wp_unschedule_event( $timestamp, CUSTOM_PRICE_SYNC_HOOK );
  79.         }
  80.     }
  81.  
  82.     /**
  83.      * The cron callback: loops through every product in batches and updates
  84.      * prices for any product/variation carrying the origin currency/price meta.
  85.      */
  86.     add_action( CUSTOM_PRICE_SYNC_HOOK, 'custom_price_sync_run' );
  87.     function custom_price_sync_run() {
  88.  
  89.         if ( ! class_exists( 'WOOGC_Currency_Converter' ) ) {
  90.             custom_price_sync_log( 'WOOGC_Currency_Converter class not found. Aborting sync.' );
  91.             return;
  92.         }
  93.  
  94.         $batch_size = apply_filters( 'custom_price_sync_batch_size', 300 );
  95.         $paged      = 1;
  96.  
  97.         do {
  98.             $query = new WP_Query( array(
  99.                 'post_type'              => 'product',
  100.                 'post_status'            => 'publish',
  101.                 'posts_per_page'         => $batch_size,
  102.                 'paged'                  => $paged,
  103.                 'fields'                 => 'ids',
  104.                 'orderby'                => 'ID',
  105.                 'order'                  => 'ASC',
  106.                 'no_found_rows'          => true,
  107.                 'update_post_meta_cache' => false,
  108.                 'update_post_term_cache' => false,
  109.             ) );
  110.  
  111.             if ( empty( $query->posts ) ) {
  112.                 break;
  113.             }
  114.  
  115.             foreach ( $query->posts as $product_id ) {
  116.  
  117.                 $product = wc_get_product( $product_id );
  118.  
  119.                 if ( ! $product ) {
  120.                     continue;
  121.                 }
  122.  
  123.                 if ( $product->is_type( 'variable' ) ) {
  124.  
  125.                     $updated_any_variation = false;
  126.  
  127.                     foreach ( $product->get_children() as $variation_id ) {
  128.                         $variation = wc_get_product( $variation_id );
  129.  
  130.                         if ( ! $variation ) {
  131.                             continue;
  132.                         }
  133.  
  134.                         if ( custom_price_sync_maybe_update_product( $variation ) ) {
  135.                             $updated_any_variation = true;
  136.                         }
  137.                     }
  138.  
  139.                     // Refresh the parent's cached price range after variations change.
  140.                     if ( $updated_any_variation && class_exists( 'WC_Product_Variable' ) ) {
  141.                         WC_Product_Variable::sync( $product_id );
  142.                         wc_delete_product_transients( $product_id );
  143.                     }
  144.                 } else {
  145.                     // Simple, external, grouped, etc.
  146.                     custom_price_sync_maybe_update_product( $product );
  147.                 }
  148.             }
  149.  
  150.             $paged++;
  151.  
  152.         } while ( true );
  153.  
  154.         custom_price_sync_log( 'Price sync run completed.' );
  155.     }
  156.  
  157.     /**
  158.      * Updates a single product/variation's price from its origin currency/price
  159.      * meta, if both metas exist. Returns true if the price was updated.
  160.      *
  161.      * @param WC_Product $product
  162.      * @return bool
  163.      */
  164.     function custom_price_sync_maybe_update_product( $product ) {
  165.  
  166.         $product_id = $product->get_id();
  167.  
  168.         // Skip products that don't have BOTH meta keys set (this is the correct
  169.         // existence check — get_post_meta()/get_meta() return '' both when a
  170.         // meta is missing AND when it's set to an empty string).
  171.         if ( ! metadata_exists( 'post', $product_id, '_origin_product_currency' )
  172.             || ! metadata_exists( 'post', $product_id, '_origin_product_price' ) ) {
  173.             return false;
  174.         }
  175.  
  176.         $origin_currency = $product->get_meta( '_origin_product_currency', true );
  177.         $origin_price    = $product->get_meta( '_origin_product_price', true );
  178.  
  179.         if ( '' === $origin_currency || '' === $origin_price || ! is_numeric( $origin_price ) ) {
  180.             custom_price_sync_log( sprintf( 'Product #%d has invalid origin meta, skipping.', $product_id ) );
  181.             return false;
  182.         }
  183.  
  184.         $shop_currency = get_option( 'woocommerce_currency' );
  185.  
  186.         // Nothing to convert if origin currency already matches the shop currency.
  187.         if ( $origin_currency === $shop_currency ) {
  188.             $new_price = floatval( $origin_price );
  189.         } else {
  190.             $new_price = WOOGC_Currency_Converter::convert(
  191.                 floatval( $origin_price ),
  192.                 $origin_currency,
  193.                 $shop_currency
  194.             );
  195.  
  196.             if ( false === $new_price ) {
  197.                 custom_price_sync_log( sprintf(
  198.                     'Currency conversion failed for product #%d (%s -> %s), skipping.',
  199.                     $product_id,
  200.                     $origin_currency,
  201.                     $shop_currency
  202.                 ) );
  203.                 return false;
  204.             }
  205.         }
  206.  
  207.         $new_price = wc_format_decimal( $new_price, wc_get_price_decimals() );
  208.  
  209.         $old_regular_price = $product->get_regular_price();
  210.         $old_sale_price    = $product->get_sale_price();
  211.  
  212.         $product->set_regular_price( $new_price );
  213.  
  214.         // If there's no active sale price, keep the active price in sync too.
  215.         // (If a sale price exists, we leave it as-is rather than guessing a new one.)
  216.         if ( '' === $old_sale_price ) {
  217.             $product->set_price( $new_price );
  218.         }
  219.  
  220.         $product->save();
  221.  
  222.         custom_price_sync_log( sprintf(
  223.             'Product #%d price updated: %s %s -> %s %s (regular price %s -> %s).',
  224.             $product_id,
  225.             $origin_price,
  226.             $origin_currency,
  227.             $new_price,
  228.             $shop_currency,
  229.             $old_regular_price,
  230.             $new_price
  231.         ) );
  232.  
  233.         return true;
  234.     }
  235.  
  236.     /**
  237.      * Simple logging helper — writes to WooCommerce > Status > Logs
  238.      * (source: custom-price-sync). Disable with:
  239.      *   add_filter( 'custom_price_sync_enable_logging', '__return_false' );
  240.      */
  241.     function custom_price_sync_log( $message ) {
  242.         if ( ! apply_filters( 'custom_price_sync_enable_logging', true ) ) {
  243.             return;
  244.         }
  245.  
  246.         if ( function_exists( 'wc_get_logger' ) ) {
  247.             wc_get_logger()->info( $message, array( 'source' => 'custom-price-sync' ) );
  248.         }
  249.     }
Advertisement
Add Comment
Please, Sign In to add comment