/** * Check whether a given subscription is using reference transactions and if so process the payment. * * @since 2.0 */ public static function process_subscription_payment($amount, $order) { // If the subscription is using reference transactions, we can process the payment ourselves $paypal_profile_id = wcs_get_paypal_id($order->id); if (wcs_is_paypal_profile_a($paypal_profile_id, 'billing_agreement')) { if (0 == $amount) { $order->payment_complete(); return; } $response = self::get_api()->do_reference_transaction($paypal_profile_id, $order, array('amount' => $amount, 'invoice_number' => self::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))))); self::process_subscription_payment_response($order, $response); } }
/** * Charge a payment against a reference token * * @link https://developer.paypal.com/docs/classic/express-checkout/integration-guide/ECReferenceTxns/#id094UM0DA0HS * @link https://developer.paypal.com/docs/classic/api/merchant/DoReferenceTransaction_API_Operation_NVP/ * * @param string $reference_id the ID of a refrence object, e.g. billing agreement ID. * @param WC_Order $order order object * @param array $args { * @type string 'payment_type' (Optional) Specifies type of PayPal payment you require for the billing agreement. It is one of the following values. 'Any' or 'InstantOnly'. Echeck is not supported for DoReferenceTransaction requests. * @type string 'payment_action' How you want to obtain payment. It is one of the following values: 'Authorization' - this payment is a basic authorization subject to settlement with PayPal Authorization and Capture; or 'Sale' - This is a final sale for which you are requesting payment. * @type string 'return_fraud_filters' (Optional) Flag to indicate whether you want the results returned by Fraud Management Filters. By default, you do not receive this information. * } * @since 2.0 */ public function do_reference_transaction($reference_id, $order, $args = array()) { $defaults = array('amount' => $order->get_total(), 'payment_type' => 'Any', 'payment_action' => 'Sale', 'return_fraud_filters' => 1, 'notify_url' => WC()->api_request_url('WC_Gateway_Paypal'), 'invoice_number' => wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number', 'woocommerce-subscriptions'))), 'custom' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key))); $args = wp_parse_args($args, $defaults); $this->set_method('DoReferenceTransaction'); // set base params $this->add_parameters(array('REFERENCEID' => $reference_id, 'BUTTONSOURCE' => 'WooThemes_Cart', 'RETURNFMFDETAILS' => $args['return_fraud_filters'], 'AMT' => $order->get_total(), 'CURRENCYCODE' => $order->get_order_currency(), 'INVNUM' => $args['invoice_number'], 'PAYMENTACTION' => $args['payment_action'], 'NOTIFYURL' => $args['notify_url'], 'CUSTOM' => $args['custom'])); $order_subtotal = $i = 0; $order_items = array(); // add line items foreach ($order->get_items() as $item) { $order_items[] = array('NAME' => wcs_get_paypal_item_name($item['name']), 'AMT' => $order->get_item_subtotal($item), 'QTY' => !empty($item['qty']) ? absint($item['qty']) : 1); $order_subtotal += $item['line_total']; } // add fees foreach ($order->get_fees() as $fee) { $order_items[] = array('NAME' => wcs_get_paypal_item_name($fee['name']), 'AMT' => $fee['line_total'], 'QTY' => 1); $order_subtotal += $fee['line_total']; } // WC 2.3+, no after-tax discounts if ($order->get_total_discount() > 0) { $order_items[] = array('NAME' => __('Total Discount', 'woocommerce-subscriptions'), 'QTY' => 1, 'AMT' => -$order->get_total_discount()); } if ($order->prices_include_tax) { $item_names = array(); foreach ($order_items as $item) { $item_names[] = sprintf('%s x %s', $item['NAME'], $item['QTY']); } // add a single item for the entire order $this->add_line_item_parameters(array('NAME' => sprintf(__('%s - Order', 'woocommerce-subscriptions'), get_option('blogname')), 'DESC' => wcs_get_paypal_item_name(implode(', ', $item_names)), 'AMT' => $order_subtotal + $order->get_cart_tax(), 'QTY' => 1), 0); // add order-level parameters - do not send the TAXAMT due to rounding errors $this->add_payment_parameters(array('ITEMAMT' => $order_subtotal + $order->get_cart_tax(), 'SHIPPINGAMT' => $order->get_total_shipping() + $order->get_shipping_tax())); } else { // add individual order items foreach ($order_items as $item) { $this->add_line_item_parameters($item, $i++); } // add order-level parameters $this->add_payment_parameters(array('ITEMAMT' => $order_subtotal, 'SHIPPINGAMT' => $order->get_total_shipping(), 'TAXAMT' => $order->get_total_tax())); } }
/** * Set up the payment details for a DoExpressCheckoutPayment or DoReferenceTransaction request * * @since 2.0.9 * @param WC_Order $order order object * @param string $type the type of transaction for the payment * @param bool $use_deprecated_params whether to use deprecated PayPal NVP parameters (required for DoReferenceTransaction API calls) */ protected function add_payment_details_parameters(WC_Order $order, $type, $use_deprecated_params = false) { $calculated_total = 0; $order_subtotal = 0; $item_count = 0; $order_items = array(); // add line items foreach ($order->get_items() as $item) { $product = new WC_Product($item['product_id']); $order_items[] = array('NAME' => wcs_get_paypal_item_name($product->get_title()), 'DESC' => $this->get_item_description($item, $product), 'AMT' => $this->round($order->get_item_subtotal($item)), 'QTY' => !empty($item['qty']) ? absint($item['qty']) : 1, 'ITEMURL' => $product->get_permalink()); $order_subtotal += $item['line_total']; } // add fees foreach ($order->get_fees() as $fee) { $order_items[] = array('NAME' => wcs_get_paypal_item_name($fee['name']), 'AMT' => $this->round($fee['line_total']), 'QTY' => 1); $order_subtotal += $fee['line_total']; } // add discounts if ($order->get_total_discount() > 0) { $order_items[] = array('NAME' => __('Total Discount', 'woocommerce-subscriptions'), 'QTY' => 1, 'AMT' => -$this->round($order->get_total_discount())); } if ($this->skip_line_items($order)) { $total_amount = $this->round($order->get_total()); // calculate the total as PayPal would $calculated_total += $this->round($order_subtotal + $order->get_cart_tax()) + $this->round($order->get_total_shipping() + $order->get_shipping_tax()); // offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the order subtotal if ($total_amount !== $calculated_total) { $order_subtotal = $order_subtotal - ($calculated_total - $total_amount); } $item_names = array(); foreach ($order_items as $item) { $item_names[] = sprintf('%1$s x %2$s', $item['NAME'], $item['QTY']); } // add a single item for the entire order $this->add_line_item_parameters(array('NAME' => sprintf(__('%s - Order', 'woocommerce-subscriptions'), get_option('blogname')), 'DESC' => wcs_get_paypal_item_name(implode(', ', $item_names)), 'AMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'QTY' => 1), 0, $use_deprecated_params); // add order-level parameters // - Do not sent the TAXAMT due to rounding errors if ($use_deprecated_params) { $this->add_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'SHIPPINGAMT' => $this->round($order->get_total_shipping() + $order->get_shipping_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } else { $this->add_payment_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal + $order->get_cart_tax()), 'SHIPPINGAMT' => $this->round($order->get_total_shipping() + $order->get_shipping_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } } else { // add individual order items foreach ($order_items as $item) { $this->add_line_item_parameters($item, $item_count++, $use_deprecated_params); $calculated_total += $this->round($item['AMT'] * $item['QTY']); } // add shipping and tax to calculated total $calculated_total += $this->round($order->get_total_shipping()) + $this->round($order->get_total_tax()); $total_amount = $this->round($order->get_total()); // add order-level parameters if ($use_deprecated_params) { $this->add_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal), 'SHIPPINGAMT' => $this->round($order->get_total_shipping()), 'TAXAMT' => $this->round($order->get_total_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } else { $this->add_payment_parameters(array('AMT' => $total_amount, 'CURRENCYCODE' => $order->get_order_currency(), 'ITEMAMT' => $this->round($order_subtotal), 'SHIPPINGAMT' => $this->round($order->get_total_shipping()), 'TAXAMT' => $this->round($order->get_total_tax()), 'INVNUM' => WCS_PayPal::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number. Used as a character to remove from the actual order number', 'woocommerce-subscriptions'))), 'PAYMENTACTION' => $type, 'PAYMENTREQUESTID' => $order->id, 'CUSTOM' => json_encode(array('order_id' => $order->id, 'order_key' => $order->order_key)))); } // offset the discrepency between the WooCommerce cart total and PayPal's calculated total by adjusting the cost of the first item if ($total_amount !== $calculated_total) { $this->parameters['L_PAYMENTREQUEST_0_AMT0'] = $this->parameters['L_PAYMENTREQUEST_0_AMT0'] - ($calculated_total - $total_amount); } } }
/** * Check whether a given subscription is using reference transactions and if so process the payment. * * @since 2.0 */ public static function process_subscription_payment($amount, $order) { // If the subscription is using reference transactions, we can process the payment ourselves $paypal_profile_id = wcs_get_paypal_id($order->id); if (wcs_is_paypal_profile_a($paypal_profile_id, 'billing_agreement')) { if (0 == $amount) { $order->payment_complete(); return; } $response = self::get_api()->do_reference_transaction($paypal_profile_id, $order, array('amount' => $amount, 'invoice_number' => self::get_option('invoice_prefix') . wcs_str_to_ascii(ltrim($order->get_order_number(), _x('#', 'hash before the order number', 'woocommerce-subscriptions'))))); if ($response->has_api_error()) { $error_message = $response->get_api_error_message(); // Some PayPal error messages end with a fullstop, others do not, we prefer our punctuation consistent, so add one if we don't already have one. if ('.' !== substr($error_message, -1)) { $error_message .= '.'; } // translators: placeholders are PayPal API error code and PayPal API error message $order->update_status('failed', sprintf(__('PayPal API error: (%d) %s', 'woocommerce-subscriptions'), $response->get_api_error_code(), $error_message)); } elseif ($response->transaction_held()) { // translators: placeholder is PayPal transaction status message $order_note = sprintf(__('PayPal Transaction Held: %s', 'woocommerce-subscriptions'), $response->get_status_message()); $order_status = apply_filters('wcs_paypal_held_payment_order_status', 'on-hold', $order, $response); // mark order as held if (!$order->has_status($order_status)) { $order->update_status($order_status, $order_note); } else { $order->add_order_note($order_note); } } elseif (!$response->transaction_approved()) { // translators: placeholder is PayPal transaction status message $order->update_status('failed', sprintf(__('PayPal payment declined: %s', 'woocommerce-subscriptions'), $response->get_status_message())); } elseif ($response->transaction_approved()) { $order->add_order_note(sprintf(__('PayPal payment approved (ID: %s)', 'woocommerce-subscriptions'), $response->get_transaction_id())); $order->payment_complete($response->get_transaction_id()); } } }