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 );
 | 
						|
}
 |