laipower/wp-content/plugins/easy-digital-downloads/includes/gateways/stripe/includes/payment-actions.php

1466 lines
45 KiB
PHP

<?php
/**
* Payment actions.
*
* @package EDD_Stripe
* @since 2.7.0
*/
/**
* Starts the process of completing a purchase with Stripe.
*
* Generates an intent that can require user authorization before proceeding.
*
* @link https://stripe.com/docs/payments/intents
* @since 2.7.0
*
* @param array $purchase_data {
* Purchase form data.
*
* }
*/
function edds_process_purchase_form( $purchase_data ) {
// Catch a straight to gateway request.
// Remove the error set by the "gateway mismatch" and allow the redirect.
if ( isset( $_REQUEST['edd_action'] ) && 'straight_to_gateway' === $_REQUEST['edd_action'] ) {
foreach ( $purchase_data['downloads'] as $download ) {
$options = isset( $download['options'] ) ? $download['options'] : array();
$options['quantity'] = isset( $download['quantity'] ) ? $download['quantity'] : 1;
edd_add_to_cart( $download['id'], $options );
}
edd_unset_error( 'edd-straight-to-gateway-error' );
edd_send_back_to_checkout();
return;
}
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'We are unable to process your payment at this time, please try again later or contact support.',
'easy-digital-downloads'
)
);
}
/**
* Allows processing before an Intent is created.
*
* @since 2.7.0
*
* @param array $purchase_data Purchase data.
*/
do_action( 'edds_pre_process_purchase_form', $purchase_data );
$payment_method_id = isset( $_POST['payment_method_id'] ) ? sanitize_text_field( $_POST['payment_method_id'] ) : false;
$payment_method_exists = isset( $_POST['payment_method_exists'] ) ? 'true' == $_POST['payment_method_exists'] : false;
if ( ! $payment_method_id ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'Unable to locate payment method. Please try again with a new payment method.',
'easy-digital-downloads'
)
);
}
// Ensure Payment Method is still valid.
$payment_method = edds_api_request( 'PaymentMethod', 'retrieve', $payment_method_id );
$card = isset( $payment_method->card ) ? $payment_method->card : null;
// ...block prepaid cards if option is not enabled.
if (
$card &&
'prepaid' === $card->funding &&
false === (bool) edd_get_option( 'stripe_allow_prepaid' )
) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'Prepaid cards are not a valid payment method. Please try again with a new payment method.',
'easy-digital-downloads'
)
);
}
if ( edds_is_zero_decimal_currency() ) {
$amount = $purchase_data['price'];
} else {
$amount = round( $purchase_data['price'] * 100, 0 );
}
// Retrieves or creates a Stripe Customer.
$customer = edds_checkout_setup_customer( $purchase_data );
if ( ! $customer ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'Unable to create customer. Please try again.',
'easy-digital-downloads'
)
);
}
/**
* Allows processing before an Intent is created, but
* after a \Stripe\Customer is available.
*
* @since 2.7.0
*
* @param array $purchase_data Purchase data.
* @param \Stripe\Customer $customer Stripe Customer object.
*/
do_action( 'edds_process_purchase_form_before_intent', $purchase_data, $customer );
// Flag if this is the first card being attached to the Customer.
$existing_payment_methods = edd_stripe_get_existing_cards( $purchase_data['user_info']['id'] );
$is_first_payment_method = empty( $existing_payment_methods );
$address_info = $purchase_data['user_info']['address'];
// Update PaymentMethod details if necessary.
if ( $payment_method_exists && ! empty( $_POST['edd_stripe_update_billing_address'] ) ) {
$billing_address = array();
foreach ( $address_info as $key => $value ) {
// Adjusts address data keys to work with PaymentMethods.
switch( $key ) {
case 'zip':
$key = 'postal_code';
break;
}
$billing_address[ $key ] = ! empty( $value ) ? sanitize_text_field( $value ) : '';
}
edds_api_request( 'PaymentMethod', 'update', $payment_method_id, array(
'billing_details' => array(
'address' => $billing_address,
),
) );
}
// Create a list of {$download_id}_{$price_id}
$payment_items = array();
foreach ( $purchase_data['cart_details'] as $item ) {
$price_id = isset( $item['item_number']['options']['price_id'] )
? $item['item_number']['options']['price_id']
: null;
$payment_items[] = $item['id'] . ( ! empty( $price_id ) ? ( '_' . $price_id ) : '' );
}
// Shared Intent arguments.
$intent_args = array(
'confirm' => true,
'payment_method' => $payment_method_id,
'customer' => $customer->id,
'metadata' => array(
'email' => esc_html( $purchase_data['user_info']['email'] ),
'edd_payment_subtotal' => esc_html( $purchase_data['subtotal'] ),
'edd_payment_discount' => esc_html( $purchase_data['discount'] ),
'edd_payment_tax' => esc_html( $purchase_data['tax'] ),
'edd_payment_tax_rate' => esc_html( $purchase_data['tax_rate'] ),
'edd_payment_fees' => esc_html( edd_get_cart_fee_total() ),
'edd_payment_total' => esc_html( $purchase_data['price'] ),
'edd_payment_items' => esc_html( implode( ', ', $payment_items ) ),
),
);
// Attempt to map existing charge arguments to PaymentIntents.
if ( has_filter( 'edds_create_charge_args' ) ) {
/**
* @deprecated 2.7.0 In favor of `edds_create_payment_intent_args`.
*
* @param array $intent_args
*/
$old_charge_args = apply_filters_deprecated(
'edds_create_charge_args',
array(
$intent_args,
),
'2.7.0',
'edds_create_payment_intent_args'
);
// Grab a few compatible arguments from the old charges filter.
$compatible_keys = array(
'amount',
'currency',
'customer',
'description',
'metadata',
'application_fee',
);
foreach ( $compatible_keys as $compatible_key ) {
if ( ! isset( $old_charge_args[ $compatible_key ] ) ) {
continue;
}
$value = $old_charge_args[ $compatible_key ];
switch ( $compatible_key ) {
case 'application_fee' :
$intent_args['application_fee_amount'] = $value;
break;
default:
// If a legacy value is an array merge it with the existing values to avoid overriding completely.
$intent_args[ $compatible_key ] = is_array( $value ) && is_array( $intent_args[ $compatible_key ] )
? wp_parse_args( $value, $intent_args[ $compatible_key ] )
: $value;
}
edd_debug_log( __( 'Charges are no longer directly created in Stripe. Please read the following for more information: https://easydigitaldownloads.com/development/', 'easy-digital-downloads' ), true );
}
}
// Create a SetupIntent for a non-payment carts.
if ( edds_is_preapprove_enabled() || 0 === $amount ) {
$intent_args = array_merge(
array(
'usage' => 'off_session',
'description' => edds_get_payment_description( $purchase_data['cart_details'] ),
),
$intent_args
);
/**
* Filters the arguments used to create a SetupIntent.
*
* @since 2.7.0
*
* @param array $intent_args SetupIntent arguments.
* @param array $purchase_data {
* Purchase form data.
*
* }
*/
$intent_args = apply_filters( 'edds_create_setup_intent_args', $intent_args, $purchase_data );
$intent = edds_api_request( 'SetupIntent', 'create', $intent_args );
// Manually attach PaymentMethod to the Customer.
if ( ! $payment_method_exists && edd_stripe_existing_cards_enabled() ) {
$payment_method = edds_api_request( 'PaymentMethod', 'retrieve', $payment_method_id );
$payment_method->attach( array(
'customer' => $customer->id,
) );
}
// Create a PaymentIntent for an immediate charge.
} else {
$purchase_summary = edds_get_payment_description( $purchase_data['cart_details'] );
$statement_descriptor = edds_get_statement_descriptor();
if ( empty( $statement_descriptor ) ) {
$statement_descriptor = substr( $purchase_summary, 0, 22 );
}
$statement_descriptor = apply_filters( 'edds_statement_descriptor', $statement_descriptor, $purchase_data );
$statement_descriptor = edds_sanitize_statement_descriptor( $statement_descriptor );
if ( empty( $statement_descriptor ) ) {
$statement_descriptor = null;
} elseif ( is_numeric( $statement_descriptor ) ) {
$statement_descriptor = edd_get_label_singular() . ' ' . $statement_descriptor;
}
$intent_args = array_merge(
array(
'amount' => $amount,
'currency' => edd_get_currency(),
'setup_future_usage' => 'off_session',
'confirmation_method' => 'manual',
'save_payment_method' => true,
'description' => $purchase_summary,
'statement_descriptor' => $statement_descriptor,
),
$intent_args
);
$stripe_connect_account_id = edd_get_option( 'stripe_connect_account_id' );
if (
! empty( $stripe_connect_account_id ) &&
true === edds_stripe_connect_account_country_supports_application_fees()
) {
$intent_args['application_fee_amount'] = round( $amount * 0.02 );
}
/**
* Filters the arguments used to create a SetupIntent.
*
* @since 2.7.0
*
* @param array $intent_args SetupIntent arguments.
* @param array $purchase_data {
* Purchase form data.
*
* }
*/
$intent_args = apply_filters( 'edds_create_payment_intent_args', $intent_args, $purchase_data );
$intent = edds_api_request( 'PaymentIntent', 'create', $intent_args );
}
// Set the default payment method when attaching the first one.
if ( $is_first_payment_method ) {
edds_api_request( 'Customer', 'update', $customer->id, array(
'invoice_settings' => array(
'default_payment_method' => $payment_method_id,
),
) );
}
/**
* Allows further processing after an Intent is created.
*
* @since 2.7.0
*
* @param array $purchase_data Purchase data.
* @param \Stripe\PaymentIntent|\Stripe\SetupIntent $intent Created Stripe Intent.
* @param int $payment_id EDD Payment ID.
*/
do_action( 'edds_process_purchase_form', $purchase_data, $intent );
return wp_send_json_success( array(
'intent' => $intent,
// Send back a new nonce because the user might have logged in.
'nonce' => wp_create_nonce( 'edd-process-checkout' ),
) );
// Catch card-specific errors to handle rate limiting.
} catch ( \Stripe\Exception\CardException $e ) {
// Increase the card error count.
edd_stripe()->rate_limiting->increment_card_error_count();
$error = $e->getJsonBody()['error'];
// Record error in log.
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
sprintf(
esc_html__( 'There was an error while processing a Stripe payment. Payment data: %s', 'easy-digital-downloads' ),
wp_json_encode( $error )
),
0
);
$decline_code = ! empty( $error['decline_code'] ) ? $error['decline_code'] : false;
return wp_send_json_error( array(
'message' => esc_html(
edds_get_localized_error_message( $error['code'], $error['message'], $decline_code )
),
) );
// Catch Stripe-specific errors.
} catch ( \Stripe\Exception\ApiErrorException $e ) {
$error = $e->getJsonBody()['error'];
// Record error in log.
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
sprintf(
esc_html__( 'There was an error while processing a Stripe payment. Payment data: %s', 'easy-digital-downloads' ),
wp_json_encode( $error )
),
0
);
return wp_send_json_error( array(
'message' => esc_html(
edds_get_localized_error_message( $error['code'], $error['message'] )
),
) );
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( \Exception $e ) {
// Safety precaution in case the payment form is submitted directly.
// Redirects back to the Checkout.
if ( isset( $_POST['edd_email'] ) && ! isset( $_POST['payment_method_id'] ) ) {
edd_set_error( $e->getCode(), $e->getMessage() );
edd_send_back_to_checkout( '?payment-mode=' . $purchase_data['gateway'] );
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'edd_gateway_stripe', 'edds_process_purchase_form' );
/**
* Retrieves an Intent.
*
* @since 2.7.0
*/
function edds_get_intent() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
$intent_id = isset( $_REQUEST['intent_id'] ) ? sanitize_text_field( $_REQUEST['intent_id'] ) : null;
$intent_type = isset( $_REQUEST['intent_type'] ) ? sanitize_text_field( $_REQUEST['intent_type'] ) : 'payment_intent';
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during Intent retrieval.'
);
}
if ( false === edds_verify_payment_form_nonce() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during Intent retrieval.'
);
}
if ( 'setup_intent' === $intent_type ) {
$intent = edds_api_request( 'SetupIntent', 'retrieve', $intent_id );
} else {
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent_id );
}
return wp_send_json_success( array(
'intent' => $intent,
) );
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit if an exception occurs mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( \Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_get_intent', 'edds_get_intent' );
add_action( 'wp_ajax_nopriv_edds_get_intent', 'edds_get_intent' );
/**
* Confirms a PaymentIntent.
*
* @since 2.7.0
*/
function edds_confirm_intent() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
$intent_id = isset( $_REQUEST['intent_id'] ) ? sanitize_text_field( $_REQUEST['intent_id'] ) : null;
$intent_type = isset( $_REQUEST['intent_type'] ) ? sanitize_text_field( $_REQUEST['intent_type'] ) : 'payment_intent';
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during Intent confirmation.'
);
}
if ( false === edds_verify_payment_form_nonce() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during Intent confirmation.'
);
}
// SetupIntent was used if the cart total is $0.
if ( 'setup_intent' === $intent_type ) {
$intent = edds_api_request( 'SetupIntent', 'retrieve', $intent_id );
} else {
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent_id );
$intent->confirm();
}
/**
* Allows further processing after an Intent is confirmed.
* Runs for all calls to confirm(), regardless of action needed.
*
* @since 2.7.0
*
* @param \Stripe\PaymentIntent|\Stripe\SetupIntent $intent Stripe intent.
*/
do_action( 'edds_confirm_payment_intent', $intent );
return wp_send_json_success( array(
'intent' => $intent,
) );
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit if an exception occurs mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_confirm_intent', 'edds_confirm_intent' );
add_action( 'wp_ajax_nopriv_edds_confirm_intent', 'edds_confirm_intent' );
/**
* Capture a PaymentIntent.
*
* @since 2.7.0
*/
function edds_capture_intent() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
$intent_id = isset( $_REQUEST['intent_id'] ) ? sanitize_text_field( $_REQUEST['intent_id'] ) : null;
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during Intent capture.'
);
}
// Verify the checkout session only.
if ( false === edds_verify() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during Intent capture.'
);
}
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent_id );
/**
* Allows processing before a PaymentIntent is captured.
*
* @since 2.7.0
*
* @param \Stripe\PaymentIntent $payment_intent Stripe PaymentIntent.
*/
do_action( 'edds_capture_payment_intent', $intent );
// Capture capturable amount if nothing else has captured the intent.
if ( 'requires_capture' === $intent->status ) {
$intent->capture( array(
'amount_to_capture' => $intent->amount_capturable,
) );
}
return wp_send_json_success( array(
'intent' => $intent,
) );
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit if an exception occurs mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_capture_intent', 'edds_capture_intent' );
add_action( 'wp_ajax_nopriv_edds_capture_intent', 'edds_capture_intent' );
/**
* Update a PaymentIntent.
*
* @since 2.7.0
*/
function edds_update_intent() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
$intent_id = isset( $_REQUEST['intent_id'] ) ? sanitize_text_field( $_REQUEST['intent_id'] ) : null;
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during Intent update.'
);
}
if ( false === edds_verify_payment_form_nonce() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during Intent update.'
);
}
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent_id );
/**
* Allows processing before a PaymentIntent is updated.
*
* @since 2.7.0
*
* @param string $intent_id Stripe PaymentIntent ID.
*/
do_action( 'edds_update_payment_intent', $intent_id );
$intent_args = array();
$intent_args_whitelist = array(
'payment_method',
);
foreach ( $intent_args_whitelist as $intent_arg ) {
if ( isset( $_POST[ $intent_arg ] ) ) {
$intent_args[ $intent_arg ] = sanitize_text_field( $_POST[ $intent_arg ] );
}
}
$intent = edds_api_request( 'PaymentIntent', 'update', $intent_id, $intent_args );
return wp_send_json_success( array(
'intent' => $intent,
) );
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit if an exception occurs mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_update_intent', 'edds_update_intent' );
add_action( 'wp_ajax_nopriv_edds_update_intent', 'edds_update_intent' );
/**
* Create an \EDD_Payment.
*
* @since 2.7.0
*/
function edds_create_payment() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
// Simulate being in an `edd_process_purchase_form()` request.
_edds_fake_process_purchase_step();
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during payment creation.'
);
}
// This must happen in the Checkout flow, so validate the Checkout nonce.
if ( false === edds_verify() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during payment creation.'
);
}
$intent = isset( $_REQUEST['intent'] ) ? $_REQUEST['intent'] : array();
if ( ! isset( $intent['id'] ) ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to retrieve Intent data during payment creation.'
);
}
$purchase_data = edd_get_purchase_session();
if ( false === $purchase_data ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to retrieve purchase data during payment creation.'
);
}
// Ensure Intent has transitioned to the correct status.
if ( 'setup_intent' === $intent['object'] ) {
$intent = edds_api_request( 'SetupIntent', 'retrieve', $intent['id'] );
} else {
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent['id'] );
}
if ( ! in_array( $intent->status, array( 'succeeded', 'requires_capture' ), true ) ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Invalid Intent status ' . $intent->status . ' during payment creation.'
);
}
$payment_data = array(
'price' => $purchase_data['price'],
'date' => $purchase_data['date'],
'user_email' => $purchase_data['user_email'],
'purchase_key' => $purchase_data['purchase_key'],
'currency' => edd_get_currency(),
'downloads' => $purchase_data['downloads'],
'cart_details' => $purchase_data['cart_details'],
'user_info' => $purchase_data['user_info'],
'status' => 'pending',
'gateway' => 'stripe',
);
// Ensure $_COOKIE is available without a new HTTP request.
if ( class_exists( 'EDD_Auto_Register' ) ) {
add_action( 'set_logged_in_cookie', 'edds_set_logged_in_cookie_global' );
}
// Record the pending payment.
$payment_id = edd_insert_payment( $payment_data );
if ( false === $payment_id ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to insert payment record.'
);
}
// Retrieve created payment.
$payment = edd_get_payment( $payment_id );
// Retrieve the relevant Intent.
if ( 'setup_intent' === $intent->object ) {
$intent = edds_api_request( 'SetupIntent', 'update', $intent->id, array(
'metadata' => array(
'edd_payment_id' => $payment_id,
),
) );
$payment->add_note( 'Stripe SetupIntent ID: ' . $intent->id );
$payment->update_meta( '_edds_stripe_setup_intent_id', $intent->id );
} else {
$intent = edds_api_request( 'PaymentIntent', 'update', $intent->id, array(
'metadata' => array(
'edd_payment_id' => $payment_id,
),
) );
$payment->add_note( 'Stripe PaymentIntent ID: ' . $intent->id );
$payment->update_meta( '_edds_stripe_payment_intent_id', $intent->id );
}
// Use Intent ID for temporary transaction ID.
// It will be updated when a charge is available.
$payment->transaction_id = $intent->id;
// Retrieves or creates a Stripe Customer.
$payment->update_meta( '_edds_stripe_customer_id', $intent->customer );
$payment->add_note( 'Stripe Customer ID: ' . $intent->customer );
// Attach the \Stripe\Customer ID to the \EDD_Customer meta if one exists.
$edd_customer = new EDD_Customer( $purchase_data['user_email'] );
if ( $edd_customer->id > 0 ) {
$edd_customer->update_meta( edd_stripe_get_customer_key(), $intent->customer );
}
$saved = $payment->save();
if ( class_exists( 'EDD_Auto_Register' ) ) {
remove_action( 'set_logged_in_cookie', 'edds_set_logged_in_cookie_global' );
}
if ( true === $saved ) {
/**
* Allows further processing after a payment is created.
*
* @since 2.7.0
*
* @param \EDD_Payment $payment EDD Payment.
* @param \Stripe\PaymentIntent|\Stripe\SetupIntent $intent Created Stripe Intent.
*/
do_action( 'edds_payment_created', $payment, $intent );
return wp_send_json_success( array(
'intent' => $intent,
'payment' => $payment,
// Send back a new nonce because the user might have logged in via Auto Register.
'nonce' => wp_create_nonce( 'edd-process-checkout' ),
) );
} else {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'Unable to create payment.',
'easy-digital-downloads'
),
'Unable to save payment record.'
);
}
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit count when something goes wrong mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( \Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_create_payment', 'edds_create_payment' );
add_action( 'wp_ajax_nopriv_edds_create_payment', 'edds_create_payment' );
/**
* Completes an \EDD_Payment (via AJAX)
*
* @since 2.7.0
*/
function edds_complete_payment() {
// Map and merge serialized `form_data` to $_POST so it's accessible to other functions.
_edds_map_form_data_to_request( $_POST );
$intent = isset( $_REQUEST['intent'] ) ? $_REQUEST['intent'] : array();
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during payment completion.'
);
}
// Verify the checkout session only.
if ( false === edds_verify() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during payment completion.'
);
}
if ( ! isset( $intent['id'] ) ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to retrieve Intent during payment completion.'
);
}
// Retrieve the intent from Stripe again to verify linked payment.
if ( 'setup_intent' === $intent['object'] ) {
$intent = edds_api_request( 'SetupIntent', 'retrieve', $intent['id'] );
} else {
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent['id'] );
}
$payment = edd_get_payment( $intent->metadata->edd_payment_id );
if ( ! $payment ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to retrieve pending payment record.'
);
}
if ( 'setup_intent' !== $intent['object'] ) {
$charge_id = sanitize_text_field( current( $intent['charges']['data'] )['id'] );
$payment->add_note( 'Stripe Charge ID: ' . $charge_id );
$payment->transaction_id = sanitize_text_field( $charge_id );
}
// Mark payment as Preapproved.
if ( edds_is_preapprove_enabled() ) {
$payment->status = 'preapproval';
// Complete payment and transition the Transaction ID to the actual Charge ID.
} else {
$payment->status = 'publish';
}
if ( $payment->save() ) {
/**
* Allows further processing after a payment is completed.
*
* Sends back just the Intent ID to avoid needing always retrieve
* the intent in this step, which has been transformed via JSON,
* and is no longer a \Stripe\PaymentIntent
*
* @since 2.7.0
*
* @param \EDD_Payment $payment EDD Payment.
* @param string $intent_id Stripe Intent ID.
*/
do_action( 'edds_payment_complete', $payment, $intent['id'] );
// Empty cart.
edd_empty_cart();
return wp_send_json_success( array(
'payment' => $payment,
'intent' => $intent,
) );
} else {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to update payment record to completion.'
);
}
// Catch gateway processing errors.
} catch ( \EDD_Stripe_Gateway_Exception $e ) {
// Increase the rate limit count when something goes wrong mid-process.
edd_stripe()->rate_limiting->increment_card_error_count();
if ( true === $e->hasLogMessage() ) {
edd_record_gateway_error(
esc_html__( 'Stripe Error', 'easy-digital-downloads' ),
$e->getLogMessage(),
0
);
}
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
// Catch any remaining error.
} catch( \Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_complete_payment', 'edds_complete_payment' );
add_action( 'wp_ajax_nopriv_edds_complete_payment', 'edds_complete_payment' );
/**
* Completes a Payment authorization.
*
* @since 2.7.0
*/
function edds_complete_payment_authorization() {
$intent_id = isset( $_REQUEST['intent_id'] ) ? sanitize_text_field( $_REQUEST['intent_id'] ) : null;
try {
if ( edd_stripe()->rate_limiting->has_hit_card_error_limit() ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Rate limit reached during payment authorization.'
);
}
$nonce_verified = edds_verify( 'edds-complete-payment-authorization', 'edds-complete-payment-authorization' );
if ( false === $nonce_verified ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Nonce verification failed during payment authorization.'
);
}
$intent = edds_api_request( 'PaymentIntent', 'retrieve', $intent_id );
$edd_payment_id = $intent->metadata->edd_payment_id ? $intent->metadata->edd_payment_id : false;
if ( ! $edd_payment_id ) {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to retrieve payment record ID from Stripe metadata.'
);
}
$payment = edd_get_payment( $edd_payment_id );
$charge_id = current( $intent->charges->data )->id;
$payment->add_note( 'Stripe Charge ID: ' . $charge_id );
$payment->transaction_id = $charge_id;
$payment->status = 'publish';
if ( $payment->save() ) {
/**
* Allows further processing after a payment authorization is completed.
*
* @since 2.7.0
*
* @param \Stripe\PaymentIntent $intent Created Stripe Intent.
* @param EDD_Payment $payment EDD Payment.
*/
do_action( 'edds_payment_authorization_complete', $intent, $payment );
return wp_send_json_success( array(
'intent' => $intent,
'payment' => $payment,
) );
} else {
throw new \EDD_Stripe_Gateway_Exception(
esc_html__(
'An error occurred, but your payment may have gone through. Please contact the site administrator.',
'easy-digital-downloads'
),
'Unable to save payment record during authorization.'
);
}
} catch( \Exception $e ) {
return wp_send_json_error( array(
'message' => esc_html( $e->getMessage() ),
) );
}
}
add_action( 'wp_ajax_edds_complete_payment_authorization', 'edds_complete_payment_authorization' );
add_action( 'wp_ajax_nopriv_edds_complete_payment_authorization', 'edds_complete_payment_authorization' );
/**
* Sets up a \Stripe\Customer object based on the current purchase data.
*
* @param array $purchase_data {
*
* }
* @return \Stripe\Customer|false $customer Stripe Customer if one is created or false on error.
*/
function edds_checkout_setup_customer( $purchase_data ) {
$customer = false;
$stripe_customer_id = '';
if ( is_user_logged_in() ) {
$stripe_customer_id = edds_get_stripe_customer_id( get_current_user_id() );
}
if ( empty( $stripe_customer_id ) ) {
// No customer ID found, let's look one up based on the email.
$stripe_customer_id = edds_get_stripe_customer_id( $purchase_data['user_email'], false );
}
$customer_name = '';
if ( ! empty( $purchase_data['user_info']['first_name'] ) ) {
$customer_name .= sanitize_text_field( $purchase_data['user_info']['first_name'] );
}
if ( ! empty( $purchase_data['user_info']['last_name'] ) ) {
$customer_name .= ' ' . sanitize_text_field( $purchase_data['user_info']['last_name'] );
}
$customer_args = array(
'email' => $purchase_data['user_email'],
'description' => $purchase_data['user_email'],
'name' => $customer_name,
);
/**
* Filters the arguments used to create a Customer in Stripe.
*
* @since unknown
*
* @param array $customer_args {
* Arguments to create a Stripe Customer.
*
* @link https://stripe.com/docs/api/customers/create
* }
* @param array $purchase_data {
* Cart purchase data if in the checkout context. Empty otherwise.
* }
*/
$customer_args = apply_filters( 'edds_create_customer_args', $customer_args, $purchase_data );
$customer = edds_get_stripe_customer( $stripe_customer_id, $customer_args );
return $customer;
}
/**
* Generates a description based on the cart details.
*
* @param array $cart_details {
*
* }
* @return string
*/
function edds_get_payment_description( $cart_details ) {
$purchase_summary = '';
if( is_array( $cart_details ) && ! empty( $cart_details ) ) {
foreach( $cart_details as $item ) {
$purchase_summary .= $item['name'];
$price_id = isset( $item['item_number']['options']['price_id'] )
? absint( $item['item_number']['options']['price_id'] )
: false;
if ( false !== $price_id ) {
$purchase_summary .= ' - ' . edd_get_price_option_name( $item['id'], $item['item_number']['options']['price_id'] );
}
$purchase_summary .= ', ';
}
$purchase_summary = rtrim( $purchase_summary, ', ' );
}
// Stripe has a maximum of 999 characters in the charge description
$purchase_summary = substr( $purchase_summary, 0, 1000 );
return html_entity_decode( $purchase_summary, ENT_COMPAT, 'UTF-8' );
}
/**
* Charge a preapproved payment
*
* @since 1.6
* @return bool
*/
function edds_charge_preapproved( $payment_id = 0 ) {
$retval = false;
if ( empty( $payment_id ) ) {
return $retval;
}
$payment = edd_get_payment( $payment_id );
$customer_id = $payment->get_meta( '_edds_stripe_customer_id' );
if ( empty( $customer_id ) ) {
return $retval;
}
if ( ! in_array( $payment->status, array( 'preapproval', 'preapproval_pending' ), true ) ) {
return $retval;
}
$setup_intent_id = $payment->get_meta( '_edds_stripe_setup_intent_id' );
try {
if ( edds_is_zero_decimal_currency() ) {
$amount = edd_get_payment_amount( $payment->ID );
} else {
$amount = edd_get_payment_amount( $payment->ID ) * 100;
}
$cart_details = edd_get_payment_meta_cart_details( $payment->ID );
$purchase_summary = edds_get_payment_description( $cart_details );
$statement_descriptor = edds_get_statement_descriptor();
if ( empty( $statement_descriptor ) ) {
$statement_descriptor = substr( $purchase_summary, 0, 22 );
}
$statement_descriptor = apply_filters( 'edds_preapproved_statement_descriptor', $statement_descriptor, $payment->ID );
$statement_descriptor = edds_sanitize_statement_descriptor( $statement_descriptor );
if ( empty( $statement_descriptor ) ) {
$statement_descriptor = null;
}
// Create a PaymentIntent using SetupIntent data.
if ( ! empty( $setup_intent_id ) ) {
$setup_intent = edds_api_request( 'SetupIntent', 'retrieve', $setup_intent_id );
$intent_args = array(
'amount' => $amount,
'currency' => edd_get_currency(),
'payment_method' => $setup_intent->payment_method,
'customer' => $setup_intent->customer,
'off_session' => true,
'confirm' => true,
'description' => $purchase_summary,
'metadata' => $setup_intent->metadata->toArray(),
'statement_descriptor' => $statement_descriptor,
);
// Process a legacy preapproval. Uses the Customer's default source.
} else {
$customer = \Stripe\Customer::retrieve( $customer_id );
$intent_args = array(
'amount' => $amount,
'currency' => edd_get_currency(),
'payment_method' => $customer->default_source,
'customer' => $customer->id,
'off_session' => true,
'confirm' => true,
'description' => $purchase_summary,
'metadata' => array(
'email' => edd_get_payment_user_email( $payment->ID ),
'edd_payment_id' => $payment->ID,
),
'statement_descriptor' => $statement_descriptor,
);
}
/** This filter is documented in includes/payment-actions.php */
$intent_args = apply_filters( 'edds_create_payment_intent_args', $intent_args, array() );
$payment_intent = edds_api_request( 'PaymentIntent', 'create', $intent_args );
if ( 'succeeded' === $payment_intent->status ) {
$charge_id = current( $payment_intent->charges->data )->id;
$payment->status = 'publish';
$payment->add_note( 'Stripe Charge ID: ' . $charge_id );
$payment->add_note( 'Stripe PaymentIntent ID: ' . $payment_intent->id );
$payment->add_meta( '_edds_stripe_payment_intent_id', $payment_intent->id );
$payment->transaction_id = $charge_id;
$retval = $payment->save();
}
} catch( \Stripe\Exception\ApiErrorException $e ) {
$error = $e->getJsonBody()['error'];
$payment->status = 'preapproval_pending';
$payment->add_note( esc_html(
edds_get_localized_error_message( $error['code'], $error['message'] )
) );
$payment->add_note( 'Stripe PaymentIntent ID: ' . $error['payment_intent']['id'] );
$payment->add_meta( '_edds_stripe_payment_intent_id', $error['payment_intent']['id'] );
$payment->save();
/**
* Allows further processing when a Preapproved payment needs further action.
*
* @since 2.7.0
*
* @param int $payment_id ID of the payment.
*/
do_action( 'edds_preapproved_payment_needs_action', $payment_id );
} catch( \Exception $e ) {
$payment->add_note( esc_html( $e->getMessage() ) );
}
return $retval;
}
/**
* Process refund in Stripe, in EDD 2.x
* For EDD 3.0, see `edd_stripe_maybe_refund_charge()`
* @see edd_stripe_maybe_refund_charge()
*
* @access public
* @since 1.8
* @return void
*/
function edd_stripe_process_refund( $payment_id, $new_status, $old_status ) {
if ( empty( $_POST['edd_refund_in_stripe'] ) ) {
return;
}
$should_process_refund = 'publish' != $old_status && 'revoked' != $old_status ? false : true;
$should_process_refund = apply_filters( 'edds_should_process_refund', $should_process_refund, $payment_id, $new_status, $old_status );
if ( false === $should_process_refund ) {
return;
}
if ( 'refunded' != $new_status ) {
return;
}
try {
edd_refund_stripe_purchase( $payment_id );
} catch ( \Exception $e ) {
wp_die( $e->getMessage(), __( 'Error', 'easy-digital-downloads' ) , array( 'response' => 400 ) );
}
}
add_action( 'edd_update_payment_status', 'edd_stripe_process_refund', 200, 3 );
/**
* If selected, refunds a charge in Stripe when creating a new refund record.
* This handles refunds in EDD 3.0+. For EDD 2.x see `edd_stripe_process_refund()`
* @see edd_stripe_process_refund()
*
* @since 2.8.7
*
* @param int $order_id ID of the order we're processing a refund for.
* @param int $refund_id ID of the newly created refund record.
* @param bool $all_refunded Whether or not this was a full refund.
*/
function edd_stripe_maybe_refund_charge( $order_id, $refund_id, $all_refunded ) {
if ( ! current_user_can( 'edit_shop_payments', $order_id ) ) {
return;
}
if ( empty( $_POST['data'] ) ) {
return;
}
$order = edd_get_order( $order_id );
if ( empty( $order->gateway ) || 'stripe' !== $order->gateway ) {
return;
}
edd_debug_log( sprintf( 'Stripe - Maybe processing refund for order #%d.', $order_id ) );
// Get our data out of the serialized string.
parse_str( $_POST['data'], $form_data );
if ( empty( $form_data['edd-stripe-refund'] ) ) {
edd_debug_log( 'Stripe - Exiting refund process, as checkbox was not selected.' );
edd_add_note( array(
'object_id' => $order_id,
'object_type' => 'order',
'user_id' => is_admin() ? get_current_user_id() : 0,
'content' => __( 'Charge not refunded in Stripe, as checkbox was not selected.', 'easy-digital-downloads' )
) );
return;
}
edd_debug_log( 'Stripe - Refund checkbox was selected, proceeding to refund charge.' );
$refund = edd_get_order( $refund_id );
if ( empty( $refund->total ) ) {
edd_debug_log( sprintf(
'Stripe - Exiting refund for order #%d - refund total is empty.',
$order_id
) );
return;
}
try {
edd_refund_stripe_purchase( $order, $refund );
} catch ( \Exception $e ) {
edd_debug_log( sprintf( 'Exception thrown while refunding order #%d. Message: %s', $order_id, $e->getMessage() ) );
}
}
add_action( 'edd_refund_order', 'edd_stripe_maybe_refund_charge', 10, 3 );