diff --git a/composer.lock b/composer.lock index 62af1eb..535c41c 100644 --- a/composer.lock +++ b/composer.lock @@ -134,7 +134,7 @@ "source": { "type": "git", "url": "git@github.hps.com:devportal/php-sdk-v3", - "reference": "5e6b36bdee69df1dfe3f6291115943088ba4e8be" + "reference": "b63d3764e04b1570d3929d8f471f30ac1f297fae" }, "require": { "ext-curl": "*", @@ -183,7 +183,7 @@ ], "description": "PHP SDK for processing payments with Global Payments, including Heartland Payment Systems and Realex Payments", "homepage": "https://developer.heartlandpaymentsystems.com/documentation", - "time": "2019-11-15T07:18:11+00:00" + "time": "2019-12-11T06:31:14+00:00" } ], "packages-dev": [ @@ -281,16 +281,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.3", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea" + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/007c053ae6f31bba39dfa19a7726f56e9763bbea", - "reference": "007c053ae6f31bba39dfa19a7726f56e9763bbea", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7", + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7", "shasum": "" }, "require": { @@ -325,7 +325,7 @@ "object", "object graph" ], - "time": "2019-08-09T12:45:53+00:00" + "time": "2019-12-15T19:12:40+00:00" }, { "name": "phar-io/manifest", @@ -896,16 +896,16 @@ }, { "name": "phpunit/phpunit", - "version": "7.5.17", + "version": "7.5.18", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4c92a15296e58191a4cd74cff3b34fc8e374174a" + "reference": "fcf6c4bfafaadc07785528b06385cce88935474d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4c92a15296e58191a4cd74cff3b34fc8e374174a", - "reference": "4c92a15296e58191a4cd74cff3b34fc8e374174a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fcf6c4bfafaadc07785528b06385cce88935474d", + "reference": "fcf6c4bfafaadc07785528b06385cce88935474d", "shasum": "" }, "require": { @@ -976,7 +976,7 @@ "testing", "xunit" ], - "time": "2019-10-28T10:37:36+00:00" + "time": "2019-12-06T05:14:37+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1145,16 +1145,16 @@ }, { "name": "sebastian/environment", - "version": "4.2.2", + "version": "4.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404" + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404", - "reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", "shasum": "" }, "require": { @@ -1194,7 +1194,7 @@ "environment", "hhvm" ], - "time": "2019-05-05T09:05:15+00:00" + "time": "2019-11-20T08:46:58+00:00" }, { "name": "sebastian/exporter", @@ -1546,16 +1546,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.2", + "version": "3.5.3", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7" + "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", - "reference": "65b12cdeaaa6cd276d4c3033a95b9b88b12701e7", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", + "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", "shasum": "" }, "require": { @@ -1593,20 +1593,20 @@ "phpcs", "standards" ], - "time": "2019-10-28T04:36:32+00:00" + "time": "2019-12-04T04:46:47+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.12.0", + "version": "v1.13.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4" + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/550ebaac289296ce228a706d0867afc34687e3f4", - "reference": "550ebaac289296ce228a706d0867afc34687e3f4", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", "shasum": "" }, "require": { @@ -1618,7 +1618,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.12-dev" + "dev-master": "1.13-dev" } }, "autoload": { @@ -1651,7 +1651,7 @@ "polyfill", "portable" ], - "time": "2019-08-06T08:03:45+00:00" + "time": "2019-11-27T13:56:44+00:00" }, { "name": "theseer/tokenizer", @@ -1695,31 +1695,29 @@ }, { "name": "webmozart/assert", - "version": "1.5.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4" + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/88e6d84706d09a236046d686bbea96f07b3a34f4", - "reference": "88e6d84706d09a236046d686bbea96f07b3a34f4", + "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", "shasum": "" }, "require": { "php": "^5.3.3 || ^7.0", "symfony/polyfill-ctype": "^1.8" }, + "conflict": { + "vimeo/psalm": "<3.6.0" + }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -1741,20 +1739,20 @@ "check", "validate" ], - "time": "2019-08-24T08:43:50+00:00" + "time": "2019-11-24T13:36:37+00:00" }, { "name": "woocommerce/woocommerce", - "version": "3.8.0", + "version": "3.8.1", "source": { "type": "git", "url": "https://github.com/woocommerce/woocommerce.git", - "reference": "163aaa029dc57b1b04f6ca583bc04c4c4e1b6767" + "reference": "674d7f65ee1bfc10908e6d4a551687a08e0bb4a5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/163aaa029dc57b1b04f6ca583bc04c4c4e1b6767", - "reference": "163aaa029dc57b1b04f6ca583bc04c4c4e1b6767", + "url": "https://api.github.com/repos/woocommerce/woocommerce/zipball/674d7f65ee1bfc10908e6d4a551687a08e0bb4a5", + "reference": "674d7f65ee1bfc10908e6d4a551687a08e0bb4a5", "shasum": "" }, "require": { @@ -1799,7 +1797,7 @@ ], "description": "An eCommerce toolkit that helps you sell anything. Beautifully.", "homepage": "https://woocommerce.com/", - "time": "2019-11-05T09:26:32+00:00" + "time": "2019-11-27T18:18:26+00:00" }, { "name": "woocommerce/woocommerce-blocks", diff --git a/src/Gateways/AbstractGateway.php b/src/Gateways/AbstractGateway.php index 2e0fb25..10caf96 100644 --- a/src/Gateways/AbstractGateway.php +++ b/src/Gateways/AbstractGateway.php @@ -5,6 +5,7 @@ defined( 'ABSPATH' ) || exit; use Exception; +use GlobalPayments\Api\Entities\Reporting\TransactionSummary; use WC_Payment_Gateway_CC; use WC_Order; use GlobalPayments\Api\Entities\Transaction; @@ -21,14 +22,18 @@ abstract class AbstractGateway extends WC_Payment_Gateway_Cc { const TXN_TYPE_VERIFY = 'verify'; // mgmt requests - const TXN_TYPE_REFUND = 'refund'; - const TXN_TYPE_VOID = 'void'; - const TXN_TYPE_CAPTURE = 'capture'; + const TXN_TYPE_REFUND = 'refund'; + const TXN_TYPE_REVERSAL = 'reverse'; + const TXN_TYPE_VOID = 'void'; + const TXN_TYPE_CAPTURE = 'capture'; // transit requests const TXN_TYPE_CREATE_TRANSACTION_KEY = 'getTransactionKey'; const TXN_TYPE_CREATE_MANIFEST = 'createManifest'; + // report requests + const TXN_TYPE_REPORT_TXN_DETAILS = 'transactionDetail'; + /** * Gateway provider. Should be overriden by individual gateway implementations * @@ -88,6 +93,7 @@ abstract class AbstractGateway extends WC_Payment_Gateway_Cc { protected $client; public function __construct() { + $this->client = new Clients\SdkClient(); $this->has_fields = true; $this->supports = array( 'products', @@ -105,7 +111,6 @@ public function __construct() { 'subscription_payment_method_change_admin', 'multiple_subscriptions', ); - $this->client = new Clients\SdkClient(); $this->configure_method_settings(); $this->init_form_fields(); @@ -449,6 +454,43 @@ public function add_payment_method() { ); } + /** + * Handle online refund requests via WP Admin > WooCommerce > Edit Order + * + * @param int $order_id + * @param null|number $amount + * @param string $reason + * + * @return array + */ + public function process_refund( $order_id, $amount = null, $reason = '' ) { + $details = $this->get_transaction_details( $order_id ); + $is_order_txn_id_active = $this->is_transaction_active( $details ); + $txn_type = $is_order_txn_id_active ? self::TXN_TYPE_REVERSAL : self::TXN_TYPE_REFUND; + + $order = new WC_Order( $order_id ); + $request = $this->prepare_request( $txn_type, $order ); + $response = $this->submit_request( $request ); + $is_successful = $this->handle_response( $request, $response ); + + return $is_successful; + } + + /** + * Handle online refund requests via WP Admin > WooCommerce > Edit Order + * + * @param int $order_id + * + * @return TransactionSummary + */ + public function get_transaction_details( $order_id ) { + $order = new WC_Order( $order_id ); + $request = $this->prepare_request( self::TXN_TYPE_REPORT_TXN_DETAILS, $order ); + $response = $this->submit_request( $request ); + + return $response; + } + /** * Creates the necessary request based on the transaction type * @@ -464,6 +506,9 @@ protected function prepare_request( $txn_type, WC_Order $order = null ) { self::TXN_TYPE_VERIFY => Requests\VerifyRequest::class, self::TXN_TYPE_CREATE_TRANSACTION_KEY => Requests\CreateTransactionKeyRequest::class, self::TXN_TYPE_CREATE_MANIFEST => Requests\CreateManifestRequest::class, + self::TXN_TYPE_REFUND => Requests\RefundRequest::class, + self::TXN_TYPE_REVERSAL => Requests\ReversalRequest::class, + self::TXN_TYPE_REPORT_TXN_DETAILS => Requests\TransactionDetailRequest::class, ); if ( ! isset( $map[ $txn_type ] ) ) { @@ -521,4 +566,9 @@ protected function handle_response( Requests\RequestInterface $request, Transact return true; } + + // should be overridden by gateway implementations + protected function is_transaction_active( TransactionSummary $details ) { + return false; + } } diff --git a/src/Gateways/Clients/SdkClient.php b/src/Gateways/Clients/SdkClient.php index fc68029..b77bd6d 100644 --- a/src/Gateways/Clients/SdkClient.php +++ b/src/Gateways/Clients/SdkClient.php @@ -8,12 +8,14 @@ use GlobalPayments\Api\Entities\Enums\AddressType; use GlobalPayments\Api\Gateways\IPaymentGateway; use GlobalPayments\Api\PaymentMethods\CreditCardData; +use GlobalPayments\Api\Services\ReportingService; use GlobalPayments\Api\ServicesConfig; use GlobalPayments\Api\ServicesContainer; use GlobalPayments\WooCommercePaymentGatewayProvider\Data\PaymentTokenData; use GlobalPayments\WooCommercePaymentGatewayProvider\Gateways\AbstractGateway; use GlobalPayments\WooCommercePaymentGatewayProvider\Gateways\Requests\RequestArg; use GlobalPayments\WooCommercePaymentGatewayProvider\Gateways\Requests\RequestInterface; + use WC_Payment_Token_CC; defined( 'ABSPATH' ) || exit; @@ -44,6 +46,12 @@ class SdkClient implements ClientInterface { AbstractGateway::TXN_TYPE_CREATE_MANIFEST, ); + protected $refund_transactions = array( + AbstractGateway::TXN_TYPE_REFUND, + AbstractGateway::TXN_TYPE_REVERSAL, + AbstractGateway::TXN_TYPE_VOID, + ); + /** * Card data * @@ -71,6 +79,10 @@ public function execute() { $this->configure_sdk(); $builder = $this->get_transaction_builder(); + if ( 'transactionDetail' === $this->args['TXN_TYPE'] ) { + return $builder->execute(); + } + if ( ! ( $builder instanceof TransactionBuilder ) ) { return $builder->{$this->get_arg( RequestArg::TXN_TYPE )}(); } @@ -108,6 +120,15 @@ protected function get_transaction_builder() { return ServicesContainer::instance()->getClient(); } + if ( $this->get_arg( RequestArg::TXN_TYPE ) === 'transactionDetail' ) { + return ReportingService::transactionDetail( $this->get_arg( 'GATEWAY_ID' ) ); + } + + if ( in_array( $this->get_arg( RequestArg::TXN_TYPE ), $this->refund_transactions, true ) ) { + $subject = Transaction::fromId( $this->get_arg( 'GATEWAY_ID' ) ); + return $subject->{$this->get_arg( RequestArg::TXN_TYPE )}(); + } + $subject = in_array( $this->get_arg( RequestArg::TXN_TYPE ), $this->auth_transactions, true ) ? $this->card_data : $this->previous_transaction; @@ -148,6 +169,14 @@ protected function prepare_request_objects() { if ( $this->has_arg( RequestArg::SHIPPING_ADDRESS ) ) { $this->prepare_address( AddressType::SHIPPING, $this->get_arg( RequestArg::SHIPPING_ADDRESS ) ); } + + if ( $this->has_arg( RequestArg::DESCRIPTION ) ) { + $this->builder_args['description'] = array( $this->get_arg( RequestArg::DESCRIPTION ) ); + } + + if ( $this->has_arg( RequestArg::AUTH_AMOUNT ) ) { + $this->builder_args['authAmount'] = array( $this->get_arg( RequestArg::AUTH_AMOUNT ) ); + } } protected function prepare_card_data( WC_Payment_Token_CC $token = null ) { diff --git a/src/Gateways/HeartlandGateway.php b/src/Gateways/HeartlandGateway.php index c4d4796..4d62935 100644 --- a/src/Gateways/HeartlandGateway.php +++ b/src/Gateways/HeartlandGateway.php @@ -3,6 +3,7 @@ namespace GlobalPayments\WooCommercePaymentGatewayProvider\Gateways; use GlobalPayments\Api\Entities\Enums\GatewayProvider; +use GlobalPayments\Api\Entities\Reporting\TransactionSummary; defined( 'ABSPATH' ) || exit; @@ -65,4 +66,9 @@ public function get_backend_gateway_options() { 'secretApiKey' => $this->secret_key, ); } + + protected function is_transaction_active( TransactionSummary $details ) { + // @phpcs:ignore WordPress.NamingConventions.ValidVariableName + return 'A' === $details->transactionStatus; + } } diff --git a/src/Gateways/Requests/RefundRequest.php b/src/Gateways/Requests/RefundRequest.php new file mode 100644 index 0000000..11f8ec4 --- /dev/null +++ b/src/Gateways/Requests/RefundRequest.php @@ -0,0 +1,27 @@ +order->get_transaction_id(); + $description = $this->data['refund_reason']; + $refund_amount = $this->data['refund_amount']; + + return array( + RequestArg::CURRENCY => $this->order->get_currency(), + RequestArg::AMOUNT => $refund_amount, + RequestArg::GATEWAY_ID => $gateway_id, + RequestArg::DESCRIPTION => $description, + ); + } +} diff --git a/src/Gateways/Requests/RequestArg.php b/src/Gateways/Requests/RequestArg.php index 4cf1370..592a15f 100644 --- a/src/Gateways/Requests/RequestArg.php +++ b/src/Gateways/Requests/RequestArg.php @@ -11,4 +11,7 @@ abstract class RequestArg { const SERVICES_CONFIG = 'SERVICES_CONFIG'; const SHIPPING_ADDRESS = 'SHIPPING_ADDRESS'; const TXN_TYPE = 'TXN_TYPE'; + const GATEWAY_ID = 'GATEWAY_ID'; + const DESCRIPTION = 'DESCRIPTION'; + const AUTH_AMOUNT = 'AUTH_AMOUNT'; } diff --git a/src/Gateways/Requests/ReversalRequest.php b/src/Gateways/Requests/ReversalRequest.php new file mode 100644 index 0000000..3e5e9e1 --- /dev/null +++ b/src/Gateways/Requests/ReversalRequest.php @@ -0,0 +1,30 @@ +order->get_transaction_id(); + $description = $this->data['refund_reason']; + + $original_amount = wc_format_decimal( $this->order->get_total(), 2 ); + $total_refunded = wc_format_decimal( $this->order->get_total_refunded(), 2 ); + $new_amount = wc_format_decimal( $original_amount - $total_refunded, 2 ); + + return array( + RequestArg::AMOUNT => null !== $this->order ? $this->order->get_total() : null, + RequestArg::AUTH_AMOUNT => $new_amount, + RequestArg::GATEWAY_ID => $gateway_id, + RequestArg::DESCRIPTION => $description, + ); + } +} diff --git a/src/Gateways/Requests/TransactionDetailRequest.php b/src/Gateways/Requests/TransactionDetailRequest.php new file mode 100644 index 0000000..00e0ed7 --- /dev/null +++ b/src/Gateways/Requests/TransactionDetailRequest.php @@ -0,0 +1,23 @@ +order->get_transaction_id(); + + return array( + RequestArg::AMOUNT => null !== $this->order ? $this->order->get_total() : null, + RequestArg::CURRENCY => null !== $this->order ? $this->order->get_currency() : null, + RequestArg::GATEWAY_ID => $gateway_id, + ); + } +} diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..006d2c5 --- /dev/null +++ b/todo.txt @@ -0,0 +1,70 @@ +button / field style matching options: + +1) pull current styles off another/default field +2) textarea in admin for style overrides +3) filter/module overload for styles +4) individual options for style overrides + +tests: + +each scenario below should be tested across all supported gateways + +checkout + new card with no allow card saving + authorize + sale + verify + new card with allow card saving and no save to account + authorize + sale + verify + new card with allow card saving and save to account (store multi-use token) + authorize + sale + verify + new card with allow card saving and save to account (store multi-use token) + overwrite previously stored card + authorize + sale + verify + stored card + authorize + sale + verify + +order pay + new card with no allow card saving + authorize + sale + verify + new card with allow card saving and no save to account + authorize + sale + verify + new card with allow card saving and save to account (store multi-use token) + authorize + sale + verify + new card with allow card saving and save to account (store multi-use token) + overwrite previously stored card + authorize + sale + verify + stored card + authorize + sale + verify + +add payment method + new card (store multi-use token) + authorize + sale + verify + new card (store multi-use token) + overwrite previously stored card + authorize + sale + verify + +admin + void + capture auth + capture verify + refund / reverse