<?php
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Exception;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use WC_Cart;
/**
* Fallback calculator for complex product types (bookings, bundles, composites) that
* need WooCommerce's full cart engine to compute accurate totals.
*
* Creates a throwaway WC_Cart with session hooks disabled so it never persists state,
* never touches WC()->cart, and cleans up via try/finally even on exceptions.
*/
class IsolatedCartSimulator
{
private \WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper $cart_products;
private LoggerInterface $logger;
public function __construct(\WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper $cart_products, LoggerInterface $logger)
{
$this->cart_products = $cart_products;
$this->logger = $logger;
}
/**
* Adds the given products to an isolated cart, runs calculate_totals(),
* and returns the resulting total and shipping fee. The isolated cart is
* discarded afterward; WC()->cart is never modified.
*
* @return array{total: float, shipping_fee: float}
* @throws Exception If simulation fails.
*/
public function simulate(array $products): array
{
$cart = null;
$simulation_filter = function () {
return \true;
};
add_filter('woocommerce_paypal_payments_is_simulating_cart', $simulation_filter);
$existing_notices = wc_get_notices();
try {
$cart = $this->create_isolated_cart();
$this->cart_products->set_cart($cart);
$this->cart_products->add_products($products);
$cart->calculate_totals();
return array('total' => (float) $cart->get_total('numeric'), 'shipping_fee' => (float) $cart->get_shipping_total());
} catch (Exception $e) {
$this->logger->error('Cart simulation failed: ' . $e->getMessage());
throw $e;
} finally {
remove_filter('woocommerce_paypal_payments_is_simulating_cart', $simulation_filter);
$current_notices = wc_get_notices();
/**
* Simulation may add WooCommerce notices (stored in the global session, not the cart).
* Snapshot + restore ensures simulation notices don't leak into the user session.
*/
if ($current_notices !== $existing_notices) {
wc_set_notices($existing_notices);
}
if ($cart instanceof WC_Cart) {
$this->cleanup_cart($cart);
}
// Restore cart_products to use the real cart if available.
if (WC()->cart) {
$this->cart_products->set_cart(WC()->cart);
}
}
}
private function create_isolated_cart(): WC_Cart
{
// Prevent the new cart's session from registering hooks (persistence, cookies, etc.).
$prevent_session = function () {
return \false;
};
add_filter('woocommerce_cart_session_initialize', $prevent_session);
$cart = new WC_Cart();
remove_filter('woocommerce_cart_session_initialize', $prevent_session);
// Remove hooks that WC_Cart::__construct() registers to prevent double-firing
// and interference with the real cart's hooks.
remove_action('woocommerce_add_to_cart', array($cart, 'calculate_totals'), 20);
remove_action('woocommerce_applied_coupon', array($cart, 'calculate_totals'), 20);
remove_action('woocommerce_removed_coupon', array($cart, 'calculate_totals'), 20);
remove_action('woocommerce_cart_item_removed', array($cart, 'calculate_totals'), 20);
remove_action('woocommerce_cart_item_restored', array($cart, 'calculate_totals'), 20);
remove_action('woocommerce_check_cart_items', array($cart, 'check_cart_items'), 1);
remove_action('woocommerce_check_cart_items', array($cart, 'check_cart_coupons'), 1);
remove_action('woocommerce_after_checkout_validation', array($cart, 'check_customer_coupons'), 1);
return $cart;
}
private function cleanup_cart(WC_Cart $cart): void
{
try {
$this->cart_products->remove_cart_items();
} catch (Exception $e) {
$this->logger->warning('Cart simulation cleanup failed: ' . $e->getMessage());
}
}
}