243 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			243 lines
		
	
	
		
			7.0 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| /**
 | |
|  * PayPal Commerce Refunds
 | |
|  *
 | |
|  * @package    easy-digital-downloads
 | |
|  * @subpackage Gateways\PayPal
 | |
|  * @copyright  Copyright (c) 2021, Sandhills Development, LLC
 | |
|  * @license    GPL2+
 | |
|  * @since      2.11
 | |
|  */
 | |
| 
 | |
| namespace EDD\Gateways\PayPal;
 | |
| 
 | |
| use EDD\Gateways\PayPal\Exceptions\API_Exception;
 | |
| use EDD\Gateways\PayPal\Exceptions\Authentication_Exception;
 | |
| use EDD\Orders\Order;
 | |
| 
 | |
| /**
 | |
|  * Shows a checkbox to automatically refund payments in PayPal.
 | |
|  *
 | |
|  * @param Order $order
 | |
|  *
 | |
|  * @since 3.0
 | |
|  */
 | |
| add_action( 'edd_after_submit_refund_table', function( Order $order ) {
 | |
| 	if ( 'paypal_commerce' !== $order->gateway ) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	$mode = ( 'live' === $order->mode ) ? API::MODE_LIVE : API::MODE_SANDBOX;
 | |
| 
 | |
| 	try {
 | |
| 		new API( $mode );
 | |
| 	} catch ( Exceptions\Authentication_Exception $e ) {
 | |
| 		// If we don't have credentials.
 | |
| 		return;
 | |
| 	}
 | |
| 	?>
 | |
| 	<div class="edd-form-group edd-paypal-refund-transaction">
 | |
| 		<div class="edd-form-group__control">
 | |
| 			<input type="checkbox" id="edd-paypal-commerce-refund" name="edd-paypal-commerce-refund" class="edd-form-group__input" value="1">
 | |
| 			<label for="edd-paypal-commerce-refund" class="edd-form-group__label">
 | |
| 				<?php esc_html_e( 'Refund transaction in PayPal', 'easy-digital-downloads' ); ?>
 | |
| 			</label>
 | |
| 		</div>
 | |
| 	</div>
 | |
| 	<?php
 | |
| } );
 | |
| 
 | |
| /**
 | |
|  * If selected, refunds a transaction in PayPal when creating a new refund record.
 | |
|  *
 | |
|  * @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.
 | |
|  *
 | |
|  * @since 3.0
 | |
|  */
 | |
| add_action( 'edd_refund_order', function( $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 ) || 'paypal_commerce' !== $order->gateway ) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	// Get our data out of the serialized string.
 | |
| 	parse_str( $_POST['data'], $form_data );
 | |
| 
 | |
| 	if ( empty( $form_data['edd-paypal-commerce-refund'] ) ) {
 | |
| 		edd_add_note( array(
 | |
| 			'object_id'   => $order_id,
 | |
| 			'object_type' => 'order',
 | |
| 			'user_id'     => is_admin() ? get_current_user_id() : 0,
 | |
| 			'content'     => __( 'Transaction not refunded in PayPal, as checkbox was not selected.', 'easy-digital-downloads' )
 | |
| 		) );
 | |
| 
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	$refund = edd_get_order( $refund_id );
 | |
| 	if ( empty( $refund->total ) ) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	try {
 | |
| 		refund_transaction( $order, $refund );
 | |
| 	} catch ( \Exception $e ) {
 | |
| 		edd_debug_log( sprintf(
 | |
| 			'Failure when processing refund #%d. Message: %s',
 | |
| 			$refund->id,
 | |
| 			$e->getMessage()
 | |
| 		), true );
 | |
| 
 | |
| 		edd_add_note( array(
 | |
| 			'object_id'   => $order->id,
 | |
| 			'object_type' => 'order',
 | |
| 			'user_id'     => is_admin() ? get_current_user_id() : 0,
 | |
| 			'content'     => sprintf(
 | |
| 				/* Translators: %d - ID of the refund; %s - error message from PayPal */
 | |
| 				__( 'Failure when processing PayPal refund #%d: %s', 'easy-digital-downloads' ),
 | |
| 				$refund->id,
 | |
| 				$e->getMessage()
 | |
| 			)
 | |
| 		) );
 | |
| 	}
 | |
| }, 10, 3 );
 | |
| 
 | |
| /**
 | |
|  * Refunds a transaction in PayPal.
 | |
|  *
 | |
|  * @link  https://developer.paypal.com/docs/api/payments/v2/#captures_refund
 | |
|  *
 | |
|  * @param \EDD_Payment|Order $payment_or_order
 | |
|  * @param Order|null         $refund_object
 | |
|  *
 | |
|  * @since 2.11
 | |
|  * @throws Authentication_Exception
 | |
|  * @throws API_Exception
 | |
|  * @throws \Exception
 | |
|  */
 | |
| function refund_transaction( $payment_or_order, Order $refund_object = null ) {
 | |
| 	/*
 | |
| 	 * Internally we want to work with an Order object, but we also need
 | |
| 	 * an EDD_Payment object for backwards compatibility in the hooks.
 | |
| 	 */
 | |
| 	$order = $payment = false;
 | |
| 	if ( $payment_or_order instanceof Order ) {
 | |
| 		$order = $payment_or_order;
 | |
| 		$payment = edd_get_payment( $order->id );
 | |
| 	} elseif ( $payment_or_order instanceof \EDD_Payment ) {
 | |
| 		$payment = $payment_or_order;
 | |
| 		$order   = edd_get_order( $payment->ID );
 | |
| 	}
 | |
| 
 | |
| 	if ( empty( $order ) || ! $order instanceof Order ) {
 | |
| 		return;
 | |
| 	}
 | |
| 
 | |
| 	$transaction_id = $order->get_transaction_id();
 | |
| 
 | |
| 	if ( empty( $transaction_id ) ) {
 | |
| 		throw new \Exception( __( 'Missing transaction ID.', 'easy-digital-downloads' ) );
 | |
| 	}
 | |
| 
 | |
| 	$mode = ( 'live' === $order->mode ) ? API::MODE_LIVE : API::MODE_SANDBOX;
 | |
| 
 | |
| 	$api = new API( $mode );
 | |
| 
 | |
| 	$args = $refund_object instanceof Order ? array( 'invoice_id' => $refund_object->id ) : array();
 | |
| 	if ( $refund_object instanceof Order && abs( $refund_object->total ) !== abs( $order->total ) ) {
 | |
| 		$args['amount'] = array(
 | |
| 			'value'         => abs( $refund_object->total ),
 | |
| 			'currency_code' => $refund_object->currency,
 | |
| 		);
 | |
| 	}
 | |
| 
 | |
| 	$response = $api->make_request(
 | |
| 		'v2/payments/captures/' . urlencode( $transaction_id ) . '/refund',
 | |
| 		$args,
 | |
| 		array(
 | |
| 			'Prefer' => 'return=representation',
 | |
| 		)
 | |
| 	);
 | |
| 
 | |
| 	if ( 201 !== $api->last_response_code ) {
 | |
| 		throw new API_Exception( sprintf(
 | |
| 		/* Translators: %d - The HTTP response code; %s - Full API response from PayPal */
 | |
| 			__( 'Unexpected response code: %d. Response: %s', 'easy-digital-downloads' ),
 | |
| 			$api->last_response_code,
 | |
| 			json_encode( $response )
 | |
| 		), $api->last_response_code );
 | |
| 	}
 | |
| 
 | |
| 	if ( empty( $response->status ) || 'COMPLETED' !== strtoupper( $response->status ) ) {
 | |
| 		throw new API_Exception( sprintf(
 | |
| 		/* Translators: %s - API response from PayPal */
 | |
| 			__( 'Missing or unexpected refund status. Response: %s', 'easy-digital-downloads' ),
 | |
| 			json_encode( $response )
 | |
| 		) );
 | |
| 	}
 | |
| 
 | |
| 	// At this point we can assume it was successful.
 | |
| 	edd_update_order_meta( $order->id, '_edd_paypal_refunded', true );
 | |
| 
 | |
| 	if ( ! empty( $response->id ) ) {
 | |
| 		// Add a note to the original order, and, if provided, the new refund object.
 | |
| 		if ( isset( $response->amount->value ) ) {
 | |
| 			$note_message = sprintf(
 | |
| 				/* Translators: %1$s - amount refunded; %$2$s - transaction ID. */
 | |
| 				__( '%1$s refunded in PayPal. Refund transaction ID: %2$s', 'easy-digital-downloads' ),
 | |
| 				edd_currency_filter( edd_format_amount( $response->amount->value ), $order->currency ),
 | |
| 				esc_html( $response->id )
 | |
| 			);
 | |
| 		} else {
 | |
| 			$note_message = sprintf(
 | |
| 				/* Translators: %s - ID of the refund in PayPal */
 | |
| 				__( 'Successfully refunded in PayPal. Refund transaction ID: %s', 'easy-digital-downloads' ),
 | |
| 				esc_html( $response->id )
 | |
| 			);
 | |
| 		}
 | |
| 
 | |
| 		$note_object_ids = array( $order->id );
 | |
| 		if ( $refund_object instanceof Order ) {
 | |
| 			$note_object_ids[] = $refund_object->id;
 | |
| 		}
 | |
| 
 | |
| 		foreach ( $note_object_ids as $note_object_id ) {
 | |
| 			edd_add_note( array(
 | |
| 				'object_id'   => $note_object_id,
 | |
| 				'object_type' => 'order',
 | |
| 				'user_id'     => is_admin() ? get_current_user_id() : 0,
 | |
| 				'content'     => $note_message
 | |
| 			) );
 | |
| 		}
 | |
| 
 | |
| 		// Add a negative transaction.
 | |
| 		if ( $refund_object instanceof Order && isset( $response->amount->value ) ) {
 | |
| 			edd_add_order_transaction( array(
 | |
| 				'object_id'      => $refund_object->id,
 | |
| 				'object_type'    => 'order',
 | |
| 				'transaction_id' => sanitize_text_field( $response->id ),
 | |
| 				'gateway'        => 'paypal_commerce',
 | |
| 				'status'         => 'complete',
 | |
| 				'total'          => edd_negate_amount( $response->amount->value ),
 | |
| 			) );
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	/**
 | |
| 	 * Triggers after a successful refund.
 | |
| 	 *
 | |
| 	 * @param \EDD_Payment $payment
 | |
| 	 */
 | |
| 	do_action( 'edd_paypal_refund_purchase', $payment );
 | |
| }
 |