<?php
/**
* Compatibility layer for subscriptions paid via PayPal Express Checkout.
*
* @package WooCommerce\PayPalCommerce\Compat\PPEC
*/
declare (strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\PPEC;
use WooCommerce\PayPalCommerce\Vendor\Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\WcSubscriptions\RenewalHandler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
/**
* Handles renewals and edit/display matters for subscriptions renewed via PayPal Express Checkout.
*/
class SubscriptionsHandler
{
const BILLING_AGREEMENT_TOKEN_TYPE = 'BILLING_AGREEMENT';
/**
* PayPal Payments subscription renewal handler.
*
* @var RenewalHandler
*/
private $ppcp_renewal_handler;
/**
* Mock gateway instance.
*
* @var MockGateway
*/
private $mock_gateway;
private \WooCommerce\PayPalCommerce\Compat\PPEC\BillingAgreementTokenConverter $token_converter;
private LoggerInterface $logger;
public function __construct(RenewalHandler $ppcp_renewal_handler, \WooCommerce\PayPalCommerce\Compat\PPEC\MockGateway $gateway, \WooCommerce\PayPalCommerce\Compat\PPEC\BillingAgreementTokenConverter $token_converter, LoggerInterface $logger)
{
$this->ppcp_renewal_handler = $ppcp_renewal_handler;
$this->mock_gateway = $gateway;
$this->token_converter = $token_converter;
$this->logger = $logger;
}
/**
* Sets up hooks.
*
* @return void
*/
public function maybe_hook()
{
if (!\WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper::use_ppec_compat_layer_for_subscriptions()) {
return;
}
// "Mock" PPEC when needed.
add_filter('woocommerce_payment_gateways', array($this, 'add_mock_ppec_gateway'));
// Add billing agreement as a valid token type.
add_filter('woocommerce_paypal_payments_valid_payment_token_types', array($this, 'add_billing_agreement_as_token_type'));
// Process PPEC renewals through PayPal Payments.
add_action('woocommerce_scheduled_subscription_payment_' . \WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper::PPEC_GATEWAY_ID, array($this, 'process_renewal'), 10, 2);
}
/**
* Adds a mock gateway to disguise as PPEC when needed. Hooked onto `woocommerce_payment_gateways`.
* The mock gateway fixes display issues where subscriptions paid via PPEC appear as "via Manual Renewal" and also
* prevents subscriptions from automatically changing the payment method to "manual" when a subscription is edited.
*
* @param array $gateways List of gateways.
* @return array
*/
public function add_mock_ppec_gateway($gateways)
{
if (!isset($gateways[\WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper::PPEC_GATEWAY_ID])) {
$gateways[\WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper::PPEC_GATEWAY_ID] = $this->mock_gateway;
}
return $gateways;
}
/**
* Registers BILLING_AGREEMENT as a valid token type for using with the PayPal REST API.
*
* @param array $types List of token types.
* @return array
*/
public function add_billing_agreement_as_token_type($types)
{
if (!in_array(self::BILLING_AGREEMENT_TOKEN_TYPE, $types, \true)) {
$types[] = self::BILLING_AGREEMENT_TOKEN_TYPE;
}
return $types;
}
/**
* Processes subscription renewals on behalf of PayPal Express Checkout.
* Hooked onto `woocommerce_scheduled_subscription_payment_ppec_paypal`.
*
* @param float $amount The order amount.
* @param \WC_Order $order The renewal order.
* @return void
*/
public function process_renewal($amount, $order)
{
add_filter('woocommerce_paypal_payments_subscriptions_get_token_for_customer', array($this, 'use_billing_agreement_as_token'), 10, 3);
$this->ppcp_renewal_handler->renew($order);
remove_filter('woocommerce_paypal_payments_subscriptions_get_token_for_customer', array($this, 'use_billing_agreement_as_token'));
}
/**
* Short-circuits `RenewalHandler::get_token_for_customer()` for PPEC orders.
*
* Tries the vault v3 conversion path first. If that is not applicable or fails,
* falls back to the legacy BILLING_AGREEMENT token path.
*/
public function use_billing_agreement_as_token($token, $customer, $order)
{
if (\WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper::PPEC_GATEWAY_ID !== $order->get_payment_method() || !wcs_order_contains_renewal($order)) {
return $token;
}
$vault_token = $this->get_vault_v3_token($order);
if ($vault_token) {
return $vault_token;
}
return $this->get_billing_agreement_token($order) ?? $token;
}
/**
* Attempts to resolve or create a Vault v3 payment token for the renewal order.
*
* Checks if the subscription already has a converted vault token. If not,
* attempts conversion from the billing agreement via the PayPal Vault v3 API.
*/
private function get_vault_v3_token(\WC_Order $order): ?PaymentToken
{
$subscriptions = wcs_get_subscriptions_for_renewal_order($order);
$subscription = !empty($subscriptions) ? reset($subscriptions) : null;
if (!$subscription) {
return null;
}
$vault_token_id = $subscription->get_meta('_ppec_ba_converted_to_vault_v3', \true);
if ($vault_token_id) {
return new PaymentToken($vault_token_id, new stdClass(), PaymentToken::TYPE_PAYMENT_METHOD_TOKEN);
}
$billing_agreement_id = $this->resolve_billing_agreement_id($order);
if (!$billing_agreement_id) {
return null;
}
$vault_token_id = $this->token_converter->convert($billing_agreement_id, $order->get_customer_id());
if (!$vault_token_id) {
return null;
}
$subscription->update_meta_data('_ppec_ba_converted_to_vault_v3', $vault_token_id);
$subscription->save();
$this->logger->info(sprintf('Subscription #%d: converted Billing Agreement %s to Vault v3 token %s.', $subscription->get_id(), $billing_agreement_id, $vault_token_id));
return new PaymentToken($vault_token_id, new stdClass(), PaymentToken::TYPE_PAYMENT_METHOD_TOKEN);
}
private function get_billing_agreement_token(\WC_Order $order): ?PaymentToken
{
$billing_agreement_id = $this->resolve_billing_agreement_id($order);
if (!$billing_agreement_id) {
return null;
}
return new PaymentToken($billing_agreement_id, new stdClass(), 'BILLING_AGREEMENT');
}
private function resolve_billing_agreement_id(\WC_Order $order): ?string
{
$billing_agreement_id = $order->get_meta('_ppec_billing_agreement_id', \true);
if ($billing_agreement_id) {
return $billing_agreement_id;
}
$subscriptions = wcs_get_subscriptions_for_renewal_order($order);
if (!empty($subscriptions)) {
$subscription = reset($subscriptions);
$parent_order = $subscription->get_parent();
if ($parent_order) {
$billing_agreement_id = $parent_order->get_meta('_ppec_billing_agreement_id', \true);
if ($billing_agreement_id) {
return $billing_agreement_id;
}
}
}
return null;
}
}