_environment = $environment; if ( is_a( $credential, 'WC_Gateway_PPEC_Client_Credential' ) ) { $this->set_credential( $credential ); } } /** * Set credential for the client. * * @param WC_Gateway_PPEC_Client_Credential $credential Client's credential */ public function set_credential( WC_Gateway_PPEC_Client_Credential $credential ) { $this->_credential = $credential; } /** * Get payer ID from API. */ public function get_payer_id() { $option_key = 'woocommerce_ppec_payer_id_' . $this->_environment . '_' . md5( $this->_credential->get_username() . ':' . $this->_credential->get_password() ); $payer_id = get_option( $option_key ); if ( $payer_id ) { return $payer_id; } else { $result = $this->get_pal_details(); if ( ! empty( $result['PAL'] ) ) { update_option( $option_key, wc_clean( $result['PAL'] ) ); return $payer_id; } } return false; } /** * Set environment for the client. * * @param string $environment Environment. Either 'live' or 'sandbox' */ public function set_environment( $environment ) { if ( ! in_array( $environment, array( 'live', 'sandbox' ), true ) ) { $environment = 'live'; } $this->_environment = $environment; } /** * Get PayPal endpoint. * * @see https://developer.paypal.com/docs/classic/api/#ec * * @return string */ public function get_endpoint() { return sprintf( 'https://%s%s.paypal.com/nvp', $this->_credential->get_endpoint_subdomain(), 'sandbox' === $this->_environment ? '.sandbox' : '' ); } /** * Make a remote request to PayPal API. * * @see https://developer.paypal.com/docs/classic/api/NVPAPIOverview/#creating-an-nvp-request * * @param array $params NVP request parameters * @return array NVP response */ protected function _request( array $params ) { try { $this->_validate_request(); // First, add in the necessary credential parameters. $body = apply_filters( 'woocommerce_paypal_express_checkout_request_body', array_merge( $params, $this->_credential->get_request_params() ) ); $args = array( 'method' => 'POST', 'body' => $body, 'user-agent' => __CLASS__, 'httpversion' => '1.1', 'timeout' => 30, ); // For cURL transport. add_action( 'http_api_curl', array( $this->_credential, 'configure_curl' ), 10, 3 ); wc_gateway_ppec_log( sprintf( '%s: remote request to %s with params: %s', __METHOD__, $this->get_endpoint(), print_r( $body, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r $resp = wp_safe_remote_post( $this->get_endpoint(), $args ); return $this->_process_response( $resp ); } catch ( Exception $e ) { remove_action( 'http_api_curl', array( $this->_credential, 'configure_curl' ), 10 ); // TODO: Maybe returns WP_Error ? $error = array( 'ACK' => 'Failure', 'L_ERRORCODE0' => $e->getCode(), 'L_SHORTMESSAGE0' => 'Error in ' . __METHOD__, 'L_LONGMESSAGE0' => $e->getMessage(), 'L_SEVERITYCODE0' => 'Error', ); wc_gateway_ppec_log( sprintf( '%s: returns error: %s', __METHOD__, print_r( $error, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r return $error; } } /** * Validate request. * * @since 1.2.0 * * @throws \Exception */ protected function _validate_request() { // Make sure $_credential and $_environment have been configured. if ( ! $this->_credential ) { throw new Exception( __( 'Missing credential', 'woocommerce-gateway-paypal-express-checkout' ), self::INVALID_CREDENTIAL_ERROR ); } if ( ! is_a( $this->_credential, 'WC_Gateway_PPEC_Client_Credential' ) ) { throw new Exception( __( 'Invalid credential object', 'woocommerce-gateway-paypal-express-checkout' ), self::INVALID_CREDENTIAL_ERROR ); } if ( ! in_array( $this->_environment, array( 'live', 'sandbox' ), true ) ) { throw new Exception( __( 'Invalid environment', 'woocommerce-gateway-paypal-express-checkout' ), self::INVALID_ENVIRONMENT_ERROR ); } } /** * Process response from API. * * @since 1.2.0 * * @throws \Exception * * @param WP_Error|array Response from remote API * * @return array */ protected function _process_response( $response ) { if ( is_wp_error( $response ) ) { // Translators: placeholder is an error message. throw new Exception( sprintf( __( 'An error occurred while trying to connect to PayPal: %s', 'woocommerce-gateway-paypal-express-checkout' ), $response->get_error_message() ), self::REQUEST_ERROR ); } parse_str( wp_remote_retrieve_body( $response ), $result ); if ( ! array_key_exists( 'ACK', $result ) ) { throw new Exception( __( 'Malformed response received from PayPal', 'woocommerce-gateway-paypal-express-checkout' ), self::REQUEST_ERROR ); } wc_gateway_ppec_log( sprintf( '%s: acknowleged response body: %s', __METHOD__, print_r( $result, true ) ) ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r remove_action( 'http_api_curl', array( $this->_credential, 'configure_curl' ), 10 ); return $result; } /** * Initiates an Express Checkout transaction. * * @see https://developer.paypal.com/docs/classic/api/merchant/SetExpressCheckout_API_Operation_NVP/ * * @param array $params NVP params * @return array NVP response */ public function set_express_checkout( array $params ) { $params['METHOD'] = 'SetExpressCheckout'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } /** * Get params for SetExpressCheckout call. * * @since 1.2.0 * * @param array $args { * Context args to retrieve SetExpressCheckout parameters. * * @type string $skip_checkout Whether checking out ahead of store checkout screen. * @type int $order_id Order ID if checking out after order is created. * @type bool $create_billing_agreement Whether billing agreement creation * is needed after returned from PayPal. * } * * @return array Params for SetExpressCheckout call */ public function get_set_express_checkout_params( array $args ) { $args = wp_parse_args( $args, array( 'skip_checkout' => true, 'order_id' => '', 'create_billing_agreement' => false, ) ); $settings = wc_gateway_ppec()->settings; $params = array(); $logo_url_or_id = $settings->logo_image_url; $header_url_or_id = $settings->header_image_url; $params['LOGOIMG'] = filter_var( $logo_url_or_id, FILTER_VALIDATE_URL ) ? $logo_url_or_id : wp_get_attachment_image_url( $logo_url_or_id, 'ppec_logo_image_size' ); $params['HDRIMG'] = filter_var( $header_url_or_id, FILTER_VALIDATE_URL ) ? $header_url_or_id : wp_get_attachment_image_url( $header_url_or_id, 'ppec_header_image_size' ); $params['PAGESTYLE'] = $settings->page_style; $params['BRANDNAME'] = $settings->get_brand_name(); $params['RETURNURL'] = $this->_get_return_url( $args ); $params['CANCELURL'] = $this->_get_cancel_url( $args ); if ( wc_gateway_ppec_is_using_credit() ) { $params['USERSELECTEDFUNDINGSOURCE'] = 'Finance'; } if ( ! $args['skip_checkout'] ) { // Display shipping address sent from checkout page, rather than selecting from addresses on file with PayPal. $params['ADDROVERRIDE'] = '1'; } if ( in_array( $settings->landing_page, array( 'Billing', 'Login' ), true ) ) { $params['LANDINGPAGE'] = $settings->landing_page; } if ( apply_filters( 'woocommerce_paypal_express_checkout_allow_guests', true ) ) { $params['SOLUTIONTYPE'] = 'Sole'; } if ( 'yes' === $settings->require_billing ) { $params['REQBILLINGADDRESS'] = '1'; } $params['PAYMENTREQUEST_0_PAYMENTACTION'] = $settings->get_paymentaction(); if ( 'yes' === $settings->instant_payments && 'sale' === $settings->get_paymentaction() ) { $params['PAYMENTREQUEST_0_ALLOWEDPAYMENTMETHOD'] = 'InstantPaymentOnly'; } $params['PAYMENTREQUEST_0_INSURANCEAMT'] = 0; $params['PAYMENTREQUEST_0_HANDLINGAMT'] = 0; $params['PAYMENTREQUEST_0_CUSTOM'] = ''; $params['PAYMENTREQUEST_0_INVNUM'] = ''; $params['PAYMENTREQUEST_0_CURRENCYCODE'] = get_woocommerce_currency(); if ( ! empty( $args['order_id'] ) ) { $details = $this->_get_details_from_order( $args['order_id'] ); } else { $details = $this->_get_details_from_cart(); } $params = array_merge( $params, array( 'PAYMENTREQUEST_0_AMT' => $details['order_total'], 'PAYMENTREQUEST_0_ITEMAMT' => $details['total_item_amount'], 'PAYMENTREQUEST_0_SHIPPINGAMT' => $details['shipping'], 'PAYMENTREQUEST_0_TAXAMT' => $details['order_tax'], 'PAYMENTREQUEST_0_SHIPDISCAMT' => $details['ship_discount_amount'], 'NOSHIPPING' => WC_Gateway_PPEC_Plugin::needs_shipping() ? 0 : 1, ) ); if ( ! empty( $details['email'] ) ) { $params['EMAIL'] = $details['email']; } if ( $args['create_billing_agreement'] ) { $params['L_BILLINGTYPE0'] = 'MerchantInitiatedBillingSingleAgreement'; $params['L_BILLINGAGREEMENTDESCRIPTION0'] = $this->_get_billing_agreement_description(); $params['L_BILLINGAGREEMENTCUSTOM0'] = ''; } if ( ! empty( $details['shipping_address'] ) ) { $params = array_merge( $params, $details['shipping_address']->getAddressParams( 'PAYMENTREQUEST_0_SHIPTO' ) ); } if ( ! empty( $details['items'] ) ) { $count = 0; foreach ( $details['items'] as $line_item_key => $values ) { $line_item_params = array( 'L_PAYMENTREQUEST_0_NAME' . $count => $values['name'], 'L_PAYMENTREQUEST_0_DESC' . $count => ! empty( $values['description'] ) ? substr( strip_tags( $values['description'] ), 0, 127 ) : '', // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags 'L_PAYMENTREQUEST_0_QTY' . $count => $values['quantity'], 'L_PAYMENTREQUEST_0_AMT' . $count => $values['amount'], ); if ( isset( $values['sku'] ) ) { $line_item_params[ 'L_PAYMENTREQUEST_0_NUMBER' . $count ] = $values['sku']; } $params = array_merge( $params, $line_item_params ); $count++; } } return $params; } /** * Get return URL. * * The URL to return from express checkout. * * @since 1.2.0 * * @param array $context_args { * Context args to retrieve SetExpressCheckout parameters. * * @type bool $create_billing_agreement Whether billing agreement creation * is needed after returned from PayPal. * } * * @return string Return URL */ protected function _get_return_url( array $context_args ) { $query_args = array( 'woo-paypal-return' => 'true', ); if ( $context_args['create_billing_agreement'] ) { $query_args['create-billing-agreement'] = 'true'; } $url = add_query_arg( $query_args, wc_get_checkout_url() ); $order_id = $context_args['order_id']; return apply_filters( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_return_url', $url, $order_id ); } /** * Get cancel URL. * * The URL to return when canceling the express checkout. * * @since 1.2.0 * * @return string Cancel URL */ protected function _get_cancel_url( $context_args ) { $url = add_query_arg( 'woo-paypal-cancel', 'true', wc_get_cart_url() ); $order_id = $context_args['order_id']; return apply_filters( 'woocommerce_paypal_express_checkout_set_express_checkout_params_get_cancel_url', $url, $order_id ); } /** * Get billing agreement description to be passed to PayPal. * * @since 1.2.0 * * @return string Billing agreement description */ protected function _get_billing_agreement_description() { /* Translators: placeholder is blogname. */ $description = sprintf( _x( 'Orders with %s', 'data sent to PayPal', 'woocommerce-gateway-paypal-express-checkout' ), get_bloginfo( 'name' ) ); if ( strlen( $description ) > 127 ) { $description = substr( $description, 0, 124 ) . '...'; } return html_entity_decode( $description, ENT_NOQUOTES, 'UTF-8' ); } /** * Get extra line item when for subtotal mismatch. * * @since 1.2.0 * * @param float $amount Item's amount * * @return array Line item */ protected function _get_extra_offset_line_item( $amount ) { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); return array( 'name' => 'Line Item Amount Offset', 'description' => 'Adjust cart calculation discrepancy', 'quantity' => 1, 'amount' => round( $amount, $decimals ), ); } /** * Get extra line item when for discount. * * @since 1.2.0 * * @param float $amount Item's amount * * @return array Line item */ protected function _get_extra_discount_line_item( $amount ) { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); return array( 'name' => 'Discount', 'quantity' => 1, 'amount' => '-' . round( $amount, $decimals ), ); } /** * Get details, not params to be passed in PayPal API request, from cart contents. * * This is the details when buyer is checking out from cart page. * * @since 1.2.0 * @version 1.2.1 * * @return array Order details */ protected function _get_details_from_cart() { $settings = wc_gateway_ppec()->settings; $old_wc = version_compare( WC_VERSION, '3.0', '<' ); WC()->cart->calculate_totals(); $decimals = $settings->get_number_of_decimal_digits(); $rounded_total = $this->_get_rounded_total_in_cart(); $discounts = WC()->cart->get_cart_discount_total(); $details = array( 'total_item_amount' => round( WC()->cart->cart_contents_total + WC()->cart->fee_total, $decimals ), 'order_tax' => round( WC()->cart->tax_total + WC()->cart->shipping_tax_total, $decimals ), 'shipping' => round( WC()->cart->shipping_total, $decimals ), 'items' => $this->_get_paypal_line_items_from_cart(), 'shipping_address' => $this->_get_address_from_customer(), 'email' => $old_wc ? WC()->customer->billing_email : WC()->customer->get_billing_email(), ); return $this->get_details( $details, $discounts, $rounded_total, WC()->cart->total ); } /** * Get line items from cart contents. * * @since 1.2.0 * * @return array Line items */ protected function _get_paypal_line_items_from_cart() { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); $items = array(); foreach ( WC()->cart->cart_contents as $cart_item_key => $values ) { $amount = round( $values['line_subtotal'] / $values['quantity'], $decimals ); if ( version_compare( WC_VERSION, '3.0', '<' ) ) { $name = $values['data']->post->post_title; $description = $values['data']->post->post_content; } else { $product = $values['data']; $name = $product->get_name(); $description = $product->get_description(); } $item = array( 'name' => $name, 'description' => $description, 'quantity' => $values['quantity'], 'amount' => $amount, 'sku' => $values['data']->get_sku(), ); $items[] = $item; } foreach ( WC()->cart->get_fees() as $fee_key => $fee_values ) { $item = array( 'name' => $fee_values->name, 'description' => '', 'quantity' => 1, 'amount' => round( $fee_values->total, $decimals ), ); $items[] = $item; } return $items; } /** * Get rounded total of items in cart. * * @since 1.2.0 * * @return float Rounded total in cart */ protected function _get_rounded_total_in_cart() { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); $rounded_total = 0; foreach ( WC()->cart->cart_contents as $cart_item_key => $values ) { $amount = round( $values['line_subtotal'] / $values['quantity'], $decimals ); $rounded_total += round( $amount * $values['quantity'], $decimals ); } foreach ( WC()->cart->get_fees() as $fee_key => $fee_values ) { $rounded_total += round( $fee_values->total, $decimals ); } return $rounded_total; } /** * Get details from populated price array * * @since 1.4.1 * * @param array $details Prices * * @return array Details */ protected function get_details( $details, $discounts, $rounded_total, $total ) { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); $discounts = round( $discounts, $decimals ); $details['order_total'] = round( $details['total_item_amount'] + $details['order_tax'] + $details['shipping'], $decimals ); // Compare WC totals with what PayPal will calculate to see if they match. // if they do not match, check to see what the merchant would like to do. // Options are to remove line items or add a line item to adjust for // the difference. $diff = 0; if ( $details['total_item_amount'] + $discounts != $rounded_total ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison if ( 'add' === $settings->get_subtotal_mismatch_behavior() ) { // Add line item to make up different between WooCommerce // calculations and PayPal calculations. $diff = round( $details['total_item_amount'] + $discounts - $rounded_total, $decimals ); if ( abs( $diff ) > 0.000001 && 0.0 !== (float) $diff ) { $extra_line_item = $this->_get_extra_offset_line_item( $diff ); $details['items'][] = $extra_line_item; $details['total_item_amount'] += $extra_line_item['amount']; $details['order_total'] += $extra_line_item['amount']; } } else { // Omit line items altogether. unset( $details['items'] ); } } // Enter discount shenanigans. Item total cannot be 0 so make modifications // accordingly. if ( 0 == $details['total_item_amount'] ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison // Omit line items altogether. unset( $details['items'] ); } elseif ( $discounts > 0 && 0 < $details['total_item_amount'] && ! empty( $details['items'] ) ) { // Else if there is discount, add them to the line-items $details['items'][] = $this->_get_extra_discount_line_item( $discounts ); } $details['ship_discount_amount'] = 0; // AMT $details['order_total'] = round( $details['order_total'], $decimals ); // ITEMAMT $details['total_item_amount'] = round( $details['total_item_amount'], $decimals ); // If the totals don't line up, adjust the tax to make it work (it's // probably a tax mismatch). $wc_order_total = round( $total, $decimals ); $discounted_total = $details['order_total']; if ( $wc_order_total != $discounted_total ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison // tax cannot be negative if ( $discounted_total < $wc_order_total ) { $details['order_tax'] += $wc_order_total - $discounted_total; $details['order_tax'] = round( $details['order_tax'], $decimals ); } else { $details['ship_discount_amount'] += $wc_order_total - $discounted_total; $details['ship_discount_amount'] = round( $details['ship_discount_amount'], $decimals ); } $details['order_total'] = $wc_order_total; } if ( ! is_numeric( $details['shipping'] ) ) { $details['shipping'] = 0; } $lisum = 0; if ( ! empty( $details['items'] ) ) { foreach ( $details['items'] as $li => $values ) { $lisum += $values['quantity'] * $values['amount']; } } if ( abs( $lisum ) > 0.000001 && 0.0 !== (float) $diff ) { $details['items'][] = $this->_get_extra_offset_line_item( $details['total_item_amount'] - $lisum ); } /** * Filter PayPal order details. * * Provide opportunity for developers to modify details passed to PayPal. * This was originally introduced to add a mechanism to allow for * decimal product quantity support. * * @since 1.6.6 * * @param array $details Current PayPal order details */ return apply_filters( 'woocommerce_paypal_express_checkout_get_details', $details ); } protected function _get_total_order_fees( $order ) { $total = 0; $fees = $order->get_fees(); foreach ( $fees as $fee ) { $total = $total + $fee->get_amount(); } return $total; } /** * Get details from given order_id. * * This is the details when buyer is checking out from checkout page. * * @since 1.2.0 * * @param int $order_id Order ID * * @return array Order details */ protected function _get_details_from_order( $order_id ) { $order = wc_get_order( $order_id ); $settings = wc_gateway_ppec()->settings; $decimals = $settings->is_currency_supports_zero_decimal() ? 0 : 2; $rounded_total = $this->_get_rounded_total_in_order( $order ); $discounts = $order->get_total_discount(); $fees = round( $this->_get_total_order_fees( $order ), $decimals ); $details = array( 'total_item_amount' => round( $order->get_subtotal() - $discounts + $fees, $decimals ), 'order_tax' => round( $order->get_total_tax(), $decimals ), 'shipping' => round( ( version_compare( WC_VERSION, '3.0', '<' ) ? $order->get_total_shipping() : $order->get_shipping_total() ), $decimals ), 'items' => $this->_get_paypal_line_items_from_order( $order ), ); $details = $this->get_details( $details, $order->get_total_discount(), $rounded_total, $order->get_total() ); // PayPal shipping address from order. $shipping_address = new PayPal_Address(); $old_wc = version_compare( WC_VERSION, '3.0', '<' ); if ( ( $old_wc && ( $order->shipping_address_1 || $order->shipping_address_2 ) ) || ( ! $old_wc && $order->has_shipping_address() ) ) { // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.SpacingBefore $shipping_first_name = $old_wc ? $order->shipping_first_name : $order->get_shipping_first_name(); $shipping_last_name = $old_wc ? $order->shipping_last_name : $order->get_shipping_last_name(); $shipping_address_1 = $old_wc ? $order->shipping_address_1 : $order->get_shipping_address_1(); $shipping_address_2 = $old_wc ? $order->shipping_address_2 : $order->get_shipping_address_2(); $shipping_city = $old_wc ? $order->shipping_city : $order->get_shipping_city(); $shipping_state = $old_wc ? $order->shipping_state : $order->get_shipping_state(); $shipping_postcode = $old_wc ? $order->shipping_postcode : $order->get_shipping_postcode(); $shipping_country = $old_wc ? $order->shipping_country : $order->get_shipping_country(); // phpcs:enable } else { // Fallback to billing in case no shipping methods are set. The address returned from PayPal // will be stored in the order as billing. // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.SpacingBefore $shipping_first_name = $old_wc ? $order->billing_first_name : $order->get_billing_first_name(); $shipping_last_name = $old_wc ? $order->billing_last_name : $order->get_billing_last_name(); $shipping_address_1 = $old_wc ? $order->billing_address_1 : $order->get_billing_address_1(); $shipping_address_2 = $old_wc ? $order->billing_address_2 : $order->get_billing_address_2(); $shipping_city = $old_wc ? $order->billing_city : $order->get_billing_city(); $shipping_state = $old_wc ? $order->billing_state : $order->get_billing_state(); $shipping_postcode = $old_wc ? $order->billing_postcode : $order->get_billing_postcode(); $shipping_country = $old_wc ? $order->billing_country : $order->get_billing_country(); // phpcs:enable } $shipping_address->setName( $shipping_first_name . ' ' . $shipping_last_name ); $shipping_address->setStreet1( $shipping_address_1 ); $shipping_address->setStreet2( $shipping_address_2 ); $shipping_address->setCity( $shipping_city ); $shipping_address->setState( $shipping_state ); $shipping_address->setZip( $shipping_postcode ); // In case merchant only expects domestic shipping and hides shipping // country, fallback to base country. // // @see https://github.com/woothemes/woocommerce-gateway-paypal-express-checkout/issues/139 if ( empty( $shipping_country ) ) { $shipping_country = WC()->countries->get_base_country(); } $shipping_address->setCountry( $shipping_country ); $shipping_address->setPhoneNumber( $old_wc ? $order->billing_phone : $order->get_billing_phone() ); $details['shipping_address'] = $shipping_address; $details['email'] = $old_wc ? $order->billing_email : $order->get_billing_email(); return $details; } /** * Get PayPal shipping address from customer. * * @return array Address */ protected function _get_address_from_customer() { $customer = WC()->customer; $shipping_address = new PayPal_Address(); $old_wc = version_compare( WC_VERSION, '3.0', '<' ); if ( $customer->get_shipping_address() || $customer->get_shipping_address_2() ) { $shipping_first_name = $old_wc ? $customer->shipping_first_name : $customer->get_shipping_first_name(); $shipping_last_name = $old_wc ? $customer->shipping_last_name : $customer->get_shipping_last_name(); $shipping_address_1 = $customer->get_shipping_address(); $shipping_address_2 = $customer->get_shipping_address_2(); $shipping_city = $customer->get_shipping_city(); $shipping_state = $customer->get_shipping_state(); $shipping_postcode = $customer->get_shipping_postcode(); $shipping_country = $customer->get_shipping_country(); } else { // Fallback to billing in case no shipping methods are set. The address returned from PayPal // will be stored in the order as billing. // phpcs:disable WordPress.WhiteSpace.OperatorSpacing.SpacingBefore $shipping_first_name = $old_wc ? $customer->billing_first_name : $customer->get_billing_first_name(); $shipping_last_name = $old_wc ? $customer->billing_last_name : $customer->get_billing_last_name(); $shipping_address_1 = $old_wc ? $customer->get_address() : $customer->get_billing_address_1(); $shipping_address_2 = $old_wc ? $customer->get_address_2() : $customer->get_billing_address_2(); $shipping_city = $old_wc ? $customer->get_city() : $customer->get_billing_city(); $shipping_state = $old_wc ? $customer->get_state() : $customer->get_billing_state(); $shipping_postcode = $old_wc ? $customer->get_postcode() : $customer->get_billing_postcode(); $shipping_country = $old_wc ? $customer->get_country() : $customer->get_billing_country(); // phpcs:enable } $shipping_address->setName( $shipping_first_name . ' ' . $shipping_last_name ); $shipping_address->setStreet1( $shipping_address_1 ); $shipping_address->setStreet2( $shipping_address_2 ); $shipping_address->setCity( $shipping_city ); $shipping_address->setState( $shipping_state ); $shipping_address->setZip( $shipping_postcode ); $shipping_address->setCountry( $shipping_country ); $shipping_address->setPhoneNumber( $old_wc ? $customer->billing_phone : $customer->get_billing_phone() ); return $shipping_address; } /** * Get line items from given order. * * @since 1.2.0 * * @param int|WC_Order $order Order ID or order object * * @return array Line items */ protected function _get_paypal_line_items_from_order( $order ) { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); $order = wc_get_order( $order ); $items = array(); foreach ( $order->get_items( array( 'line_item', 'fee' ) ) as $cart_item_key => $order_item ) { if ( 'fee' === $order_item['type'] ) { $item = array( 'name' => $order_item['name'], 'quantity' => 1, 'amount' => round( $order_item['line_total'], $decimals ), ); } else { $amount = round( $order_item['line_subtotal'] / $order_item['qty'], $decimals ); $product = version_compare( WC_VERSION, '3.0', '<' ) ? $order->get_product_from_item( $order_item ) : $order_item->get_product(); $item = array( 'name' => $order_item['name'], 'quantity' => $order_item['qty'], 'amount' => $amount, 'sku' => $product->get_sku(), ); } $items[] = $item; } return $items; } /** * Get rounded total of a given order. * * @since 1.2.0 * * @param int|WC_Order Order ID or order object * * @return float */ protected function _get_rounded_total_in_order( $order ) { $settings = wc_gateway_ppec()->settings; $decimals = $settings->get_number_of_decimal_digits(); $order = wc_get_order( $order ); $rounded_total = 0; foreach ( $order->get_items( array( 'line_item', 'fee', 'coupon' ) ) as $cart_item_key => $values ) { if ( 'coupon' === $values['type'] ) { $amount = round( $values['line_total'], $decimals ); $rounded_total -= $amount; continue; } if ( 'fee' === $values['type'] ) { $amount = round( $values['line_total'], $decimals ); } else { $amount = round( $values['line_subtotal'] / $values['qty'], $decimals ); $amount = round( $amount * $values['qty'], $decimals ); } $rounded_total += $amount; } return $rounded_total; } /** * Get details from a given token. * * @see https://developer.paypal.com/docs/classic/api/merchant/GetExpressCheckoutDetails_API_Operation_NVP/ * * @param string $token Token from SetExpressCheckout response * @return array NVP response */ public function get_express_checkout_details( $token ) { $params = array( 'METHOD' => 'GetExpressCheckoutDetails', 'VERSION' => self::API_VERSION, 'TOKEN' => $token, ); return $this->_request( $params ); } /** * Completes an Express Checkout transaction. If you set up a billing agreement * in your 'SetExpressCheckout' API call, the billing agreement is created * when you call the DoExpressCheckoutPayment API operation. * * @see https://developer.paypal.com/docs/classic/api/merchant/DoExpressCheckoutPayment_API_Operation_NVP/ * * @param array $params NVP params * @return array NVP response */ public function do_express_checkout_payment( array $params ) { $params['METHOD'] = 'DoExpressCheckoutPayment'; $params['VERSION'] = self::API_VERSION; $params['BUTTONSOURCE'] = 'WooThemes_EC'; return $this->_request( $params ); } /** * Get params for DoExpressCheckoutPayment call. * * @since 1.2.0 * * @param array $args Args * * @return array Params for DoExpressCheckoutPayment call */ public function get_do_express_checkout_params( array $args ) { $settings = wc_gateway_ppec()->settings; $order = wc_get_order( $args['order_id'] ); $old_wc = version_compare( WC_VERSION, '3.0', '<' ); $order_id = $old_wc ? $order->id : $order->get_id(); $order_number = $order->get_order_number(); $details = $this->_get_details_from_order( $order_id ); $order_key = $old_wc ? $order->order_key : $order->get_order_key(); $params = array( 'TOKEN' => $args['token'], 'PAYERID' => $args['payer_id'], 'PAYMENTREQUEST_0_AMT' => $details['order_total'], 'PAYMENTREQUEST_0_ITEMAMT' => $details['total_item_amount'], 'PAYMENTREQUEST_0_SHIPPINGAMT' => $details['shipping'], 'PAYMENTREQUEST_0_TAXAMT' => $details['order_tax'], 'PAYMENTREQUEST_0_SHIPDISCAMT' => $details['ship_discount_amount'], 'PAYMENTREQUEST_0_INSURANCEAMT' => 0, 'PAYMENTREQUEST_0_HANDLINGAMT' => 0, 'PAYMENTREQUEST_0_CURRENCYCODE' => get_woocommerce_currency(), 'PAYMENTREQUEST_0_NOTIFYURL' => WC()->api_request_url( 'WC_Gateway_PPEC' ), 'PAYMENTREQUEST_0_PAYMENTACTION' => $settings->get_paymentaction(), 'PAYMENTREQUEST_0_INVNUM' => $settings->invoice_prefix . $order->get_order_number(), 'PAYMENTREQUEST_0_CUSTOM' => wp_json_encode( array( 'order_id' => $order_id, 'order_number' => $order_number, 'order_key' => $order_key, ) ), 'NOSHIPPING' => WC_Gateway_PPEC_Plugin::needs_shipping() ? 0 : 1, ); if ( WC_Gateway_PPEC_Plugin::needs_shipping() && ! empty( $details['shipping_address'] ) ) { $params = array_merge( $params, $details['shipping_address']->getAddressParams( 'PAYMENTREQUEST_0_SHIPTO' ) ); } if ( ! empty( $details['items'] ) ) { $count = 0; foreach ( $details['items'] as $line_item_key => $values ) { $line_item_params = array( 'L_PAYMENTREQUEST_0_NAME' . $count => $values['name'], 'L_PAYMENTREQUEST_0_DESC' . $count => ! empty( $values['description'] ) ? strip_tags( $values['description'] ) : '', // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags 'L_PAYMENTREQUEST_0_QTY' . $count => $values['quantity'], 'L_PAYMENTREQUEST_0_AMT' . $count => $values['amount'], ); if ( isset( $values['sku'] ) ) { $line_item_params[ 'L_PAYMENTREQUEST_0_NUMBER' . $count ] = $values['sku']; } $params = array_merge( $params, $line_item_params ); $count++; } } return $params; } /** * Creates a billing agreement with a PayPal account holder. * * Used for subscription products in the purchase. * * @see https://developer.paypal.com/docs/classic/api/merchant/CreateBillingAgreement_API_Operation_NVP/ * * @since 1.2.0 * * @param string $token Token from SetExpressCheckout response */ public function create_billing_agreement( $token ) { $params = array( 'METHOD' => 'CreateBillingAgreement', 'VERSION' => self::API_VERSION, 'TOKEN' => $token, ); return $this->_request( $params ); } /** * Processes a payment from a buyer's account, which is identified by a * previous transaction * * @see https://developer.paypal.com/docs/classic/api/merchant/DoReferenceTransaction_API_Operation_NVP/ * * @since 1.2.0 * * @param array $params NVP params * @return array NVP response */ public function do_reference_transaction( array $params ) { $params['METHOD'] = 'DoReferenceTransaction'; $params['VERSION'] = self::API_VERSION; $params['BUTTONSOURCE'] = 'WooThemes_EC'; return $this->_request( $params ); } /** * Get params for DoReferenceTransaction call. * * @since 1.2.0 * * @param array $args Args * * @return array Params for DoReferenceTransaction call */ public function get_do_reference_transaction_params( array $args ) { $settings = wc_gateway_ppec()->settings; $order = wc_get_order( $args['order_id'] ); $old_wc = version_compare( WC_VERSION, '3.0', '<' ); $order_id = $old_wc ? $order->id : $order->get_id(); $details = $this->_get_details_from_order( $order_id ); $order_key = $old_wc ? $order->order_key : $order->get_order_key(); $params = array( 'REFERENCEID' => $args['reference_id'], 'AMT' => $args['amount'], 'ITEMAMT' => $details['total_item_amount'], 'SHIPPINGAMT' => $details['shipping'], 'TAXAMT' => $details['order_tax'], 'SHIPDISCAMT' => $details['ship_discount_amount'], 'INSURANCEAMT' => 0, 'HANDLINGAMT' => 0, 'CURRENCYCODE' => $old_wc ? $order->order_currency : $order->get_currency(), 'NOTIFYURL' => WC()->api_request_url( 'WC_Gateway_PPEC' ), 'PAYMENTACTION' => $settings->get_paymentaction(), 'INVNUM' => $settings->invoice_prefix . $order->get_order_number(), 'CUSTOM' => wp_json_encode( array( 'order_id' => $order_id, 'order_key' => $order_key, ) ), ); // We want to add the shipping parameters only if we have all of the required // parameters for a DoReferenceTransaction call. Otherwise, we don't want to // include any of the shipping parameters, even if we have some of them. // The call will fail if not all of the required paramters are present. if ( ! empty( $details['shipping_address'] ) && $details['shipping_address']->has_all_required_shipping_params() ) { $params = array_merge( $params, $details['shipping_address']->getAddressParams( 'SHIPTO' ) ); $params['SHIPTOCOUNTRY'] = $params['SHIPTOCOUNTRYCODE']; unset( $params['SHIPTOCOUNTRYCODE'] ); } if ( ! empty( $details['items'] ) ) { $count = 0; foreach ( $details['items'] as $line_item_key => $values ) { $line_item_params = array( 'L_NAME' . $count => $values['name'], 'L_DESC' . $count => ! empty( $values['description'] ) ? strip_tags( $values['description'] ) : '', // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags 'L_QTY' . $count => $values['quantity'], 'L_AMT' . $count => $values['amount'], 'L_NUMBER' . $count => $values['sku'], ); $params = array_merge( $params, $line_item_params ); $count++; } } return $params; } public function do_express_checkout_capture( $params ) { $params['METHOD'] = 'DoCapture'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } public function do_express_checkout_void( $params ) { $params['METHOD'] = 'DoVoid'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } public function get_transaction_details( $params ) { $params['METHOD'] = 'GetTransactionDetails'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } /** * Obtain your Pal ID, which is the PayPal–assigned merchant account number, * and other informaton about your account. * * @see https://developer.paypal.com/docs/classic/api/merchant/GetPalDetails_API_Operation_NVP/ * * @return array NVP response */ public function get_pal_details() { $params['METHOD'] = 'GetPalDetails'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } /** * Issues a refund to the PayPal account holder associated with a transaction. * * @see https://developer.paypal.com/docs/classic/api/merchant/RefundTransaction_API_Operation_NVP/ * * @param array $params NVP params * @return array NVP response */ public function refund_transaction( $params ) { $params['METHOD'] = 'RefundTransaction'; $params['VERSION'] = self::API_VERSION; return $this->_request( $params ); } public function test_api_credentials( $credentials, $environment = 'sandbox' ) { $this->set_credential( $credentials ); $this->set_environment( $environment ); $result = $this->get_pal_details(); if ( 'Success' !== $result['ACK'] && 'SuccessWithWarning' !== $result['ACK'] ) { // Look at the result a little more closely to make sure it's a credentialing issue. $found_10002 = false; foreach ( $result as $index => $value ) { if ( preg_match( '/^L_ERRORCODE\d+$/', $index ) ) { if ( '10002' == $value ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $found_10002 = true; } } } if ( $found_10002 ) { return false; } else { // Call failed for some other reason. throw new PayPal_API_Exception( $result ); } } update_option( 'woocommerce_ppec_payer_id_' . $this->_environment . '_' . md5( $this->_credential->get_username() . ':' . $this->_credential->get_password() ), wc_clean( $result['PAL'] ) ); return $result['PAL']; } // Probe to see whether the merchant has the billing address feature enabled. We do this // by running a SetExpressCheckout call with REQBILLINGADDRESS set to 1; if the merchant has // this feature enabled, the call will complete successfully; if they do not, the call will // fail with error code 11601. public function test_for_billing_address_enabled( $credentials, $environment = 'sandbox' ) { $this->set_credential( $credentials ); $this->set_environment( $environment ); $req = array( 'RETURNURL' => home_url( '/' ), 'CANCELURL' => home_url( '/' ), 'REQBILLINGADDRESS' => '1', 'AMT' => '1.00', ); $result = $this->set_express_checkout( $req ); if ( 'Success' !== $result['ACK'] && 'SuccessWithWarning' !== $result['ACK'] ) { $found_11601 = false; foreach ( $result as $index => $value ) { if ( preg_match( '/^L_ERRORCODE\d+$/', $index ) ) { if ( '11601' == $value ) { // phpcs:ignore WordPress.PHP.StrictComparisons.LooseComparison $found_11601 = true; } } } if ( $found_11601 ) { return false; } else { throw new PayPal_API_Exception( $result ); } } return true; } /** * Checks whether response indicates a successful operation. * * @since 1.2.0 * * @param array $response NVP response * * @return bool Returns true if response indicates a successful operation */ public function response_has_success_status( $response ) { return ( isset( $response['ACK'] ) && in_array( $response['ACK'], array( 'Success', 'SuccessWithWarning' ), true ) ); } /** Deprecated Functions */ /** * Updates or deletes a billing agreement. * * @see https://developer.paypal.com/docs/classic/api/merchant/BAUpdate_API_Operation_NVP/ * * @since 1.2.0 * @deprecated 1.7.0 * * @param string $billing_agreement_id Billing agreement ID */ public function update_billing_agreement( $billing_agreement_id ) { _deprecated_function( __METHOD__, '1.7.0' ); $params = array( 'METHOD' => 'BillAgreementUpdate', 'VERSION' => self::API_VERSION, 'REFERENCEID' => $billing_agreement_id, ); } }