init
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin notices for Multi-Currency.
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that will display admin notices.
|
||||
*/
|
||||
class AdminNotices {
|
||||
/**
|
||||
* Notices.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $notices = [];
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'admin_notices', [ $this, 'admin_notices' ] );
|
||||
add_action( 'wp_loaded', [ $this, 'hide_notices' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display any notices we've collected thus far.
|
||||
*/
|
||||
public function admin_notices() {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->check_for_notices();
|
||||
|
||||
foreach ( $this->notices as $notice_key => $notice ) {
|
||||
echo '<div class="' . esc_attr( $notice['class'] ) . '" style="position:relative;">';
|
||||
|
||||
if ( $notice['dismissible'] ) {
|
||||
?>
|
||||
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'wcpay-multi-currency-hide-notice', $notice_key ), 'wcpay_multi_currency_hide_notices_nonce', '_wcpay_multi_currency_notice_nonce' ) ); ?>" class="woocommerce-message-close notice-dismiss" style="position:relative;float:right;padding:9px 0 9px 9px;text-decoration:none;"></a>
|
||||
<?php
|
||||
}
|
||||
|
||||
echo '<p>';
|
||||
echo wp_kses(
|
||||
$notice['message'],
|
||||
[
|
||||
'a' => [
|
||||
'href' => [],
|
||||
'target' => [],
|
||||
],
|
||||
]
|
||||
);
|
||||
echo '</p></div>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides any admin notices.
|
||||
*/
|
||||
public function hide_notices() {
|
||||
if ( isset( $_GET['wcpay-multi-currency-hide-notice'] ) && isset( $_GET['_wcpay_multi_currency_notice_nonce'] ) ) {
|
||||
if ( ! wp_verify_nonce( wc_clean( wp_unslash( $_GET['_wcpay_multi_currency_notice_nonce'] ) ), 'wcpay_multi_currency_hide_notices_nonce' ) ) {
|
||||
wp_die( esc_html__( 'Action failed. Please refresh the page and retry.', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'Cheatin’ huh?', 'woocommerce-payments' ) );
|
||||
}
|
||||
|
||||
$notice = wc_clean( wp_unslash( $_GET['wcpay-multi-currency-hide-notice'] ) );
|
||||
|
||||
if ( 'currency_changed' === $notice ) {
|
||||
update_option( 'wcpay_multi_currency_show_store_currency_changed_notice', 'no' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds admin notice to be displayed.
|
||||
*
|
||||
* @param string $slug Slug for the notice.
|
||||
* @param string $class Class(es) for the notice.
|
||||
* @param string $message Message in the notice.
|
||||
* @param bool $dismissible Whether the notice can be dismissed or not.
|
||||
*/
|
||||
private function add_admin_notice( $slug, $class, $message, $dismissible = false ) {
|
||||
$this->notices[ $slug ] = [
|
||||
'class' => $class,
|
||||
'message' => $message,
|
||||
'dismissible' => $dismissible,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for notices and add them.
|
||||
*/
|
||||
private function check_for_notices() {
|
||||
$manual_currencies = get_option( 'wcpay_multi_currency_show_store_currency_changed_notice', false );
|
||||
|
||||
if ( is_array( $manual_currencies ) ) {
|
||||
$currencies = implode( ', ', $manual_currencies );
|
||||
// translators: %s List of currencies that are already translated in WooCommerce core.
|
||||
$this->add_admin_notice( 'currency_changed', 'notice notice-warning', sprintf( __( 'The store currency was recently changed. The following currencies are set to manual rates and may need updates: %s', 'woocommerce-payments' ), $currencies ), true );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,640 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Compatibility
|
||||
*
|
||||
* @package WooCommerce\Payments\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use Automattic\WooCommerce\Blocks\Package;
|
||||
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
|
||||
use Automattic\WooCommerce\Utilities\OrderUtil;
|
||||
use WC_Order;
|
||||
use WC_Order_Refund;
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencySettingsInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that contains Multi-Currency related support for WooCommerce analytics.
|
||||
*/
|
||||
class Analytics {
|
||||
const PRIORITY_EARLY = 1;
|
||||
const PRIORITY_DEFAULT = 10;
|
||||
const PRIORITY_LATE = 20;
|
||||
const PRIORITY_LATEST = 99999;
|
||||
const SCRIPT_NAME = 'WCPAY_MULTI_CURRENCY_ANALYTICS';
|
||||
|
||||
const SUPPORTED_CONTEXTS = [ 'orders', 'products', 'variations', 'categories', 'coupons', 'taxes' ];
|
||||
|
||||
/**
|
||||
* SQL string replacements made by the analytics Multi-Currency extension.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $sql_replacements = [];
|
||||
|
||||
/**
|
||||
* Instance of MultiCurrency.
|
||||
*
|
||||
* @var MultiCurrency $multi_currency
|
||||
*/
|
||||
private $multi_currency;
|
||||
|
||||
/**
|
||||
* Instance of MultiCurrencySettingsInterface.
|
||||
*
|
||||
* @var MultiCurrencySettingsInterface $settings_service
|
||||
*/
|
||||
private $settings_service;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param MultiCurrency $multi_currency Instance of MultiCurrency.
|
||||
* @param MultiCurrencySettingsInterface $settings_service Instance of MultiCurrencySettingsInterface.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, MultiCurrencySettingsInterface $settings_service ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->settings_service = $settings_service;
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise all actions, filters and hooks related to analytics support.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
if ( is_admin() && current_user_can( 'manage_woocommerce' ) ) {
|
||||
add_filter( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_scripts' ] );
|
||||
$this->register_customer_currencies();
|
||||
}
|
||||
|
||||
if ( $this->settings_service->is_dev_mode() ) {
|
||||
add_filter( 'woocommerce_analytics_report_should_use_cache', [ $this, 'disable_report_caching' ] );
|
||||
}
|
||||
|
||||
// Add a filter when the order stats table is updated.
|
||||
add_filter( 'woocommerce_analytics_update_order_stats_data', [ $this, 'update_order_stats_data' ], self::PRIORITY_LATEST, 2 );
|
||||
|
||||
// Add filters when the query args are updated.
|
||||
add_filter( 'woocommerce_analytics_orders_query_args', [ $this, 'apply_customer_currency_args' ] );
|
||||
add_filter( 'woocommerce_analytics_orders_stats_query_args', [ $this, 'apply_customer_currency_args' ] );
|
||||
|
||||
// If we aren't making a REST request, or no multi currency orders exist in the merchant's store,
|
||||
// return before adding these filters.
|
||||
|
||||
if ( ! WC()->is_rest_api_request() || ! $this->has_multi_currency_orders() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->set_sql_replacements();
|
||||
|
||||
// Add the filters that are applied in each analytics query.
|
||||
add_filter( 'woocommerce_analytics_clauses_select', [ $this, 'filter_select_clauses' ], self::PRIORITY_LATE, 2 );
|
||||
add_filter( 'woocommerce_analytics_clauses_join', [ $this, 'filter_join_clauses' ], self::PRIORITY_LATE, 2 );
|
||||
|
||||
// Add the WHERE clause filter which is applied only on Order related queries.
|
||||
add_filter( 'woocommerce_analytics_clauses_where_orders_subquery', [ $this, 'filter_where_clauses' ] );
|
||||
add_filter( 'woocommerce_analytics_clauses_where_orders_stats_total', [ $this, 'filter_where_clauses' ] );
|
||||
add_filter( 'woocommerce_analytics_clauses_where_orders_stats_interval', [ $this, 'filter_where_clauses' ] );
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! empty( $_GET['currency'] ) && $_GET['currency'] !== $this->multi_currency->get_default_currency()->get_code() ) {
|
||||
add_filter( 'woocommerce_analytics_clauses_select_orders_subquery', [ $this, 'filter_select_orders_clauses' ] );
|
||||
add_filter( 'woocommerce_analytics_clauses_select_orders_stats_total', [ $this, 'filter_select_orders_clauses' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the CSS and JS scripts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_admin_scripts() {
|
||||
$this->multi_currency->register_script_with_dependencies( self::SCRIPT_NAME, 'dist/multi-currency-analytics' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the list of currencies used on the store to the wcSettings to allow it to be accessed by the front-end JS script.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_customer_currencies() {
|
||||
$data_registry = Package::container()->get( AssetDataRegistry::class );
|
||||
if ( $data_registry->exists( 'customerCurrencies' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$currencies = $this->multi_currency->get_all_customer_currencies();
|
||||
$available_currencies = $this->multi_currency->get_available_currencies();
|
||||
$currency_options = [];
|
||||
|
||||
$default_currency = $this->multi_currency->get_default_currency();
|
||||
|
||||
// Add default currency to the list if it does not exist.
|
||||
if ( ! in_array( $default_currency->get_code(), $currencies, true ) ) {
|
||||
$currencies[] = $default_currency->get_code();
|
||||
}
|
||||
|
||||
foreach ( $currencies as $currency ) {
|
||||
if ( ! isset( $available_currencies[ $currency ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currency_details = $available_currencies[ $currency ];
|
||||
$currency_options[] = [
|
||||
'label' => html_entity_decode( $currency_details->get_name() ),
|
||||
'value' => $currency_details->get_code(),
|
||||
];
|
||||
}
|
||||
|
||||
$data_registry->add( 'customerCurrencies', $currency_options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts used on the analytics WP Admin pages.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function enqueue_admin_scripts() {
|
||||
$this->register_admin_scripts();
|
||||
|
||||
wp_enqueue_script( self::SCRIPT_NAME );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables report caching. Used for development of analytics related functionality.
|
||||
* To disable report caching
|
||||
*
|
||||
* @param array $args Filter arguments.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function disable_report_caching( $args ): bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the customer currency is set, add it as a query parameter to requests to the data store.
|
||||
* This ensures that the cache is updated when this value is changed between requests.
|
||||
*
|
||||
* @param array $args The arguments passed in via the GET request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function apply_customer_currency_args( $args ): array {
|
||||
$currency_args = $this->get_customer_currency_args_from_request();
|
||||
return array_merge( $args, $currency_args );
|
||||
}
|
||||
|
||||
/**
|
||||
* When an order is updated in the stats table, perform a check to see if it is a Multi-Currency order
|
||||
* and convert the information into the store's default currency if it is.
|
||||
*
|
||||
* @param array $args - An array of the arguments to be inserted into the order stats table.
|
||||
* @param WC_Order $order - The order itself.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function update_order_stats_data( array $args, $order ): array {
|
||||
if ( ! $this->should_convert_order_stats( $order ) ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$stripe_exchange_rate = $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true )
|
||||
? (float) $order->get_meta( '_wcpay_multi_currency_stripe_exchange_rate', true )
|
||||
: null;
|
||||
$order_exchange_rate = ( 1 / (float) $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) );
|
||||
|
||||
$exchange_rate = $stripe_exchange_rate ?? $order_exchange_rate;
|
||||
|
||||
$dp = wc_get_price_decimals();
|
||||
$args['net_total'] = round( $this->convert_amount( (float) $args['net_total'], $exchange_rate ), $dp );
|
||||
$args['shipping_total'] = round( $this->convert_amount( (float) $args['shipping_total'], $exchange_rate ), $dp );
|
||||
$args['tax_total'] = round( $this->convert_amount( (float) $args['tax_total'], $exchange_rate ), $dp );
|
||||
$args['total_sales'] = $args['net_total'] + $args['shipping_total'] + $args['tax_total'];
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add columns to get the currency and converted amount (if required).
|
||||
*
|
||||
* @param string[] $clauses - An array containing the SELECT clauses to be applied.
|
||||
* @param string $context - The context in which this SELECT clause is being called.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function filter_select_clauses( array $clauses, $context ): array {
|
||||
// If we are unable to identify a context, just return the clauses as is.
|
||||
if ( is_null( $context ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_select_clauses', false ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$context_parts = explode( '_', $context );
|
||||
$context_page = $context_parts[0] ?? 'generic';
|
||||
$context_type = $context_parts[1] ?? null;
|
||||
|
||||
// If we can't identify the type of context we are running in (stats or subquery), then return the clauses as is.
|
||||
if ( ! in_array( $context_type, [ 'stats', 'subquery' ], true ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$new_clauses = [];
|
||||
$sql_replacements = $this->get_sql_replacements();
|
||||
|
||||
foreach ( $clauses as $clause ) {
|
||||
if ( ! array_key_exists( $context_page, $sql_replacements ) ) {
|
||||
$replacements_array = $sql_replacements['generic'] ?? [];
|
||||
} else {
|
||||
$replacements_array = $sql_replacements[ $context_page ] ?? [];
|
||||
}
|
||||
|
||||
foreach ( $replacements_array as $find => $replace ) {
|
||||
if ( strpos( $clause, $find ) !== false ) {
|
||||
$clause = str_replace(
|
||||
$find,
|
||||
$replace,
|
||||
$clause
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$new_clauses[] = $clause;
|
||||
}
|
||||
|
||||
if ( $this->is_supported_context( $context ) && ( in_array( $context_page, self::SUPPORTED_CONTEXTS, true ) || $this->is_order_stats_table_used_in_clauses( $clauses ) ) ) {
|
||||
if ( $this->is_cot_enabled() ) {
|
||||
$new_clauses[] = ', wcpay_multicurrency_order_currency.currency AS order_currency';
|
||||
} else {
|
||||
$new_clauses[] = ', wcpay_multicurrency_currency_meta.meta_value AS order_currency';
|
||||
}
|
||||
$new_clauses[] = ', wcpay_multicurrency_default_currency_meta.meta_value AS order_default_currency';
|
||||
$new_clauses[] = ', wcpay_multicurrency_exchange_rate_meta.meta_value AS exchange_rate';
|
||||
$new_clauses[] = ', wcpay_multicurrency_stripe_exchange_rate_meta.meta_value AS stripe_exchange_rate';
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_select_clauses', $new_clauses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a JOIN so that we can get the currency information.
|
||||
*
|
||||
* @param string[] $clauses - An array containing the JOIN clauses to be applied.
|
||||
* @param string $context - The context in which this SELECT clause is being called.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function filter_join_clauses( array $clauses, $context ): array {
|
||||
global $wpdb;
|
||||
|
||||
if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_join_clauses', false ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$context_parts = explode( '_', $context, 2 );
|
||||
$context_page = $context_parts[0] ?? 'generic';
|
||||
|
||||
$prefix = 'wcpay_multicurrency_';
|
||||
$currency_tbl = $prefix . 'currency_meta';
|
||||
$default_currency_tbl = $prefix . 'default_currency_meta';
|
||||
$exchange_rate_tbl = $prefix . 'exchange_rate_meta';
|
||||
$stripe_exchange_rate_tbl = $prefix . 'stripe_exchange_rate_meta';
|
||||
|
||||
// Allow this to work with custom order tables as well.
|
||||
if ( $this->is_cot_enabled() ) {
|
||||
$meta_table = $wpdb->prefix . 'wc_orders_meta';
|
||||
$id_field = 'order_id';
|
||||
|
||||
$currency_tbl = $prefix . 'order_currency';
|
||||
} else {
|
||||
$meta_table = $wpdb->postmeta;
|
||||
$id_field = 'post_id';
|
||||
}
|
||||
|
||||
// If this is a supported context, add the joins. If this is an unsupported context, see if we can add the joins.
|
||||
if ( $this->is_supported_context( $context ) && ( in_array( $context_page, self::SUPPORTED_CONTEXTS, true ) || $this->is_order_stats_table_used_in_clauses( $clauses ) ) ) {
|
||||
if ( $this->is_cot_enabled() ) {
|
||||
$clauses[] = "LEFT JOIN {$wpdb->prefix}wc_orders {$currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$currency_tbl}.id";
|
||||
} else {
|
||||
$clauses[] = "LEFT JOIN {$meta_table} {$currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$currency_tbl}.{$id_field} AND {$currency_tbl}.meta_key = '_order_currency'";
|
||||
|
||||
}
|
||||
$clauses[] = "LEFT JOIN {$meta_table} {$default_currency_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$default_currency_tbl}.{$id_field} AND {$default_currency_tbl}.meta_key = '_wcpay_multi_currency_order_default_currency'";
|
||||
$clauses[] = "LEFT JOIN {$meta_table} {$exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$exchange_rate_tbl}.{$id_field} AND {$exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_order_exchange_rate'";
|
||||
$clauses[] = "LEFT JOIN {$meta_table} {$stripe_exchange_rate_tbl} ON {$wpdb->prefix}wc_order_stats.order_id = {$stripe_exchange_rate_tbl}.{$id_field} AND {$stripe_exchange_rate_tbl}.meta_key = '_wcpay_multi_currency_stripe_exchange_rate'";
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_join_clauses', $clauses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the WHERE clauses (if a customer currency has been selected).
|
||||
*
|
||||
* @param string[] $clauses - An array containing the JOIN clauses to be applied.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function filter_where_clauses( array $clauses ): array {
|
||||
if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_where_clauses', false ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
$prefix = 'wcpay_multicurrency_';
|
||||
if ( $this->is_cot_enabled() ) {
|
||||
$currency_field = $prefix . 'order_currency.currency';
|
||||
} else {
|
||||
$currency_field = $prefix . 'currency_meta.meta_value';
|
||||
}
|
||||
|
||||
$currency_args = $this->get_customer_currency_args_from_request();
|
||||
if ( ! empty( $currency_args['currency_is'] ) ) {
|
||||
/**
|
||||
* Skip implode complaining array_map as wrong argument.
|
||||
*
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
$currency_is = sprintf( "'%s'", implode( "', '", array_map( 'esc_sql', $currency_args['currency_is'] ) ) );
|
||||
$clauses[] = "AND {$currency_field} IN ({$currency_is})";
|
||||
}
|
||||
|
||||
if ( ! empty( $currency_args['currency_is_not'] ) ) {
|
||||
/**
|
||||
* Skip implode complaining array_map as wrong argument.
|
||||
*
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
$currency_is_not = sprintf( "'%s'", implode( "', '", array_map( 'esc_sql', $currency_args['currency_is_not'] ) ) );
|
||||
$clauses[] = "AND {$currency_field} NOT IN ({$currency_is_not})";
|
||||
}
|
||||
|
||||
if ( ! empty( $currency_args['currency'] ) ) {
|
||||
global $wpdb;
|
||||
$expression = "AND {$currency_field} = '%s'";
|
||||
$clauses[] = $wpdb->prepare( $expression, $currency_args['currency'] ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_where_clauses', $clauses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert amounts back to order currency (if a currency has been selected).
|
||||
* Skipping it for default currency.
|
||||
*
|
||||
* @param string[] $clauses - An array containing the SELECT orders clauses to be applied.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_select_orders_clauses( array $clauses ): array {
|
||||
if ( apply_filters( MultiCurrency::FILTER_PREFIX . 'disable_filter_select_orders_clauses', false ) ) {
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
$exchange_rate = 'wcpay_multicurrency_exchange_rate_meta.meta_value';
|
||||
$stripe_exchange_rate = 'wcpay_multicurrency_stripe_exchange_rate_meta.meta_value';
|
||||
$net_total = "{$wpdb->prefix}wc_order_stats.net_total";
|
||||
|
||||
foreach ( $clauses as $k => $clause ) {
|
||||
if ( strpos( $clause, $net_total ) !== false ) {
|
||||
$is_orders_subquery = strpos( $clause, $net_total . ',' ) !== false;
|
||||
$variable = $is_orders_subquery ? "$net_total," : $net_total;
|
||||
$alias = $is_orders_subquery ? ' as net_total,' : '';
|
||||
$dp = wc_get_price_decimals();
|
||||
|
||||
$clauses[ $k ] = str_replace(
|
||||
$variable,
|
||||
$this->generate_case_when(
|
||||
$stripe_exchange_rate,
|
||||
"ROUND($net_total / $stripe_exchange_rate, $dp)",
|
||||
"ROUND($net_total * $exchange_rate, $dp)"
|
||||
) . $alias,
|
||||
$clause
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'filter_select_orders_clauses', $clauses );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see whether we should convert an order to store in the order stats table.
|
||||
*
|
||||
* @param WC_Order|WC_Order_Refund $order The order.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function should_convert_order_stats( $order ): bool {
|
||||
$default_currency = $this->multi_currency->get_default_currency();
|
||||
|
||||
// If this order was in the default currency, or the meta information isn't set on the order, return false.
|
||||
if ( ! $order ||
|
||||
$order->get_currency() === $default_currency->get_code() ||
|
||||
! $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) ||
|
||||
$order->get_meta( '_wcpay_multi_currency_order_default_currency', true ) !== $default_currency->get_code()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an amount to the store's default currency in order to store in the stats table.
|
||||
*
|
||||
* @param float $amount The amount to convert into the store's default currency.
|
||||
* @param float $exchange_rate The exchange rate to use for the conversion.
|
||||
*
|
||||
* @return float The converted amount.
|
||||
*/
|
||||
private function convert_amount( float $amount, float $exchange_rate ): float {
|
||||
return $amount * $exchange_rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the order stats table is referenced in the clauses, to work out whether
|
||||
* to add the JOIN columns for Multi-Currency.
|
||||
*
|
||||
* @param array $clauses The array containing the clauses used.
|
||||
*
|
||||
* @return boolean Whether the order stats table is referenced.
|
||||
*/
|
||||
private function is_order_stats_table_used_in_clauses( array $clauses ): bool {
|
||||
global $wpdb;
|
||||
|
||||
foreach ( $clauses as $clause ) {
|
||||
if ( strpos( $clause, "{$wpdb->prefix}wc_order_stats" ) !== false ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* There are some queries which are made in the analytics which are actually sub-queries
|
||||
* which are used to join on an individual item/coupon/tax code. In these cases, rather than
|
||||
* the context being the expected format e.g. product_stats_total, it will simply be 'product'.
|
||||
* In these cases, we don't want to add the join columns or select them.
|
||||
*
|
||||
* @param string $context The context the query was made in.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function is_supported_context( string $context ): bool {
|
||||
$unsupported_contexts = [ 'products', 'coupons', 'taxes', 'variations', 'categories' ];
|
||||
|
||||
if ( in_array( $context, $unsupported_contexts, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a case when statement using the provided variables.
|
||||
*
|
||||
* @param string $variable The SQL variable we want to check for NULL.
|
||||
* @param string $then The THEN clause.
|
||||
* @param string $else The ELSE clause.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function generate_case_when( string $variable, string $then, string $else ): string {
|
||||
return "CASE WHEN {$variable} IS NOT NULL THEN {$then} ELSE {$else} END";
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform an SQL query to determine whether Multi Currency has ever been used on this store,
|
||||
* by checking how many orders are in the database where an exchange currency rate has been stored.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function has_multi_currency_orders() {
|
||||
global $wpdb;
|
||||
|
||||
// Using full SQL instead of variables to keep WPCS happy.
|
||||
if ( $this->is_cot_enabled() ) {
|
||||
$result = $wpdb->get_var(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM {$wpdb->prefix}wc_orders_meta
|
||||
WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate'
|
||||
LIMIT 1)
|
||||
AS count;"
|
||||
);
|
||||
} else {
|
||||
$result = $wpdb->get_var(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE meta_key = '_wcpay_multi_currency_order_exchange_rate'
|
||||
LIMIT 1)
|
||||
AS count;"
|
||||
);
|
||||
}
|
||||
|
||||
return intval( $result ) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SQL replacements variable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_sql_replacements(): array {
|
||||
return $this->sql_replacements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the passed in query params to see if currency has been passed in.
|
||||
* Will return null if no currency variable was passed in, otherwise will
|
||||
* return the currency.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_customer_currency_args_from_request(): array {
|
||||
$args = [
|
||||
'currency_is' => [],
|
||||
'currency_is_not' => [],
|
||||
'currency' => null,
|
||||
];
|
||||
|
||||
/* phpcs:disable WordPress.Security.NonceVerification */
|
||||
if ( isset( $_GET['currency_is'] ) && is_array( $_GET['currency_is'] ) ) {
|
||||
$args['currency_is'] = array_map( 'sanitize_text_field', wp_unslash( $_GET['currency_is'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $_GET['currency_is_not'] ) ) {
|
||||
$args['currency_is_not'] = array_map( 'sanitize_text_field', wp_unslash( $_GET['currency_is_not'] ) );
|
||||
}
|
||||
|
||||
if ( isset( $_GET['currency'] ) ) {
|
||||
$args['currency'] = sanitize_text_field( wp_unslash( $_GET['currency'] ) );
|
||||
}
|
||||
/* phpcs:enable WordPress.Security.NonceVerification */
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the SQL replacements variable.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function set_sql_replacements() {
|
||||
$default_currency = 'wcpay_multicurrency_default_currency_meta.meta_value';
|
||||
$exchange_rate = 'wcpay_multicurrency_exchange_rate_meta.meta_value';
|
||||
$stripe_exchange_rate = 'wcpay_multicurrency_stripe_exchange_rate_meta.meta_value';
|
||||
|
||||
$discount_amount = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(discount_amount * {$stripe_exchange_rate}, 2)", "ROUND(discount_amount * (1 / {$exchange_rate} ), 2)" ), 'discount_amount' );
|
||||
$product_net_revenue = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(product_net_revenue * {$stripe_exchange_rate}, 2)", "ROUND(product_net_revenue * (1 / {$exchange_rate} ), 2)" ), 'product_net_revenue' );
|
||||
$product_gross_revenue = $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(product_gross_revenue * {$stripe_exchange_rate}, 2)", "ROUND(product_gross_revenue * (1 / {$exchange_rate} ), 2)" ), 'product_gross_revenue' );
|
||||
|
||||
$this->sql_replacements = [
|
||||
'generic' => [
|
||||
'discount_amount' => $discount_amount,
|
||||
'product_net_revenue' => $product_net_revenue,
|
||||
'product_gross_revenue' => $product_gross_revenue,
|
||||
],
|
||||
'orders' => [
|
||||
'discount_amount' => $discount_amount,
|
||||
],
|
||||
'products' => [
|
||||
'product_net_revenue' => $product_net_revenue,
|
||||
'product_gross_revenue' => $product_gross_revenue,
|
||||
],
|
||||
'variations' => [
|
||||
'product_net_revenue' => $product_net_revenue,
|
||||
'product_gross_revenue' => $product_gross_revenue,
|
||||
],
|
||||
'categories' => [
|
||||
'product_net_revenue' => $product_net_revenue,
|
||||
'product_gross_revenue' => $product_gross_revenue,
|
||||
],
|
||||
'taxes' => [
|
||||
'SUM(total_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(total_tax * {$stripe_exchange_rate}, 2)", "ROUND(total_tax * (1 / {$exchange_rate} ), 2)" ), 'total_tax' ) . ')',
|
||||
'SUM(order_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(order_tax * {$stripe_exchange_rate}, 2)", "ROUND(order_tax * (1 / {$exchange_rate} ), 2)" ), 'order_tax' ) . ')',
|
||||
'SUM(shipping_tax)' => 'SUM(' . $this->generate_case_when( $default_currency, $this->generate_case_when( $stripe_exchange_rate, "ROUND(shipping_tax * {$stripe_exchange_rate}, 2)", "ROUND(shipping_tax * (1 / {$exchange_rate} ), 2)" ), 'shipping_tax' ) . ')',
|
||||
],
|
||||
'coupons' => [
|
||||
'discount_amount' => $discount_amount,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether Custom Order Tables are enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_cot_enabled(): bool {
|
||||
return class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled();
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency Backend Currencies
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that formats Multi-Currency currencies on the backend.
|
||||
*/
|
||||
class BackendCurrencies {
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* MultiCurrencyLocalizationInterface instance.
|
||||
*
|
||||
* @var MultiCurrencyLocalizationInterface
|
||||
*/
|
||||
protected $localization_service;
|
||||
|
||||
/**
|
||||
* Multi-Currency currency formatting map.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $currency_format = [];
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
* @param MultiCurrencyLocalizationInterface $localization_service The Localization Service instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, MultiCurrencyLocalizationInterface $localization_service ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->localization_service = $localization_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
// Skip if no additional currencies are enabled.
|
||||
if ( ! $this->multi_currency->has_additional_currencies_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to check first if it's a request coming from the backend, frontend REST requests shouldn't be
|
||||
// affected by this.
|
||||
$is_backend_request = 0 === stripos( wp_get_referer(), admin_url() );
|
||||
|
||||
// Add the filter if it's an admin request or a REST request from the admin side.
|
||||
if ( ( is_admin() || ( WC()->is_rest_api_request() && $is_backend_request ) ) && ! defined( 'DOING_CRON' ) ) {
|
||||
// Currency hooks. Be aware that this should not run after Explicit Price hook, its priority should be less
|
||||
// than explicit price hooks to run before them.
|
||||
add_filter( 'wc_price_args', [ $this, 'build_wc_price_args' ], 50 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the store currency code.
|
||||
*
|
||||
* @return string The store currency code
|
||||
*/
|
||||
public function get_store_currency(): string {
|
||||
return get_woocommerce_currency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency code to be used by the formatter.
|
||||
*
|
||||
* @param array $args The arguments containing the currency code.
|
||||
*
|
||||
* @return string The code of the currency to be used.
|
||||
*/
|
||||
public function get_price_currency( $args ): string {
|
||||
return $args['currency'] ?? $this->get_store_currency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of decimals to be used by the formatter.
|
||||
*
|
||||
* @param string $currency_code The currency code to fetch formatting specs.
|
||||
*
|
||||
* @return int The number of decimals.
|
||||
*/
|
||||
public function get_price_decimals( $currency_code ): int {
|
||||
return absint( $this->localization_service->get_currency_format( $currency_code )['num_decimals'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the currency is a zero decimal point currency.
|
||||
*
|
||||
* @param string $currency_code The currency code to fetch formatting specs.
|
||||
*
|
||||
* @return bool Whether the currency is a zero decimal point currency or not.
|
||||
*/
|
||||
public function is_zero_decimal_currency( $currency_code ): bool {
|
||||
return 0 === $this->get_price_decimals( $currency_code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decimal separator to be used by the formatter.
|
||||
*
|
||||
* @param string $currency_code The currency code to fetch formatting specs.
|
||||
*
|
||||
* @return string The decimal separator.
|
||||
*/
|
||||
public function get_price_decimal_separator( $currency_code ): string {
|
||||
return $this->localization_service->get_currency_format( $currency_code )['decimal_sep'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thousand separator to be used by the formatter.
|
||||
*
|
||||
* @param string $currency_code The currency code to fetch formatting specs.
|
||||
*
|
||||
* @return string The thousand separator.
|
||||
*/
|
||||
public function get_price_thousand_separator( $currency_code ): string {
|
||||
return $this->localization_service->get_currency_format( $currency_code )['thousand_sep'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency format to be used by the formatter.
|
||||
*
|
||||
* @param string $currency_code The currency code to fetch formatting specs.
|
||||
*
|
||||
* @return string The currency format.
|
||||
*/
|
||||
public function get_woocommerce_price_format( $currency_code ): string {
|
||||
$currency_pos = $this->localization_service->get_currency_format( $currency_code )['currency_pos'];
|
||||
|
||||
switch ( $currency_pos ) {
|
||||
case 'left':
|
||||
return '%1$s%2$s';
|
||||
case 'right':
|
||||
return '%2$s%1$s';
|
||||
case 'left_space':
|
||||
return '%1$s %2$s';
|
||||
case 'right_space':
|
||||
return '%2$s %1$s';
|
||||
default:
|
||||
return '%1$s%2$s';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatting arguments for the given currency using the locale-info
|
||||
*
|
||||
* @param array $args Currency formatter original arguments.
|
||||
*
|
||||
* @return array New arguments matching with the locale-info
|
||||
*/
|
||||
public function build_wc_price_args( $args ) {
|
||||
$currency_code = $this->get_price_currency( $args );
|
||||
|
||||
return wp_parse_args(
|
||||
[
|
||||
'currency' => $currency_code,
|
||||
'decimal_separator' => $this->get_price_decimal_separator( $this->get_store_currency() ),
|
||||
'thousand_separator' => $this->get_price_thousand_separator( $this->get_store_currency() ),
|
||||
'decimals' => $this->is_zero_decimal_currency( $currency_code ) ? 0 : $this->get_price_decimals( $currency_code ),
|
||||
'price_format' => $this->get_woocommerce_price_format( $currency_code ),
|
||||
],
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Compatibility
|
||||
*
|
||||
* @package WooCommerce\Payments\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WC_Order;
|
||||
use WC_Order_Refund;
|
||||
use WCPay\MultiCurrency\Compatibility\BaseCompatibility;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceBookings;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceFedEx;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceNameYourPrice;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommercePreOrders;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceProductAddOns;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceSubscriptions;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceUPS;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommerceDeposits;
|
||||
use WCPay\MultiCurrency\Compatibility\WooCommercePointsAndRewards;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that controls Multi-Currency Compatibility.
|
||||
*/
|
||||
class Compatibility extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Compatibility classes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $compatibility_classes = [];
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
add_action( 'init', [ $this, 'init_compatibility_classes' ], 11 );
|
||||
|
||||
if ( defined( 'DOING_CRON' ) ) {
|
||||
add_filter( 'woocommerce_admin_sales_record_milestone_enabled', [ $this, 'attach_order_modifier' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes our compatibility classes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_compatibility_classes() {
|
||||
if ( 1 < count( $this->multi_currency->get_enabled_currencies() ) ) {
|
||||
$this->compatibility_classes[] = new WooCommerceBookings( $this->multi_currency, $this->utils, $this->multi_currency->get_frontend_currencies() );
|
||||
$this->compatibility_classes[] = new WooCommerceFedEx( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommerceNameYourPrice( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommercePreOrders( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommerceProductAddOns( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommerceSubscriptions( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommerceUPS( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommerceDeposits( $this->multi_currency, $this->utils );
|
||||
$this->compatibility_classes[] = new WooCommercePointsAndRewards( $this->multi_currency, $this->utils );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the compatibility classes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_compatibility_classes(): array {
|
||||
return $this->compatibility_classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the selected currency needs to be overridden.
|
||||
*
|
||||
* @return mixed Three letter currency code or false if not.
|
||||
*/
|
||||
public function override_selected_currency() {
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'override_selected_currency', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* Deprecated method, please use should_disable_currency_switching.
|
||||
*
|
||||
* @return bool False if it shouldn't be hidden, true if it should.
|
||||
*/
|
||||
public function should_hide_widgets(): bool {
|
||||
wc_deprecated_function( __FUNCTION__, '6.5.0', 'Compatibility::should_disable_currency_switching' );
|
||||
return $this->should_disable_currency_switching();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if currency switching should be disabled, such as the widgets and the automatic geolocation switching.
|
||||
*
|
||||
* @return bool False if no, true if yes.
|
||||
*/
|
||||
public function should_disable_currency_switching(): bool {
|
||||
$return = false;
|
||||
|
||||
/**
|
||||
* If the pay_for_order parameter is set, we disable currency switching.
|
||||
*
|
||||
* WooCommerce itself handles all the heavy lifting and verification on the Order Pay page, we just need to
|
||||
* make sure the currency switchers are not displayed. This is due to once the order is created, the currency
|
||||
* itself should remain static.
|
||||
*/
|
||||
if ( isset( $_GET['pay_for_order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$return = true;
|
||||
}
|
||||
|
||||
// If someone has hooked into the deprecated filter, throw a notice and then apply the filtering.
|
||||
if ( has_action( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets' ) ) {
|
||||
wc_deprecated_hook( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', '6.5.0', MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching' );
|
||||
$return = apply_filters( MultiCurrency::FILTER_PREFIX . 'should_hide_widgets', $return );
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', $return );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the coupon's amount should be converted.
|
||||
*
|
||||
* @param object $coupon Coupon object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_coupon_amount( $coupon = null ): bool {
|
||||
if ( ! $coupon ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_convert_coupon_amount', true, $coupon );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param object $product Product object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( $product = null ): bool {
|
||||
if ( ! $product ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', true, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the store currency should be returned or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_return_store_currency(): bool {
|
||||
return apply_filters( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', false );
|
||||
}
|
||||
|
||||
/**
|
||||
* This filter is called when the best sales day logic is called. We use it to add another filter which will
|
||||
* convert the order prices used in this inbox notification.
|
||||
*
|
||||
* @param bool $arg Whether or not the best sales day logic should execute. We will just return this as is to
|
||||
* respect the existing behaviour.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function attach_order_modifier( $arg ) {
|
||||
// Attach our filter to modify the order prices.
|
||||
add_filter( 'woocommerce_order_query', [ $this, 'convert_order_prices' ] );
|
||||
|
||||
// This will be a bool value indication whether the best day logic should be run. Let's just return it as is.
|
||||
return $arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* When a request is made by the "Best Sales Day" Inbox notification, we want to hook into this and convert
|
||||
* the order totals to the store default currency.
|
||||
*
|
||||
* @param WC_Order[]|WC_Order_Refund[] $results The results returned by the orders query.
|
||||
*
|
||||
* @return array|object of WC_Order objects
|
||||
*/
|
||||
public function convert_order_prices( $results ) {
|
||||
$backtrace_calls = [
|
||||
'Automattic\WooCommerce\Admin\Notes\NewSalesRecord::sum_sales_for_date',
|
||||
'Automattic\WooCommerce\Admin\Notes\NewSalesRecord::possibly_add_note',
|
||||
];
|
||||
|
||||
// If the results are not an array, or if the call we're expecting isn't in the backtrace, then just do nothing and return the results.
|
||||
if ( ! is_array( $results ) || ! $this->utils->is_call_in_backtrace( $backtrace_calls ) ) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
$default_currency = $this->multi_currency->get_default_currency();
|
||||
if ( ! $default_currency ) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
foreach ( $results as $order ) {
|
||||
if ( ! $order ||
|
||||
$order->get_currency() === $default_currency->get_code() ||
|
||||
! $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true ) ||
|
||||
$order->get_meta( '_wcpay_multi_currency_order_default_currency', true ) !== $default_currency->get_code()
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exchange_rate = $order->get_meta( '_wcpay_multi_currency_order_exchange_rate', true );
|
||||
$order->set_total( number_format( $order->get_total() * ( 1 / $exchange_rate ), wc_get_price_decimals() ) );
|
||||
}
|
||||
|
||||
remove_filter( 'woocommerce_order_query', [ $this, 'convert_order_prices' ] );
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
/**
|
||||
* Class BaseCompatibility
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
use WCPay\MultiCurrency\Utils;
|
||||
|
||||
/**
|
||||
* Class that sets up base options for compatibility classes.
|
||||
*/
|
||||
abstract class BaseCompatibility {
|
||||
|
||||
/**
|
||||
* MultiCurrency class.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Utils class.
|
||||
*
|
||||
* @var Utils
|
||||
*/
|
||||
protected $utils;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency MultiCurrency class.
|
||||
* @param Utils $utils Utils class.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, Utils $utils ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->utils = $utils;
|
||||
$this->init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function init();
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceBookings
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\FrontendCurrencies;
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
use WCPay\MultiCurrency\Utils;
|
||||
use WC_Product;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Bookings Plugin.
|
||||
*/
|
||||
class WooCommerceBookings extends BaseCompatibility {
|
||||
/**
|
||||
* Front-end currencies.
|
||||
*
|
||||
* @var FrontendCurrencies
|
||||
*/
|
||||
private $frontend_currencies;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency MultiCurrency class.
|
||||
* @param Utils $utils Utils class.
|
||||
* @param FrontendCurrencies $frontend_currencies FrontendCurrencies class.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, Utils $utils, FrontendCurrencies $frontend_currencies ) {
|
||||
parent::__construct( $multi_currency, $utils );
|
||||
$this->frontend_currencies = $frontend_currencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if Bookings is active.
|
||||
if ( class_exists( 'WC_Bookings' ) ) {
|
||||
if ( ! is_admin() || wp_doing_ajax() ) {
|
||||
add_filter( 'woocommerce_bookings_calculated_booking_cost', [ $this, 'adjust_amount_for_calculated_booking_cost' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_get_block_cost', [ $this, 'get_price' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_get_cost', [ $this, 'get_price' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_get_display_cost', [ $this, 'get_price' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_booking_person_type_get_block_cost', [ $this, 'get_price' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_booking_person_type_get_cost', [ $this, 'get_price' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_get_resource_base_costs', [ $this, 'get_resource_prices' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_get_resource_block_costs', [ $this, 'get_resource_prices' ], 50, 1 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
|
||||
add_action( 'wp_ajax_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 );
|
||||
add_action( 'wp_ajax_nopriv_wc_bookings_calculate_costs', [ $this, 'add_wc_price_args_filter_for_ajax' ], 9 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the calculated booking cost for the selected currency, applying rounding and charm pricing as necessary.
|
||||
*
|
||||
* @param mixed $costs The original calculated booking costs.
|
||||
* @return mixed The booking cost adjusted for the selected currency.
|
||||
*/
|
||||
public function adjust_amount_for_calculated_booking_cost( $costs ) {
|
||||
/**
|
||||
* Prevents adjustment of the calculated booking cost during cart addition.
|
||||
*
|
||||
* When a booking is added to the cart, the Booking plugin calculates the booking cost and
|
||||
* overrides the cart item price with this calculated amount. To avoid interfering with this process,
|
||||
* this function skips any additional adjustments at this stage.
|
||||
*/
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) ) {
|
||||
return $costs;
|
||||
}
|
||||
|
||||
return $this->multi_currency->adjust_amount_for_selected_currency( $costs );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the price for an item, converting it based on the selected currency and context.
|
||||
*
|
||||
* @param mixed $price The item's price.
|
||||
*
|
||||
* @return mixed The converted item's price.
|
||||
*/
|
||||
public function get_price( $price ) {
|
||||
if ( ! $price ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
// Skip conversion during specific booking cost calculations to avoid double conversion.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Cart->add_to_cart' ] ) && $this->utils->is_call_in_backtrace( [ 'WC_Bookings_Cost_Calculation::calculate_booking_cost' ] ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
/**
|
||||
* When showing the price in HTML, the function applies currency conversion, charm pricing,
|
||||
* and rounding. For internal calculations, it uses the raw exchange rate, with charm pricing
|
||||
* and rounding adjustments applied only to the final calculated amount (handled in
|
||||
* adjust_amount_for_calculated_booking_cost).
|
||||
*/
|
||||
return $this->multi_currency->get_price( $price, $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ? 'product' : 'exchange_rate' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the prices for a resource.
|
||||
*
|
||||
* @param mixed $prices The resource's prices in array format.
|
||||
*
|
||||
* @return mixed The converted resource's prices.
|
||||
*/
|
||||
public function get_resource_prices( $prices ) {
|
||||
if ( is_array( $prices ) ) {
|
||||
foreach ( $prices as $key => $price ) {
|
||||
$prices[ $key ] = $this->get_price( $price );
|
||||
}
|
||||
}
|
||||
return $prices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the product's price or not. Default is true.
|
||||
* @param WC_Product $product The product instance being checked.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( bool $return, WC_Product $product ): bool {
|
||||
// If it's already false, or the product is not a booking, ignore it.
|
||||
if ( ! $return || $product->get_type() !== 'booking' ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
// Fixes price display on product page and in shop.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Product_Booking->get_price_html' ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a filter for when there is an ajax call to calculate the booking cost.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_wc_price_args_filter_for_ajax() {
|
||||
add_filter( 'wc_price_args', [ $this, 'filter_wc_price_args' ], 100 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formatting arguments to use when a booking price is calculated on the product.
|
||||
*
|
||||
* @param array $args Original args from wc_price().
|
||||
*
|
||||
* @return array New arguments matching the selected currency.
|
||||
*/
|
||||
public function filter_wc_price_args( $args ): array {
|
||||
return wp_parse_args(
|
||||
[
|
||||
'currency' => $this->multi_currency->get_selected_currency()->get_code(),
|
||||
'decimal_separator' => $this->frontend_currencies->get_price_decimal_separator( $args['decimal_separator'] ),
|
||||
'thousand_separator' => $this->frontend_currencies->get_price_thousand_separator( $args['thousand_separator'] ),
|
||||
'decimals' => $this->frontend_currencies->get_price_decimals( $args['decimals'] ),
|
||||
'price_format' => $this->frontend_currencies->get_woocommerce_price_format( $args['price_format'] ),
|
||||
],
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceDeposits
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WC_Product;
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Deposits Plugin.
|
||||
*/
|
||||
class WooCommerceDeposits extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
if ( class_exists( 'WC_Deposits' ) ) {
|
||||
/*
|
||||
* Multi-currency support was added to WooCommerce Deposits in version 2.0.1.
|
||||
*
|
||||
* This prevents the loading of the compatibility class for Deposits in versions
|
||||
* of Deposits that support multi-currency.
|
||||
*
|
||||
* @see https://github.com/woocommerce/woocommerce-deposits/pull/425
|
||||
* @see https://github.com/woocommerce/woocommerce-deposits/issues/506
|
||||
*/
|
||||
if ( version_compare( WC_DEPOSITS_VERSION, '2.0.1', '>=' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add compatibility filters here.
|
||||
add_action( 'woocommerce_deposits_create_order', [ $this, 'modify_order_currency' ] );
|
||||
add_filter( 'woocommerce_get_cart_contents', [ $this, 'modify_cart_item_deposit_amounts' ] );
|
||||
add_filter( 'woocommerce_product_get__wc_deposit_amount', [ $this, 'modify_cart_item_deposit_amount_meta' ], 10, 2 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'maybe_convert_product_prices_for_deposits' ], 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the currency for deposit amounts of each cart item, only applies if deposits are enabled on the product.
|
||||
*
|
||||
* @param array $cart_contents The items on the cart.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function modify_cart_item_deposit_amounts( $cart_contents ) {
|
||||
foreach ( $cart_contents as $cart_item_key => $cart_item ) {
|
||||
if ( ! empty( $cart_item['is_deposit'] ) && isset( $cart_item['deposit_amount'] ) ) {
|
||||
$deposit_amount = (float) $cart_item['deposit_amount'];
|
||||
$cart_contents[ $cart_item_key ]['deposit_amount'] = $this->multi_currency->get_price( $deposit_amount, 'product' );
|
||||
}
|
||||
}
|
||||
|
||||
return $cart_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the currency for deposit amounts of each cart item, only applies if deposits are enabled on the product.
|
||||
*
|
||||
* @param float $amount The amount to convert.
|
||||
* @param \WC_Product $product The product to check for.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
public function modify_cart_item_deposit_amount_meta( $amount, $product ) {
|
||||
if ( 'percent' === $this->get_product_deposit_type( $product ) && $this->utils->is_call_in_backtrace( [ 'WC_Deposits_Cart_Manager->deposits_form_output' ] ) ) {
|
||||
return $this->multi_currency->get_price( $amount, 'product' );
|
||||
}
|
||||
return $amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines if the product prices need to be converted when calculating totals,
|
||||
* if the product's deposit type is a payment plan, then it shouldn't convert it.
|
||||
*
|
||||
* @param bool $result The previous flag for converting the price.
|
||||
* @param \WC_Product $product The product to check for.
|
||||
*
|
||||
* @return bool Whether the price should be converted or not.
|
||||
*/
|
||||
public function maybe_convert_product_prices_for_deposits( $result, $product ) {
|
||||
if ( 'plan' === $this->get_product_deposit_type( $product ) && $this->utils->is_call_in_backtrace( [ 'WC_Cart->calculate_totals' ] ) ) {
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* When creating a new order for the remaining amount, forces the new order currency
|
||||
* to be the same with the deposited order currency.
|
||||
*
|
||||
* @param integer $order_id The created order ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function modify_order_currency( $order_id ) {
|
||||
// We need to get the original order from the first item meta.
|
||||
$order = wc_get_order( $order_id );
|
||||
$order_items = $order->get_items();
|
||||
$first_order_item = 0 < ( is_countable( $order_items ) ? count( $order_items ) : 0 ) ? reset( $order_items ) : null;
|
||||
|
||||
if ( ! $first_order_item ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the original order ID is attached to the order item.
|
||||
$original_order_id = wc_get_order_item_meta( $first_order_item->get_id(), '_original_order_id', true );
|
||||
if ( ! $original_order_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the original order still exists.
|
||||
$original_order = wc_get_order( $original_order_id );
|
||||
if ( ! $original_order ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the new and old order currencies, and match them if unmatched.
|
||||
$saved_currency = $order->get_currency( 'view' );
|
||||
$original_currency = $original_order->get_currency( 'view' );
|
||||
if ( $saved_currency !== $original_currency ) {
|
||||
$order->set_currency( $original_currency );
|
||||
$order->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the deposit type of a product if deposits are enabled for the product.
|
||||
*
|
||||
* @param \WC_Product $product The product to check.
|
||||
*
|
||||
* @return string|false The product deposit type if deposits are enabled on it, or false.
|
||||
*/
|
||||
private function get_product_deposit_type( $product ) {
|
||||
$product_has_deposit = class_exists( 'WC_Deposits_Product_Manager' ) && call_user_func( [ 'WC_Deposits_Product_Manager', 'deposits_enabled' ], $product );
|
||||
if ( $product_has_deposit ) {
|
||||
return call_user_func( [ 'WC_Deposits_Product_Manager', 'get_deposit_type' ], $product );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceFedEx
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce FedEx Plugin.
|
||||
*/
|
||||
class WooCommerceFedEx extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Calls to look for in the backtrace when determining whether
|
||||
* to return store currency or skip converting product prices.
|
||||
*/
|
||||
private const WC_SHIPPING_FEDEX_CALLS = [
|
||||
'WC_Shipping_Fedex->set_settings',
|
||||
'WC_Shipping_Fedex->per_item_shipping',
|
||||
'WC_Shipping_Fedex->box_shipping',
|
||||
'WC_Shipping_Fedex->get_fedex_api_request',
|
||||
'WC_Shipping_Fedex->get_fedex_requests',
|
||||
'WC_Shipping_Fedex->process_result',
|
||||
];
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if FedEx is active.
|
||||
if ( class_exists( 'WC_Shipping_Fedex_Init' ) ) {
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ] );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the product's price or not. Default is true.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( bool $return ): bool {
|
||||
// If it's already false, return it.
|
||||
if ( ! $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to return the store currency or not.
|
||||
*
|
||||
* @param bool $return Whether to return the store currency or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_return_store_currency( bool $return ): bool {
|
||||
// If it's already true, return it.
|
||||
if ( $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ( $this->utils->is_call_in_backtrace( self::WC_SHIPPING_FEDEX_CALLS ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceNameYourPrice
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Name Your Price Plugin.
|
||||
*/
|
||||
class WooCommerceNameYourPrice extends BaseCompatibility {
|
||||
|
||||
const NYP_CURRENCY = '_wcpay_multi_currency_nyp_currency';
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if Name Your Price is active.
|
||||
if ( class_exists( 'WC_Name_Your_Price' ) ) {
|
||||
// Convert meta prices.
|
||||
add_filter( 'wc_nyp_raw_minimum_price', [ $this, 'get_nyp_prices' ] );
|
||||
add_filter( 'wc_nyp_raw_maximum_price', [ $this, 'get_nyp_prices' ] );
|
||||
add_filter( 'wc_nyp_raw_suggested_price', [ $this, 'get_nyp_prices' ] );
|
||||
|
||||
// Maybe translate cart prices.
|
||||
add_action( 'woocommerce_add_cart_item_data', [ $this, 'add_initial_currency' ], 20, 3 );
|
||||
add_filter( 'woocommerce_get_cart_item_from_session', [ $this, 'convert_cart_currency' ], 20, 2 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
|
||||
|
||||
// Convert cart editing price.
|
||||
add_filter( 'wc_nyp_edit_in_cart_args', [ $this, 'edit_in_cart_args' ], 10, 2 );
|
||||
add_filter( 'wc_nyp_get_initial_price', [ $this, 'get_initial_price' ], 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the min/max/suggested prices of Name Your Price extension.
|
||||
*
|
||||
* @param mixed $price The price to be filtered.
|
||||
* @return mixed The price as a string or float.
|
||||
*/
|
||||
public function get_nyp_prices( $price ) {
|
||||
return ! $price ? $price : $this->multi_currency->get_price( $price, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the inintial currency when item is added.
|
||||
*
|
||||
* @param array $cart_item Extra cart item data being passed to the cart item.
|
||||
* @param int $product_id The id of the product being added to the cart.
|
||||
* @param int $variation_id The id of the variation being added to the cart.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function add_initial_currency( $cart_item, $product_id, $variation_id ) {
|
||||
|
||||
$nyp_id = $variation_id ? $variation_id : $product_id;
|
||||
|
||||
if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) {
|
||||
$currency = $this->multi_currency->get_selected_currency();
|
||||
$cart_item['nyp_currency'] = $currency->get_code();
|
||||
$cart_item['nyp_original'] = $cart_item['nyp'];
|
||||
}
|
||||
|
||||
return $cart_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the cart price when currency changes.
|
||||
* Do not convert price if in the same currency as when added to the cart.
|
||||
* This prevevents USD > EUR > USD style conversions and potential rounding problems.
|
||||
*
|
||||
* @param array $cart_item Cart item array.
|
||||
* @param array $values Cart item values e.g. quantity and product_id.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function convert_cart_currency( $cart_item, $values ) {
|
||||
|
||||
if ( isset( $cart_item['nyp_original'] ) && isset( $cart_item['nyp_currency'] ) ) {
|
||||
|
||||
// Store the original currency in $product meta.
|
||||
$cart_item['data']->update_meta_data( self::NYP_CURRENCY, $cart_item['nyp_currency'] );
|
||||
|
||||
$selected_currency = $this->multi_currency->get_selected_currency();
|
||||
|
||||
// If the currency is currently the same as at time price entered, restore NYP to original value.
|
||||
if ( $cart_item['nyp_currency'] === $selected_currency->get_code() ) {
|
||||
$cart_item['nyp'] = $cart_item['nyp_original'];
|
||||
} else {
|
||||
|
||||
$from_currency = $cart_item['nyp_currency'];
|
||||
$raw_price = $cart_item['nyp_original'];
|
||||
|
||||
$cart_item['nyp'] = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line.
|
||||
$cart_item = WC_Name_Your_Price()->cart->set_cart_item( $cart_item );
|
||||
}
|
||||
|
||||
return $cart_item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the product's price or not. Default is true.
|
||||
* @param object $product Product object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( bool $return, $product ): bool {
|
||||
// If it's already false, return it.
|
||||
if ( ! $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$currency = $this->multi_currency->get_selected_currency();
|
||||
|
||||
// Check for cart items to see if they are in the original currency.
|
||||
if ( $currency->get_code() === $product->get_meta( self::NYP_CURRENCY ) ) {
|
||||
$return = false;
|
||||
}
|
||||
|
||||
// Check to see if the product is a NYP product.
|
||||
if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add currency to cart edit link.
|
||||
*
|
||||
* @param array $args The cart args.
|
||||
* @param array $cart_item The current cart item.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function edit_in_cart_args( $args, $cart_item ) {
|
||||
$args['nyp_currency'] = $this->multi_currency->get_selected_currency()->get_code();
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe convert any prices being edited from the cart
|
||||
*
|
||||
* @param string $initial_price The initial price.
|
||||
* @param mixed $product The product being queried.
|
||||
* @param string $suffix The suffix needed for composites and bundles.
|
||||
*
|
||||
* @return float|string
|
||||
*/
|
||||
public function get_initial_price( $initial_price, $product, $suffix ) {
|
||||
|
||||
if ( isset( $_REQUEST[ 'nyp_raw' . $suffix ] ) && isset( $_REQUEST['nyp_currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$from_currency = wc_clean( wp_unslash( $_REQUEST['nyp_currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$raw_price = (float) wc_clean( wp_unslash( $_REQUEST[ 'nyp_raw' . $suffix ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$selected_currency = $this->multi_currency->get_selected_currency();
|
||||
|
||||
if ( $from_currency !== $selected_currency->get_code() ) {
|
||||
$initial_price = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
|
||||
}
|
||||
}
|
||||
|
||||
return $initial_price;
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommercePointsAndRewards
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\Currency;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Points & Rewards Plugin.
|
||||
*/
|
||||
class WooCommercePointsAndRewards extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Default Currency Code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $default_currency_code;
|
||||
|
||||
/**
|
||||
* Selected Currency Code.
|
||||
*
|
||||
* @var Currency
|
||||
*/
|
||||
private $selected_currency;
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed filters if Points & Rewards is active and it's not an admin request.
|
||||
if ( is_admin() || ! class_exists( 'WC_Points_Rewards' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_filter( 'option_wc_points_rewards_earn_points_ratio', [ $this, 'convert_points_ratio' ], 50 );
|
||||
add_filter( 'option_wc_points_rewards_redeem_points_ratio', [ $this, 'convert_points_ratio' ], 50 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts points ratio applying selected currency rate to the monetary value.
|
||||
*
|
||||
* @param string $ratio Store currency points ratio.
|
||||
* @return string Converted points ratio.
|
||||
*/
|
||||
public function convert_points_ratio( string $ratio = '' ): string {
|
||||
// Skip conversion if selected and default currencies are the same.
|
||||
if ( $this->selected_and_default_currency_match() ) {
|
||||
return $ratio;
|
||||
}
|
||||
|
||||
// Skip conversion on discount to avoid doing it twice.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Points_Rewards_Discount->get_discount_data' ] ) ) {
|
||||
return $ratio;
|
||||
}
|
||||
|
||||
$ratio = explode( ':', $ratio );
|
||||
$points = (float) ( $ratio[0] ?? 0 );
|
||||
$value = (float) ( $ratio[1] ?? 0 );
|
||||
|
||||
$rate = $this->selected_currency->get_rate();
|
||||
$value = $value * $rate;
|
||||
|
||||
return "$points:$value";
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the selected and default currency are the same.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
private function selected_and_default_currency_match(): bool {
|
||||
if ( empty( $this->default_currency_code ) ) {
|
||||
$this->default_currency_code = $this->multi_currency->get_default_currency()->get_code();
|
||||
}
|
||||
if ( empty( $this->selected_currency ) ) {
|
||||
$this->selected_currency = $this->multi_currency->get_selected_currency();
|
||||
}
|
||||
|
||||
return $this->default_currency_code === $this->selected_currency->get_code();
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommercePreOrders
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
use WCPay\MultiCurrency\Utils;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Pre-Orders Plugin.
|
||||
*/
|
||||
class WooCommercePreOrders extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if Pre-Orders is active.
|
||||
if ( class_exists( 'WC_Pre_Orders' ) ) {
|
||||
add_filter( 'wc_pre_orders_fee', [ $this, 'wc_pre_orders_fee' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Pre-Orders fee.
|
||||
*
|
||||
* @param array $args Array of args for the Pre-Orders fee.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function wc_pre_orders_fee( array $args ): array {
|
||||
$args['amount'] = $this->multi_currency->get_price( $args['amount'], 'product' );
|
||||
return $args;
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceProductAddOns
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WC_Product;
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
use WCPay\MultiCurrency\Utils;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Product Add Ons Plugin.
|
||||
*/
|
||||
class WooCommerceProductAddOns extends BaseCompatibility {
|
||||
|
||||
const ADDONS_CONVERTED_META_KEY = '_wcpay_multi_currency_addons_converted';
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if Product Add Ons is active.
|
||||
if ( class_exists( 'WC_Product_Addons' ) ) {
|
||||
if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) {
|
||||
add_filter( 'woocommerce_product_addons_option_price_raw', [ $this, 'get_addons_price' ], 50, 2 );
|
||||
add_filter( 'woocommerce_product_addons_price_raw', [ $this, 'get_addons_price' ], 50, 2 );
|
||||
add_filter( 'woocommerce_product_addons_params', [ $this, 'product_addons_params' ], 50, 1 );
|
||||
add_filter( 'woocommerce_product_addons_get_item_data', [ $this, 'get_item_data' ], 50, 3 );
|
||||
add_filter( 'woocommerce_product_addons_update_product_price', [ $this, 'update_product_price' ], 50, 4 );
|
||||
add_filter( 'woocommerce_product_addons_order_line_item_meta', [ $this, 'order_line_item_meta' ], 50, 4 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
|
||||
}
|
||||
|
||||
if ( wp_doing_ajax() ) {
|
||||
add_filter( 'woocommerce_product_addons_ajax_get_product_price_including_tax', [ $this, 'get_product_calculation_price' ], 50, 3 );
|
||||
add_filter( 'woocommerce_product_addons_ajax_get_product_price_excluding_tax', [ $this, 'get_product_calculation_price' ], 50, 3 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the product's price or not. Default is true.
|
||||
* @param object $product Product object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( bool $return, $product ): bool {
|
||||
// If it's already false, return it.
|
||||
if ( ! $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
// Check for cart items to see if they have already been converted.
|
||||
if ( 1 === $product->get_meta( self::ADDONS_CONVERTED_META_KEY ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the price of an addon from WooCommerce Products Add-on extension.
|
||||
*
|
||||
* @param mixed $price The price to be filtered.
|
||||
* @param array $type The type of the addon.
|
||||
|
||||
* @return mixed The price as a string or float.
|
||||
*/
|
||||
public function get_addons_price( $price, $type ) {
|
||||
if ( 'percentage_based' === $type['price_type'] ) {
|
||||
// If the addon is a percentage_based type $price is actually a percentage
|
||||
// and doesn't need any conversion.
|
||||
return $price;
|
||||
}
|
||||
|
||||
return $this->multi_currency->get_price( $price, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes currency formatting issues in Product Add-Ons. PAO gets these values directly from the db options,
|
||||
* so those values aren't filtered. Luckily, there's a filter.
|
||||
*
|
||||
* @param array $params Product Add-Ons global parameters.
|
||||
*
|
||||
* @return array Adjust parameters.
|
||||
*/
|
||||
public function product_addons_params( array $params ): array {
|
||||
$params['currency_format_num_decimals'] = wc_get_price_decimals();
|
||||
$params['currency_format_decimal_sep'] = wc_get_price_decimal_separator();
|
||||
$params['currency_format_thousand_sep'] = wc_get_price_thousand_separator();
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the cart item data meta so we can provide the proper name with converted add on price.
|
||||
*
|
||||
* @param array $addon_data The addon data we are filtering/replacing.
|
||||
* @param array $addon The addon being processed.
|
||||
* @param array $cart_item The cart item being processed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_item_data( $addon_data, $addon, $cart_item ): array {
|
||||
$price = isset( $cart_item['addons_price_before_calc'] ) ? $cart_item['addons_price_before_calc'] : $addon['price'];
|
||||
$value = $addon['value'];
|
||||
|
||||
/*
|
||||
* 'woocommerce_addons_add_cart_price_to_value'
|
||||
*
|
||||
* Use this filter to display the price next to each selected add-on option.
|
||||
* By default, add-on prices show up only next to flat fee add-ons.
|
||||
*
|
||||
* @param boolean
|
||||
*/
|
||||
$add_price_to_value = apply_filters( 'woocommerce_addons_add_cart_price_to_value', false, $cart_item );
|
||||
|
||||
if ( 0.0 === (float) $addon['price'] ) {
|
||||
$value .= '';
|
||||
} elseif ( 'percentage_based' === $addon['price_type'] && 0.0 === (float) $price ) {
|
||||
$value .= '';
|
||||
} elseif ( 'custom_price' === $addon['field_type'] && $addon['price'] ) {
|
||||
if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
|
||||
$addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon['price'], $cart_item['data'] ) );
|
||||
/* translators: %1$s custom addon price in cart */
|
||||
$value .= sprintf( _x( ' (%1$s)', 'custom price addon price in cart', 'woocommerce-payments' ), $addon_price );
|
||||
$addon['display'] = $value;
|
||||
}
|
||||
} elseif ( 'flat_fee' === $addon['price_type'] && $addon['price'] ) {
|
||||
if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
|
||||
if ( 'input_multiplier' === $addon['field_type'] ) {
|
||||
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
|
||||
}
|
||||
$addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $cart_item['data'] ) );
|
||||
/* translators: %1$s flat fee addon price in order */
|
||||
$value .= sprintf( _x( ' (+ %1$s)', 'flat fee addon price in cart', 'woocommerce-payments' ), $addon_price );
|
||||
}
|
||||
} elseif ( 'quantity_based' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) {
|
||||
if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
|
||||
if ( 'input_multiplier' === $addon['field_type'] ) {
|
||||
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
|
||||
}
|
||||
$addon_price = wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $cart_item['data'] ) );
|
||||
/* translators: %1$s addon price in order */
|
||||
$value .= sprintf( _x( ' (%1$s)', 'quantity based addon price in cart', 'woocommerce-payments' ), $addon_price );
|
||||
}
|
||||
} elseif ( 'percentage_based' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) {
|
||||
// Get the percentage cost in the currency in use, and set the meta data on the product that the value was converted.
|
||||
$_product = wc_get_product( $cart_item['product_id'] );
|
||||
$price = $this->multi_currency->get_price( $price, 'product' );
|
||||
$_product->set_price( $price * ( $addon['price'] / 100 ) );
|
||||
$_product->update_meta_data( self::ADDONS_CONVERTED_META_KEY, 1 );
|
||||
/* translators: %1$s addon price in order */
|
||||
$value .= sprintf( _x( ' (%1$s)', 'percentage based addon price in cart', 'woocommerce-payments' ), WC()->cart->get_product_price( $_product ) );
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $addon['name'],
|
||||
'value' => $value,
|
||||
'display' => isset( $addon['display'] ) ? $addon['display'] : '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the product price according to converted add on values.
|
||||
*
|
||||
* @param array $updated_prices Prices updated by Product Add-Ons (unused).
|
||||
* @param array $cart_item Cart item meta data.
|
||||
* @param array $prices Original prices passed to Product Add-Ons for calculations.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function update_product_price( $updated_prices, $cart_item, $prices ): array {
|
||||
$price = $this->multi_currency->get_price( $prices['price'], 'product' );
|
||||
$regular_price = $this->multi_currency->get_price( $prices['regular_price'], 'product' );
|
||||
$sale_price = $this->multi_currency->get_price( $prices['sale_price'], 'product' );
|
||||
$flat_fees = 0;
|
||||
$quantity = $cart_item['quantity'];
|
||||
$price_before_addons = $price;
|
||||
$regular_price_before_addons = $regular_price;
|
||||
$sale_price_before_addons = $sale_price;
|
||||
|
||||
// TODO: Check compat with Smart Coupons.
|
||||
// Compatibility with Smart Coupons self declared gift amount purchase.
|
||||
$credit_called = ! empty( $_POST['credit_called'] ) ? $_POST['credit_called'] : null; // phpcs:ignore
|
||||
if ( empty( $price ) && ! empty( $credit_called ) ) {
|
||||
// Variable $_POST['credit_called'] is an array.
|
||||
if ( isset( $credit_called[ $cart_item['data']->get_id() ] ) ) {
|
||||
$price = (float) $credit_called[ $cart_item['data']->get_id() ];
|
||||
$regular_price = $price;
|
||||
$sale_price = $price;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $price ) && ! empty( $cart_item['credit_amount'] ) ) {
|
||||
$price = (float) $cart_item['credit_amount'];
|
||||
$regular_price = $price;
|
||||
$sale_price = $price;
|
||||
}
|
||||
|
||||
foreach ( $cart_item['addons'] as $addon ) {
|
||||
// Percentage based and custom defined addon prices do not get converted, all others do.
|
||||
if ( 'percentage_based' === $addon['price_type'] || 'custom_price' === $addon['field_type'] ) {
|
||||
$addon_price = $addon['price'];
|
||||
} elseif ( 'input_multiplier' === $addon['field_type'] ) {
|
||||
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
|
||||
} else {
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
|
||||
}
|
||||
|
||||
switch ( $addon['price_type'] ) {
|
||||
case 'percentage_based':
|
||||
$price += (float) ( $price_before_addons * ( $addon_price / 100 ) );
|
||||
$regular_price += (float) ( $regular_price_before_addons * ( $addon_price / 100 ) );
|
||||
$sale_price += (float) ( $sale_price_before_addons * ( $addon_price / 100 ) );
|
||||
break;
|
||||
case 'flat_fee':
|
||||
$flat_fee = $quantity > 0 ? (float) ( $addon_price / $quantity ) : 0;
|
||||
$price += $flat_fee;
|
||||
$regular_price += $flat_fee;
|
||||
$sale_price += $flat_fee;
|
||||
$flat_fees += $flat_fee;
|
||||
break;
|
||||
default:
|
||||
$price += (float) $addon_price;
|
||||
$regular_price += (float) $addon_price;
|
||||
$sale_price += (float) $addon_price;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Let ourselves know this item has had add ons converted.
|
||||
$cart_item['data']->update_meta_data( self::ADDONS_CONVERTED_META_KEY, 1 );
|
||||
|
||||
return [
|
||||
'price' => $price,
|
||||
'regular_price' => $regular_price,
|
||||
'sale_price' => $sale_price,
|
||||
'addons_flat_fees_sum' => $flat_fees,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the meta data for order line items so that we can properly set values in the names.
|
||||
*
|
||||
* @param array $meta_data A key/value for the meta data to be inserted for the line item.
|
||||
* @param array $addon The addon being processed.
|
||||
* @param \WC_Order_Item_Product $item Order item data.
|
||||
* @param array $values Order item values.
|
||||
*
|
||||
* @return array A key/value for the meta data to be inserted for the line item.
|
||||
*/
|
||||
public function order_line_item_meta( array $meta_data, array $addon, \WC_Order_Item_Product $item, array $values ): array {
|
||||
|
||||
$add_price_to_value = apply_filters( 'woocommerce_addons_add_order_price_to_value', false, $item );
|
||||
|
||||
$value = $addon['value'];
|
||||
|
||||
// Pass the timestamp as the add-on value in order to save the timestamp to the DB.
|
||||
if ( isset( $addon['timestamp'] ) ) {
|
||||
$value = $addon['timestamp'];
|
||||
}
|
||||
|
||||
// If there is an add-on price, add the price of the add-on to the label name.
|
||||
if ( $addon['price'] && $add_price_to_value ) {
|
||||
$product = $item->get_product();
|
||||
|
||||
if ( 'percentage_based' === $addon['price_type'] && 0.0 !== (float) $product->get_price() ) {
|
||||
// Calculate the percentage price.
|
||||
$addon_price = $product->get_price() * ( $addon['price'] / 100 );
|
||||
} elseif ( 'custom_price' === $addon['field_type'] ) {
|
||||
// Custom prices do not get converted.
|
||||
$addon_price = $addon['price'];
|
||||
} elseif ( 'input_multiplier' === $addon['field_type'] ) {
|
||||
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
|
||||
} else {
|
||||
// Convert all others.
|
||||
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
|
||||
}
|
||||
if ( class_exists( 'WC_Product_Addons_Helper' ) ) {
|
||||
$price = html_entity_decode(
|
||||
wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ),
|
||||
ENT_QUOTES,
|
||||
get_bloginfo( 'charset' )
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'flat_fee' === $addon['price_type'] && $addon['price'] && $add_price_to_value ) {
|
||||
/* translators: %1$s flat fee addon price in order */
|
||||
$value .= sprintf( _x( ' (+ %1$s)', 'flat fee addon price in order', 'woocommerce-payments' ), $price );
|
||||
} elseif ( ( 'quantity_based' === $addon['price_type'] || 'percentage_based' === $addon['price_type'] ) && $addon['price'] && $add_price_to_value ) {
|
||||
/* translators: %1$s addon price in order */
|
||||
$value .= sprintf( _x( ' (%1$s)', 'addon price in order', 'woocommerce-payments' ), $price );
|
||||
} elseif ( 'custom_price' === $addon['field_type'] ) {
|
||||
/* translators: %1$s custom addon price in order */
|
||||
$value = sprintf( _x( ' (%1$s)', 'custom addon price in order', 'woocommerce-payments' ), $price );
|
||||
}
|
||||
|
||||
$meta_data['raw_price'] = $this->multi_currency->get_price( $addon['price'], 'product' );
|
||||
}
|
||||
|
||||
$meta_data['value'] = $value;
|
||||
return $meta_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the product price during ajax requests from the product page.
|
||||
*
|
||||
* @param float $price Price to get converted.
|
||||
* @param int $quantity Quantity of the product selected.
|
||||
* @param \WC_Product $product WC_Product related to the price.
|
||||
*
|
||||
* @return float Adjusted price.
|
||||
*/
|
||||
public function get_product_calculation_price( float $price, int $quantity, \WC_Product $product ): float {
|
||||
return $this->multi_currency->get_price( $price / $quantity, 'product' ) * $quantity;
|
||||
}
|
||||
}
|
||||
+566
@@ -0,0 +1,566 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceSubscriptions
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WC_Subscription;
|
||||
use WCPay\MultiCurrency\Logger;
|
||||
use WCPay\MultiCurrency\FrontendCurrencies;
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce Subscriptions Plugin and WCPay Subscriptions.
|
||||
*/
|
||||
class WooCommerceSubscriptions extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Our allowed subscription types.
|
||||
*/
|
||||
const SUBSCRIPTION_TYPES = [ 'renewal', 'resubscribe', 'switch' ];
|
||||
|
||||
/**
|
||||
* Subscription switch cart item.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $switch_cart_item = '';
|
||||
|
||||
/**
|
||||
* The current subscription being iterated through on the My Account > Subscriptions page.
|
||||
*
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*
|
||||
* @var WC_Subscription|null
|
||||
*/
|
||||
public $current_my_account_subscription = null;
|
||||
|
||||
/**
|
||||
* The FrontendCurrencies object.
|
||||
*
|
||||
* @var FrontendCurrencies
|
||||
*/
|
||||
public $frontend_currencies;
|
||||
|
||||
/**
|
||||
* If we are running through our filters.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $running_override_selected_currency_filters = false;
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if WC Subscriptions or WCPay Subscriptions are active.
|
||||
if ( class_exists( 'WC_Subscriptions' ) || class_exists( 'WC_Payments_Subscriptions' ) ) {
|
||||
if ( ! is_admin() && ! defined( 'DOING_CRON' ) ) {
|
||||
$this->frontend_currencies = $this->multi_currency->get_frontend_currencies();
|
||||
|
||||
add_filter( 'woocommerce_subscriptions_product_price', [ $this, 'get_subscription_product_price' ], 50, 2 );
|
||||
add_filter( 'woocommerce_product_get__subscription_sign_up_fee', [ $this, 'get_subscription_product_signup_fee' ], 50, 2 );
|
||||
add_filter( 'woocommerce_product_variation_get__subscription_sign_up_fee', [ $this, 'get_subscription_product_signup_fee' ], 50, 2 );
|
||||
add_filter( 'option_woocommerce_subscriptions_multiple_purchase', [ $this, 'maybe_disable_mixed_cart' ], 50 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'override_selected_currency', [ $this, 'override_selected_currency' ], 50 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_product_price', [ $this, 'should_convert_product_price' ], 50, 2 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_convert_coupon_amount', [ $this, 'should_convert_coupon_amount' ], 50, 2 );
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_disable_currency_switching', [ $this, 'should_disable_currency_switching' ], 50 );
|
||||
add_filter( 'woocommerce_subscription_price_string_details', [ $this, 'maybe_set_current_my_account_subscription' ], 50, 2 );
|
||||
add_filter( 'woocommerce_get_formatted_subscription_total', [ $this, 'maybe_clear_current_my_account_subscription' ], 50, 2 );
|
||||
add_filter( 'wc_price', [ $this, 'maybe_get_explicit_format_for_subscription_total' ], 50, 5 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts subscription prices, if needed.
|
||||
*
|
||||
* @param mixed $price The price to be filtered.
|
||||
* @param object $product The product that will have a filtered price.
|
||||
*
|
||||
* @return mixed The price as a string or float.
|
||||
*/
|
||||
public function get_subscription_product_price( $price, $product ) {
|
||||
if ( ! $price || ! $this->should_convert_product_price( true, $product ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
return $this->multi_currency->get_price( $price, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts subscription sign up prices, if needed.
|
||||
*
|
||||
* @param mixed $price The price to be filtered.
|
||||
* @param object $product The product that will have a filtered price.
|
||||
*
|
||||
* @return mixed The price as a string or float.
|
||||
*/
|
||||
public function get_subscription_product_signup_fee( $price, $product ) {
|
||||
if ( ! $price ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
$item = $this->get_subscription_type_from_cart( 'switch' );
|
||||
if ( $item ) {
|
||||
$item_id = ! empty( $item['variation_id'] ) ? $item['variation_id'] : $item['product_id'];
|
||||
$switch_cart_item = $this->switch_cart_item;
|
||||
$this->switch_cart_item = $item['key'];
|
||||
|
||||
if ( $product->get_id() === $item_id ) {
|
||||
|
||||
/**
|
||||
* These tests get mildly complex due to, when switching, the sign up fee is queried
|
||||
* several times to determine prorated costs. This means we have to test to see when
|
||||
* the fee actually needs be converted.
|
||||
*/
|
||||
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Subscriptions_Cart::set_subscription_prices_for_calculation' ] ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
// Check to see if it's currently determining prorated prices.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Subscriptions_Product::get_sign_up_fee' ] )
|
||||
&& $this->utils->is_call_in_backtrace( [ 'WC_Cart->calculate_totals' ] )
|
||||
&& $item['key'] === $switch_cart_item
|
||||
&& ! $this->utils->is_call_in_backtrace( [ 'WCS_Switch_Totals_Calculator->apportion_sign_up_fees' ] ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
// Check to see if the _subscription_sign_up_fee meta for the product has already been updated.
|
||||
if ( $item['key'] === $switch_cart_item ) {
|
||||
foreach ( $product->get_meta_data() as $meta ) {
|
||||
if ( '_subscription_sign_up_fee' === $meta->get_data()['key'] && ! empty( $meta->get_changes() ) ) {
|
||||
return $price;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->multi_currency->get_price( $price, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables the mixed cart if needed.
|
||||
*
|
||||
* @param string|bool $value Option from the database, or false.
|
||||
*
|
||||
* @return mixed False, yes, or no.
|
||||
*/
|
||||
public function maybe_disable_mixed_cart( $value ) {
|
||||
// If there's a subscription switch in the cart, disable multiple items in the cart.
|
||||
// This is so that subscriptions with different currencies cannot be added to the cart.
|
||||
if ( $this->get_subscription_type_from_cart( 'switch' ) ) {
|
||||
return 'no';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the if the selected currency needs to be overridden.
|
||||
*
|
||||
* The running_override_selected_currency_filters property is used here to avoid infinite loops.
|
||||
*
|
||||
* @param mixed $return Default is false, but could be three letter currency code.
|
||||
*
|
||||
* @return mixed Three letter currency code or false if not.
|
||||
*/
|
||||
public function override_selected_currency( $return ) {
|
||||
// If it's not false, or we are already running filters, exit.
|
||||
if ( $return || $this->running_override_selected_currency_filters ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
// If we have a subscription in $current_my_account_subscription, we want to use the currency from that subscription.
|
||||
if ( $this->is_current_my_account_subscription_set() ) {
|
||||
/**
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*/
|
||||
return $this->current_my_account_subscription->get_currency();
|
||||
}
|
||||
|
||||
// Loop through subscription types and check for cart items.
|
||||
foreach ( self::SUBSCRIPTION_TYPES as $type ) {
|
||||
$cart_item = $this->get_subscription_type_from_cart( $type );
|
||||
if ( $cart_item ) {
|
||||
$this->running_override_selected_currency_filters = true;
|
||||
|
||||
// If we have a cart item, then we can get the order or subscription to pull the currency from.
|
||||
$subscription_type = 'subscription_' . $type;
|
||||
$subscription = $this->get_subscription( $cart_item[ $subscription_type ]['subscription_id'] );
|
||||
|
||||
$this->running_override_selected_currency_filters = false;
|
||||
/**
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*/
|
||||
return $subscription ? $subscription->get_currency() : $return;
|
||||
}
|
||||
}
|
||||
|
||||
// This instance is for when the customer lands on the product page to choose a new subscription tier.
|
||||
$switch_subscription = $this->get_subscription_from_superglobal_switch_id();
|
||||
/**
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*/
|
||||
return $switch_subscription ? $switch_subscription->get_currency() : $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the product's price should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the product's price or not. Default is true.
|
||||
* @param object $product Product object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_product_price( bool $return, $product ): bool {
|
||||
// If it's already false, return it.
|
||||
if ( ! $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$calls = [
|
||||
'WC_Cart_Totals->calculate_item_totals',
|
||||
'WC_Cart->get_product_subtotal',
|
||||
'wc_get_price_excluding_tax',
|
||||
'wc_get_price_including_tax',
|
||||
];
|
||||
|
||||
// Check for renewal.
|
||||
if ( $this->get_subscription_type_from_cart( 'renewal' ) ) {
|
||||
// When WCPay Subs programmatically sets up the cart, we need to return the
|
||||
// converted price so the user lands at the checkout with the correct price.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WCS_Cart_Renewal->setup_cart' ] ) ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ( $this->utils->is_call_in_backtrace( $calls ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for resubscribe.
|
||||
if ( $this->get_subscription_type_from_cart( 'resubscribe' )
|
||||
&& $this->utils->is_call_in_backtrace( $calls ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// WCPay Subs does a check against the product price and the total, we need to return the actual product price for this check.
|
||||
if ( $this->utils->is_call_in_backtrace( [ 'WC_Payments_Subscription_Service->get_recurring_item_data_for_subscription' ] )
|
||||
&& $this->utils->is_call_in_backtrace( [ 'WC_Product->get_price' ] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the coupon's amount should be converted.
|
||||
*
|
||||
* @param bool $return Whether to convert the coupon's price or not. Default is true.
|
||||
* @param object $coupon Coupon object to test.
|
||||
*
|
||||
* @return bool True if it should be converted.
|
||||
*/
|
||||
public function should_convert_coupon_amount( bool $return, $coupon ): bool {
|
||||
// If it's already false, return it.
|
||||
if ( ! $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
// We do not need to convert percentage coupons.
|
||||
if ( $this->is_coupon_type( $coupon, 'subscription_percent' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there's not a renewal in the cart, we can convert.
|
||||
$subscription_renewal = $this->get_subscription_type_from_cart( 'renewal' );
|
||||
if ( ! $subscription_renewal ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* We need to allow the early renewal to convert the cost, as it pulls the original value of the coupon.
|
||||
* Subsequent queries for the amount use the first converted amount.
|
||||
* This also works for normal manual renewals.
|
||||
*/
|
||||
if ( ! $this->utils->is_call_in_backtrace( [ 'WCS_Cart_Early_Renewal->setup_cart' ] )
|
||||
&& $this->utils->is_call_in_backtrace( [ 'WC_Discounts->apply_coupon' ] )
|
||||
&& $this->is_coupon_type( $coupon, 'subscription_recurring' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if currency switching should be disabled.
|
||||
*
|
||||
* @param bool $return Whether widgets should be hidden or not. Default is false.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_disable_currency_switching( bool $return ): bool {
|
||||
// If it's already true, return it.
|
||||
if ( $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
if ( $this->get_subscription_type_from_cart( 'renewal' )
|
||||
|| $this->get_subscription_type_from_cart( 'resubscribe' )
|
||||
|| $this->get_subscription_type_from_cart( 'switch' )
|
||||
|| $this->get_subscription_from_superglobal_switch_id() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe sets the current_my_account_subscription var and clears the FrontendCurrencies cache.
|
||||
*
|
||||
* The my-subscriptions.php template file calls $subscription->get_formatted_order_total(), which then calls $subscription->get_price_string_details().
|
||||
* At that point in time, if we have certain calls in the backtrace, we need to add the subscription into $current_my_account_subscription so that we
|
||||
* are able to use it later on in the maybe_get_explicit_format_for_subscription_total filter.
|
||||
*
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*
|
||||
* @param array $subscription_details The details related to the subscription.
|
||||
* @param WC_Subscription $subscription The subscription being acted on.
|
||||
*
|
||||
* @return array The unmodified subscription details.
|
||||
*/
|
||||
public function maybe_set_current_my_account_subscription( $subscription_details, $subscription ): array {
|
||||
$calls = [
|
||||
'WCS_Template_Loader::get_my_subscriptions ',
|
||||
'WC_Subscription->get_formatted_order_total',
|
||||
];
|
||||
if ( $this->utils->is_call_in_backtrace( $calls ) ) {
|
||||
// If we have our calls in the backtrace, we set our cached sub and clear the FrontendCurrencies cache.
|
||||
$this->current_my_account_subscription = $subscription;
|
||||
$this->frontend_currencies->selected_currency_changed();
|
||||
}
|
||||
|
||||
return $subscription_details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe clears the current_my_account_subscription var and clears the FrontendCurrencies cache.
|
||||
*
|
||||
* During the $subscription->get_formatted_order_total() call we may set the current_my_account_subscription var, and we want to clear it as soon as
|
||||
* we no longer need it. The woocommerce_get_formatted_subscription_total filter is at the end of that call, so we check to see if the var is set,
|
||||
* and if it is, we clear it, and we also clear the FrontendCurrencies cache.
|
||||
*
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*
|
||||
* @param string $formatted The subscription formatted total.
|
||||
* @param WC_Subscription $subscription The subscription being acted on.
|
||||
*
|
||||
* @return string The unmodified subscription formatted total.
|
||||
*/
|
||||
public function maybe_clear_current_my_account_subscription( $formatted, $subscription ): string {
|
||||
if ( $this->is_current_my_account_subscription_set() ) {
|
||||
$this->current_my_account_subscription = null;
|
||||
$this->frontend_currencies->selected_currency_changed();
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the current_my_account_subscription var is set, then we use that subscription's currency in order to set the explicit total.
|
||||
*
|
||||
* @param string $html_price Price HTML markup.
|
||||
* @param string $price Formatted price.
|
||||
* @param array $args Pass on the args.
|
||||
* @param float $unformatted_price Price as float to allow plugins custom formatting. Since 3.2.0.
|
||||
* @param float|string $original_price Original price as float, or empty string. Since 5.0.0.
|
||||
*
|
||||
* @return string The wc_price with HTML wrapping, possibly with the currency code added for explicit formatting.
|
||||
*/
|
||||
public function maybe_get_explicit_format_for_subscription_total( $html_price, $price, $args, $unformatted_price, $original_price ): string {
|
||||
if ( ! $this->is_current_my_account_subscription_set() ) {
|
||||
return $html_price;
|
||||
}
|
||||
|
||||
if ( ! $this->multi_currency->has_additional_currencies_enabled() ) {
|
||||
return $html_price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currency code from the subscription, then return the explicit price.
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*/
|
||||
$currency_code = $this->current_my_account_subscription->get_currency() ?? get_woocommerce_currency();
|
||||
|
||||
// This is sourced from WC_Payments_Explicit_Price_Formatter::get_explicit_price_with_currency.
|
||||
$price_to_check = html_entity_decode( wp_strip_all_tags( $html_price ) );
|
||||
|
||||
if ( false === strpos( $price_to_check, trim( $currency_code ) ) ) {
|
||||
return $html_price . ' ' . $currency_code;
|
||||
}
|
||||
|
||||
return $html_price;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple check to see if the current_my_account_subscription is a WC_Subscription object.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_current_my_account_subscription_set(): bool {
|
||||
/**
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedClass
|
||||
*/
|
||||
return is_a( $this->current_my_account_subscription, 'WC_Subscription' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the cart values to see if there are subscriptions with specific types present.
|
||||
*
|
||||
* This checks both the cart itself and the session. This is due to there are times when an item may be present in
|
||||
* one place and not the other. We need to make sure that if an item is in either we are not creating double conversions.
|
||||
*
|
||||
* @param string $type The type of subscription to look for in the cart.
|
||||
*
|
||||
* @return mixed False if none found, or the subscription cart item as an array.
|
||||
*/
|
||||
private function get_subscription_type_from_cart( $type ) {
|
||||
// Make sure we're looking for allowed types.
|
||||
if ( ! in_array( $type, self::SUBSCRIPTION_TYPES, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set the sub type cart key.
|
||||
$subscription_type = 'subscription_' . $type;
|
||||
|
||||
// Go through each cart item and if it matches the type, return that item.
|
||||
if ( isset( WC()->cart ) && is_array( WC()->cart->cart_contents ) && ! empty( WC()->cart->cart_contents ) ) {
|
||||
foreach ( WC()->cart->cart_contents as $cart_item ) {
|
||||
if ( isset( $cart_item[ $subscription_type ] ) ) {
|
||||
return $cart_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go through each session cart item and if it matches the type, return that item.
|
||||
if ( isset( WC()->session ) && is_array( WC()->session->get( 'cart' ) ) && ! empty( WC()->session->get( 'cart' ) ) ) {
|
||||
foreach ( WC()->session->get( 'cart' ) as $cart_item ) {
|
||||
if ( isset( $cart_item[ $subscription_type ] ) ) {
|
||||
return $cart_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Getter for subscription objects.
|
||||
*
|
||||
* @param mixed $the_subscription Post object or post ID of the order.
|
||||
*
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*
|
||||
* @return WC_Subscription|bool The subscription object, or false if it cannot be found.
|
||||
*/
|
||||
private function get_subscription( $the_subscription ) {
|
||||
if ( ! function_exists( 'wcs_get_subscription' ) ) {
|
||||
return false;
|
||||
}
|
||||
return wcs_get_subscription( $the_subscription );
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks $_GET superglobal for a switch ID from the `switch-subscription` param if it exists.
|
||||
* This `switch-subscription` param is added to the URL when a customer
|
||||
* has initiated a switch from the My Account → Subscription page.
|
||||
*
|
||||
* Tell Psalm to ignore the WC_Subscription class, this class is only loaded if Subscriptions is active.
|
||||
*
|
||||
* @psalm-suppress UndefinedDocblockClass
|
||||
*
|
||||
* @return WC_Subscription|bool The subscription object, or false if it cannot be found.
|
||||
*/
|
||||
private function get_subscription_from_superglobal_switch_id() {
|
||||
// Return false if there's no nonce, or if it fails.
|
||||
if ( ! isset( $_GET['_wcsnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wcsnonce'] ), 'wcs_switch_request' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return false if the param isn't set, or if it isn't numeric.
|
||||
if ( ! isset( $_GET['switch-subscription'] ) || ! is_numeric( $_GET['switch-subscription'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the switch ID from the param.
|
||||
$switch_id = (int) sanitize_key( $_GET['switch-subscription'] );
|
||||
|
||||
// Get the sub, preventing an infinite loop with running_override_selected_currency_filters.
|
||||
$this->running_override_selected_currency_filters = true;
|
||||
$switch_subscription = $this->get_subscription( $switch_id );
|
||||
$this->running_override_selected_currency_filters = false;
|
||||
|
||||
// Confirm the sub user matches current user, and return the sub.
|
||||
if ( $switch_subscription && $switch_subscription->get_customer_id() === get_current_user_id() ) {
|
||||
return $switch_subscription;
|
||||
} else {
|
||||
Logger::notice( 'User (' . get_current_user_id() . ') attempted to switch a subscription (' . $switch_subscription->get_id() . ') not assigned to them.' );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the coupon passed is of a specified type.
|
||||
*
|
||||
* @param \WC_Coupon $coupon Coupon to test.
|
||||
* @param string $type Type of coupon to test for.
|
||||
*
|
||||
* @return bool True on match.
|
||||
*/
|
||||
private function is_coupon_type( $coupon, string $type ) {
|
||||
|
||||
$types = null;
|
||||
switch ( $type ) {
|
||||
case 'subscription_percent':
|
||||
$types = [ 'recurring_percent', 'sign_up_fee_percent', 'renewal_percent' ];
|
||||
break;
|
||||
|
||||
case 'subscription_recurring':
|
||||
$types = [ 'recurring_fee', 'recurring_percent', 'renewal_fee', 'renewal_percent', 'renewal_cart' ];
|
||||
break;
|
||||
}
|
||||
|
||||
if ( in_array( $coupon->get_discount_type(), $types, true ) ) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* Class WooCommerceUPS
|
||||
*
|
||||
* @package WCPay\MultiCurrency\Compatibility
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Compatibility;
|
||||
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
use WCPay\MultiCurrency\Utils;
|
||||
|
||||
/**
|
||||
* Class that controls Multi Currency Compatibility with WooCommerce UPS Plugin.
|
||||
*/
|
||||
class WooCommerceUPS extends BaseCompatibility {
|
||||
|
||||
/**
|
||||
* Init the class.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init() {
|
||||
// Add needed actions and filters if UPS is active.
|
||||
if ( class_exists( 'WC_Shipping_UPS_Init' ) ) {
|
||||
add_filter( MultiCurrency::FILTER_PREFIX . 'should_return_store_currency', [ $this, 'should_return_store_currency' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether to return the store currency or not.
|
||||
*
|
||||
* @param bool $return Whether to return the store currency or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_return_store_currency( bool $return ): bool {
|
||||
// If it's already true, return it.
|
||||
if ( $return ) {
|
||||
return $return;
|
||||
}
|
||||
|
||||
$calls = [
|
||||
'WC_Shipping_UPS->per_item_shipping',
|
||||
'WC_Shipping_UPS->box_shipping',
|
||||
'WC_Shipping_UPS->calculate_shipping',
|
||||
];
|
||||
if ( $this->utils->is_call_in_backtrace( $calls ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
/**
|
||||
* Class CountryFlags
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that bring flags per country/currency.
|
||||
*/
|
||||
class CountryFlags {
|
||||
|
||||
const EMOJI_COUNTRIES_FLAGS = [
|
||||
'AD' => '🇦🇩',
|
||||
'AE' => '🇦🇪',
|
||||
'AF' => '🇦🇫',
|
||||
'AG' => '🇦🇬',
|
||||
'AI' => '🇦🇮',
|
||||
'AL' => '🇦🇱',
|
||||
'AM' => '🇦🇲',
|
||||
'AO' => '🇦🇴',
|
||||
'AQ' => '🇦🇶',
|
||||
'AR' => '🇦🇷',
|
||||
'AS' => '🇦🇸',
|
||||
'AT' => '🇦🇹',
|
||||
'AU' => '🇦🇺',
|
||||
'AW' => '🇦🇼',
|
||||
'AX' => '🇦🇽',
|
||||
'AZ' => '🇦🇿',
|
||||
'BA' => '🇧🇦',
|
||||
'BB' => '🇧🇧',
|
||||
'BD' => '🇧🇩',
|
||||
'BE' => '🇧🇪',
|
||||
'BF' => '🇧🇫',
|
||||
'BG' => '🇧🇬',
|
||||
'BH' => '🇧🇭',
|
||||
'BI' => '🇧🇮',
|
||||
'BJ' => '🇧🇯',
|
||||
'BL' => '🇧🇱',
|
||||
'BM' => '🇧🇲',
|
||||
'BN' => '🇧🇳',
|
||||
'BO' => '🇧🇴',
|
||||
'BQ' => '🇧🇶',
|
||||
'BR' => '🇧🇷',
|
||||
'BS' => '🇧🇸',
|
||||
'BT' => '🇧🇹',
|
||||
'BV' => '🇧🇻',
|
||||
'BW' => '🇧🇼',
|
||||
'BY' => '🇧🇾',
|
||||
'BZ' => '🇧🇿',
|
||||
'CA' => '🇨🇦',
|
||||
'CC' => '🇨🇨',
|
||||
'CD' => '🇨🇩',
|
||||
'CF' => '🇨🇫',
|
||||
'CG' => '🇨🇬',
|
||||
'CH' => '🇨🇭',
|
||||
'CI' => '🇨🇮',
|
||||
'CK' => '🇨🇰',
|
||||
'CL' => '🇨🇱',
|
||||
'CM' => '🇨🇲',
|
||||
'CN' => '🇨🇳',
|
||||
'CO' => '🇨🇴',
|
||||
'CR' => '🇨🇷',
|
||||
'CU' => '🇨🇺',
|
||||
'CV' => '🇨🇻',
|
||||
'CW' => '🇨🇼',
|
||||
'CX' => '🇨🇽',
|
||||
'CY' => '🇨🇾',
|
||||
'CZ' => '🇨🇿',
|
||||
'DE' => '🇩🇪',
|
||||
'DJ' => '🇩🇯',
|
||||
'DK' => '🇩🇰',
|
||||
'DM' => '🇩🇲',
|
||||
'DO' => '🇩🇴',
|
||||
'DZ' => '🇩🇿',
|
||||
'EC' => '🇪🇨',
|
||||
'EE' => '🇪🇪',
|
||||
'EG' => '🇪🇬',
|
||||
'EH' => '🇪🇭',
|
||||
'ER' => '🇪🇷',
|
||||
'ES' => '🇪🇸',
|
||||
'ET' => '🇪🇹',
|
||||
'EU' => '🇪🇺',
|
||||
'FI' => '🇫🇮',
|
||||
'FJ' => '🇫🇯',
|
||||
'FK' => '🇫🇰',
|
||||
'FM' => '🇫🇲',
|
||||
'FO' => '🇫🇴',
|
||||
'FR' => '🇫🇷',
|
||||
'GA' => '🇬🇦',
|
||||
'GB' => '🇬🇧',
|
||||
'GD' => '🇬🇩',
|
||||
'GE' => '🇬🇪',
|
||||
'GF' => '🇬🇫',
|
||||
'GG' => '🇬🇬',
|
||||
'GH' => '🇬🇭',
|
||||
'GI' => '🇬🇮',
|
||||
'GL' => '🇬🇱',
|
||||
'GM' => '🇬🇲',
|
||||
'GN' => '🇬🇳',
|
||||
'GP' => '🇬🇵',
|
||||
'GQ' => '🇬🇶',
|
||||
'GR' => '🇬🇷',
|
||||
'GS' => '🇬🇸',
|
||||
'GT' => '🇬🇹',
|
||||
'GU' => '🇬🇺',
|
||||
'GW' => '🇬🇼',
|
||||
'GY' => '🇬🇾',
|
||||
'HK' => '🇭🇰',
|
||||
'HM' => '🇭🇲',
|
||||
'HN' => '🇭🇳',
|
||||
'HR' => '🇭🇷',
|
||||
'HT' => '🇭🇹',
|
||||
'HU' => '🇭🇺',
|
||||
'ID' => '🇮🇩',
|
||||
'IE' => '🇮🇪',
|
||||
'IL' => '🇮🇱',
|
||||
'IM' => '🇮🇲',
|
||||
'IN' => '🇮🇳',
|
||||
'IO' => '🇮🇴',
|
||||
'IQ' => '🇮🇶',
|
||||
'IR' => '🇮🇷',
|
||||
'IS' => '🇮🇸',
|
||||
'IT' => '🇮🇹',
|
||||
'JE' => '🇯🇪',
|
||||
'JM' => '🇯🇲',
|
||||
'JO' => '🇯🇴',
|
||||
'JP' => '🇯🇵',
|
||||
'KE' => '🇰🇪',
|
||||
'KG' => '🇰🇬',
|
||||
'KH' => '🇰🇭',
|
||||
'KI' => '🇰🇮',
|
||||
'KM' => '🇰🇲',
|
||||
'KN' => '🇰🇳',
|
||||
'KP' => '🇰🇵',
|
||||
'KR' => '🇰🇷',
|
||||
'KW' => '🇰🇼',
|
||||
'KY' => '🇰🇾',
|
||||
'KZ' => '🇰🇿',
|
||||
'LA' => '🇱🇦',
|
||||
'LB' => '🇱🇧',
|
||||
'LC' => '🇱🇨',
|
||||
'LI' => '🇱🇮',
|
||||
'LK' => '🇱🇰',
|
||||
'LR' => '🇱🇷',
|
||||
'LS' => '🇱🇸',
|
||||
'LT' => '🇱🇹',
|
||||
'LU' => '🇱🇺',
|
||||
'LV' => '🇱🇻',
|
||||
'LY' => '🇱🇾',
|
||||
'MA' => '🇲🇦',
|
||||
'MC' => '🇲🇨',
|
||||
'MD' => '🇲🇩',
|
||||
'ME' => '🇲🇪',
|
||||
'MF' => '🇲🇫',
|
||||
'MG' => '🇲🇬',
|
||||
'MH' => '🇲🇭',
|
||||
'MK' => '🇲🇰',
|
||||
'ML' => '🇲🇱',
|
||||
'MM' => '🇲🇲',
|
||||
'MN' => '🇲🇳',
|
||||
'MO' => '🇲🇴',
|
||||
'MP' => '🇲🇵',
|
||||
'MQ' => '🇲🇶',
|
||||
'MR' => '🇲🇷',
|
||||
'MS' => '🇲🇸',
|
||||
'MT' => '🇲🇹',
|
||||
'MU' => '🇲🇺',
|
||||
'MV' => '🇲🇻',
|
||||
'MW' => '🇲🇼',
|
||||
'MX' => '🇲🇽',
|
||||
'MY' => '🇲🇾',
|
||||
'MZ' => '🇲🇿',
|
||||
'NA' => '🇳🇦',
|
||||
'NC' => '🇳🇨',
|
||||
'NE' => '🇳🇪',
|
||||
'NF' => '🇳🇫',
|
||||
'NG' => '🇳🇬',
|
||||
'NI' => '🇳🇮',
|
||||
'NL' => '🇳🇱',
|
||||
'NO' => '🇳🇴',
|
||||
'NP' => '🇳🇵',
|
||||
'NR' => '🇳🇷',
|
||||
'NU' => '🇳🇺',
|
||||
'NZ' => '🇳🇿',
|
||||
'OM' => '🇴🇲',
|
||||
'PA' => '🇵🇦',
|
||||
'PE' => '🇵🇪',
|
||||
'PF' => '🇵🇫',
|
||||
'PG' => '🇵🇬',
|
||||
'PH' => '🇵🇭',
|
||||
'PK' => '🇵🇰',
|
||||
'PL' => '🇵🇱',
|
||||
'PM' => '🇵🇲',
|
||||
'PN' => '🇵🇳',
|
||||
'PR' => '🇵🇷',
|
||||
'PS' => '🇵🇸',
|
||||
'PT' => '🇵🇹',
|
||||
'PW' => '🇵🇼',
|
||||
'PY' => '🇵🇾',
|
||||
'QA' => '🇶🇦',
|
||||
'RE' => '🇷🇪',
|
||||
'RO' => '🇷🇴',
|
||||
'RS' => '🇷🇸',
|
||||
'RU' => '🇷🇺',
|
||||
'RW' => '🇷🇼',
|
||||
'SA' => '🇸🇦',
|
||||
'SB' => '🇸🇧',
|
||||
'SC' => '🇸🇨',
|
||||
'SD' => '🇸🇩',
|
||||
'SE' => '🇸🇪',
|
||||
'SG' => '🇸🇬',
|
||||
'SH' => '🇸🇭',
|
||||
'SI' => '🇸🇮',
|
||||
'SJ' => '🇸🇯',
|
||||
'SK' => '🇸🇰',
|
||||
'SL' => '🇸🇱',
|
||||
'SM' => '🇸🇲',
|
||||
'SN' => '🇸🇳',
|
||||
'SO' => '🇸🇴',
|
||||
'SR' => '🇸🇷',
|
||||
'SS' => '🇸🇸',
|
||||
'ST' => '🇸🇹',
|
||||
'SV' => '🇸🇻',
|
||||
'SX' => '🇸🇽',
|
||||
'SY' => '🇸🇾',
|
||||
'SZ' => '🇸🇿',
|
||||
'TC' => '🇹🇨',
|
||||
'TD' => '🇹🇩',
|
||||
'TF' => '🇹🇫',
|
||||
'TG' => '🇹🇬',
|
||||
'TH' => '🇹🇭',
|
||||
'TJ' => '🇹🇯',
|
||||
'TK' => '🇹🇰',
|
||||
'TL' => '🇹🇱',
|
||||
'TM' => '🇹🇲',
|
||||
'TN' => '🇹🇳',
|
||||
'TO' => '🇹🇴',
|
||||
'TR' => '🇹🇷',
|
||||
'TT' => '🇹🇹',
|
||||
'TV' => '🇹🇻',
|
||||
'TW' => '🇹🇼',
|
||||
'TZ' => '🇹🇿',
|
||||
'UA' => '🇺🇦',
|
||||
'UG' => '🇺🇬',
|
||||
'UM' => '🇺🇲',
|
||||
'US' => '🇺🇸',
|
||||
'UY' => '🇺🇾',
|
||||
'UZ' => '🇺🇿',
|
||||
'VA' => '🇻🇦',
|
||||
'VC' => '🇻🇨',
|
||||
'VE' => '🇻🇪',
|
||||
'VG' => '🇻🇬',
|
||||
'VI' => '🇻🇮',
|
||||
'VN' => '🇻🇳',
|
||||
'VU' => '🇻🇺',
|
||||
'WF' => '🇼🇫',
|
||||
'WS' => '🇼🇸',
|
||||
'XK' => '🇽🇰',
|
||||
'YE' => '🇾🇪',
|
||||
'YT' => '🇾🇹',
|
||||
'ZA' => '🇿🇦',
|
||||
'ZM' => '🇿🇲',
|
||||
'ZW' => '🇿🇼',
|
||||
];
|
||||
|
||||
/**
|
||||
* Retrieves a flag by country code.
|
||||
*
|
||||
* @param string $country country alpha-2 code (ISO 3166) like US.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_by_country( string $country ): string {
|
||||
return self::EMOJI_COUNTRIES_FLAGS[ $country ] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a flag by currency code.
|
||||
*
|
||||
* @param string $currency currency code (ISO 4217) like USD.
|
||||
* @return string
|
||||
*/
|
||||
public static function get_by_currency( string $currency ): string {
|
||||
$exceptions = [
|
||||
'ANG' => '',
|
||||
'BTC' => '',
|
||||
'XAF' => '',
|
||||
'XCD' => '',
|
||||
'XOF' => '',
|
||||
'XPF' => '',
|
||||
];
|
||||
|
||||
$flag = $exceptions[ $currency ] ?? self::get_by_country( substr( $currency, 0, -1 ) );
|
||||
|
||||
return $flag;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Currency
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Multi-Currency Currency object.
|
||||
*/
|
||||
class Currency implements \JsonSerializable {
|
||||
|
||||
/**
|
||||
* Three letter currency code.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $code;
|
||||
|
||||
/**
|
||||
* Currency conversion rate.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $rate;
|
||||
|
||||
/**
|
||||
* Currency charm rate after conversion and rounding.
|
||||
*
|
||||
* @var float|null
|
||||
*/
|
||||
private $charm;
|
||||
|
||||
/**
|
||||
* Is currency default for store?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_default = false;
|
||||
|
||||
/**
|
||||
* Currency rounding rate after conversion.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
private $rounding;
|
||||
|
||||
/**
|
||||
* Is currency zero decimal?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_zero_decimal = false;
|
||||
|
||||
/**
|
||||
* A timestamp representing the time this currency was last fetched successfully from the server.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $last_updated;
|
||||
|
||||
/**
|
||||
* Instance of MultiCurrencyLocalizationInterface.
|
||||
*
|
||||
* @var MultiCurrencyLocalizationInterface
|
||||
*/
|
||||
private $localization_service;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrencyLocalizationInterface $localization_service Localization service instance.
|
||||
* @param string $code Three letter currency code.
|
||||
* @param float $rate The conversion rate.
|
||||
* @param int|null $last_updated The time this currency was last updated.
|
||||
*/
|
||||
public function __construct( MultiCurrencyLocalizationInterface $localization_service, $code = '', float $rate = 1.0, $last_updated = null ) {
|
||||
$this->localization_service = $localization_service;
|
||||
$this->code = $code;
|
||||
$this->rate = $rate;
|
||||
|
||||
if ( get_woocommerce_currency() === $code ) {
|
||||
$this->is_default = true;
|
||||
}
|
||||
|
||||
// Set zero-decimal style based on WC locale information.
|
||||
$this->is_zero_decimal = 0 === $this->localization_service->get_currency_format( $code )['num_decimals'];
|
||||
|
||||
if ( ! is_null( $last_updated ) ) {
|
||||
$this->last_updated = $last_updated;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's translated name from WooCommerce core.
|
||||
*
|
||||
* @param string $code The currency code.
|
||||
*/
|
||||
public function get_currency_name_from_code( $code ): string {
|
||||
$wc_currencies = get_woocommerce_currencies();
|
||||
return $wc_currencies[ $code ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's code.
|
||||
*
|
||||
* @return string Three letter currency code.
|
||||
*/
|
||||
public function get_code(): string {
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's charm rate.
|
||||
*
|
||||
* @return float Charm rate.
|
||||
*/
|
||||
public function get_charm(): float {
|
||||
return is_null( $this->charm ) ? 0.00 : $this->charm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's flag.
|
||||
*
|
||||
* @return string Currency flag.
|
||||
*/
|
||||
public function get_flag(): string {
|
||||
// Maybe add param img/emoji to return which you want?
|
||||
return CountryFlags::get_by_currency( $this->code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency code lowercased.
|
||||
*
|
||||
* @return string Currency code lowercased.
|
||||
*/
|
||||
public function get_id(): string {
|
||||
return strtolower( $this->code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves if the currency is default for the store.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_is_default(): bool {
|
||||
return $this->is_default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's name from WooCommerce core.
|
||||
*
|
||||
* @return string Currency name.
|
||||
*/
|
||||
public function get_name(): string {
|
||||
$wc_currencies = get_woocommerce_currencies();
|
||||
return $wc_currencies[ $this->code ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's conversion rate.
|
||||
*
|
||||
* @return float The conversion rate.
|
||||
*/
|
||||
public function get_rate(): float {
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's rounding rate.
|
||||
*
|
||||
* @return string Rounding rate.
|
||||
*/
|
||||
public function get_rounding(): string {
|
||||
return (string) ( $this->rounding ?? '0' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's symbol from WooCommerce core.
|
||||
*
|
||||
* @return string Currency symbol.
|
||||
*/
|
||||
public function get_symbol(): string {
|
||||
return get_woocommerce_currency_symbol( $this->code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the currency's symbol position from Localization Service
|
||||
*
|
||||
* @return string Currency position (left/right).
|
||||
*/
|
||||
public function get_symbol_position(): string {
|
||||
return $this->localization_service->get_currency_format( $this->code )['currency_pos'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves if the currency is zero decimal.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function get_is_zero_decimal(): bool {
|
||||
return $this->is_zero_decimal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timestamp reprenting when the currency was last updated.
|
||||
*
|
||||
* @return int|null A timestamp representing when the currency was last updated.
|
||||
*/
|
||||
public function get_last_updated() {
|
||||
return $this->last_updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currency's charm rate.
|
||||
*
|
||||
* @param float $charm Charm rate.
|
||||
*/
|
||||
public function set_charm( $charm ) {
|
||||
$this->charm = $charm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currency's conversion rate.
|
||||
*
|
||||
* @param float $rate Conversion rate.
|
||||
*/
|
||||
public function set_rate( $rate ) {
|
||||
$this->rate = $rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the currency's rounding rate.
|
||||
*
|
||||
* @param string $rounding Rounding rate.
|
||||
* @return void
|
||||
*/
|
||||
public function set_rounding( $rounding ) {
|
||||
$this->rounding = $rounding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currency's last updated timestamp.
|
||||
*
|
||||
* @param int $last_updated A timestamp representing when the currency was last updated.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_last_updated( int $last_updated ) {
|
||||
$this->last_updated = $last_updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the data that should be serialized to JSON.
|
||||
*
|
||||
* @return array Serialized Currency object.
|
||||
*/
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'code' => $this->code,
|
||||
'rate' => $this->get_rate(),
|
||||
'name' => html_entity_decode( $this->get_name() ),
|
||||
'id' => $this->get_id(),
|
||||
'is_default' => $this->get_is_default(),
|
||||
'flag' => $this->get_flag(),
|
||||
'symbol' => html_entity_decode( $this->get_symbol() ),
|
||||
'symbol_position' => $this->get_symbol_position(),
|
||||
'is_zero_decimal' => $this->get_is_zero_decimal(),
|
||||
'last_updated' => $this->get_last_updated(),
|
||||
];
|
||||
}
|
||||
}
|
||||
+245
@@ -0,0 +1,245 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Currency Switcher Widget
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use function http_build_query;
|
||||
use function urldecode;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Currency Switcher Gutenberg Block.
|
||||
*/
|
||||
class CurrencySwitcherBlock {
|
||||
|
||||
/**
|
||||
* Instance of Multi-Currency.
|
||||
*
|
||||
* @var MultiCurrency $multi_currency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Instance of Compatibility.
|
||||
*
|
||||
* @var Compatibility $compatibility
|
||||
*/
|
||||
protected $compatibility;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency Instance of Multi-Currency.
|
||||
* @param Compatibility $compatibility Instance of Compatibility.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->compatibility = $compatibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'init', [ $this, 'init_block_widget' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the block currency switcher widget.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_block_widget() {
|
||||
// Automatically load dependencies and version.
|
||||
$this->multi_currency->register_script_with_dependencies( 'woocommerce-payments/multi-currency-switcher', 'dist/multi-currency-switcher-block' );
|
||||
|
||||
register_block_type(
|
||||
'woocommerce-payments/multi-currency-switcher',
|
||||
[
|
||||
'api_version' => '2',
|
||||
'editor_script' => 'woocommerce-payments/multi-currency-switcher',
|
||||
'render_callback' => [ $this, 'render_block_widget' ],
|
||||
'attributes' => [
|
||||
'symbol' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
],
|
||||
'flag' => [
|
||||
'type' => 'boolean',
|
||||
'default' => false,
|
||||
],
|
||||
'fontSize' => [
|
||||
'type' => 'integer',
|
||||
'default' => 14,
|
||||
],
|
||||
'fontLineHeight' => [
|
||||
'type' => 'number',
|
||||
'default' => 1.5,
|
||||
],
|
||||
'fontColor' => [
|
||||
'type' => 'string',
|
||||
'default' => '#000000',
|
||||
],
|
||||
'border' => [
|
||||
'type' => 'boolean',
|
||||
'default' => true,
|
||||
],
|
||||
'borderRadius' => [
|
||||
'type' => 'integer',
|
||||
'default' => 3,
|
||||
],
|
||||
'borderColor' => [
|
||||
'type' => 'string',
|
||||
'default' => '#000000',
|
||||
],
|
||||
'backgroundColor' => [
|
||||
'type' => 'string',
|
||||
'default' => 'transparent',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the content of the block widget. Called by the render_callback callback.
|
||||
* Normally, this could be all done on the JS side, however we need to use a dynamic
|
||||
* block here because the currencies enabled on a site could change, and this would not update
|
||||
* properly on the Gutenberg block, because it is cached.
|
||||
*
|
||||
* @param array $block_attributes The attributes (settings) applicable to this block. We expect this will contain
|
||||
* the widget title, and whether or not we should render both flags and symbols.
|
||||
*
|
||||
* @return string The content to be displayed inside the block widget.
|
||||
*/
|
||||
public function render_block_widget( $block_attributes ): string {
|
||||
if ( $this->compatibility->should_disable_currency_switching() ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$enabled_currencies = $this->multi_currency->get_enabled_currencies();
|
||||
|
||||
if ( 1 === count( $enabled_currencies ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$with_symbol = $block_attributes['symbol'] ?? true;
|
||||
$with_flag = $block_attributes['flag'] ?? false;
|
||||
|
||||
$styles = $this->get_widget_styles( $block_attributes );
|
||||
$div_styles = $this->implode_styles_array( $styles['div'] );
|
||||
$select_styles = $this->implode_styles_array( $styles['select'] );
|
||||
|
||||
$widget_content = '<form>';
|
||||
$widget_content .= $this->get_get_params();
|
||||
$widget_content .= '<div class="currency-switcher-holder" style="' . esc_attr( $div_styles ) . '">';
|
||||
$widget_content .= '<select name="currency" onchange="this.form.submit()" style="' . esc_attr( $select_styles ) . '">';
|
||||
|
||||
foreach ( $enabled_currencies as $currency ) {
|
||||
$widget_content .= $this->render_currency_option( $currency, $with_symbol, $with_flag );
|
||||
}
|
||||
|
||||
$widget_content .= '</select></div></form>';
|
||||
|
||||
// Silence XSS warning because we are manually constructing the content and escaping everything above.
|
||||
// nosemgrep: audit.php.wp.security.xss.block-attr -- reason: we are manually constructing the content and escaping everything above.
|
||||
return $widget_content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an <option> element with provided currency. With symbol and flag if requested.
|
||||
*
|
||||
* @param Currency $currency Currency to use for <option> element.
|
||||
* @param boolean $with_symbol Whether to show the currency symbol.
|
||||
* @param boolean $with_flag Whether to show the currency flag.
|
||||
*
|
||||
* @return string Display HTML of currency <option>, as a string.
|
||||
*/
|
||||
private function render_currency_option( Currency $currency, bool $with_symbol, bool $with_flag ): string {
|
||||
$code = $currency->get_code();
|
||||
$same_symbol = html_entity_decode( $currency->get_symbol() ) === $code;
|
||||
$text = $code;
|
||||
$selected = $this->multi_currency->get_selected_currency()->code === $code ? 'selected' : '';
|
||||
|
||||
if ( $with_symbol && ! $same_symbol ) {
|
||||
$text = $currency->get_symbol() . ' ' . $text;
|
||||
}
|
||||
if ( $with_flag ) {
|
||||
$text = $currency->get_flag() . ' ' . $text;
|
||||
}
|
||||
|
||||
return '<option value="' . esc_attr( $code ) . '" ' . $selected . '>' . esc_html( $text ) . '</option>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of styling rules, output them as a string containing valid CSS.
|
||||
*
|
||||
* @param array $styles An array of CSS styles.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function implode_styles_array( array $styles ): string {
|
||||
$return_str = '';
|
||||
foreach ( $styles as $key => $value ) {
|
||||
$return_str .= $key . ': ' . $value . '; ';
|
||||
}
|
||||
|
||||
return $return_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the styles that need to be applied to the widget based on the block attributes.
|
||||
*
|
||||
* @param array $block_attributes The block attributes.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_widget_styles( array $block_attributes ): array {
|
||||
return [
|
||||
'div' => [
|
||||
'line-height' => $block_attributes['fontLineHeight'] ?? 1.2,
|
||||
],
|
||||
'select' => [
|
||||
'padding' => '2px',
|
||||
'border' => ! empty( $block_attributes['border'] ) ? '1px solid' : '0px solid',
|
||||
'border-radius' => isset( $block_attributes['borderRadius'] ) ? $block_attributes['borderRadius'] . 'px' : '3px',
|
||||
'border-color' => $block_attributes['borderColor'] ?? '#000000',
|
||||
'font-size' => isset( $block_attributes['fontSize'] ) ? $block_attributes['fontSize'] . 'px' : '11px',
|
||||
'color' => $block_attributes['fontColor'] ?? '#000000',
|
||||
'background-color' => $block_attributes['backgroundColor'] ?? '#000000',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hidden inputs for every $_GET param.
|
||||
* This prevents the switcher form to remove them on submit.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function get_get_params() {
|
||||
if ( empty( $_GET ) ) { // phpcs:disable WordPress.Security.NonceVerification
|
||||
return null;
|
||||
}
|
||||
|
||||
$params = explode( '&', urldecode( http_build_query( $_GET ) ) );
|
||||
$return = '';
|
||||
foreach ( $params as $param ) {
|
||||
$name_value = explode( '=', $param );
|
||||
$name = $name_value[0];
|
||||
$value = $name_value[1];
|
||||
if ( 'currency' === $name ) {
|
||||
continue;
|
||||
}
|
||||
$return .= sprintf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $name ), esc_attr( $value ) );
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Currency Switcher Widget
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WC_Widget;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Currency Switcher Widget Class
|
||||
*/
|
||||
class CurrencySwitcherWidget extends WC_Widget {
|
||||
|
||||
const DEFAULT_SETTINGS = [
|
||||
'title' => '',
|
||||
'symbol' => true,
|
||||
'flag' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* Compatibility instance.
|
||||
*
|
||||
* @var Compatibility
|
||||
*/
|
||||
protected $compatibility;
|
||||
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Register widget with WordPress.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
* @param Compatibility $compatibility The Compatibility instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->compatibility = $compatibility;
|
||||
|
||||
$this->widget_id = 'currency_switcher_widget';
|
||||
$this->widget_name = __( 'Currency Switcher Widget', 'woocommerce-payments' );
|
||||
$this->widget_description = __( 'Let your customers switch between your enabled currencies', 'woocommerce-payments' );
|
||||
$this->settings = [
|
||||
'title' => [
|
||||
'type' => 'text',
|
||||
'std' => '',
|
||||
'label' => __( 'Title', 'woocommerce-payments' ),
|
||||
],
|
||||
'symbol' => [
|
||||
'type' => 'checkbox',
|
||||
'std' => true,
|
||||
'label' => __( 'Display currency symbols', 'woocommerce-payments' ),
|
||||
],
|
||||
'flag' => [
|
||||
'type' => 'checkbox',
|
||||
'std' => false,
|
||||
'label' => __( 'Display flags in supported devices', 'woocommerce-payments' ),
|
||||
],
|
||||
];
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Front-end display of widget.
|
||||
*
|
||||
* @param array $args Widget arguments.
|
||||
* @param array $instance Saved values from database.
|
||||
*/
|
||||
public function widget( $args, $instance ) {
|
||||
if ( $this->compatibility->should_disable_currency_switching() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$enabled_currencies = $this->multi_currency->get_enabled_currencies();
|
||||
|
||||
if ( 1 === count( $enabled_currencies ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$instance = wp_parse_args(
|
||||
$instance,
|
||||
self::DEFAULT_SETTINGS
|
||||
);
|
||||
|
||||
$title = apply_filters( 'widget_title', $instance['title'], $instance, $this->id_base );
|
||||
|
||||
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
if ( ! empty( $title ) ) {
|
||||
echo $args['before_title'] . $title . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
|
||||
?>
|
||||
<form>
|
||||
<?php $this->output_get_params(); ?>
|
||||
<select
|
||||
name="currency"
|
||||
aria-label="<?php echo esc_attr( $title ); ?>"
|
||||
onchange="this.form.submit()"
|
||||
>
|
||||
<?php
|
||||
foreach ( $enabled_currencies as $currency ) {
|
||||
$this->display_currency_option( $currency, $instance['symbol'], $instance['flag'] );
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
</form>
|
||||
<?php
|
||||
|
||||
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an <option> element with provided currency. With symbol and flag if requested.
|
||||
*
|
||||
* @param Currency $currency Currency to use for <option> element.
|
||||
* @param boolean $with_symbol Whether to show the currency symbol.
|
||||
* @param boolean $with_flag Whether to show the currency flag.
|
||||
*
|
||||
* @return void Displays HTML of currency <option>
|
||||
*/
|
||||
private function display_currency_option( Currency $currency, bool $with_symbol, bool $with_flag ) {
|
||||
$code = $currency->get_code();
|
||||
$same_symbol = html_entity_decode( $currency->get_symbol() ) === $code;
|
||||
$text = $code;
|
||||
$selected = $this->multi_currency->get_selected_currency()->code === $code ? ' selected' : '';
|
||||
|
||||
if ( $with_symbol && ! $same_symbol ) {
|
||||
$text = $currency->get_symbol() . ' ' . $text;
|
||||
}
|
||||
if ( $with_flag ) {
|
||||
$text = $currency->get_flag() . ' ' . $text;
|
||||
}
|
||||
|
||||
echo "<option value=\"$code\"$selected>$text</option>"; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
|
||||
/**
|
||||
* Output hidden inputs for every $_GET param.
|
||||
* This prevent the switcher form to remove them on submit.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function output_get_params() {
|
||||
// phpcs:disable WordPress.Security.NonceVerification
|
||||
if ( empty( $_GET ) ) {
|
||||
return;
|
||||
}
|
||||
$params = explode( '&', urldecode( http_build_query( $_GET ) ) );
|
||||
foreach ( $params as $param ) {
|
||||
$name_value = explode( '=', $param );
|
||||
$name = $name_value[0];
|
||||
$value = $name_value[1];
|
||||
if ( 'currency' === $name ) {
|
||||
continue;
|
||||
}
|
||||
echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '" />';
|
||||
}
|
||||
// phpcs:enable WordPress.Security.NonceVerification
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Class InvalidCurrencyException
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Exception for throwing errors when an invalid currency is used.
|
||||
*/
|
||||
class InvalidCurrencyException extends Exception {}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Class InvalidCurrencyRateException
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Exception for throwing errors when an invalid currency rate is used.
|
||||
*/
|
||||
class InvalidCurrencyRateException extends Exception {}
|
||||
+440
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency Frontend Currencies
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WC_Order;
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that formats Multi-Currency currencies on the frontend.
|
||||
*/
|
||||
class FrontendCurrencies {
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* MultiCurrencyLocalizationInterface instance.
|
||||
*
|
||||
* @var MultiCurrencyLocalizationInterface
|
||||
*/
|
||||
protected $localization_service;
|
||||
|
||||
/**
|
||||
* Multi-currency utils instance.
|
||||
*
|
||||
* @var Utils
|
||||
*/
|
||||
protected $utils;
|
||||
|
||||
/**
|
||||
* Multi-currency compatibility instance.
|
||||
*
|
||||
* @var Compatibility
|
||||
*/
|
||||
protected $compatibility;
|
||||
|
||||
/**
|
||||
* Multi-Currency currency formatting map.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $currency_format = [];
|
||||
|
||||
/**
|
||||
* Order currency code.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $order_currency;
|
||||
|
||||
/**
|
||||
* WOO currency cache.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $woocommerce_currency;
|
||||
|
||||
/**
|
||||
* Price Decimal Separator cache.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $price_decimal_separators = [];
|
||||
|
||||
/**
|
||||
* Selected Currency Code cache.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $selected_currency_code;
|
||||
|
||||
/**
|
||||
* Store Currency cache.
|
||||
*
|
||||
* @var Currency
|
||||
*/
|
||||
private $store_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
* @param MultiCurrencyLocalizationInterface $localization_service The Localization Service instance.
|
||||
* @param Utils $utils Utils instance.
|
||||
* @param Compatibility $compatibility Compatibility instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, MultiCurrencyLocalizationInterface $localization_service, Utils $utils, Compatibility $compatibility ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->localization_service = $localization_service;
|
||||
$this->utils = $utils;
|
||||
$this->compatibility = $compatibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) {
|
||||
// Currency hooks.
|
||||
add_filter( 'woocommerce_currency', [ $this, 'get_woocommerce_currency' ], 900 );
|
||||
add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 );
|
||||
add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 );
|
||||
add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 );
|
||||
add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 );
|
||||
add_action( 'before_woocommerce_pay', [ $this, 'init_order_currency_from_query_vars' ] );
|
||||
add_action( 'woocommerce_order_get_total', [ $this, 'maybe_init_order_currency_from_order_total_prop' ], 900, 2 );
|
||||
add_action( 'woocommerce_get_formatted_order_total', [ $this, 'maybe_clear_order_currency_after_formatted_order_total' ], 900, 4 );
|
||||
}
|
||||
|
||||
add_filter( 'woocommerce_thankyou_order_id', [ $this, 'init_order_currency' ] );
|
||||
add_action( 'woocommerce_account_view-order_endpoint', [ $this, 'init_order_currency' ], 9 );
|
||||
add_filter( 'woocommerce_cart_hash', [ $this, 'add_currency_to_cart_hash' ], 900 );
|
||||
add_filter( 'woocommerce_shipping_method_add_rate_args', [ $this, 'fix_price_decimals_for_shipping_rates' ], 900, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The selected currency changed. We discard some cache.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function selected_currency_changed() {
|
||||
$this->selected_currency_code = null;
|
||||
$this->price_decimal_separators = [];
|
||||
$this->woocommerce_currency = null;
|
||||
$this->store_currency = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes 'min_price' and 'max_price' from the URL query parameters.
|
||||
*
|
||||
* Clears existing price filters when the currency is changed to prevent inconsistencies.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clear_url_price_params() {
|
||||
if ( isset( $_GET['min_price'] ) || isset( $_GET['max_price'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$url = remove_query_arg( [ 'min_price', 'max_price' ] );
|
||||
|
||||
wp_safe_redirect( $url );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the store currency.
|
||||
*
|
||||
* @return Currency The store currency wrapped as a Currency object
|
||||
*/
|
||||
public function get_store_currency(): Currency {
|
||||
if ( empty( $this->store_currency ) ) {
|
||||
$this->store_currency = $this->multi_currency->get_default_currency();
|
||||
}
|
||||
return $this->store_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency code to be used by WooCommerce.
|
||||
*
|
||||
* @return string The code of the currency to be used.
|
||||
*/
|
||||
public function get_woocommerce_currency(): string {
|
||||
if ( $this->compatibility->should_return_store_currency() ) {
|
||||
return $this->get_store_currency()->get_code();
|
||||
}
|
||||
|
||||
if ( empty( $this->woocommerce_currency ) ) {
|
||||
$this->woocommerce_currency = $this->get_selected_currency_code();
|
||||
}
|
||||
return $this->woocommerce_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of decimals to be used by WooCommerce.
|
||||
*
|
||||
* @param int $decimals The original decimal count.
|
||||
*
|
||||
* @return int The number of decimals.
|
||||
*/
|
||||
public function get_price_decimals( $decimals ): int {
|
||||
$currency_code = $this->get_currency_code();
|
||||
if ( $currency_code !== $this->get_store_currency()->get_code() ) {
|
||||
return absint( $this->localization_service->get_currency_format( $currency_code )['num_decimals'] );
|
||||
}
|
||||
return $decimals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decimal separator to be used by WooCommerce.
|
||||
*
|
||||
* @param string $separator The original separator.
|
||||
*
|
||||
* @return string The decimal separator.
|
||||
*/
|
||||
public function get_price_decimal_separator( $separator ): string {
|
||||
$currency_code = $this->get_currency_code();
|
||||
$store_currency_code = $this->get_store_currency()->get_code();
|
||||
|
||||
if ( $currency_code === $store_currency_code ) {
|
||||
$currency_code = $store_currency_code;
|
||||
$this->price_decimal_separators[ $currency_code ] = $separator;
|
||||
}
|
||||
|
||||
if ( empty( $this->price_decimal_separators[ $currency_code ] ) ) {
|
||||
$this->price_decimal_separators[ $currency_code ] = $this->localization_service->get_currency_format( $currency_code )['decimal_sep'];
|
||||
}
|
||||
|
||||
return $this->price_decimal_separators[ $currency_code ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the thousand separator to be used by WooCommerce.
|
||||
*
|
||||
* @param string $separator The original separator.
|
||||
*
|
||||
* @return string The thousand separator.
|
||||
*/
|
||||
public function get_price_thousand_separator( $separator ): string {
|
||||
$currency_code = $this->get_currency_code();
|
||||
if ( $currency_code !== $this->get_store_currency()->get_code() ) {
|
||||
return $this->localization_service->get_currency_format( $currency_code )['thousand_sep'];
|
||||
}
|
||||
return $separator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currency format to be used by WooCommerce.
|
||||
*
|
||||
* @param string $format The original currency format.
|
||||
*
|
||||
* @return string The currency format.
|
||||
*/
|
||||
public function get_woocommerce_price_format( $format ): string {
|
||||
$currency_code = $this->get_currency_code();
|
||||
if ( $currency_code !== $this->get_store_currency()->get_code() ) {
|
||||
$currency_pos = $this->localization_service->get_currency_format( $currency_code )['currency_pos'];
|
||||
|
||||
switch ( $currency_pos ) {
|
||||
case 'left':
|
||||
return '%1$s%2$s';
|
||||
case 'right':
|
||||
return '%2$s%1$s';
|
||||
case 'left_space':
|
||||
return '%1$s %2$s';
|
||||
case 'right_space':
|
||||
return '%2$s %1$s';
|
||||
default:
|
||||
return '%1$s%2$s';
|
||||
}
|
||||
}
|
||||
return $format;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the currency and exchange rate to the cart hash so it's recalculated properly.
|
||||
*
|
||||
* @param string $hash The cart hash.
|
||||
*
|
||||
* @return string The adjusted cart hash.
|
||||
*/
|
||||
public function add_currency_to_cart_hash( $hash ): string {
|
||||
$currency = $this->multi_currency->get_selected_currency();
|
||||
return md5( $hash . $currency->get_code() . $currency->get_rate() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits order currency code.
|
||||
*
|
||||
* @param mixed $arg Either WC_Order or the id of an order are expected, but can be empty.
|
||||
*
|
||||
* @return int|mixed The order id or what was passed as $arg.
|
||||
*/
|
||||
public function init_order_currency( $arg ) {
|
||||
if ( null !== $this->order_currency ) {
|
||||
return $arg;
|
||||
}
|
||||
|
||||
// We remove these filters here because 'wc_get_order'
|
||||
// can trigger them, leading to an infinitely recursive call.
|
||||
remove_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 );
|
||||
remove_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 );
|
||||
remove_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 );
|
||||
remove_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 );
|
||||
$order = ! $arg instanceof WC_Order ? wc_get_order( $arg ) : $arg;
|
||||
add_filter( 'wc_get_price_decimals', [ $this, 'get_price_decimals' ], 900 );
|
||||
add_filter( 'wc_get_price_decimal_separator', [ $this, 'get_price_decimal_separator' ], 900 );
|
||||
add_filter( 'wc_get_price_thousand_separator', [ $this, 'get_price_thousand_separator' ], 900 );
|
||||
add_filter( 'woocommerce_price_format', [ $this, 'get_woocommerce_price_format' ], 900 );
|
||||
|
||||
if ( $order ) {
|
||||
$this->order_currency = $order->get_currency();
|
||||
return $order->get_id();
|
||||
}
|
||||
|
||||
$this->order_currency = $this->multi_currency->get_selected_currency()->get_code();
|
||||
return $arg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the order id from the wp query_vars and then calls init_order_currency.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_order_currency_from_query_vars() {
|
||||
global $wp;
|
||||
if ( ! empty( $wp->query_vars['order-pay'] ) ) {
|
||||
$this->init_order_currency( $wp->query_vars['order-pay'] );
|
||||
} elseif ( ! empty( $wp->query_vars['order-received'] ) ) {
|
||||
$this->init_order_currency( $wp->query_vars['order-received'] );
|
||||
} elseif ( ! empty( $wp->query_vars['view-order'] ) ) {
|
||||
$this->init_order_currency( $wp->query_vars['view-order'] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixes the decimals for the store currency when shipping rates are being determined.
|
||||
* Our `wc_get_price_decimals` filter returns the decimals for the selected currency during this calculation, which leads to incorrect results.
|
||||
*
|
||||
* @param array $args The argument array to be filtered.
|
||||
* @param object $method The shipping method being calculated.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fix_price_decimals_for_shipping_rates( array $args, $method ): array {
|
||||
$args['price_decimals'] = absint( $this->localization_service->get_currency_format( $this->get_store_currency()->get_code() )['num_decimals'] );
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of order_currency.
|
||||
*
|
||||
* @return ?string The currency code or null.
|
||||
*/
|
||||
public function get_order_currency() {
|
||||
return $this->order_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe init the order_currency when the order total is queried if we should_use_order_currency.
|
||||
*
|
||||
* This works off of filtering during WC_Abstract_Order->get_total, which states it returns a float, however, in the instances of orders with negative
|
||||
* amounts, such as refund orders, it will return a string.
|
||||
*
|
||||
* @param mixed $total The order total.
|
||||
* @param WC_Order $order The order being worked on.
|
||||
*
|
||||
* @return mixed The unmodified total.
|
||||
*/
|
||||
public function maybe_init_order_currency_from_order_total_prop( $total, $order ) {
|
||||
if ( $this->should_use_order_currency() ) {
|
||||
$this->init_order_currency( $order );
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the order_currency is set and we should be using the order currency, clear it.
|
||||
*
|
||||
* This should only be happening on the instances tested for in should_use_order_currency. We would need to clear the order currency once the total
|
||||
* filter is run so that if another total comes up, like in the order list, we use the next order's currency.
|
||||
*
|
||||
* @param string $formatted_total Total to display.
|
||||
* @param WC_Order $order Order data.
|
||||
* @param string $tax_display Type of tax display.
|
||||
* @param bool $display_refunded If should include refunded value.
|
||||
*
|
||||
* @return string The unmodified formatted total.
|
||||
*/
|
||||
public function maybe_clear_order_currency_after_formatted_order_total( $formatted_total, $order, $tax_display, $display_refunded ): string {
|
||||
if ( null !== $this->order_currency && $this->should_use_order_currency() ) {
|
||||
$this->order_currency = null;
|
||||
}
|
||||
|
||||
return $formatted_total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currency code for us to use.
|
||||
*
|
||||
* @return string|null Three letter currency code.
|
||||
*/
|
||||
private function get_currency_code() {
|
||||
if ( $this->should_use_order_currency() ) {
|
||||
$this->init_order_currency_from_query_vars();
|
||||
|
||||
return $this->order_currency;
|
||||
}
|
||||
|
||||
$this->selected_currency_code = $this->get_selected_currency_code();
|
||||
|
||||
return $this->selected_currency_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to "cache" the selected currency.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_selected_currency_code(): string {
|
||||
if ( empty( $this->selected_currency_code ) ) {
|
||||
$this->selected_currency_code = $this->multi_currency->get_selected_currency()->get_code();
|
||||
}
|
||||
return $this->selected_currency_code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether currency code used for formatting should be overridden.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_use_order_currency(): bool {
|
||||
$pages = [ 'my-account', 'checkout' ];
|
||||
$vars = [ 'order-received', 'order-pay', 'orders', 'view-order' ];
|
||||
|
||||
if ( $this->utils->is_page_with_vars( $pages, $vars ) ) {
|
||||
return $this->utils->is_call_in_backtrace(
|
||||
[
|
||||
'WC_Shortcode_My_Account::view_order',
|
||||
'WC_Shortcode_Checkout::order_received',
|
||||
'WC_Shortcode_Checkout::order_pay',
|
||||
'WC_Order->get_formatted_order_total',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency Frontend Prices
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WC_Order;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that applies Multi-Currency prices on the frontend.
|
||||
*/
|
||||
class FrontendPrices {
|
||||
/**
|
||||
* Compatibility instance.
|
||||
*
|
||||
* @var Compatibility
|
||||
*/
|
||||
protected $compatibility;
|
||||
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
* @param Compatibility $compatibility The Compatibility instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, Compatibility $compatibility ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->compatibility = $compatibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
if ( ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) {
|
||||
// Simple product price hooks.
|
||||
add_filter( 'woocommerce_product_get_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
add_filter( 'woocommerce_product_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
add_filter( 'woocommerce_product_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
|
||||
// Variation price hooks.
|
||||
add_filter( 'woocommerce_product_variation_get_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
add_filter( 'woocommerce_product_variation_get_regular_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
add_filter( 'woocommerce_product_variation_get_sale_price', [ $this, 'get_product_price_string' ], 99, 2 );
|
||||
|
||||
// Variation price range hooks.
|
||||
add_filter( 'woocommerce_variation_prices', [ $this, 'get_variation_price_range' ], 99 );
|
||||
add_filter( 'woocommerce_get_variation_prices_hash', [ $this, 'add_exchange_rate_to_variation_prices_hash' ], 99 );
|
||||
|
||||
// Shipping methods hooks.
|
||||
add_action( 'init', [ $this, 'register_free_shipping_filters' ], 99 );
|
||||
add_filter( 'woocommerce_shipping_method_add_rate_args', [ $this, 'convert_shipping_method_rate_cost' ], 99 );
|
||||
|
||||
// Coupon hooks.
|
||||
add_filter( 'woocommerce_coupon_get_amount', [ $this, 'get_coupon_amount' ], 99, 2 );
|
||||
add_filter( 'woocommerce_coupon_get_minimum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 );
|
||||
add_filter( 'woocommerce_coupon_get_maximum_amount', [ $this, 'get_coupon_min_max_amount' ], 99 );
|
||||
|
||||
// Order hooks.
|
||||
add_filter( 'woocommerce_new_order', [ $this, 'add_order_meta' ], 99, 2 );
|
||||
|
||||
// Price Filter Hooks.
|
||||
add_filter( 'rest_post_dispatch', [ $this, 'maybe_modify_price_ranges_rest_response' ], 10, 3 );
|
||||
add_filter( 'query_loop_block_query_vars', [ $this, 'maybe_modify_price_ranges_query_var' ], 10, 3 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the price range query parameters when the selected currency is not the same as the store currency.
|
||||
*
|
||||
* This method converts the '_price' parameters based on the selected currency.
|
||||
*
|
||||
* @param array $query The current query variables.
|
||||
* @param \WP_Block $block The current block instance.
|
||||
* @param int $page The current page number.
|
||||
*
|
||||
* @return array The modified query variables.
|
||||
*/
|
||||
public function maybe_modify_price_ranges_query_var( $query, $block, $page ) {
|
||||
if ( 'product' !== $query['post_type'] ) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
if ( empty( $query['meta_query'] ) || ! is_array( $query['meta_query'] ) ) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$store_currency = $this->multi_currency->get_default_currency()->get_code();
|
||||
$selected_currency = $this->multi_currency->get_selected_currency()->get_code();
|
||||
|
||||
// If currencies are the same, no need to convert prices in the query.
|
||||
if ( $store_currency === $selected_currency ) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Traverse and modify the meta_query array.
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
$query['meta_query'] = $this->convert_meta_query_price_filters( $query['meta_query'], $store_currency, $selected_currency );
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively traverses and modifies the meta_query array to adjust '_price' values
|
||||
* from the 'from_currency' to the 'target_currency'.
|
||||
*
|
||||
* @param array $meta_query The meta_query array to traverse.
|
||||
* @param string $from_currency The from currency code.
|
||||
* @param string $target_currency The target currency code.
|
||||
* @param int $depth The current depth of the recursion.
|
||||
*
|
||||
* @return array The modified meta_query array.
|
||||
*/
|
||||
private function convert_meta_query_price_filters( $meta_query, $from_currency, $target_currency, $depth = 0 ) {
|
||||
// Prevent infinite recursion in a malformed meta_query.
|
||||
if ( $depth > 4 ) {
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
foreach ( $meta_query as &$mq ) {
|
||||
// If the current element is a nested meta_query with a relation.
|
||||
if ( isset( $mq['relation'] ) && is_array( $mq ) ) {
|
||||
// Recursively modify the nested meta_query.
|
||||
if ( isset( $mq['relation'] ) ) {
|
||||
// Extract the relation and the nested queries.
|
||||
$relation = $mq['relation'];
|
||||
|
||||
$modified_nested = $this->convert_meta_query_price_filters( $mq, $from_currency, $target_currency, $depth + 1 );
|
||||
|
||||
// Reconstruct the meta_query with the modified nested queries.
|
||||
$mq = array_merge( [ 'relation' => $relation ], $modified_nested );
|
||||
}
|
||||
} elseif ( isset( $mq['key'] ) && '_price' === $mq['key'] && isset( $mq['value'] ) && is_numeric( $mq['value'] ) ) {
|
||||
$converted_price = $this->multi_currency->get_raw_conversion( $mq['value'], $from_currency, $target_currency );
|
||||
|
||||
if ( is_numeric( $converted_price ) ) {
|
||||
// Apply floor or ceil based on the 'compare' operator.
|
||||
if ( isset( $mq['compare'] ) ) {
|
||||
if ( '<=' === $mq['compare'] ) {
|
||||
$mq['value'] = (string) ceil( $converted_price ); // max_price.
|
||||
} elseif ( '>=' === $mq['compare'] ) {
|
||||
$mq['value'] = (string) floor( $converted_price ); // min_price.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
unset( $mq );
|
||||
|
||||
return $meta_query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify the products/collection-data REST API response to include converted price ranges.
|
||||
*
|
||||
* @param \WP_REST_Response $response The original REST response.
|
||||
* @param \WP_REST_Server $server The REST server instance.
|
||||
* @param \WP_REST_Request $request The REST request instance.
|
||||
*
|
||||
* @return \WP_REST_Response The modified REST response.
|
||||
*/
|
||||
public function maybe_modify_price_ranges_rest_response( $response, $server, $request ) {
|
||||
if ( '/wc/store/v1/products/collection-data' !== $request->get_route() ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = $response->get_data();
|
||||
|
||||
if ( empty( $data['price_range'] ) || ! is_object( $data['price_range'] ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$store_currency = $this->multi_currency->get_default_currency()->get_code();
|
||||
$selected_currency = $this->multi_currency->get_selected_currency()->get_code();
|
||||
|
||||
if ( $store_currency === $selected_currency ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$price_fields = [ 'min_price', 'max_price' ];
|
||||
|
||||
foreach ( $price_fields as $field ) {
|
||||
if ( property_exists( $data['price_range'], $field ) && is_numeric( $data['price_range']->$field ) ) {
|
||||
$converted_price = $this->multi_currency->get_price( $data['price_range']->$field, 'product' );
|
||||
|
||||
if ( is_numeric( $converted_price ) ) {
|
||||
$data['price_range']->$field = (string) $converted_price;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$response->set_data( $data );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price for a product.
|
||||
*
|
||||
* @param mixed $price The product's price.
|
||||
* @param mixed $product WC_Product or null.
|
||||
*
|
||||
* @return mixed The converted product's price.
|
||||
*/
|
||||
public function get_product_price( $price, $product = null ) {
|
||||
if ( ! $price || ! $this->compatibility->should_convert_product_price( $product ) ) {
|
||||
return $price;
|
||||
}
|
||||
|
||||
return $this->multi_currency->get_price( $price, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the stringified price for a product.
|
||||
*
|
||||
* @param mixed $price The product's price.
|
||||
* @param mixed $product WC_Product or null.
|
||||
*
|
||||
* @return string The converted product's price.
|
||||
*/
|
||||
public function get_product_price_string( $price, $product = null ): string {
|
||||
return (string) $this->get_product_price( $price, $product );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the price range for a variation.
|
||||
*
|
||||
* @param array $variation_prices The variation's prices.
|
||||
*
|
||||
* @return array The converted variation's prices.
|
||||
*/
|
||||
public function get_variation_price_range( $variation_prices ) {
|
||||
foreach ( $variation_prices as $price_type => $prices ) {
|
||||
foreach ( $prices as $variation_id => $price ) {
|
||||
$variation_prices[ $price_type ][ $variation_id ] = $this->get_product_price_string( $price );
|
||||
}
|
||||
}
|
||||
|
||||
return $variation_prices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the exchange rate into account for the variation prices hash.
|
||||
* This is used to recalculate the variation price range when the exchange
|
||||
* rate changes, otherwise the old prices will be cached.
|
||||
*
|
||||
* @param array $prices_hash The variation prices hash.
|
||||
*
|
||||
* @return array The variation prices hash with the current exchange rate.
|
||||
*/
|
||||
public function add_exchange_rate_to_variation_prices_hash( $prices_hash ) {
|
||||
$prices_hash[] = $this->get_product_price( 1 );
|
||||
return $prices_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shipping add rate args with cost converted.
|
||||
*
|
||||
* @param array $args Shipping rate args.
|
||||
*
|
||||
* @return array Shipping rate args with converted cost.
|
||||
*/
|
||||
public function convert_shipping_method_rate_cost( $args ) {
|
||||
$cost = is_array( $args['cost'] ) ? array_sum( $args['cost'] ) : $args['cost'];
|
||||
$args = wp_parse_args(
|
||||
[
|
||||
'cost' => $this->multi_currency->get_price( $cost, 'shipping' ),
|
||||
],
|
||||
$args
|
||||
);
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount for a coupon.
|
||||
*
|
||||
* @param mixed $amount The coupon's amount.
|
||||
* @param object $coupon The coupon object.
|
||||
*
|
||||
* @return mixed The converted coupon's amount.
|
||||
*/
|
||||
public function get_coupon_amount( $amount, $coupon ) {
|
||||
$percent_coupon_types = [ 'percent' ];
|
||||
|
||||
if ( ! $amount
|
||||
|| $coupon->is_type( $percent_coupon_types )
|
||||
|| ! $this->compatibility->should_convert_coupon_amount( $coupon ) ) {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
return $this->multi_currency->get_price( $amount, 'coupon' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the min or max amount for a coupon.
|
||||
*
|
||||
* @param mixed $amount The coupon's min or max amount.
|
||||
*
|
||||
* @return mixed The converted coupon's min or max amount.
|
||||
*/
|
||||
public function get_coupon_min_max_amount( $amount ) {
|
||||
if ( ! $amount ) {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
// Coupon mix/max prices are treated as products to avoid inconsistencies with charm pricing
|
||||
// making a coupon invalid when the coupon min/max amount is the same as the product's price.
|
||||
return $this->multi_currency->get_price( $amount, 'product' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the free shipping zone settings with converted min_amount.
|
||||
*
|
||||
* @param array $data The shipping zone settings.
|
||||
*
|
||||
* @return array The shipping zone settings with converted min_amount.
|
||||
*/
|
||||
public function get_free_shipping_min_amount( $data ) {
|
||||
if ( empty( $data['min_amount'] ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Free shipping min amount is treated as products to avoid inconsistencies with charm pricing
|
||||
// making a method invalid when its min amount is the same as the product's price.
|
||||
$data['min_amount'] = $this->multi_currency->get_price( $data['min_amount'], 'product' );
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the hooks to set the min amount for free shipping methods.
|
||||
*/
|
||||
public function register_free_shipping_filters() {
|
||||
$shipping_zones = \WC_Shipping_Zones::get_zones();
|
||||
|
||||
$default_zone = \WC_Shipping_Zones::get_zone( 0 );
|
||||
if ( $default_zone ) {
|
||||
$shipping_zones[] = [ 'shipping_methods' => $default_zone->get_shipping_methods() ];
|
||||
}
|
||||
|
||||
foreach ( $shipping_zones as $shipping_zone ) {
|
||||
foreach ( $shipping_zone['shipping_methods'] as $shipping_method ) {
|
||||
if ( 'free_shipping' === $shipping_method->id ) {
|
||||
$option_name = 'option_woocommerce_' . trim( $shipping_method->id ) . '_' . (int) $shipping_method->instance_id . '_settings';
|
||||
add_filter( $option_name, [ $this, 'get_free_shipping_min_amount' ], 99 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the exchange rate and default currency to the order's meta if prices have been converted.
|
||||
*
|
||||
* @param int $order_id The order ID.
|
||||
* @param WC_Order $order The order object.
|
||||
*/
|
||||
public function add_order_meta( $order_id, $order ) {
|
||||
$default_currency = $this->multi_currency->get_default_currency();
|
||||
|
||||
// Do not add exchange rate if order was made in the store's default currency.
|
||||
if ( $default_currency->get_code() === $order->get_currency() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exchange_rate = $this->multi_currency->get_price( 1, 'exchange_rate' );
|
||||
|
||||
$order->update_meta_data( '_wcpay_multi_currency_order_exchange_rate', $exchange_rate );
|
||||
$order->update_meta_data( '_wcpay_multi_currency_order_default_currency', $default_currency->get_code() );
|
||||
$order->save_meta_data();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency Geolocation Class
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyLocalizationInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Geolocation.
|
||||
*/
|
||||
class Geolocation {
|
||||
/**
|
||||
* MultiCurrencyLocalizationInterface instance.
|
||||
*
|
||||
* @var MultiCurrencyLocalizationInterface
|
||||
*/
|
||||
protected $localization_service;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrencyLocalizationInterface $localization_service The Localization Service instance.
|
||||
*/
|
||||
public function __construct( MultiCurrencyLocalizationInterface $localization_service ) {
|
||||
$this->localization_service = $localization_service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the customer's currency based on their location.
|
||||
*
|
||||
* @return string|null Currency code or null if not found.
|
||||
*/
|
||||
public function get_currency_by_customer_location() {
|
||||
$country = $this->get_country_by_customer_location();
|
||||
|
||||
return $this->localization_service->get_country_locale_data( $country )['currency_code'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the customer's country based on their location.
|
||||
*
|
||||
* @return string Country code.
|
||||
*/
|
||||
public function get_country_by_customer_location() {
|
||||
$country = $this->geolocate_customer();
|
||||
|
||||
if ( $country ) {
|
||||
// Once we have a location, ensure it's valid, otherwise fallback to the default country.
|
||||
$allowed_country_codes = WC()->countries->get_allowed_countries();
|
||||
if ( ! array_key_exists( $country, $allowed_country_codes ) ) {
|
||||
$country = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $country ) {
|
||||
$default_location = get_option( 'woocommerce_default_country', '' );
|
||||
$location = wc_format_country_state_string( apply_filters( 'woocommerce_customer_default_location', $default_location ) );
|
||||
$country = $location['country'];
|
||||
}
|
||||
|
||||
return $country;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to guess the customer's country based on their IP.
|
||||
*
|
||||
* @return string|null Country code, or NULL if it couldn't be determined.
|
||||
*/
|
||||
private function geolocate_customer() {
|
||||
// Exclude common bots from geolocation by user agent.
|
||||
$ua = wc_get_user_agent();
|
||||
if ( stripos( $ua, 'bot' ) !== false || stripos( $ua, 'spider' ) !== false || stripos( $ua, 'crawl' ) !== false ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$geolocation = \WC_Geolocation::geolocate_ip( '', true, true );
|
||||
return $geolocation['country'] ?? null;
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface MultiCurrencyAccountInterface
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency\Interfaces
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Interfaces;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface MultiCurrencyAccountInterface {
|
||||
|
||||
/**
|
||||
* Checks if the account is connected to the payment provider.
|
||||
*
|
||||
* @param bool $on_error Value to return on server error, defaults to false.
|
||||
*
|
||||
* @return bool True if the account is connected, false otherwise, $on_error on error.
|
||||
*/
|
||||
public function is_provider_connected( bool $on_error = false ): bool;
|
||||
|
||||
/**
|
||||
* Checks if the account has been rejected, assumes the value of false on any account retrieval error.
|
||||
* Returns false if the account is not connected.
|
||||
*
|
||||
* Note: We might want to use a more generic method to check if the account is in an enabled or
|
||||
* disabled state for better compatibility between V1 and V2.
|
||||
*
|
||||
* @return bool True if the account is connected and rejected, false otherwise or on error.
|
||||
*/
|
||||
public function is_account_rejected(): bool;
|
||||
|
||||
/**
|
||||
* Gets and caches the data for the account connected to this site.
|
||||
*
|
||||
* @param bool $force_refresh Forces data to be fetched from the server, rather than using the cache.
|
||||
*
|
||||
* @return array|bool Account data or false if failed to retrieve account data.
|
||||
*/
|
||||
public function get_cached_account_data( bool $force_refresh = false );
|
||||
|
||||
/**
|
||||
* Gets the customer currencies supported for the account.
|
||||
*
|
||||
* @return array Currencies.
|
||||
*/
|
||||
public function get_account_customer_supported_currencies(): array;
|
||||
|
||||
/**
|
||||
* Get list of countries supported by the provider.
|
||||
*/
|
||||
public function get_supported_countries(): array;
|
||||
|
||||
/**
|
||||
* Get provider onboarding page url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_provider_onboarding_page_url(): string;
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface MultiCurrencyApiClientInterface
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency\Interfaces
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Interfaces;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface MultiCurrencyApiClientInterface {
|
||||
|
||||
/**
|
||||
* Whether the API client is connected to the server.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_server_connected(): bool;
|
||||
|
||||
/**
|
||||
* Get currency rates from the server.
|
||||
*
|
||||
* @param string $currency_from - The currency to convert from.
|
||||
* @param ?array $currencies_to - An array of the currencies we want to convert into. If left empty, will get all supported currencies.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_currency_rates( string $currency_from, $currencies_to = null ): array;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface MultiCurrencyCacheInterface
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency\Interfaces
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Interfaces;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface MultiCurrencyCacheInterface {
|
||||
const CURRENCIES_KEY = 'wcpay_multi_currency_cached_currencies';
|
||||
|
||||
/**
|
||||
* Gets a value from the cache.
|
||||
*
|
||||
* @param string $key The key to look for.
|
||||
* @param bool $force If set, return from the cache without checking for expiry.
|
||||
*
|
||||
* @return mixed The cache contents.
|
||||
*/
|
||||
public function get( string $key, bool $force = false );
|
||||
|
||||
/**
|
||||
* Gets a value from cache or regenerates and adds it to the cache.
|
||||
*
|
||||
* @param string $key The options key to cache the data under.
|
||||
* @param callable $generator Function/callable regenerating the missing value. If null or false is returned, it will be treated as an error.
|
||||
* @param callable $validate_data Function/callable validating the data after it is retrieved from the cache. If it returns false, the cache will be refreshed.
|
||||
* @param boolean $force_refresh Regenerates the cache regardless of its state if true.
|
||||
* @param boolean $refreshed Is set to true if the cache has been refreshed without errors and with a non-empty value.
|
||||
*
|
||||
* @return mixed|null The cache contents. NULL on failure
|
||||
*/
|
||||
public function get_or_add( string $key, callable $generator, callable $validate_data, bool $force_refresh = false, bool &$refreshed = false );
|
||||
|
||||
/**
|
||||
* Deletes a value from the cache.
|
||||
*
|
||||
* @param string $key The key to delete.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete( string $key );
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface MultiCurrencyLocalizationInterface
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency\Interfaces
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Interfaces;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface MultiCurrencyLocalizationInterface {
|
||||
|
||||
/**
|
||||
* Retrieves the currency's format from mapped data.
|
||||
*
|
||||
* @param string $currency_code The currency code.
|
||||
*
|
||||
* @return array The currency's format.
|
||||
*/
|
||||
public function get_currency_format( $currency_code ): array;
|
||||
|
||||
/**
|
||||
* Returns the locale data for a country.
|
||||
*
|
||||
* @param string $country Country code.
|
||||
*
|
||||
* @return array Array with the country's locale data. Empty array if country not found.
|
||||
*/
|
||||
public function get_country_locale_data( $country ): array;
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface MultiCurrencySettingsInterface
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency\Interfaces
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Interfaces;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
interface MultiCurrencySettingsInterface {
|
||||
|
||||
/**
|
||||
* Checks if dev mode is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_dev_mode(): bool;
|
||||
|
||||
/**
|
||||
* Gets the plugin file path.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_plugin_file_path(): string;
|
||||
|
||||
/**
|
||||
* Gets the plugin version.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_plugin_version(): string;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Logger
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
/**
|
||||
* Logger for multi-currency.
|
||||
*/
|
||||
class Logger {
|
||||
|
||||
/**
|
||||
* Log source identifier.
|
||||
*/
|
||||
const LOG_FILE = 'woopayments-multi-currency';
|
||||
|
||||
/**
|
||||
* The WooCommerce logger instance.
|
||||
*
|
||||
* @var \WC_Logger|null
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Log a debug message.
|
||||
*
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
public static function debug( $message ) {
|
||||
self::log( 'debug', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log an error message.
|
||||
*
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
public static function error( $message ) {
|
||||
self::log( 'error', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a notice message.
|
||||
*
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
public static function notice( $message ) {
|
||||
self::log( 'notice', $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message with a specific level.
|
||||
*
|
||||
* @param string $level The log level (debug, error, notice, etc.).
|
||||
* @param string $message The message to log.
|
||||
*/
|
||||
private static function log( $level, $message ) {
|
||||
if ( ! function_exists( 'wc_get_logger' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$logger = wc_get_logger();
|
||||
$logger->log( $level, $message, [ 'source' => self::LOG_FILE ] );
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+88
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
/**
|
||||
* Notify merchant that Multi-Currency is available.
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency\Notes;
|
||||
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\NoteTraits;
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class NoteMultiCurrencyAvailable
|
||||
*/
|
||||
class NoteMultiCurrencyAvailable {
|
||||
use NoteTraits;
|
||||
|
||||
/**
|
||||
* Name of the note for use in the database.
|
||||
*/
|
||||
const NOTE_NAME = 'wc-payments-notes-multi-currency-available';
|
||||
|
||||
/**
|
||||
* Url to start the setup process. Now redirects to the wizard page.
|
||||
*/
|
||||
// TODO: Proper url needed for setup process.
|
||||
const NOTE_SETUP_URL = 'admin.php?page=wc-admin&path=/payments/multi-currency-setup';
|
||||
|
||||
/**
|
||||
* The account service instance.
|
||||
*
|
||||
* @var MultiCurrencyAccountInterface
|
||||
*/
|
||||
private static $account;
|
||||
|
||||
/**
|
||||
* Get the note.
|
||||
*/
|
||||
public static function get_note() {
|
||||
$note = new Note();
|
||||
|
||||
$note->set_title( __( 'Sell worldwide in multiple currencies', 'woocommerce-payments' ) );
|
||||
$note->set_content( __( 'Boost your international sales by allowing your customers to shop and pay in their local currency.', 'woocommerce-payments' ) );
|
||||
$note->set_content_data( (object) [] );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_INFORMATIONAL );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_source( 'woocommerce-payments' );
|
||||
$note->add_action(
|
||||
self::NOTE_NAME,
|
||||
__( 'Set up now', 'woocommerce-payments' ),
|
||||
self::NOTE_SETUP_URL,
|
||||
'unactioned',
|
||||
true
|
||||
);
|
||||
|
||||
return $note;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the note if it passes predefined conditions.
|
||||
*/
|
||||
public static function possibly_add_note() {
|
||||
// Don't add the note if the merchant didn't create a WCPay account yet.
|
||||
if ( is_null( self::$account ) || ! self::$account->is_provider_connected() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! self::can_be_added() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$note = self::get_note();
|
||||
$note->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the account service instance reference on the class.
|
||||
*
|
||||
* @param MultiCurrencyAccountInterface $account account service instance.
|
||||
*/
|
||||
public static function set_account( MultiCurrencyAccountInterface $account ) {
|
||||
self::$account = $account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# WooPayments multi-currency directory
|
||||
|
||||
This directory contains the multi-currency module, which has been decoupled and extracted from the gateway code.
|
||||
|
||||
The module is responsible for handling all multi-currency functionality, both back-end and front-end.
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
/**
|
||||
* Class RestController
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use Exception;
|
||||
use WCPay\MultiCurrency\Exceptions\InvalidCurrencyException;
|
||||
use WCPay\MultiCurrency\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* REST controller for multi-currency.
|
||||
*/
|
||||
class RestController extends \WP_REST_Controller {
|
||||
|
||||
/**
|
||||
* Endpoint namespace.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = 'wc/v3';
|
||||
|
||||
/**
|
||||
* Endpoint path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'payments/multi-currency';
|
||||
|
||||
/**
|
||||
* MultiCurrency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency MultiCurrency instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Configure REST API routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/currencies',
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_store_currencies' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/update-enabled-currencies',
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'args' => [
|
||||
'enabled' => [
|
||||
'type' => 'array',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'update_enabled_currencies' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/currencies/(?P<currency_code>[A-Za-z]{3})',
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'args' => [
|
||||
'currency_code' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'get_single_currency_settings' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/get-settings',
|
||||
[
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => [ $this, 'get_settings' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/currencies/(?P<currency_code>[A-Za-z]{3})',
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'args' => [
|
||||
'currency_code' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
'exchange_rate_type' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
'manual_rate' => [
|
||||
'type' => 'number',
|
||||
'required' => false,
|
||||
],
|
||||
'price_rounding' => [
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
],
|
||||
'price_charm' => [
|
||||
'type' => 'number',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'update_single_currency_settings' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/update-settings',
|
||||
[
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'args' => [
|
||||
'wcpay_multi_currency_enable_auto_currency' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
'wcpay_multi_currency_enable_storefront_switcher' => [
|
||||
'type' => 'string',
|
||||
'format' => 'text-field',
|
||||
'required' => true,
|
||||
],
|
||||
],
|
||||
'callback' => [ $this, 'update_settings' ],
|
||||
'permission_callback' => [ $this, 'check_permission' ],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve currencies for the store.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error Array of the store currencies structure.
|
||||
*/
|
||||
public function get_store_currencies() {
|
||||
return rest_ensure_response( $this->multi_currency->get_store_currencies() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update enabled currencies based on posted data.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The store currencies structure or WP_Error.
|
||||
*/
|
||||
public function update_enabled_currencies( $request ) {
|
||||
$enabled = $request->get_param( 'enabled' );
|
||||
try {
|
||||
$this->multi_currency->set_enabled_currencies( $enabled );
|
||||
$response = $this->get_store_currencies();
|
||||
} catch ( InvalidCurrencyException $e ) {
|
||||
$response = new \WP_Error( $e->getCode(), $e->getMessage() );
|
||||
}
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the currency settings for a single currency.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The single currency settings as an array.
|
||||
*/
|
||||
public function get_single_currency_settings( $request ) {
|
||||
$currency_code = $request->get_param( 'currency_code' );
|
||||
|
||||
try {
|
||||
$response = $this->multi_currency->get_single_currency_settings( $currency_code );
|
||||
} catch ( InvalidCurrencyException $e ) {
|
||||
$response = new \WP_Error( $e->getCode(), $e->getMessage() );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the currency settings for a single currency.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The single currency settings as an array.
|
||||
*/
|
||||
public function update_single_currency_settings( $request ) {
|
||||
$currency_code = $request->get_param( 'currency_code' );
|
||||
$exchange_rate_type = $request->get_param( 'exchange_rate_type' );
|
||||
$price_rounding = $request->get_param( 'price_rounding' );
|
||||
$price_charm = $request->get_param( 'price_charm' );
|
||||
$manual_rate = $request->get_param( 'manual_rate' ) ?? null;
|
||||
|
||||
try {
|
||||
$this->multi_currency->update_single_currency_settings( $currency_code, $exchange_rate_type, $price_rounding, $price_charm, $manual_rate );
|
||||
$response = $this->multi_currency->get_single_currency_settings( $currency_code );
|
||||
} catch ( Exception $e ) {
|
||||
$response = new \WP_Error( $e->getCode(), $e->getMessage() );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the store settings for Multi-Currency.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The store settings as an array.
|
||||
*/
|
||||
public function get_settings() {
|
||||
return rest_ensure_response( $this->multi_currency->get_settings() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates Multi-Currency store settings parameters.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full data about the request.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The store settings as an array.
|
||||
*/
|
||||
public function update_settings( $request ) {
|
||||
$params = $request->get_params();
|
||||
$this->multi_currency->update_settings( $params );
|
||||
return rest_ensure_response( $this->multi_currency->get_settings() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify access.
|
||||
*/
|
||||
public function check_permission() {
|
||||
return current_user_can( 'manage_woocommerce' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-currency Settings
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Settings.
|
||||
*/
|
||||
class Settings extends \WC_Settings_Page {
|
||||
|
||||
/**
|
||||
* The id of the plugin.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The tab label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* Instance of MultiCurrency class.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->id = $this->multi_currency->id;
|
||||
$this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
// TODO: Only register emoji script in settings page. Until WC Admin decide if they will enable it too: https://github.com/woocommerce/woocommerce-admin/issues/6388.
|
||||
add_action( 'admin_print_scripts', [ $this, 'maybe_add_print_emoji_detection_script' ] );
|
||||
add_action( 'woocommerce_admin_field_wcpay_multi_currency_settings_page', [ $this, 'wcpay_multi_currency_settings_page' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings array.
|
||||
*
|
||||
* @param string $current_section Section being shown.
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings( $current_section = '' ) {
|
||||
return [
|
||||
[
|
||||
'type' => 'wcpay_multi_currency_settings_page',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Output container for enabled currencies list.
|
||||
*/
|
||||
public function wcpay_multi_currency_settings_page() {
|
||||
// Hide original save button.
|
||||
$GLOBALS['hide_save_button'] = true;
|
||||
?>
|
||||
<div id="wcpay_multi_currency_settings_container" aria-describedby="wcpay_multi_currency_settings_container-description"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Load inline Emoji detection script on multi-currency settings page
|
||||
*/
|
||||
public function maybe_add_print_emoji_detection_script() {
|
||||
if ( $this->multi_currency->is_multi_currency_settings_page() ) {
|
||||
print_emoji_detection_script();
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency Settings
|
||||
*
|
||||
* @package WooCommerce\Admin
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
use WCPay\MultiCurrency\Interfaces\MultiCurrencyAccountInterface;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* MultiCurrency settings placeholder containing a CTA to connect the account.
|
||||
*/
|
||||
class SettingsOnboardCta extends \WC_Settings_Page {
|
||||
/**
|
||||
* Link to the Multi-Currency documentation page.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const LEARN_MORE_URL = 'https://woocommerce.com/document/woopayments/currencies/multi-currency-setup/';
|
||||
|
||||
/**
|
||||
* MultiCurrency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
private $multi_currency;
|
||||
|
||||
/**
|
||||
* Instance of MultiCurrencyAccountInterface.
|
||||
*
|
||||
* @var MultiCurrencyAccountInterface
|
||||
*/
|
||||
private $payments_account;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
* @param MultiCurrencyAccountInterface $payments_account Payments Account instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency, MultiCurrencyAccountInterface $payments_account ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->payments_account = $payments_account;
|
||||
$this->id = $this->multi_currency->id;
|
||||
$this->label = _x( 'Multi-currency', 'Settings tab label', 'woocommerce-payments' );
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_action( 'woocommerce_admin_field_wcpay_currencies_settings_onboarding_cta', [ $this, 'currencies_settings_onboarding_cta' ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the call to action button if needing to onboard.
|
||||
*/
|
||||
public function currencies_settings_onboarding_cta() {
|
||||
$href = $this->payments_account->get_provider_onboarding_page_url();
|
||||
?>
|
||||
<div>
|
||||
<p>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: WooPayments */
|
||||
esc_html__( 'To add new currencies to your store, please finish setting up %s.', 'woocommerce-payments' ),
|
||||
'WooPayments'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<a href="<?php echo esc_url( $href ); ?>" id="wcpay_enabled_currencies_onboarding_cta" type="button" class="button-primary">
|
||||
<?php esc_html_e( 'Get started', 'woocommerce-payments' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings array.
|
||||
*
|
||||
* @param string $current_section Section being shown.
|
||||
* @return array
|
||||
*/
|
||||
public function get_settings( $current_section = '' ) {
|
||||
// Hide the save button because there are no settings to save.
|
||||
global $hide_save_button;
|
||||
$hide_save_button = true;
|
||||
|
||||
return [
|
||||
[
|
||||
'title' => __( 'Enabled currencies', 'woocommerce-payments' ),
|
||||
'desc' => sprintf(
|
||||
/* translators: %s: url to documentation. */
|
||||
__( 'Accept payments in multiple currencies. Prices are converted based on exchange rates and rounding rules. <a href="%s">Learn more</a>', 'woocommerce-payments' ),
|
||||
self::LEARN_MORE_URL
|
||||
),
|
||||
'type' => 'title',
|
||||
'id' => $this->id . '_enabled_currencies',
|
||||
],
|
||||
[
|
||||
'type' => 'wcpay_currencies_settings_onboarding_cta',
|
||||
],
|
||||
[
|
||||
'type' => 'sectionend',
|
||||
'id' => $this->id . '_enabled_currencies',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
/**
|
||||
* Class StorefrontIntegration
|
||||
*
|
||||
* @package WooCommerce\Payments\StorefrontIntegration
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that controls Multi-Currency Storefront Integration.
|
||||
*/
|
||||
class StorefrontIntegration {
|
||||
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
$this->init_actions_and_filters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the CSS to the head of the page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_inline_css() {
|
||||
$css = '
|
||||
#woocommerce-payments-multi-currency-storefront-widget {
|
||||
float: right;
|
||||
}
|
||||
#woocommerce-payments-multi-currency-storefront-widget form {
|
||||
margin: 0;
|
||||
}
|
||||
';
|
||||
|
||||
wp_add_inline_style(
|
||||
'storefront-style',
|
||||
apply_filters( MultiCurrency::FILTER_PREFIX . 'storefront_widget_css', $css )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This modifies the breadcrumb defaults for us to be able to place the widget.
|
||||
*
|
||||
* @param array $defaults The defaults breadcrumb properties.
|
||||
*
|
||||
* @return array The modified defaults properties.
|
||||
*/
|
||||
public function modify_breadcrumb_defaults( array $defaults ): array {
|
||||
// Set the instance and args arrays for the widget.
|
||||
$instance = apply_filters( MultiCurrency::FILTER_PREFIX . 'storefront_widget_instance', [] );
|
||||
$args = apply_filters(
|
||||
MultiCurrency::FILTER_PREFIX . 'storefront_widget_args',
|
||||
[
|
||||
'before_widget' => '<div id="woocommerce-payments-multi-currency-storefront-widget" class="woocommerce-breadcrumb">',
|
||||
'after_widget' => '</div>',
|
||||
]
|
||||
);
|
||||
|
||||
/**
|
||||
* Some storefront child themes use different wrappers and styles. We need to place the widget before
|
||||
* the <nav> to display it properly.
|
||||
*/
|
||||
$defaults['wrap_before'] = str_replace( '<nav', $this->multi_currency->get_switcher_widget_markup( $instance, $args ) . '<nav', $defaults['wrap_before'] );
|
||||
|
||||
return $defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the actions and filters.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function init_actions_and_filters() {
|
||||
// Do not enable the breadcrumb widget if there's only one currency active.
|
||||
if ( 1 >= count( $this->multi_currency->get_enabled_currencies() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Simulation overrides for Multi-Currency onboarding preview.
|
||||
$simulation_variables = $this->multi_currency->get_multi_currency_onboarding_simulation_variables() ?? [];
|
||||
$simulation_enabled = false;
|
||||
$simulation_hide_switcher = false;
|
||||
|
||||
if ( 0 < count( $simulation_variables ) && isset( $simulation_variables['enable_storefront_switcher'] ) ) {
|
||||
// We have a incoming override request! Simulate the flag.
|
||||
$simulation_enabled = true;
|
||||
$enable_storefront_switcher = (bool) $simulation_variables['enable_storefront_switcher'];
|
||||
// If the Storefront switcher is not enabled on the onboarding page, hide it.
|
||||
if ( ! $enable_storefront_switcher ) {
|
||||
$simulation_hide_switcher = true;
|
||||
}
|
||||
}
|
||||
|
||||
// We want this enabled by default, so we default the option to 'yes'.
|
||||
if ( ! $simulation_hide_switcher
|
||||
&& (
|
||||
$simulation_enabled
|
||||
|| $this->multi_currency->is_using_storefront_switcher()
|
||||
)
|
||||
) {
|
||||
add_filter( 'woocommerce_breadcrumb_defaults', [ $this, 'modify_breadcrumb_defaults' ], 9999 );
|
||||
add_action( 'wp_enqueue_scripts', [ $this, 'add_inline_css' ], 50 );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Tracking
|
||||
*
|
||||
* @package WooCommerce\Payments\MultiCurrency
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that controls Multi-Currency Tracking functionality.
|
||||
*/
|
||||
class Tracking {
|
||||
/**
|
||||
* MultiCurrency class.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
private $multi_currency;
|
||||
|
||||
/**
|
||||
* Class constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency MultiCurrency class.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
add_filter( 'woocommerce_tracker_data', [ $this, 'add_tracker_data' ], 50 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add our data to the tracking data from WC core.
|
||||
*
|
||||
* @param array $data The array of data WC core has already built.
|
||||
*
|
||||
* @return array Our modified data.
|
||||
*/
|
||||
public function add_tracker_data( array $data ): array {
|
||||
$data[ $this->multi_currency->id ] = [
|
||||
'enabled_currencies' => $this->get_enabled_currencies(),
|
||||
'default_currency' => $this->get_currency_data_array( $this->multi_currency->get_default_currency() ),
|
||||
'order_counts' => $this->get_mc_order_count(),
|
||||
];
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an assoc array of the data we want from a Currency object.
|
||||
*
|
||||
* @param Currency $currency The Currency object we want data from.
|
||||
*
|
||||
* @return array Assoc array with the Currency data.
|
||||
*/
|
||||
private function get_currency_data_array( Currency $currency ): array {
|
||||
$data = [
|
||||
'code' => $currency->get_code(),
|
||||
'name' => html_entity_decode( $currency->get_name() ),
|
||||
];
|
||||
|
||||
// Return early if it's the default currency.
|
||||
if ( $currency->get_code() === $this->multi_currency->get_default_currency()->get_code() ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// Is it a zero decimal currency?
|
||||
$is_zero_decimal = $currency->get_is_zero_decimal();
|
||||
|
||||
// Is it using a custom or automatic rate?
|
||||
$rate_type = get_option( $this->multi_currency->id . '_exchange_rate_' . $currency->get_id(), 'automatic' );
|
||||
$rate_type = 'automatic' === $rate_type ? $rate_type . ' (default)' : $rate_type;
|
||||
|
||||
// What is the price rounding setting?
|
||||
$price_rounding_default = $is_zero_decimal ? '100' : '1.00';
|
||||
$price_rounding = $currency->get_rounding();
|
||||
$price_rounding = $price_rounding_default === $price_rounding ? $price_rounding . ' (default)' : $price_rounding;
|
||||
|
||||
// What is the price charm setting?
|
||||
$price_charm = $currency->get_charm();
|
||||
$price_charm = 0.00 === $price_charm ? '0.00 (default)' : $price_charm;
|
||||
|
||||
$additional_data = [
|
||||
'is_zero_decimal' => $is_zero_decimal,
|
||||
'rate_type' => $rate_type,
|
||||
'price_rounding' => $price_rounding,
|
||||
'price_charm' => $price_charm,
|
||||
];
|
||||
|
||||
return array_merge( $data, $additional_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the enabled currencies as an associative array. Excludes the store/default currency.
|
||||
*
|
||||
* @return array Array of currencies, or empty array if none found.
|
||||
*/
|
||||
private function get_enabled_currencies(): array {
|
||||
$enabled_currencies = $this->multi_currency->get_enabled_currencies();
|
||||
$default_currency = $this->multi_currency->get_default_currency();
|
||||
unset( $enabled_currencies[ $default_currency->get_code() ] );
|
||||
$enabled_array = [];
|
||||
|
||||
foreach ( $enabled_currencies as $currency ) {
|
||||
$enabled_array[ $currency->get_code() ] = $this->get_currency_data_array( $currency );
|
||||
}
|
||||
|
||||
return $enabled_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the database to see how many orders have been made using Multi-Currency.
|
||||
*
|
||||
* @return array Result count.
|
||||
*/
|
||||
private function get_mc_order_count(): array {
|
||||
global $wpdb;
|
||||
$query_on_orders = "
|
||||
SELECT
|
||||
gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts
|
||||
FROM (
|
||||
SELECT
|
||||
orders.id AS order_id, orders.payment_method as gateway, orders.total_amount as total, orders.currency as currency
|
||||
FROM
|
||||
{$wpdb->prefix}wc_orders orders
|
||||
LEFT JOIN
|
||||
{$wpdb->prefix}wc_orders_meta order_meta ON order_meta.order_id = orders.id
|
||||
INNER JOIN
|
||||
{$wpdb->prefix}wc_orders_meta mc_meta ON mc_meta.order_id = orders.id
|
||||
AND mc_meta.meta_key = '_wcpay_multi_currency_order_exchange_rate'
|
||||
WHERE orders.type = 'shop_order'
|
||||
AND orders.status in ( 'wc-completed', 'wc-processing', 'wc-refunded' )
|
||||
GROUP BY orders.id
|
||||
) order_gateways
|
||||
GROUP BY currency, gateway
|
||||
";
|
||||
$query_on_posts = "
|
||||
SELECT
|
||||
gateway, currency, SUM(total) AS totals, COUNT(order_id) AS counts
|
||||
FROM (
|
||||
SELECT
|
||||
orders.id AS order_id,
|
||||
MAX(CASE WHEN order_meta.meta_key = '_payment_method' THEN order_meta.meta_value END) gateway,
|
||||
MAX(CASE WHEN order_meta.meta_key = '_order_total' THEN order_meta.meta_value END) total,
|
||||
MAX(CASE WHEN order_meta.meta_key = '_order_currency' THEN order_meta.meta_value END) currency
|
||||
FROM
|
||||
{$wpdb->prefix}posts orders
|
||||
LEFT JOIN
|
||||
{$wpdb->prefix}postmeta order_meta ON order_meta.post_id = orders.id
|
||||
INNER JOIN
|
||||
{$wpdb->prefix}postmeta mc_meta ON mc_meta.post_id = orders.id
|
||||
AND mc_meta.meta_key = '_wcpay_multi_currency_order_exchange_rate'
|
||||
WHERE orders.post_type = 'shop_order'
|
||||
AND orders.post_status in ( 'wc-completed', 'wc-processing', 'wc-refunded' )
|
||||
AND order_meta.meta_key in ( '_payment_method', '_order_total', '_order_currency' )
|
||||
GROUP BY orders.id
|
||||
) order_gateways
|
||||
GROUP BY currency, gateway
|
||||
";
|
||||
|
||||
if ( class_exists( 'Automattic\WooCommerce\Utilities\OrderUtil' ) &&
|
||||
\Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() ) {
|
||||
$orders_by_currency = $wpdb->get_results( $query_on_orders ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
} else {
|
||||
$orders_by_currency = $wpdb->get_results( $query_on_posts ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
|
||||
}
|
||||
|
||||
$currencies = [];
|
||||
$total_count = 0;
|
||||
foreach ( $orders_by_currency as $group ) {
|
||||
// Get current counts and totals.
|
||||
$counts = $currencies[ $group->currency ]['counts'] ?? 0;
|
||||
$totals = $currencies[ $group->currency ]['totals'] ?? 0;
|
||||
|
||||
// Update the counts and totals for the currency.
|
||||
$currencies[ $group->currency ]['counts'] = $counts + $group->counts;
|
||||
$currencies[ $group->currency ]['totals'] = $totals + $group->totals;
|
||||
|
||||
// If something provides a 100% discount, the payment method is null. This could be coupons, gift cards, etc.
|
||||
$gateway = $group->gateway ?? 'unknown';
|
||||
|
||||
// Update the counts and totals per gateway for the currency.
|
||||
$currencies[ $group->currency ]['gateways'][ $gateway ] = [
|
||||
'counts' => $group->counts,
|
||||
'totals' => $group->totals,
|
||||
];
|
||||
|
||||
// Update the total count.
|
||||
$total_count += $group->counts;
|
||||
}
|
||||
|
||||
return [
|
||||
'counts' => $total_count,
|
||||
'currencies' => $currencies,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
/**
|
||||
* WooCommerce Payments Multi-Currency User Settings
|
||||
*
|
||||
* @package WooCommerce\Payments
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that add Multi-Currency settings to user my account page.
|
||||
*/
|
||||
class UserSettings {
|
||||
|
||||
/**
|
||||
* Multi-Currency instance.
|
||||
*
|
||||
* @var MultiCurrency
|
||||
*/
|
||||
protected $multi_currency;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param MultiCurrency $multi_currency The MultiCurrency instance.
|
||||
*/
|
||||
public function __construct( MultiCurrency $multi_currency ) {
|
||||
$this->multi_currency = $multi_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes this class' WP hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function init_hooks() {
|
||||
// Only show currency selector if more than one currency is enabled.
|
||||
if ( 1 < count( $this->multi_currency->get_enabled_currencies() ) ) {
|
||||
add_action( 'woocommerce_edit_account_form', [ $this, 'add_presentment_currency_switch' ] );
|
||||
add_action( 'woocommerce_save_account_details', [ $this, 'save_presentment_currency' ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a select to allow user choose default currency in `my account > account details`.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function add_presentment_currency_switch() {
|
||||
?>
|
||||
<p class="woocommerce-form-row woocommerce-form-row--first form-row form-row-first">
|
||||
<label for="wcpay_selected_currency"><?php esc_html_e( 'Default currency', 'woocommerce-payments' ); ?></label>
|
||||
<select
|
||||
name="wcpay_selected_currency"
|
||||
id="wcpay_selected_currency"
|
||||
>
|
||||
<?php
|
||||
foreach ( $this->multi_currency->get_enabled_currencies() as $currency ) {
|
||||
$code = $currency->get_code();
|
||||
$symbol = $currency->get_symbol();
|
||||
$selected = $this->multi_currency->get_selected_currency()->code === $code ? ' selected' : '';
|
||||
|
||||
echo "<option value=\"$code\"$selected>$symbol $code</option>"; // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
}
|
||||
?>
|
||||
</select>
|
||||
<span><em><?php esc_html_e( 'Select your preferred currency for shopping and payments.', 'woocommerce-payments' ); ?></em></span>
|
||||
</p>
|
||||
<div class="clear"></div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into save account details to capture the new value `wcpay_selected_currency` and persist it.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save_presentment_currency() {
|
||||
if ( isset( $_POST['wcpay_selected_currency'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$currency_code = wc_clean( wp_unslash( $_POST['wcpay_selected_currency'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$this->multi_currency->update_selected_currency( $currency_code );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
/**
|
||||
* Class Utils
|
||||
*
|
||||
* @package WooCommerce\Payments\Utils
|
||||
*/
|
||||
|
||||
namespace WCPay\MultiCurrency;
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Class that controls Multi-Currency Utils.
|
||||
*/
|
||||
class Utils {
|
||||
/**
|
||||
* Checks backtrace calls to see if a certain call has been made.
|
||||
*
|
||||
* @param array $calls Array of the calls to check for.
|
||||
*
|
||||
* @return bool True if found, false if not.
|
||||
*/
|
||||
public function is_call_in_backtrace( array $calls ): bool {
|
||||
$backtrace = wp_debug_backtrace_summary( null, 0, false ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
foreach ( $calls as $call ) {
|
||||
if ( in_array( $call, $backtrace, true ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the query_vars array for a particular pagename and variable to be set.
|
||||
*
|
||||
* @param array $pages Array of the pagenames to check for.
|
||||
* @param array $vars Array of the vars to check for.
|
||||
*
|
||||
* @return bool True if found, false if not.
|
||||
*/
|
||||
public function is_page_with_vars( array $pages, array $vars ): bool {
|
||||
global $wp;
|
||||
|
||||
if ( $wp->query_vars && isset( $wp->query_vars['pagename'] ) && in_array( $wp->query_vars['pagename'], $pages, true ) ) {
|
||||
foreach ( $vars as $var ) {
|
||||
if ( isset( $wp->query_vars[ $var ] ) ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if is a REST API request and the HTTP referer matches admin url.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_admin_api_request(): bool {
|
||||
return 0 === stripos( wp_get_referer(), admin_url() ) && WC()->is_rest_api_request() && ! self::is_store_api_request();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes the session into the client cookie.
|
||||
*
|
||||
* @param bool $set Should the session cookie be set.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function set_customer_session_cookie( bool $set ) {
|
||||
WC()->session->set_customer_session_cookie( $set );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the request is a store REST API request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_store_api_request() {
|
||||
if ( function_exists( 'WC' ) && method_exists( WC(), 'is_store_api_request' ) ) {
|
||||
return WC()->is_store_api_request();
|
||||
}
|
||||
// The logic below is sourced from `WC()->is_store_api_request()`.
|
||||
if ( empty( $_SERVER['REQUEST_URI'] ) ) {
|
||||
return false;
|
||||
}
|
||||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
return false !== strpos( $_SERVER['REQUEST_URI'], trailingslashit( rest_get_url_prefix() ) . 'wc/store/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the request that's currently being processed is a Store API batch request.
|
||||
*
|
||||
* @return bool True if the request is a Store API batch request, false otherwise.
|
||||
*/
|
||||
public static function is_store_batch_request(): bool {
|
||||
// @TODO We should move to a more robust way of getting to the route, like WC is doing in the StoreAPI library. https://github.com/woocommerce/woocommerce/blob/9ac48232a944baa2dbfaa7dd47edf9027cca9519/plugins/woocommerce/src/StoreApi/Authentication.php#L15-L15
|
||||
if ( isset( $_REQUEST['rest_route'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
|
||||
$rest_route = sanitize_text_field( $_REQUEST['rest_route'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.NonceVerification
|
||||
} else {
|
||||
// Extract the request path from the request URL.
|
||||
$url_parts = wp_parse_url( esc_url_raw( $_SERVER['REQUEST_URI'] ?? '' ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$request_path = ! empty( $url_parts['path'] ) ? rtrim( $url_parts['path'], '/' ) : '';
|
||||
// Remove the REST API prefix from the request path to end up with the route.
|
||||
$rest_route = str_replace( trailingslashit( rest_get_url_prefix() ), '', $request_path );
|
||||
}
|
||||
|
||||
// Bail early if the rest route is empty.
|
||||
if ( empty( $rest_route ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 1 === preg_match( '@^\/wc\/store(\/v[\d]+)?\/batch@', $rest_route );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user