__( 'Cash App Pay (Square)', 'woocommerce-square' ), 'method_description' => __( 'Allow customers to securely pay with Cash App', 'woocommerce-square' ), 'payment_type' => self::PAYMENT_TYPE_CASH_APP_PAY, 'supports' => array( self::FEATURE_PRODUCTS, self::FEATURE_REFUNDS, self::FEATURE_AUTHORIZATION, self::FEATURE_CHARGE, self::FEATURE_CHARGE_VIRTUAL, self::FEATURE_CAPTURE, ), 'countries' => array( 'US' ), 'currencies' => array( 'USD' ), ) ); // Payment method image. $this->icon = $this->get_payment_method_image_url(); // Transaction URL format. $this->view_transaction_url = $this->get_transaction_url_format(); // Ajax hooks add_action( 'wc_ajax_square_cash_app_pay_get_payment_request', array( $this, 'ajax_get_payment_request' ) ); add_action( 'wc_ajax_square_cash_app_pay_set_continuation_session', array( $this, 'ajax_set_continuation_session' ) ); add_action( 'wc_ajax_square_cash_app_log_js_data', array( $this, 'log_js_data' ) ); // restore refunded Square inventory add_action( 'woocommerce_order_refunded', array( $this, 'restore_refunded_inventory' ), 10, 2 ); // Admin hooks. add_action( 'admin_notices', array( $this, 'add_admin_notices' ) ); } /** * Enqueue the necessary scripts & styles for the gateway. * * @since 4.5.0 */ public function enqueue_scripts() { if ( ! $this->is_configured() ) { return; } // Enqueue payment gateway assets. $this->enqueue_gateway_assets(); } /** * Payment form on checkout page. * * @since 4.5.0 */ public function payment_fields() { parent::payment_fields(); ?>
get_id_dasherized(); } /** * Enqueue the gateway-specific assets if present, including JS, CSS, and * localized script params * * @since 4.5.0 */ protected function enqueue_gateway_assets() { $is_checkout = is_checkout() || ( function_exists( 'has_block' ) && has_block( 'woocommerce/checkout' ) ); // bail if not a checkout page or cash app pay is not enabled if ( ! $is_checkout || ! $this->is_configured() ) { return; } if ( $this->get_plugin()->get_settings_handler()->is_sandbox() ) { $url = 'https://sandbox.web.squarecdn.com/v1/square.js'; } else { $url = 'https://web.squarecdn.com/v1/square.js'; } wp_enqueue_script( 'wc-' . $this->get_plugin()->get_id_dasherized() . '-payment-form', $url, array(), Plugin::VERSION, true ); parent::enqueue_gateway_assets(); // Render Payment JS $this->render_js(); } /** * Validates the entered payment fields. * * @since 4.5.0 * * @return bool */ public function validate_fields() { $is_valid = true; try { if ( ! Square_Helper::get_post( 'wc-' . $this->get_id_dasherized() . '-payment-nonce' ) ) { throw new \Exception( 'Payment nonce is missing.' ); } } catch ( \Exception $exception ) { $is_valid = false; Square_Helper::wc_add_notice( __( 'An error occurred, please try again or try an alternate form of payment.', 'woocommerce-square' ), 'error' ); $this->add_debug_message( $exception->getMessage(), 'error' ); } return $is_valid; } /** Admin methods *************************************************************************************************/ /** * Adds admin notices. * * @since 4.5.0 */ public function add_admin_notices() { $base_location = wc_get_base_location(); $is_plugin_settings = $this->get_plugin()->is_payment_gateway_configuration_page( $this->get_id() ); $is_connected = $this->get_plugin()->get_settings_handler()->is_connected() && $this->get_plugin()->get_settings_handler()->get_location_id(); $is_enabled = $this->is_enabled() && $is_connected; // Add a notice for cash app pay if the base location is not the US. if ( ( $is_enabled || $is_plugin_settings ) && isset( $base_location['country'] ) && 'US' !== $base_location['country'] ) { $this->get_plugin()->get_admin_notice_handler()->add_admin_notice( sprintf( /* translators: Placeholders: %1$s - tag, %2$s - tag, %3$s - 2-character country code, %4$s - comma separated list of 2-character country codes */ __( '%1$sCash App Pay (Square):%2$s Your base country is %3$s, but Cash App Pay can’t accept transactions from merchants outside of US.', 'woocommerce-square' ), '', '', esc_html( $base_location['country'] ) ), 'wc-square-cash-app-pay-base-location', array( 'notice_class' => 'notice-error', ) ); } // Add a notice to enable cash app pay and start accept payments using cash app pay. if ( $is_connected && ! $this->is_enabled() && ! $is_plugin_settings && isset( $base_location['country'] ) && 'US' === $base_location['country'] ) { $this->get_plugin()->get_admin_notice_handler()->add_admin_notice( sprintf( /* translators: Placeholders: %1$s - tag, %2$s - tag */ __( 'You are ready to accept payments using Cash App Pay (Square)! %1$sEnable it%2$s now to start accepting payments.', 'woocommerce-square' ), '', '' ), 'wc-square-enable-cash-app-pay', array( 'always_show_on_settings' => false, ) ); } } /** * Get the default payment method title, which is configurable within the * admin and displayed on checkout * * @since 4.5.0 * @return string payment method title to show on checkout */ protected function get_default_title() { return esc_html__( 'Cash App Pay', 'woocommerce-square' ); } /** * Get the default payment method description, which is configurable * within the admin and displayed on checkout * * @since 4.5.0 * @return string payment method description to show on checkout */ protected function get_default_description() { return esc_html__( 'Pay securely using Cash App Pay.', 'woocommerce-square' ); } /** * Get transaction URL format. * * @since 4.5.0 * * @return string URL format */ public function get_transaction_url_format() { return $this->get_plugin()->get_settings_handler()->is_sandbox() ? 'https://squareupsandbox.com/dashboard/sales/transactions/%s' : 'https://squareup.com/dashboard/sales/transactions/%s'; } /** * Initialize payment gateway settings fields * * @since 4.5.0 * @see WC_Settings_API::init_form_fields() */ public function init_form_fields() { $this->form_fields = array(); } /** Conditional methods *******************************************************************************************/ /** * Determines if the gateway is available. * * @since 4.5.0 * * @return bool */ public function is_available() { return parent::is_available() && $this->get_plugin()->get_settings_handler()->is_connected() && $this->get_plugin()->get_settings_handler()->get_location_id(); } /** * Returns true if the gateway is properly configured to perform transactions * * @since 4.5.0 * @return boolean true if the gateway is properly configured */ public function is_configured() { // Only available in the US and USD currency. $base_location = wc_get_base_location(); $us_only = isset( $base_location['country'] ) && 'US' === $base_location['country']; return $this->is_enabled() && $us_only && $this->get_plugin()->get_settings_handler()->is_connected() && $this->get_plugin()->get_settings_handler()->get_location_id(); } /** Getter methods ************************************************************************************************/ /** * Gets the API instance. * * @since 4.5.0 * * @return Gateway\API */ public function get_api() { if ( ! $this->api ) { $settings = $this->get_plugin()->get_settings_handler(); $this->api = new Gateway\API( $settings->get_access_token(), $settings->get_location_id(), $settings->is_sandbox() ); $this->api->set_api_id( $this->get_id() ); } return $this->api; } /** * Gets the gateway settings fields. * * @since 4.5.0 * * @return array */ protected function get_method_form_fields() { return array(); } /** * Initialize payment tokens handler. * * @since 4.5.1 */ protected function init_payment_tokens_handler() { // No payment tokens for Cash App Pay, do nothing. } /** * Gets a user's stored customer ID. * * Overridden to avoid auto-creating customer IDs, as Square generates them. * * @since 4.5.0 * * @param int $user_id user ID * @param array $args arguments * @return string */ public function get_customer_id( $user_id, $args = array() ) { // Square generates customer IDs $args['autocreate'] = false; return parent::get_customer_id( $user_id, $args ); } /** * Gets a guest's customer ID. * * @since 4.5.0 * * @param \WC_Order $order order object * @return string|bool */ public function get_guest_customer_id( \WC_Order $order ) { // is there a customer id already tied to this order? $customer_id = $this->get_order_meta( $order, 'customer_id' ); if ( $customer_id ) { return $customer_id; } return false; } /** * Gets the order object with payment information added. * * @since 4.5.0 * * @param int|\WC_Order $order_id order ID or object * @return \WC_Order */ public function get_order( $order_id ) { $order = parent::get_order( $order_id ); $order->payment->nonce = new \stdClass(); if ( $this->is_gift_card_applied() ) { $order->payment->nonce->gift_card = Square_Helper::get_post( 'square-gift-card-payment-nonce' ); } $order->payment->nonce->cash_app_pay = Square_Helper::get_post( 'wc-' . $this->get_id_dasherized() . '-payment-nonce' ); $order->square_customer_id = $order->customer_id; $order->square_order_id = $this->get_order_meta( $order, 'square_order_id' ); $order->square_version = $this->get_order_meta( $order, 'square_version' ); // look up in the index for guest customers if ( ! $order->get_user_id() ) { $indexed_customers = Customer_Helper::get_customers_by_email( $order->get_billing_email() ); // only use an indexed customer ID if there was a single one returned, otherwise we can't know which to use if ( ! empty( $indexed_customers ) && count( $indexed_customers ) === 1 ) { $order->square_customer_id = $order->customer_id = $indexed_customers[0]; } } // if no previous customer could be found, always create a new customer if ( empty( $order->square_customer_id ) ) { try { $response = $this->get_api()->create_customer( $order ); $order->square_customer_id = $order->customer_id = $response->get_customer_id(); // set $customer_id since we know this customer can be associated with this user // store the guests customers in our index to avoid future duplicates if ( ! $order->get_user_id() ) { Customer_Helper::add_customer( $order->square_customer_id, $order->get_billing_email() ); } } catch ( \Exception $exception ) { // log the error, but continue with payment if ( $this->debug_log() ) { $this->get_plugin()->log( $exception->getMessage(), $this->get_id() ); } } } return $order; } /** * Gets an order with capture data attached. * * @since 4.6.0 * * @param int|\WC_Order $order order object * @param null|float $amount amount to capture * @return \WC_Order */ public function get_order_for_capture( $order, $amount = null ) { $order = parent::get_order_for_capture( $order, $amount ); $order->capture->location_id = $this->get_order_meta( $order, 'square_location_id' ); $order->square_version = $this->get_order_meta( $order, 'square_version' ); return $order; } /** * Gets an order with refund data attached. * * @since 4.5.0 * * @param int|\WC_Order $order order object * @param float $amount amount to refund * @param string $reason response for the refund * * @return \WC_Order|\WP_Error */ protected function get_order_for_refund( $order, $amount, $reason ) { $order = parent::get_order_for_refund( $order, $amount, $reason ); $order->square_version = $this->get_order_meta( $order, 'square_version' ); $transaction_date = $this->get_order_meta( $order, 'trans_date' ); if ( $transaction_date ) { // refunds with the Refunds API can be made up to 1 year after payment and up to 120 days with the Transactions API $max_refund_time = version_compare( $order->square_version, '2.2', '>=' ) ? '+1 year' : '+120 days'; // throw an error if the payment cannot be refunded if ( time() >= strtotime( $max_refund_time, strtotime( $transaction_date ) ) ) { /* translators: %s maximum refund date. */ return new \WP_Error( 'wc_square_refund_age_exceeded', sprintf( __( 'Refunds must be made within %s of the original payment date.', 'woocommerce-square' ), '+1 year' === $max_refund_time ? 'a year' : '120 days' ) ); } } $order->refund->location_id = $this->get_order_meta( $order, 'square_location_id' ); $order->refund->tender_id = $this->get_order_meta( $order, 'authorization_code' ); if ( ! $order->refund->tender_id ) { try { $response = version_compare( $order->square_version, '2.2', '>=' ) ? $this->get_api()->get_payment( $order->refund->trans_id ) : $this->get_api()->get_transaction( $order->refund->trans_id, $order->refund->location_id ); if ( ! $response->get_authorization_code() ) { throw new \Exception( 'Tender missing' ); } $this->update_order_meta( $order, 'authorization_code', $response->get_authorization_code() ); $this->update_order_meta( $order, 'square_location_id', $response->get_location_id() ); $order->refund->location_id = $response->get_location_id(); $order->refund->tender_id = $response->get_authorization_code(); } catch ( \Exception $exception ) { return new \WP_Error( 'wc_square_refund_tender_missing', __( 'Could not find original transaction tender. Please refund this transaction from your Square dashboard.', 'woocommerce-square' ) ); } } return $order; } /** * Gets the configured environment ID. * * @since 4.5.0 * * @return string */ public function get_environment() { return self::ENVIRONMENT_PRODUCTION; } /** * Gets the configured application ID. * * @since 4.5.0 * * @return string */ public function get_application_id() { $square_application_id = 'sq0idp-wGVapF8sNt9PLrdj5znuKA'; if ( $this->get_plugin()->get_settings_handler()->is_sandbox() ) { $square_application_id = $this->get_plugin()->get_settings_handler()->get_option( 'sandbox_application_id' ); } /** * Filters the configured application ID. * * @since 4.5.0 * * @param string $application_id application ID */ return apply_filters( 'wc_square_application_id', $square_application_id ); } /** * Returns the $order object with a unique transaction ref member added * * @since 4.5.0 * @param WC_Order_Square $order the order object * @return WC_Order_Square order object with member named unique_transaction_ref */ protected function get_order_with_unique_transaction_ref( $order ) { $order_id = $order->get_id(); // generate a unique retry count if ( is_numeric( $this->get_order_meta( $order_id, 'retry_count' ) ) ) { $retry_count = $this->get_order_meta( $order_id, 'retry_count' ); ++$retry_count; } else { $retry_count = 0; } // keep track of the retry count $this->update_order_meta( $order, 'retry_count', $retry_count ); $order->unique_transaction_ref = time() . '-' . $order_id . ( $retry_count >= 0 ? '-' . $retry_count : '' ); return $order; } /** * Returns the payment method image URL. * * @since 4.5.0 * @param string $type the payment method type or name * @return string the image URL or null */ public function get_payment_method_image_url( $type = '' ) { /** * Payment Gateway Fallback to PNG Filter. * * Allow actors to enable the use of PNGs over SVGs for payment icon images. * * @since 4.5.0 * @param bool $use_svg true by default, false to use PNGs */ $image_extension = apply_filters( 'wc_payment_gateway_' . $this->get_id() . '_use_svg', true ) ? '.svg' : '.png'; // first, is the image available within the plugin? if ( is_readable( $this->get_plugin()->get_plugin_path() . '/build/images/cash-app' . $image_extension ) ) { return \WC_HTTPS::force_https_url( $this->get_plugin()->get_plugin_url() . '/build/images/cash-app' . $image_extension ); } // Fall back to framework image URL. return parent::get_payment_method_image_url( $type ); } /** * Get the Cash App Pay button styles. * * @return array Button styles. */ public function get_button_styles() { $button_styles = array( 'theme' => $this->settings['button_theme'] ?? 'dark', 'shape' => $this->settings['button_shape'] ?? 'semiround', 'size' => 'medium', 'width' => 'full', ); /** * Filters the Cash App Pay button styles. * * @since 4.5.0 * @param array $button_styles Button styles. * @return array Button styles. */ return apply_filters( 'wc_' . $this->get_id() . '_button_styles', $button_styles ); } /** * Mark an order as refunded. This should only be used when the full order * amount has been refunded. * * @since 4.5.0 * * @param \WC_Order $order order object */ public function mark_order_as_refunded( $order ) { /* translators: Placeholders: %s - payment gateway title (such as Authorize.net, Braintree, etc) */ $order_note = sprintf( esc_html__( '%s Order completely refunded.', 'woocommerce-square' ), $this->get_method_title() ); // Add order note and continue with WC refund process. $order->add_order_note( $order_note ); } /** * Build the payment request object for the cash app pay payment form. * * Payment request objects are used by the Payments and need to be in a specific format. * Reference: https://developer.squareup.com/docs/api/paymentform#paymentform-paymentrequestobjects * * @since 4.5.0 * @return array */ public function get_payment_request() { // Ignoring nonce verification checks as it is already handled in the parent function. $payment_request = array(); $is_pay_for_order_page = isset( $_POST['is_pay_for_order_page'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['is_pay_for_order_page'] ) ) : is_wc_endpoint_url( 'order-pay' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing $order_id = isset( $_POST['order_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : absint( get_query_var( 'order-pay' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( is_wc_endpoint_url( 'order-pay' ) || $is_pay_for_order_page ) { $order = wc_get_order( $order_id ); $payment_request = $this->build_payment_request( $order->get_total(), array( 'order_id' => $order_id, 'is_pay_for_order_page' => $is_pay_for_order_page, ) ); } elseif ( isset( WC()->cart ) ) { WC()->cart->calculate_totals(); $amount = WC()->cart->total; // Check if a gift card is applied. $check_for_giftcard = isset( $_POST['check_for_giftcard'] ) ? 'true' === sanitize_text_field( wp_unslash( $_POST['check_for_giftcard'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing $gift_card_applied = false; if ( $check_for_giftcard ) { $partial_amount = $this->get_partial_cash_app_amount(); if ( $partial_amount < $amount ) { $amount = $partial_amount; $gift_card_applied = true; } } $payment_request = $this->build_payment_request( $amount, array(), $gift_card_applied ); } return $payment_request; } /** * Get the partial amount to be paid by Cash App Pay. * This is the amount after deducting the gift card balance. * * @since 4.6.0 * @return float Partial amount to be paid by Cash App Pay. */ public function get_partial_cash_app_amount() { $amount = WC()->cart->total; $payment_token = WC()->session->woocommerce_square_gift_card_payment_token; if ( ! Gift_Card::does_checkout_support_gift_card() || ! $payment_token ) { return $amount; } $is_sandbox = wc_square()->get_settings_handler()->is_sandbox(); if ( $is_sandbox ) { // The card allowed for testing with the Sandbox account has fund of $1. $balance = 1; $amount = $amount - $balance; } else { $api_response = $this->get_api()->retrieve_gift_card( $payment_token ); $gift_card_data = $api_response->get_data(); if ( $gift_card_data instanceof \Square\Models\RetrieveGiftCardFromNonceResponse ) { $gift_card = $gift_card_data->getGiftCard(); $balance_money = $gift_card->getBalanceMoney(); $balance = (float) Square_Helper::number_format( Money_Utility::cents_to_float( $balance_money->getAmount() ) ); $amount = $amount - $balance; } } return $amount; } /** * Build a payment request object to be sent to Payments. * * Documentation: https://developer.squareup.com/docs/api/paymentform#paymentform-paymentrequestobjects * * @since 4.5.0 * @param string $amount - format '100.00' * @param array $data * @return array */ public function build_payment_request( $amount, $data = array(), $gift_card_applied = false ) { $is_pay_for_order_page = isset( $data['is_pay_for_order_page'] ) ? $data['is_pay_for_order_page'] : false; $order_id = isset( $data['order_id'] ) ? $data['order_id'] : 0; $order_data = array(); $data = wp_parse_args( $data, array( 'countryCode' => substr( get_option( 'woocommerce_default_country' ), 0, 2 ), 'currencyCode' => get_woocommerce_currency(), ) ); if ( $is_pay_for_order_page ) { $order = wc_get_order( $order_id ); $order_data = array( 'subtotal' => $order->get_subtotal(), 'discount' => $order->get_discount_total(), 'shipping' => $order->get_shipping_total(), 'fees' => $order->get_total_fees(), 'taxes' => $order->get_total_tax(), ); // Set currency of order if order-pay page. if ( $order && $order->get_currency() ) { $data['currencyCode'] = $order->get_currency(); } unset( $data['is_pay_for_order_page'], $data['order_id'] ); } if ( ! isset( $data['lineItems'] ) && ! $gift_card_applied ) { $data['lineItems'] = $this->build_payment_request_line_items( $order_data ); } /** * Filters the payment request Total Label Suffix. * * @since 4.5.0 * @param string $total_label_suffix * @return string */ $total_label_suffix = apply_filters( 'woocommerce_square_payment_request_total_label_suffix', __( 'via WooCommerce', 'woocommerce-square' ) ); $total_label_suffix = $total_label_suffix ? " ($total_label_suffix)" : ''; $data['total'] = array( 'label' => get_bloginfo( 'name', 'display' ) . esc_html( $total_label_suffix ), 'amount' => number_format( $amount, 2, '.', '' ), 'pending' => false, ); return $data; } /** * Builds an array of line items/totals to be sent back to Square in the lineItems array. * * @since 4.5.0 * @param array $totals * @return array */ public function build_payment_request_line_items( $totals = array() ) { // Ignoring nonce verification checks as it is already handled in the parent function. $totals = empty( $totals ) ? $this->get_cart_totals() : $totals; $line_items = array(); $order_id = isset( $_POST['order_id'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : absint( get_query_var( 'order-pay' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing if ( $order_id ) { $order = wc_get_order( $order_id ); $iterable = $order->get_items(); } else { $iterable = WC()->cart->get_cart(); } foreach ( $iterable as $item ) { $amount = number_format( $order_id ? $order->get_subtotal() : $item['line_subtotal'], 2, '.', '' ); if ( $order_id ) { $quantity_label = 1 < $item->get_quantity() ? ' x ' . $item->get_quantity() : ''; } else { $quantity_label = 1 < $item['quantity'] ? ' x ' . $item['quantity'] : ''; } $item = array( 'label' => $order_id ? $item->get_name() . $quantity_label : $item['data']->get_name() . $quantity_label, 'amount' => $amount, 'pending' => false, ); $line_items[] = $item; } if ( $totals['shipping'] > 0 ) { $line_items[] = array( 'label' => __( 'Shipping', 'woocommerce-square' ), 'amount' => number_format( $totals['shipping'], 2, '.', '' ), 'pending' => false, ); } if ( $totals['taxes'] > 0 ) { $line_items[] = array( 'label' => __( 'Tax', 'woocommerce-square' ), 'amount' => number_format( $totals['taxes'], 2, '.', '' ), 'pending' => false, ); } if ( $totals['discount'] > 0 ) { $line_items[] = array( 'label' => __( 'Discount', 'woocommerce-square' ), 'amount' => number_format( $totals['discount'], 2, '.', '' ), 'pending' => false, ); } if ( $totals['fees'] > 0 ) { $line_items[] = array( 'label' => __( 'Fees', 'woocommerce-square' ), 'amount' => number_format( $totals['fees'], 2, '.', '' ), 'pending' => false, ); } return $line_items; } /** * Get the payment request object in an ajax request * * @since 4.5.0 * @return void */ public function ajax_get_payment_request() { check_ajax_referer( 'wc-cash-app-get-payment-request', 'security' ); $payment_request = array(); try { $payment_request = $this->get_payment_request(); if ( empty( $payment_request ) ) { /* translators: Context (product, cart, checkout or page) */ throw new \Exception( esc_html__( 'Empty payment request data for page.', 'woocommerce-square' ) ); } } catch ( \Exception $e ) { wp_send_json_error( $e->getMessage() ); } wp_send_json_success( wp_json_encode( $payment_request ) ); } /** * Set continuation session to select the cash app payment method after the redirect back from the cash app * * @since 4.5.0 * @return void */ public function ajax_set_continuation_session() { check_ajax_referer( 'wc-cash-app-set-continuation-session', 'security' ); $clear_session = ( isset( $_POST['clear'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['clear'] ) ) ); try { if ( $clear_session ) { WC()->session->set( 'wc_square_cash_app_pay_continuation', null ); } else { WC()->session->set( 'wc_square_cash_app_pay_continuation', 'yes' ); } } catch ( \Exception $e ) { wp_send_json_error( $e->getMessage() ); } wp_send_json_success( wp_json_encode( array( 'success' => true ) ) ); } /** * Determines if the current request is a continuation of a cash app pay payment. * * @return boolean */ public function is_cash_app_pay_continuation() { return WC()->session && 'yes' === WC()->session->get( 'wc_square_cash_app_pay_continuation' ); } /** * Returns cart totals in an array format * * @since 4.5.0 * @throws \Exception if no cart is found * @return array */ public function get_cart_totals() { if ( ! isset( WC()->cart ) ) { throw new \Exception( 'Cart data cannot be found.' ); } return array( 'subtotal' => WC()->cart->subtotal_ex_tax, 'discount' => WC()->cart->get_cart_discount_total(), 'shipping' => WC()->cart->shipping_total, 'fees' => WC()->cart->fee_total, 'taxes' => WC()->cart->tax_total + WC()->cart->shipping_tax_total, ); } /** * Handles payment processing. * * @see WC_Payment_Gateway::process_payment() * * @since 4.5.0 * * @param int|string $order_id * @return array associative array with members 'result' and 'redirect' */ public function process_payment( $order_id ) { $default = parent::process_payment( $order_id ); /** * Direct Gateway Process Payment Filter. * * Allow actors to intercept and implement the process_payment() call for * this transaction. Return an array value from this filter will return it * directly to the checkout processing code and skip this method entirely. * * @since 4.5.0 * @param bool $result default true * @param int|string $order_id order ID for the payment * @param Cash_App_Pay_Gateway $this instance */ $result = apply_filters( 'wc_payment_gateway_' . $this->get_id() . '_process_payment', true, $order_id, $this ); if ( is_array( $result ) ) { return $result; } // add payment information to order $order = $this->get_order( $order_id ); try { // Charge the order. $transcation_result = $this->do_transaction( $order ); if ( $transcation_result ) { /** * Filters the order status that's considered to be "held". * * @since 4.5.0 * * @param string $status held order status * @param \WC_Order $order order object * @param \WooCommerce\Square\Gateway\API\Response|null $response API response object, if any */ $held_order_status = apply_filters( 'wc_' . $this->get_id() . '_held_order_status', 'on-hold', $order, null ); if ( $order->has_status( $held_order_status ) ) { /** * Although `wc_reduce_stock_levels` accepts $order, it's necessary to pass * the order ID instead as `wc_reduce_stock_levels` reloads the order from the DB. * * Refer to the following PR link for more details: * @see https://github.com/woocommerce/woocommerce-square/pull/728 */ wc_reduce_stock_levels( $order->get_id() ); // reduce stock for held orders, but don't complete payment } else { $order->payment_complete(); // mark order as having received payment } // process_payment() can sometimes be called in an admin-context if ( isset( WC()->cart ) ) { WC()->cart->empty_cart(); } /** * Payment Gateway Payment Processed Action. * * Fired when a payment is processed for an order. * * @since 4.5.0 * @param \WC_Order $order order object * @param Payment_Gateway $this instance */ do_action( 'wc_payment_gateway_' . $this->get_id() . '_payment_processed', $order, $this ); // To create/activate/load a gift card, a payment must be in COMPLETE state. if ( $this->perform_charge( $order ) ) { $gift_card_purchase_type = Order::get_gift_card_purchase_type( $order ); if ( 'new' === $gift_card_purchase_type ) { $this->create_gift_card( $order ); } elseif ( 'load' === $gift_card_purchase_type ) { $gan = Order::get_gift_card_gan( $order ); $this->load_gift_card( $gan, $order ); } } return array( 'result' => 'success', 'redirect' => $this->get_return_url( $order ), ); } } catch ( \Exception $e ) { $this->mark_order_as_failed( $order, $e->getMessage() ); return array( 'result' => 'failure', 'message' => $e->getMessage(), ); } return $default; } /** * Do the transaction. * * @since 4.5.0 * * @param WC_Order_Square $order * @return bool * @throws \Exception */ protected function do_transaction( $order ) { // if there is no associated Square order ID, create one if ( empty( $order->square_order_id ) ) { try { $location_id = $this->get_plugin()->get_settings_handler()->get_location_id(); $response = $this->get_api()->create_order( $location_id, $order ); $this->maybe_save_gift_card_order_details( $response, $order ); $order->square_order_id = $response->getId(); // adjust order by difference between WooCommerce and Square order totals $wc_total = Money_Utility::amount_to_cents( $order->get_total() ); $square_total = $response->getTotalMoney()->getAmount(); $delta_total = $wc_total - $square_total; if ( abs( $delta_total ) > 0 ) { $response = $this->get_api()->adjust_order( $location_id, $order, $response->getVersion(), $delta_total ); // since a downward adjustment causes (downward) tax recomputation, perform an additional (untaxed) upward adjustment if necessary $square_total = $response->getTotalMoney()->getAmount(); $delta_total = $wc_total - $square_total; if ( $delta_total > 0 ) { $response = $this->get_api()->adjust_order( $location_id, $order, $response->getVersion(), $delta_total ); } } // reset the payment total to the total calculated by Square to prevent errors $order->payment_total = Square_Helper::number_format( Money_Utility::cents_to_float( $response->getTotalMoney()->getAmount() ) ); } catch ( \Exception $exception ) { // log the error, but continue with payment if ( $this->debug_log() ) { $this->get_plugin()->log( $exception->getMessage(), $this->get_id() ); } } } return parent::do_transaction( $order ); } /** * Performs a credit card transaction for the given order and returns the result. * * @since 4.6.0 * * @param WC_Order_Square $order the order object * @param Create_Payment|null $response optional credit card transaction response * @return Create_Payment the response * @throws \Exception network timeouts, etc */ protected function do_payment_method_transaction( $order, $response = null ) { // Generate a new transaction ref if the order payment is split using multiple payment methods. if ( isset( $order->payment->partial_total ) ) { $order->unique_transaction_ref = $this->get_order_with_unique_transaction_ref( $order ); } // Charge/Authorize the order. if ( $this->perform_charge( $order ) && self::CHARGE_TYPE_PARTIAL !== $this->get_charge_type() ) { $response = $this->get_api()->cash_app_pay_charge( $order ); } else { $response = $this->get_api()->cash_app_pay_authorization( $order ); } // success! update order record if ( $response->transaction_approved() ) { $payment_response = $response->get_data(); $payment = $payment_response->getPayment(); // credit card order note $message = sprintf( /* translators: Placeholders: %1$s - payment method title, %2$s - environment ("Test"), %3$s - transaction type (authorization/charge), %4$s - card type (mastercard, visa, ...), %5$s - last four digits of the card */ esc_html__( '%1$s %2$s %3$s Approved for an amount of %4$s', 'woocommerce-square' ), $this->get_method_title(), wc_square()->get_settings_handler()->is_sandbox() ? esc_html_x( 'Test', 'noun, software environment', 'woocommerce-square' ) : '', 'APPROVED' === $response->get_payment()->getStatus() ? esc_html_x( 'Authorization', 'Cash App transaction type', 'woocommerce-square' ) : esc_html_x( 'Charge', 'noun, Cash App transaction type', 'woocommerce-square' ), wc_price( Money_Utility::cents_to_float( $payment->getTotalMoney()->getAmount(), $order->get_currency() ) ) ); // adds the transaction id (if any) to the order note if ( $response->get_transaction_id() ) { /* translators: Placeholders: %s - transaction ID */ $message .= ' ' . sprintf( esc_html__( '(Transaction ID %s)', 'woocommerce-square' ), $response->get_transaction_id() ); } /** * Direct Gateway Credit Card Transaction Approved Order Note Filter. * * Allow actors to modify the order note added when a Credit Card transaction * is approved. * * @since 4.5.0 * * @param string $message order note * @param \WC_Order $order order object * @param \WooCommerce\Square\Gateway\API\Response $response transaction response * @param Cash_App_Pay_Gateway $this instance */ $message = apply_filters( 'wc_payment_gateway_' . $this->get_id() . '_transaction_approved_order_note', $message, $order, $response, $this ); $this->update_order_meta( $order, 'is_tender_type_cash_app_wallet', true ); $order->add_order_note( $message ); } return $response; } /** * Adds transaction data to the order. * * @since 4.5.0 * * @param \WC_Order $order order object * @param \WooCommerce\Square\Gateway\API\Responses\Create_Payment $response API response object */ public function add_payment_gateway_transaction_data( $order, $response ) { $location_id = $response->get_location_id() ? $response->get_location_id() : $this->get_plugin()->get_settings_handler()->get_location_id(); if ( $location_id ) { $this->update_order_meta( $order, 'square_location_id', $location_id ); } if ( $response->get_square_order_id() ) { $this->update_order_meta( $order, 'square_order_id', $response->get_square_order_id() ); } // store the plugin version on the order $this->update_order_meta( $order, 'square_version', Plugin::VERSION ); } /** * Renders the payment form JS. * * @since 4.5.0 */ public function render_js() { try { $payment_request = $this->get_payment_request(); } catch ( \Exception $e ) { $this->get_plugin()->log( 'Error: ' . $e->getMessage() ); } $args = array( 'application_id' => $this->get_application_id(), 'ajax_log_nonce' => wp_create_nonce( 'wc_' . $this->get_id() . '_log_js_data' ), 'location_id' => wc_square()->get_settings_handler()->get_location_id(), 'gateway_id' => $this->get_id(), 'gateway_id_dasherized' => $this->get_id_dasherized(), 'payment_request' => $payment_request, 'general_error' => __( 'An error occurred, please try again or try an alternate form of payment.', 'woocommerce-square' ), 'ajax_url' => \WC_AJAX::get_endpoint( '%%endpoint%%' ), 'payment_request_nonce' => wp_create_nonce( 'wc-cash-app-get-payment-request' ), 'checkout_logging' => $this->debug_checkout(), 'logging_enabled' => $this->debug_log(), 'is_pay_for_order_page' => is_checkout() && is_wc_endpoint_url( 'order-pay' ), 'order_id' => absint( get_query_var( 'order-pay' ) ), 'button_styles' => $this->get_button_styles(), 'reference_id' => WC()->cart ? WC()->cart->get_cart_hash() : '', ); /** * Payment Gateway Payment JS Arguments Filter. * * Filter the arguments passed to the Payment handler JS class * * @since 4.5.0 * * @param array $args arguments passed to the Payment Gateway handler JS class * @param Payment_Gateway $this payment gateway instance */ $args = apply_filters( 'wc_' . $this->get_id() . '_payment_js_args', $args, $this ); wc_enqueue_js( sprintf( 'window.wc_%s_payment_handler = new WC_Square_Cash_App_Pay_Handler( %s );', esc_js( $this->get_id() ), wp_json_encode( $args ) ) ); } /** * Logs any data sent by the payment form JS via AJAX. * * @since 4.5.0 */ public function log_js_data() { check_ajax_referer( 'wc_' . $this->get_id() . '_log_js_data', 'security' ); $message = sprintf( "wc-square-cash-app-pay.js %1\$s:\n ", ! empty( $_REQUEST['type'] ) ? ucfirst( wc_clean( wp_unslash( $_REQUEST['type'] ) ) ) : 'Request' ); // add the data if ( ! empty( $_REQUEST['data'] ) ) { // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r $message .= print_r( wc_clean( wp_unslash( $_REQUEST['data'] ) ), true ); } $this->get_plugin()->log( $message, $this->get_id() ); } }