| 1 |
<?php namespace TierPricingTable\Integrations\Plugins; |
| 2 |
|
| 3 |
use TierPricingTable\PriceManager; |
| 4 |
use WC_Product; |
| 5 |
|
| 6 |
/** |
| 7 |
* Compatibility with WooCommerce Subscriptions "All Products for Subscriptions" (APFS) plans. |
| 8 |
* |
| 9 |
* APFS applies a subscription plan's price/discount to a product purely through view-context |
| 10 |
* `woocommerce_product_get_price` filters (it never writes the price prop). WooCommerce Subscriptions |
| 11 |
* additionally filters non-subscription product prices to 0 during its "recurring totals" pass. Both |
| 12 |
* are `view`-context filters, so reading `get_price('edit')` returns the raw, unmodified price. |
| 13 |
* |
| 14 |
* This integration keeps tiered pricing correct on plan items without changing behaviour for any other |
| 15 |
* store: |
| 16 |
* |
| 17 |
* - Charging: it recomputes a plan item's tiered price in the "edit" context, so a percentage tier is |
| 18 |
* based on the raw price rather than the plan-discounted (and, in the recurring pass, zeroed) "view" |
| 19 |
* price. The plan discount is then applied exactly once — by Subscriptions, when the stored tier |
| 20 |
* price is read during totals — and the tier no longer collapses to false on renewals. |
| 21 |
* - Display: it defers a plan item's cart price/subtotal to WooCommerce, which already reflects the |
| 22 |
* plan discount, instead of the plugin rebuilding it from a fresh (plan-unaware) product instance. |
| 23 |
* |
| 24 |
* Everything is scoped to items actually purchased on a plan and is a no-op unless APFS is active. |
| 25 |
*/ |
| 26 |
class WooCommerceSubscriptions extends PluginIntegrationAbstract { |
| 27 |
|
| 28 |
public function run() { |
| 29 |
|
| 30 |
// These hooks are registered unconditionally: run() executes at plugin-include time, before |
| 31 |
// WooCommerce Subscriptions lazily loads its "All Products for Subscriptions" cart API on |
| 32 |
// plugins_loaded. Availability is instead checked inside each callback (isSubscriptionPlanItem), |
| 33 |
// which runs at cart-calculation time, so the callbacks are safe no-ops when APFS is absent. |
| 34 |
|
| 35 |
// Charging: recompute plan items in the "edit" context (raw price base). Runs even when the |
| 36 |
// incoming price is false — during the recurring pass getTierPrice() returns false because the |
| 37 |
// product's "view" price has been filtered to 0. |
| 38 |
add_filter( 'tiered_pricing_table/cart/product_cart_price', |
| 39 |
function ( $price, $cartItem, $cartItemKey, $totalQuantity ) { |
| 40 |
|
| 41 |
if ( ! $this->isSubscriptionPlanItem( $cartItem ) ) { |
| 42 |
return $price; |
| 43 |
} |
| 44 |
|
| 45 |
return PriceManager::getPriceByRules( $totalQuantity, $cartItem['data']->get_id(), 'edit', 'cart', |
| 46 |
false ); |
| 47 |
}, 10, 4 ); |
| 48 |
|
| 49 |
// Display: defer the subtotal column to WooCommerce for plan items so it matches the charge. |
| 50 |
add_filter( 'tiered_pricing_table/cart/recalculate_cart_item_subtotal', |
| 51 |
function ( $state, $cartItem ) { |
| 52 |
return $this->isSubscriptionPlanItem( $cartItem ) ? false : $state; |
| 53 |
}, 10, 2 ); |
| 54 |
|
| 55 |
// Display: defer the price column to WooCommerce for plan items. |
| 56 |
add_filter( 'tiered_pricing_table/cart/need_price_recalculation/item', |
| 57 |
function ( $state, $cartItem ) { |
| 58 |
return $this->isSubscriptionPlanItem( $cartItem ) ? false : $state; |
| 59 |
}, 10, 2 ); |
| 60 |
} |
| 61 |
|
| 62 |
/** |
| 63 |
* Whether a cart item is currently purchased on a subscription plan (APFS). |
| 64 |
* |
| 65 |
* Reads the scheme stored on the cart item itself, which survives the cloned recurring cart, rather |
| 66 |
* than the product object's runtime state. |
| 67 |
* |
| 68 |
* @param array $cartItem |
| 69 |
* |
| 70 |
* @return bool |
| 71 |
*/ |
| 72 |
protected function isSubscriptionPlanItem( $cartItem ): bool { |
| 73 |
|
| 74 |
if ( ! class_exists( '\WCS_ATT_Cart' ) ) { |
| 75 |
return false; |
| 76 |
} |
| 77 |
|
| 78 |
if ( empty( $cartItem['data'] ) || ! ( $cartItem['data'] instanceof WC_Product ) ) { |
| 79 |
return false; |
| 80 |
} |
| 81 |
|
| 82 |
$scheme = \WCS_ATT_Cart::get_subscription_scheme( $cartItem ); |
| 83 |
|
| 84 |
// A non-empty, non-'0' scheme key means the item is on a plan; '0'/null means a one-time purchase. |
| 85 |
return ! empty( $scheme ) && '0' !== (string) $scheme; |
| 86 |
} |
| 87 |
|
| 88 |
public function getTitle(): string { |
| 89 |
return 'WooCommerce Subscriptions'; |
| 90 |
} |
| 91 |
|
| 92 |
public function getDescription(): string { |
| 93 |
return __( 'Apply tiered pricing correctly to products purchased on a WooCommerce Subscriptions plan, in both the initial and recurring totals.', 'tier-pricing-table' ); |
| 94 |
} |
| 95 |
|
| 96 |
public function getSlug(): string { |
| 97 |
return 'woocommerce-subscriptions'; |
| 98 |
} |
| 99 |
|
| 100 |
public function getAuthorURL(): string { |
| 101 |
return 'https://woocommerce.com/products/woocommerce-subscriptions/'; |
| 102 |
} |
| 103 |
|
| 104 |
public function getIconURL(): string { |
| 105 |
return $this->getContainer()->getFileManager()->locateAsset( 'admin/integrations/woocommerce-develop.jpeg' ); |
| 106 |
} |
| 107 |
|
| 108 |
public function getIntegrationCategory(): string { |
| 109 |
return 'custom_product_types'; |
| 110 |
} |
| 111 |
} |
| 112 |
|