supports = array_merge( $this->supports, array( 'subscriptions', 'subscription_cancellation', 'subscription_reactivation', 'subscription_suspension', 'multiple_subscriptions', 'subscription_payment_method_change_customer', 'subscription_payment_method_change_admin', 'subscription_amount_changes', 'subscription_date_changes', ) ); $this->_maybe_register_callback_in_subscriptions(); } /** * Maybe register callback in WooCommerce Subscription hooks. * * @since 1.2.0 */ protected function _maybe_register_callback_in_subscriptions() { if ( ! class_exists( 'WC_Subscriptions_Order' ) ) { return; } add_action( 'woocommerce_scheduled_subscription_payment_' . $this->id, array( $this, 'scheduled_subscription_payment' ), 10, 2 ); add_action( 'woocommerce_subscription_failing_payment_method_' . $this->id, array( $this, 'update_failing_payment_method' ) ); // When changing the payment method for a WooCommerce Subscription to PayPal Checkout, let WooCommerce Subscription // know that the payment method for that subscription should not be changed immediately. Instead, it should // wait for the IPN notification, after the user confirmed the payment method change with PayPal. add_filter( 'woocommerce_subscriptions_update_payment_via_pay_shortcode', array( $this, 'indicate_async_payment_method_update' ), 10, 2 ); // Add extra parameter when updating the subscription payment method to PayPal. add_filter( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_return_url', array( $this, 'add_query_param_to_url_subscription_payment_method_change' ), 10, 2 ); add_filter( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_cancel_url', array( $this, 'add_query_param_to_url_subscription_payment_method_change' ), 10, 2 ); } /** * Checks whether order is part of subscription. * * @since 1.2.0 * * @param int $order_id Order ID * * @return bool Returns true if order is part of subscription */ public function is_subscription( $order_id ) { return ( function_exists( 'wcs_order_contains_subscription' ) && ( wcs_order_contains_subscription( $order_id ) || wcs_is_subscription( $order_id ) || wcs_order_contains_renewal( $order_id ) ) ); } /** * Checks whether the order associated with the given order_id is * for changing a payment method for a WooCommerce Subscription. * * @since 1.7.0 * * @param int $order_id Order ID. * * @return bool Returns true if the order is changing the payment method for a subscription. */ private function is_order_changing_payment_method_for_subscription( $order_id ) { $order = wc_get_order( $order_id ); return ( is_callable( array( $order, 'get_type' ) ) && 'shop_subscription' === $order->get_type() && isset( $_POST['_wcsnonce'] ) && wp_verify_nonce( sanitize_key( $_POST['_wcsnonce'] ), 'wcs_change_payment_method' ) && isset( $_POST['woocommerce_change_payment'] ) && $order->get_id() === absint( $_POST['woocommerce_change_payment'] ) && isset( $_GET['key'] ) && $order->get_order_key() === $_GET['key'] && 0 === $order->get_total() // WooCommerce Subscriptions uses $0 orders to update payment method for the subscription. ); } /** * Process payment. * * @since 1.2.0 * * @param int $order_id Order ID * * @return array */ public function process_payment( $order_id ) { if ( $this->is_subscription( $order_id ) ) { // Is this a subscription payment method change? if ( $this->is_order_changing_payment_method_for_subscription( $order_id ) ) { return $this->change_subscription_payment_method( $order_id ); } // Otherwise, it's a subscription payment. return $this->process_subscription( $order_id ); } return parent::process_payment( $order_id ); } /** * Process initial subscription. * * @since 1.2.0 * * @param int $order_id Order ID * * @return array */ public function process_subscription( $order_id ) { $old_wc = version_compare( WC_VERSION, '3.0', '<' ); $resp = parent::process_payment( $order_id ); $order = wc_get_order( $order_id ); $subscriptions = array(); if ( function_exists( 'wcs_order_contains_subscription' ) && wcs_order_contains_subscription( $order_id ) ) { $subscriptions = wcs_get_subscriptions_for_order( $order_id ); } elseif ( function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order_id ) ) { $subscriptions = wcs_get_subscriptions_for_renewal_order( $order_id ); } $billing_agreement_id = $old_wc ? get_post_meta( $order_id, '_ppec_billing_agreement_id', true ) : $order->get_meta( '_ppec_billing_agreement_id', true ); // Shipping / billing addresses and billing agreement were not copied // because it's not available during subscription creation. foreach ( $subscriptions as $subscription ) { wcs_copy_order_address( $order, $subscription ); update_post_meta( is_callable( array( $subscription, 'get_id' ) ) ? $subscription->get_id() : $subscription->id, '_ppec_billing_agreement_id', $billing_agreement_id ); } return $resp; } /** * Process scheduled subscription payment. * * @since 1.2.0 * * @param float $amount Subscription amount * @param int|WC_Order $order Order ID or order object */ public function scheduled_subscription_payment( $amount, $order ) { $old_wc = version_compare( WC_VERSION, '3.0', '<' ); $order = wc_get_order( $order ); $order_id = $old_wc ? $order->id : $order->get_id(); $billing_agreement_id = $old_wc ? get_post_meta( $order_id, '_ppec_billing_agreement_id', true ) : $order->get_meta( '_ppec_billing_agreement_id', true ); if ( empty( $billing_agreement_id ) ) { wc_gateway_ppec_log( sprintf( '%s: Could not found billing agreement. Skip reference transaction', __METHOD__ ) ); return; } if ( 0.0 === (float) $amount ) { $order->payment_complete(); return; } $client = wc_gateway_ppec()->client; $params = $client->get_do_reference_transaction_params( array( 'reference_id' => $billing_agreement_id, 'amount' => $amount, 'order_id' => $order_id, ) ); $resp = $client->do_reference_transaction( $params ); $this->_process_reference_transaction_response( $order, $resp ); } /** * Process reference transaction response used when creating payment for * scheduled subscription. * * @since 1.2.0 * * @param WC_Order $order Order object * @param array $response Response from DoReferenceTransaction */ protected function _process_reference_transaction_response( $order, $response ) { $client = wc_gateway_ppec()->client; try { if ( ! $client->response_has_success_status( $response ) ) { throw new Exception( __( 'PayPal API error', 'woocommerce-gateway-paypal-express-checkout' ) ); } wc_gateway_ppec_save_transaction_data( $order, $response ); $status = ! empty( $response['PAYMENTSTATUS'] ) ? $response['PAYMENTSTATUS'] : ''; switch ( $status ) { case 'Pending': /* translators: placeholder is pending reason from PayPal API. */ $order_note = sprintf( __( 'PayPal transaction held: %s', 'woocommerce-gateway-paypal-express-checkout' ), $response['PENDINGREASON'] ); if ( ! $order->has_status( 'on-hold' ) ) { $order->update_status( 'on-hold', $order_note ); } else { $order->add_order_note( $order_note ); } break; case 'Completed': case 'Processed': case 'In-Progress': $transaction_id = $response['TRANSACTIONID']; // Translators: %s is a transaction ID. $order->add_order_note( sprintf( __( 'PayPal payment approved (ID: %s)', 'woocommerce-gateway-paypal-express-checkout' ), $transaction_id ) ); $order->payment_complete( $transaction_id ); break; default: throw new Exception( __( 'PayPal payment declined', 'woocommerce-gateway-paypal-express-checkout' ) ); } } catch ( Exception $e ) { $order->update_status( 'failed', $e->getMessage() ); } } /** * Update billing agreement ID for a subscription after using PPEC to complete * a payment to make up for an automatic renewal payment which previously * failed. * * @since 1.2.0 * * @param WC_Subscription $subscription The subscription for which the failing * payment method relates * @param WC_Order $renewal_order The order which recorded the successful * payment (to make up for the failed * automatic payment) */ public function update_failing_payment_method( $subscription, $renewal_order ) { update_post_meta( is_callable( array( $subscription, 'get_id' ) ) ? $subscription->get_id() : $subscription->id, '_ppec_billing_agreement_id', $renewal_order->ppec_billing_agreement_id ); } /** * Indicate to WooCommerce Subscriptions that the payment method change for PayPal Checkout * should be asynchronous. * * WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode uses the * result to decide whether or not to change the payment method information on the subscription * right away or not. * * In our case, the payment method will not be updated until after the user confirms the * payment method change with PayPal. Once that's done, we'll take care of finishing * the payment method update with the subscription. * * @since 1.7.0 * * @param bool $should_update Current value of whether the payment method should be updated immediately. * @param string $new_payment_method The new payment method name. * * @return bool Whether the subscription's payment method should be updated on checkout or async when a response is returned. */ public function indicate_async_payment_method_update( $should_update, $new_payment_method ) { if ( 'ppec_paypal' === $new_payment_method ) { $should_update = false; } return $should_update; } /** * Start the process to update the payment method for a WooCommerce Subscriptions. * * This function is called by `process_payment` when changing a payment method for WooCommerce Subscriptions. * When it's successful, `WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode` will * redirect to the redirect URL provided and the user will be prompted to confirm the payment update. * * @since 1.7.0 * * @param int $order_id Order ID. * * @return array Array used by WC_Subscriptions_Change_Payment_Gateway::change_payment_method_via_pay_shortcode. */ public function change_subscription_payment_method( $order_id ) { try { return array( 'result' => 'success', 'redirect' => wc_gateway_ppec()->checkout->start_checkout_from_order( $order_id, false ), ); } catch ( PayPal_API_Exception $e ) { wc_add_notice( $e->getMessage(), 'error' ); return array( 'result' => 'failure', ); } } /** * Add query param to return and cancel URLs when making a payment change for * a WooCommerce Subscription. * * @since 1.7.0 * * @param string $url The original URL. * @param int $order_id Order ID. * * @return string The new URL. */ public function add_query_param_to_url_subscription_payment_method_change( $url, $order_id ) { if ( $this->is_order_changing_payment_method_for_subscription( $order_id ) ) { return add_query_arg( 'update_subscription_payment_method', 'true', $url ); } return $url; } }