resolve merge conflict

This commit is contained in:
David Remer 2020-10-05 15:35:37 +03:00
commit 2608f19f6b
29 changed files with 1339 additions and 529 deletions

View file

@ -23,6 +23,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
@ -43,7 +44,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WpOop\TransientCache\CachePoolFactory;
return array( return array(
'api.host' => function( $container ) : string { 'api.host' => function( $container ) : string {
@ -214,6 +214,11 @@ return array(
'api.factory.webhook' => static function ( $container ): WebhookFactory { 'api.factory.webhook' => static function ( $container ): WebhookFactory {
return new WebhookFactory(); return new WebhookFactory();
}, },
'api.factory.capture' => static function ( $container ): CaptureFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory( $amount_factory );
},
'api.factory.purchase-unit' => static function ( $container ): PurchaseUnitFactory { 'api.factory.purchase-unit' => static function ( $container ): PurchaseUnitFactory {
$amount_factory = $container->get( 'api.factory.amount' ); $amount_factory = $container->get( 'api.factory.amount' );
@ -277,7 +282,8 @@ return array(
}, },
'api.factory.payments' => static function ( $container ): PaymentsFactory { 'api.factory.payments' => static function ( $container ): PaymentsFactory {
$authorizations_factory = $container->get( 'api.factory.authorization' ); $authorizations_factory = $container->get( 'api.factory.authorization' );
return new PaymentsFactory( $authorizations_factory ); $capture_factory = $container->get( 'api.factory.capture' );
return new PaymentsFactory( $authorizations_factory, $capture_factory );
}, },
'api.factory.authorization' => static function ( $container ): AuthorizationFactory { 'api.factory.authorization' => static function ( $container ): AuthorizationFactory {
return new AuthorizationFactory(); return new AuthorizationFactory();

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
@ -188,4 +189,63 @@ class PaymentsEndpoint {
$authorization = $this->authorizations_factory->from_paypal_response( $json ); $authorization = $this->authorizations_factory->from_paypal_response( $json );
return $authorization; return $authorization;
} }
/**
* Refunds a payment.
*
* @param Refund $refund The refund to be processed.
*
* @return bool
* @throws RuntimeException If the request fails.
*/
public function refund( Refund $refund ) : bool {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $refund->to_array() ),
);
$response = $this->request( $url, $args );
$json = json_decode( $response['body'] );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not refund payment.', 'paypal-payments-for-woocommerce' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
$error = new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
return true;
}
} }

View file

@ -0,0 +1,197 @@
<?php
/**
* The capture entity.
*
* @link https://developer.paypal.com/docs/api/orders/v2/#definition-capture
*
* @package Woocommerce\PayPalCommerce\ApiClient\Entity
*/
declare( strict_types=1 );
namespace Woocommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Capture
*/
class Capture {
/**
* The ID.
*
* @var string
*/
private $id;
/**
* The status.
*
* @var string
*/
private $status;
/**
* The status details.
*
* @var string
*/
private $status_details;
/**
* The amount.
*
* @var Amount
*/
private $amount;
/**
* Whether this is the final capture or not.
*
* @var bool
*/
private $final_capture;
/**
* The seller protection.
*
* @var string
*/
private $seller_protection;
/**
* The invoice id.
*
* @var string
*/
private $invoice_id;
/**
* The custom id.
*
* @var string
*/
private $custom_id;
/**
* Capture constructor.
*
* @param string $id The ID.
* @param string $status The status.
* @param string $status_details The status details.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
*/
public function __construct(
string $id,
string $status,
string $status_details,
Amount $amount,
bool $final_capture,
string $seller_protection,
string $invoice_id,
string $custom_id
) {
$this->id = $id;
$this->status = $status;
$this->status_details = $status_details;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
}
/**
* Returns the ID.
*
* @return string
*/
public function id() : string {
return $this->id;
}
/**
* Returns the status.
*
* @return string
*/
public function status() : string {
return $this->status;
}
/**
* Returns the status details object.
*
* @return \stdClass
*/
public function status_details() : \stdClass {
return (object) array( 'reason' => $this->status_details );
}
/**
* Returns the amount.
*
* @return Amount
*/
public function amount() : Amount {
return $this->amount;
}
/**
* Returns whether this is the final capture or not.
*
* @return bool
*/
public function final_capture() : bool {
return $this->final_capture;
}
/**
* Returns the seller protection object.
*
* @return \stdClass
*/
public function seller_protection() : \stdClass {
return (object) array( 'status' => $this->seller_protection );
}
/**
* Returns the invoice id.
*
* @return string
*/
public function invoice_id() : string {
return $this->invoice_id;
}
/**
* Returns the custom id.
*
* @return string
*/
public function custom_id() : string {
return $this->custom_id;
}
/**
* Returns the entity as array.
*
* @return array
*/
public function to_array() : array {
return array(
'id' => $this->id(),
'status' => $this->status(),
'status_details' => (array) $this->status_details(),
'amount' => $this->amount()->to_array(),
'final_capture' => $this->final_capture(),
'seller_protection' => (array) $this->seller_protection(),
'invoice_id' => $this->invoice_id(),
'custom_id' => $this->custom_id(),
);
}
}

View file

@ -21,13 +21,34 @@ class Payments {
*/ */
private $authorizations; private $authorizations;
/**
* The Captures.
*
* @var Capture[]
*/
private $captures;
/** /**
* Payments constructor. * Payments constructor.
* *
* @param Authorization ...$authorizations The Authorizations. * @param array $authorizations The Authorizations.
* @param array $captures The Captures.
*/ */
public function __construct( Authorization ...$authorizations ) { public function __construct( array $authorizations, array $captures ) {
foreach ( $authorizations as $key => $authorization ) {
if ( is_a( $authorization, Authorization::class ) ) {
continue;
}
unset( $authorizations[ $key ] );
}
foreach ( $captures as $key => $capture ) {
if ( is_a( $capture, Capture::class ) ) {
continue;
}
unset( $captures[ $key ] );
}
$this->authorizations = $authorizations; $this->authorizations = $authorizations;
$this->captures = $captures;
} }
/** /**
@ -43,6 +64,12 @@ class Payments {
}, },
$this->authorizations() $this->authorizations()
), ),
'captures' => array_map(
static function ( Capture $capture ): array {
return $capture->to_array();
},
$this->captures()
),
); );
} }
@ -54,4 +81,13 @@ class Payments {
public function authorizations(): array { public function authorizations(): array {
return $this->authorizations; return $this->authorizations;
} }
/**
* Returns the Captures.
*
* @return Capture[]
**/
public function captures(): array {
return $this->captures;
}
} }

View file

@ -0,0 +1,118 @@
<?php
/**
* The refund object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Refund
*/
class Refund {
/**
* The Capture.
*
* @var Capture
*/
private $capture;
/**
* The invoice id.
*
* @var string
*/
private $invoice_id;
/**
* The note to the payer.
*
* @var string
*/
private $note_to_payer;
/**
* The Amount.
*
* @var Amount|null
*/
private $amount;
/**
* Refund constructor.
*
* @param Capture $capture The capture where the refund is supposed to be applied at.
* @param string $invoice_id The invoice id.
* @param string $note_to_payer The note to the payer.
* @param Amount|null $amount The Amount.
*/
public function __construct(
Capture $capture,
string $invoice_id,
string $note_to_payer = '',
Amount $amount = null
) {
$this->capture = $capture;
$this->invoice_id = $invoice_id;
$this->note_to_payer = $note_to_payer;
$this->amount = $amount;
}
/**
* Returns the capture for the refund.
*
* @return Capture
*/
public function for_capture() : Capture {
return $this->capture;
}
/**
* Return the invoice id.
*
* @return string
*/
public function invoice_id() : string {
return $this->invoice_id;
}
/**
* Returns the note to the payer.
*
* @return string
*/
public function note_to_payer() : string {
return $this->note_to_payer;
}
/**
* Returns the Amount.
*
* @return Amount|null
*/
public function amount() {
return $this->amount;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() : array {
$data = array(
'invoice_id' => $this->invoice_id(),
);
if ( $this->note_to_payer() ) {
$data['note_to_payer'] = $this->note_to_payer();
}
if ( $this->amount() ) {
$data['amount'] = $this->amount()->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,57 @@
<?php
/**
* The capture factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
/**
* Class CaptureFactory
*/
class CaptureFactory {
/**
* The Amount factory.
*
* @var AmountFactory
*/
private $amount_factory;
/**
* CaptureFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
*/
public function __construct( AmountFactory $amount_factory ) {
$this->amount_factory = $amount_factory;
}
/**
* Returns the capture object based off the PayPal response.
*
* @param \stdClass $data The PayPal response.
*
* @return Capture
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = isset( $data->status_details->reason ) ? (string) $data->status_details->reason : '';
return new Capture(
(string) $data->id,
(string) $data->status,
$reason,
$this->amount_factory->from_paypal_response( $data->amount ),
(bool) $data->final_capture,
(string) $data->seller_protection->status,
(string) $data->invoice_id,
(string) $data->custom_id
);
}
}

View file

@ -36,6 +36,42 @@ class PayerFactory {
$this->address_factory = $address_factory; $this->address_factory = $address_factory;
} }
/**
* Returns a Payer entity from a WooCommerce order.
*
* @param \WC_Order $wc_order The WooCommerce order.
*
* @return Payer
*/
public function from_wc_order( \WC_Order $wc_order ): Payer {
$payer_id = '';
$birthdate = null;
$phone = null;
if ( $wc_order->get_billing_phone() ) {
// make sure the phone number contains only numbers and is max 14. chars long.
$national_number = $wc_order->get_billing_phone();
$national_number = preg_replace( '/[^0-9]/', '', $national_number );
$national_number = substr( $national_number, 0, 14 );
$phone = new PhoneWithType(
'HOME',
new Phone( $national_number )
);
}
return new Payer(
new PayerName(
$wc_order->get_billing_first_name(),
$wc_order->get_billing_last_name()
),
$wc_order->get_billing_email(),
$payer_id,
$this->address_factory->from_wc_order( $wc_order, 'billing' ),
$birthdate,
$phone
);
}
/** /**
* Returns a Payer object based off a WooCommerce customer. * Returns a Payer object based off a WooCommerce customer.
* *

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory; namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
/** /**
@ -24,16 +25,26 @@ class PaymentsFactory {
*/ */
private $authorization_factory; private $authorization_factory;
/**
* The Capture factory.
*
* @var CaptureFactory
*/
private $capture_factory;
/** /**
* PaymentsFactory constructor. * PaymentsFactory constructor.
* *
* @param AuthorizationFactory $authorization_factory The AuthorizationFactory. * @param AuthorizationFactory $authorization_factory The Authorization factory.
* @param CaptureFactory $capture_factory The Capture factory.
*/ */
public function __construct( public function __construct(
AuthorizationFactory $authorization_factory AuthorizationFactory $authorization_factory,
CaptureFactory $capture_factory
) { ) {
$this->authorization_factory = $authorization_factory; $this->authorization_factory = $authorization_factory;
$this->capture_factory = $capture_factory;
} }
/** /**
@ -50,7 +61,13 @@ class PaymentsFactory {
}, },
isset( $data->authorizations ) ? $data->authorizations : array() isset( $data->authorizations ) ? $data->authorizations : array()
); );
$payments = new Payments( ...$authorizations ); $captures = array_map(
function ( \stdClass $authorization ): Capture {
return $this->capture_factory->from_paypal_response( $authorization );
},
isset( $data->captures ) ? $data->captures : array()
);
$payments = new Payments( $authorizations, $captures );
return $payments; return $payments;
} }
} }

View file

@ -38,115 +38,7 @@ class DccApplies {
'SGD', 'SGD',
'USD', 'USD',
), ),
'BE' => array( 'ES' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'BG' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'CY' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'CZ' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'DK' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'EE' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'FI' => array(
'AUD', 'AUD',
'CAD', 'CAD',
'CHF', 'CHF',
@ -182,25 +74,7 @@ class DccApplies {
'SGD', 'SGD',
'USD', 'USD',
), ),
'GR' => array( 'GB' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'HU' => array(
'AUD', 'AUD',
'CAD', 'CAD',
'CHF', 'CHF',
@ -236,258 +110,6 @@ class DccApplies {
'SGD', 'SGD',
'USD', 'USD',
), ),
'LV' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'LI' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'LT' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'LU' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'MT' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'NL' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'NO' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'PL' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'PT' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'RO' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'SK' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'SI' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'ES' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'SE' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
'US' => array( 'US' => array(
'AUD', 'AUD',
'CAD', 'CAD',
@ -496,25 +118,45 @@ class DccApplies {
'JPY', 'JPY',
'USD', 'USD',
), ),
'GB' => array( );
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
/**
* Which countries support which credit cards. Empty credit card arrays mean no restriction on
* currency. Otherwise only the currencies in the array are supported.
*
* @var array
*/
private $country_card_matrix = array(
'AU' => array(
'mastercard' => array(),
'visa' => array(),
),
'ES' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'FR' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'GB' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'GBP', 'USD' ),
),
'IT' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'EUR' ),
),
'US' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'USD' ),
'discover' => array( 'USD' ),
),
); );
/** /**
@ -523,8 +165,7 @@ class DccApplies {
* @return bool * @return bool
*/ */
public function for_country_currency(): bool { public function for_country_currency(): bool {
$region = wc_get_base_location(); $country = $this->country();
$country = $region['country'];
$currency = get_woocommerce_currency(); $currency = get_woocommerce_currency();
if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) { if ( ! in_array( $country, array_keys( $this->allowed_country_currency_matrix ), true ) ) {
return false; return false;
@ -532,4 +173,67 @@ class DccApplies {
$applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true ); $applies = in_array( $currency, $this->allowed_country_currency_matrix[ $country ], true );
return $applies; return $applies;
} }
/**
* Returns credit cards, which can be used.
*
* @return array
*/
public function valid_cards() : array {
$country = $this->country();
$cards = array();
if ( ! isset( $this->country_card_matrix[ $country ] ) ) {
return $cards;
}
$supported_currencies = $this->country_card_matrix[ $country ];
foreach ( $supported_currencies as $card => $currencies ) {
if ( $this->can_process_card( $card ) ) {
$cards[] = $card;
}
}
if ( in_array( 'amex', $cards, true ) ) {
$cards[] = 'american-express';
}
if ( in_array( 'mastercard', $cards, true ) ) {
$cards[] = 'master-card';
}
return $cards;
}
/**
* Whether a card can be used or not.
*
* @param string $card The card.
*
* @return bool
*/
public function can_process_card( string $card ) : bool {
$country = $this->country();
if ( ! isset( $this->country_card_matrix[ $country ] ) ) {
return false;
}
if ( ! isset( $this->country_card_matrix[ $country ][ $card ] ) ) {
return false;
}
/**
* If the supported currencies array is empty, there are no
* restrictions, which currencies are supported by a card.
*/
$supported_currencies = $this->country_card_matrix[ $country ][ $card ];
$currency = get_woocommerce_currency();
return empty( $supported_currencies ) || in_array( $currency, $supported_currencies, true );
}
/**
* Returns the country code of the shop.
*
* @return string
*/
private function country() : string {
$region = wc_get_base_location();
$country = $region['country'];
return $country;
}
} }

View file

@ -2,15 +2,18 @@ import MiniCartBootstap from './modules/ContextBootstrap/MiniCartBootstap';
import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap'; import SingleProductBootstap from './modules/ContextBootstrap/SingleProductBootstap';
import CartBootstrap from './modules/ContextBootstrap/CartBootstap'; import CartBootstrap from './modules/ContextBootstrap/CartBootstap';
import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap'; import CheckoutBootstap from './modules/ContextBootstrap/CheckoutBootstap';
import PayNowBootstrap from "./modules/ContextBootstrap/PayNowBootstrap";
import Renderer from './modules/Renderer/Renderer'; import Renderer from './modules/Renderer/Renderer';
import ErrorHandler from './modules/ErrorHandler'; import ErrorHandler from './modules/ErrorHandler';
import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer";
import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler"; import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler";
import MessageRenderer from "./modules/Renderer/MessageRenderer"; import MessageRenderer from "./modules/Renderer/MessageRenderer";
import Spinner from "./modules/Helper/Spinner";
const bootstrap = () => { const bootstrap = () => {
const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic); const errorHandler = new ErrorHandler(PayPalCommerceGateway.labels.error.generic);
const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler); const spinner = new Spinner();
const creditCardRenderer = new CreditCardRenderer(PayPalCommerceGateway, errorHandler, spinner);
const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway); const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway);
const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages); const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages);
const context = PayPalCommerceGateway.context; const context = PayPalCommerceGateway.context;
@ -46,12 +49,22 @@ const bootstrap = () => {
const checkoutBootstap = new CheckoutBootstap( const checkoutBootstap = new CheckoutBootstap(
PayPalCommerceGateway, PayPalCommerceGateway,
renderer, renderer,
messageRenderer messageRenderer,
spinner
); );
checkoutBootstap.init(); checkoutBootstap.init();
} }
if (context === 'pay-now' ) {
const payNowBootstrap = new PayNowBootstrap(
PayPalCommerceGateway,
renderer,
messageRenderer
);
payNowBootstrap.init();
}
if (context !== 'checkout') { if (context !== 'checkout') {
messageRenderer.render(); messageRenderer.render();
} }

View file

@ -3,12 +3,14 @@ import {payerData} from "../Helper/PayerData";
class CheckoutActionHandler { class CheckoutActionHandler {
constructor(config, errorHandler) { constructor(config, errorHandler, spinner) {
this.config = config; this.config = config;
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
this.spinner = spinner;
} }
configuration() { configuration() {
const spinner = this.spinner;
const createOrder = (data, actions) => { const createOrder = (data, actions) => {
const payer = payerData(); const payer = payerData();
const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ? const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ?
@ -16,7 +18,8 @@ class CheckoutActionHandler {
const errorHandler = this.errorHandler; const errorHandler = this.errorHandler;
const formValues = jQuery('form.checkout').serialize(); const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review';
const formValues = jQuery(formSelector).serialize();
return fetch(this.config.ajax.create_order.endpoint, { return fetch(this.config.ajax.create_order.endpoint, {
method: 'POST', method: 'POST',
@ -25,12 +28,14 @@ class CheckoutActionHandler {
payer, payer,
bn_code:bnCode, bn_code:bnCode,
context:this.config.context, context:this.config.context,
order_id:this.config.order_id,
form:formValues form:formValues
}) })
}).then(function (res) { }).then(function (res) {
return res.json(); return res.json();
}).then(function (data) { }).then(function (data) {
if (!data.success) { if (!data.success) {
spinner.unblock();
errorHandler.message(data.data.message, true); errorHandler.message(data.data.message, true);
return; return;
} }
@ -38,13 +43,13 @@ class CheckoutActionHandler {
input.setAttribute('type', 'hidden'); input.setAttribute('type', 'hidden');
input.setAttribute('name', 'ppcp-resume-order'); input.setAttribute('name', 'ppcp-resume-order');
input.setAttribute('value', data.data.purchase_units[0].custom_id); input.setAttribute('value', data.data.purchase_units[0].custom_id);
document.querySelector('form.checkout').append(input); document.querySelector(formSelector).append(input);
return data.data.id; return data.data.id;
}); });
} }
return { return {
createOrder, createOrder,
onApprove:onApprove(this, this.errorHandler), onApprove:onApprove(this, this.errorHandler, this.spinner),
onError: (error) => { onError: (error) => {
this.errorHandler.genericError(); this.errorHandler.genericError();
} }

View file

@ -2,10 +2,11 @@ import ErrorHandler from '../ErrorHandler';
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler'; import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
class CheckoutBootstap { class CheckoutBootstap {
constructor(gateway, renderer, messages) { constructor(gateway, renderer, messages, spinner) {
this.gateway = gateway; this.gateway = gateway;
this.renderer = renderer; this.renderer = renderer;
this.messages = messages this.messages = messages;
this.spinner = spinner;
} }
init() { init() {
@ -41,6 +42,7 @@ class CheckoutBootstap {
const actionHandler = new CheckoutActionHandler( const actionHandler = new CheckoutActionHandler(
PayPalCommerceGateway, PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic), new ErrorHandler(this.gateway.labels.error.generic),
this.spinner
); );
this.renderer.render( this.renderer.render(

View file

@ -0,0 +1,80 @@
import ErrorHandler from '../ErrorHandler';
import CheckoutActionHandler from '../ActionHandler/CheckoutActionHandler';
class PayNowBootstrap {
constructor(gateway, renderer, messages) {
this.gateway = gateway;
this.renderer = renderer;
this.messages = messages
}
init() {
this.render();
jQuery(document.body).on('updated_checkout', () => {
this.render();
});
jQuery(document.body).
on('updated_checkout payment_method_selected', () => {
this.switchBetweenPayPalandOrderButton();
});
this.switchBetweenPayPalandOrderButton();
}
shouldRender() {
if (document.querySelector(this.gateway.button.cancel_wrapper)) {
return false;
}
return document.querySelector(this.gateway.button.wrapper) !== null || document.querySelector(this.gateway.hosted_fields.wrapper) !== null;
}
render() {
if (!this.shouldRender()) {
return;
}
if (document.querySelector(this.gateway.hosted_fields.wrapper + '>div')) {
document.querySelector(this.gateway.hosted_fields.wrapper + '>div').setAttribute('style', '');
}
const actionHandler = new CheckoutActionHandler(
PayPalCommerceGateway,
new ErrorHandler(this.gateway.labels.error.generic),
);
this.renderer.render(
this.gateway.button.wrapper,
this.gateway.hosted_fields.wrapper,
actionHandler.configuration(),
);
}
switchBetweenPayPalandOrderButton() {
const currentPaymentMethod = jQuery(
'input[name="payment_method"]:checked').val();
if (currentPaymentMethod !== 'ppcp-gateway' && currentPaymentMethod !== 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.messages.wrapper);
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
jQuery('#place_order').show();
}
else {
jQuery('#place_order').hide();
if (currentPaymentMethod === 'ppcp-gateway') {
this.renderer.showButtons(this.gateway.button.wrapper);
this.renderer.showButtons(this.gateway.messages.wrapper);
this.messages.render();
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
}
if (currentPaymentMethod === 'ppcp-credit-card-gateway') {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.messages.wrapper);
this.renderer.showButtons(this.gateway.hosted_fields.wrapper);
}
}
}
}
export default PayNowBootstrap;

View file

@ -0,0 +1,23 @@
class Spinner {
constructor() {
this.target = 'form.woocommerce-checkout';
}
block() {
jQuery( this.target ).block({
message: null,
overlayCSS: {
background: '#fff',
opacity: 0.6
}
});
}
unblock() {
jQuery( this.target ).unblock();
}
}
export default Spinner;

View file

@ -1,5 +1,6 @@
const onApprove = (context, errorHandler) => { const onApprove = (context, errorHandler, spinner) => {
return (data, actions) => { return (data, actions) => {
spinner.block();
return fetch(context.config.ajax.approve_order.endpoint, { return fetch(context.config.ajax.approve_order.endpoint, {
method: 'POST', method: 'POST',
body: JSON.stringify({ body: JSON.stringify({
@ -9,10 +10,14 @@ const onApprove = (context, errorHandler) => {
}).then((res)=>{ }).then((res)=>{
return res.json(); return res.json();
}).then((data)=>{ }).then((data)=>{
spinner.unblock();
if (!data.success) { if (!data.success) {
errorHandler.genericError(); if (data.data.code === 100) {
console.error(data); errorHandler.message(data.data.message);
if (typeof actions.restart !== 'undefined') { } else {
errorHandler.genericError();
}
if (typeof actions !== 'undefined' && typeof actions.restart !== 'undefined') {
return actions.restart(); return actions.restart();
} }
throw new Error(data.data.message); throw new Error(data.data.message);

View file

@ -2,15 +2,20 @@ import dccInputFactory from "../Helper/DccInputFactory";
class CreditCardRenderer { class CreditCardRenderer {
constructor(defaultConfig, errorHandler) { constructor(defaultConfig, errorHandler, spinner) {
this.defaultConfig = defaultConfig; this.defaultConfig = defaultConfig;
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
this.spinner = spinner;
this.cardValid = false;
} }
render(wrapper, contextConfig) { render(wrapper, contextConfig) {
if ( if (
this.defaultConfig.context !== 'checkout' (
this.defaultConfig.context !== 'checkout'
&& this.defaultConfig.context !== 'pay-now'
)
|| wrapper === null || wrapper === null
|| document.querySelector(wrapper) === null || document.querySelector(wrapper) === null
) { ) {
@ -87,6 +92,7 @@ class CreditCardRenderer {
} }
}).then(hostedFields => { }).then(hostedFields => {
const submitEvent = (event) => { const submitEvent = (event) => {
this.spinner.block();
if (event) { if (event) {
event.preventDefault(); event.preventDefault();
} }
@ -96,7 +102,7 @@ class CreditCardRenderer {
return state.fields[key].isValid; return state.fields[key].isValid;
}); });
if (formValid) { if (formValid && this.cardValid) {
let vault = document.querySelector(wrapper + ' .ppcp-credit-card-vault') ? let vault = document.querySelector(wrapper + ' .ppcp-credit-card-vault') ?
document.querySelector(wrapper + ' .ppcp-credit-card-vault').checked : false; document.querySelector(wrapper + ' .ppcp-credit-card-vault').checked : false;
@ -107,15 +113,26 @@ class CreditCardRenderer {
vault vault
}).then((payload) => { }).then((payload) => {
payload.orderID = payload.orderId; payload.orderID = payload.orderId;
this.spinner.unblock();
return contextConfig.onApprove(payload); return contextConfig.onApprove(payload);
}); });
} else { } else {
this.errorHandler.message(this.defaultConfig.hosted_fields.labels.fields_not_valid); this.spinner.unblock();
const message = ! this.cardValid ? this.defaultConfig.hosted_fields.labels.card_not_supported : this.defaultConfig.hosted_fields.labels.fields_not_valid;
this.errorHandler.message(message);
} }
} }
hostedFields.on('inputSubmitRequest', function () { hostedFields.on('inputSubmitRequest', function () {
submitEvent(null); submitEvent(null);
}); });
hostedFields.on('cardTypeChange', (event) => {
if ( ! event.cards.length ) {
this.cardValid = false;
return;
}
const validCards = this.defaultConfig.hosted_fields.valid_cards;
this.cardValid = validCards.indexOf(event.cards[0].type) !== -1;
})
document.querySelector(wrapper + ' button').addEventListener( document.querySelector(wrapper + ' button').addEventListener(
'click', 'click',
submitEvent submitEvent

View file

@ -9,7 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button; namespace WooCommerce\PayPalCommerce\Button;
use Dhii\Data\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
@ -107,16 +106,18 @@ return array(
return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store ); return new ChangeCartEndpoint( $cart, $shipping, $request_data, $repository, $data_store );
}, },
'button.endpoint.create-order' => static function ( $container ): CreateOrderEndpoint { 'button.endpoint.create-order' => static function ( $container ): CreateOrderEndpoint {
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );
$repository = $container->get( 'api.repository.cart' ); $cart_repository = $container->get( 'api.repository.cart' );
$order_endpoint = $container->get( 'api.endpoint.order' ); $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' );
$payer_factory = $container->get( 'api.factory.payer' ); $order_endpoint = $container->get( 'api.endpoint.order' );
$session_handler = $container->get( 'session.handler' ); $payer_factory = $container->get( 'api.factory.payer' );
$settings = $container->get( 'wcgateway.settings' ); $session_handler = $container->get( 'session.handler' );
$early_order_handler = $container->get( 'button.helper.early-order-handler' ); $settings = $container->get( 'wcgateway.settings' );
$early_order_handler = $container->get( 'button.helper.early-order-handler' );
return new CreateOrderEndpoint( return new CreateOrderEndpoint(
$request_data, $request_data,
$repository, $cart_repository,
$purchase_unit_factory,
$order_endpoint, $order_endpoint,
$payer_factory, $payer_factory,
$session_handler, $session_handler,
@ -134,10 +135,19 @@ return array(
}, },
'button.endpoint.approve-order' => static function ( $container ): ApproveOrderEndpoint { 'button.endpoint.approve-order' => static function ( $container ): ApproveOrderEndpoint {
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );
$order_endpoint = $container->get( 'api.endpoint.order' ); $order_endpoint = $container->get( 'api.endpoint.order' );
$session_handler = $container->get( 'session.handler' ); $session_handler = $container->get( 'session.handler' );
$three_d_secure = $container->get( 'button.helper.three-d-secure' ); $three_d_secure = $container->get( 'button.helper.three-d-secure' );
return new ApproveOrderEndpoint( $request_data, $order_endpoint, $session_handler, $three_d_secure ); $settings = $container->get( 'wcgateway.settings' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
return new ApproveOrderEndpoint(
$request_data,
$order_endpoint,
$session_handler,
$three_d_secure,
$settings,
$dcc_applies
);
}, },
'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint { 'button.endpoint.data-client-id' => static function( $container ) : DataClientIdEndpoint {
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );

View file

@ -177,6 +177,15 @@ class SmartButton implements SmartButtonInterface {
), ),
11 11
); );
add_action(
'woocommerce_pay_order_after_submit',
array(
$this,
'dcc_renderer',
),
11
);
} }
return true; return true;
} }
@ -232,6 +241,14 @@ class SmartButton implements SmartButtonInterface {
), ),
11 11
); );
add_action(
'woocommerce_pay_order_after_submit',
array(
$this,
'message_renderer',
),
11
);
} }
return true; return true;
} }
@ -294,6 +311,7 @@ class SmartButton implements SmartButtonInterface {
} }
add_action( 'woocommerce_review_order_after_submit', array( $this, 'button_renderer' ), 10 ); add_action( 'woocommerce_review_order_after_submit', array( $this, 'button_renderer' ), 10 );
add_action( 'woocommerce_pay_order_after_submit', array( $this, 'button_renderer' ), 10 );
return true; return true;
} }
@ -321,7 +339,7 @@ class SmartButton implements SmartButtonInterface {
$load_script = true; $load_script = true;
} }
if ( is_checkout() && $this->can_render_dcc() ) { if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) && $this->can_render_dcc() ) {
wp_enqueue_style( wp_enqueue_style(
'ppcp-hosted-fields', 'ppcp-hosted-fields',
$this->module_url . '/assets/css/hosted-fields.css', $this->module_url . '/assets/css/hosted-fields.css',
@ -474,7 +492,8 @@ class SmartButton implements SmartButtonInterface {
*/ */
private function can_render_dcc() : bool { private function can_render_dcc() : bool {
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); $can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency();
return $can_render;
} }
/** /**
@ -502,6 +521,8 @@ class SmartButton implements SmartButtonInterface {
esc_html__( 'Save your card', 'paypal-payments-for-woocommerce' ) esc_html__( 'Save your card', 'paypal-payments-for-woocommerce' )
) : ''; ) : '';
$label = 'checkout' === $this->context() ? __( 'Place order', 'paypal-payments-for-woocommerce' ) : __( 'Pay for order', 'paypal-payments-for-woocommerce' );
printf( printf(
'<div id="%1$s" style="display:none;"> '<div id="%1$s" style="display:none;">
<button class="button alt">%6$s</button> <button class="button alt">%6$s</button>
@ -512,7 +533,7 @@ class SmartButton implements SmartButtonInterface {
esc_html__( 'CVV', 'paypal-payments-for-woocommerce' ), esc_html__( 'CVV', 'paypal-payments-for-woocommerce' ),
//phpcs:ignore //phpcs:ignore
$save_card, $save_card,
esc_html__( 'Place order', 'paypal-payments-for-woocommerce' ) esc_html( $label )
); );
} }
@ -555,6 +576,8 @@ class SmartButton implements SmartButtonInterface {
* @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException If a setting hasn't been found. * @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException If a setting hasn't been found.
*/ */
private function localize_script(): array { private function localize_script(): array {
global $wp;
$this->request_data->enqueue_nonce_fix(); $this->request_data->enqueue_nonce_fix();
$localize = array( $localize = array(
'script_attributes' => $this->attributes(), 'script_attributes' => $this->attributes(),
@ -615,7 +638,12 @@ class SmartButton implements SmartButtonInterface {
'Unfortunatly, your credit card details are not valid.', 'Unfortunatly, your credit card details are not valid.',
'paypal-payments-for-woocommerce' 'paypal-payments-for-woocommerce'
), ),
'card_not_supported' => __(
'Unfortunatly, we do not support your credit card.',
'paypal-payments-for-woocommerce'
),
), ),
'valid_cards' => $this->dcc_applies->valid_cards(),
), ),
'messages' => $this->message_values(), 'messages' => $this->message_values(),
'labels' => array( 'labels' => array(
@ -626,6 +654,7 @@ class SmartButton implements SmartButtonInterface {
), ),
), ),
), ),
'order_id' => 'pay-now' === $this->context() ? absint( $wp->query_vars['order-pay'] ) : 0,
); );
if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) { if ( $this->style_for_context( 'layout', 'mini-cart' ) !== 'horizontal' ) {
@ -698,9 +727,9 @@ class SmartButton implements SmartButtonInterface {
if ( 'GB' === $country ) { if ( 'GB' === $country ) {
$disable_funding[] = 'card'; $disable_funding[] = 'card';
} }
$params['disable-funding'] = implode( ',', array_unique( $disable_funding ) ); $params['disable-funding'] = implode( ',', array_unique( $disable_funding ) );
$smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' );
$smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' );
return $smart_button_url; return $smart_button_url;
} }
@ -818,6 +847,9 @@ class SmartButton implements SmartButtonInterface {
if ( is_checkout() && ! $this->session_handler->order() ) { if ( is_checkout() && ! $this->session_handler->order() ) {
$context = 'checkout'; $context = 'checkout';
} }
if ( is_checkout_pay_page() ) {
$context = 'pay-now';
}
return $context; return $context;
} }

View file

@ -13,9 +13,11 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException; use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/** /**
* Class ApproveOrderEndpoint * Class ApproveOrderEndpoint
@ -53,6 +55,20 @@ class ApproveOrderEndpoint implements EndpointInterface {
*/ */
private $threed_secure; private $threed_secure;
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* The DCC applies object.
*
* @var DccApplies
*/
private $dcc_applies;
/** /**
* ApproveOrderEndpoint constructor. * ApproveOrderEndpoint constructor.
* *
@ -60,18 +76,24 @@ class ApproveOrderEndpoint implements EndpointInterface {
* @param OrderEndpoint $order_endpoint The order endpoint. * @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler. * @param SessionHandler $session_handler The session handler.
* @param ThreeDSecure $three_d_secure The 3d secure helper object. * @param ThreeDSecure $three_d_secure The 3d secure helper object.
* @param Settings $settings The settings.
* @param DccApplies $dcc_applies The DCC applies object.
*/ */
public function __construct( public function __construct(
RequestData $request_data, RequestData $request_data,
OrderEndpoint $order_endpoint, OrderEndpoint $order_endpoint,
SessionHandler $session_handler, SessionHandler $session_handler,
ThreeDSecure $three_d_secure ThreeDSecure $three_d_secure,
Settings $settings,
DccApplies $dcc_applies
) { ) {
$this->request_data = $request_data; $this->request_data = $request_data;
$this->api_endpoint = $order_endpoint; $this->api_endpoint = $order_endpoint;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->threed_secure = $three_d_secure; $this->threed_secure = $three_d_secure;
$this->settings = $settings;
$this->dcc_applies = $dcc_applies;
} }
/** /**
@ -110,6 +132,23 @@ class ApproveOrderEndpoint implements EndpointInterface {
} }
if ( $order->payment_source() && $order->payment_source()->card() ) { if ( $order->payment_source() && $order->payment_source()->card() ) {
if ( $this->settings->has( 'disable_cards' ) ) {
$disabled_cards = (array) $this->settings->get( 'disable_cards' );
$card = strtolower( $order->payment_source()->card()->brand() );
if ( 'master_card' === $card ) {
$card = 'mastercard';
}
if ( ! $this->dcc_applies->can_process_card( $card ) || in_array( $card, $disabled_cards, true ) ) {
throw new RuntimeException(
__(
'Unfortunately, we do not accept this card.',
'paypal-payments-for-woocommerce'
),
100
);
}
}
$proceed = $this->threed_secure->proceed_with_order( $order ); $proceed = $this->threed_secure->proceed_with_order( $order );
if ( ThreeDSecure::RETRY === $proceed ) { if ( ThreeDSecure::RETRY === $proceed ) {
throw new RuntimeException( throw new RuntimeException(

View file

@ -10,9 +10,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint; namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository; use WooCommerce\PayPalCommerce\ApiClient\Repository\CartRepository;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
@ -39,7 +41,14 @@ class CreateOrderEndpoint implements EndpointInterface {
* *
* @var CartRepository * @var CartRepository
*/ */
private $repository; private $cart_repository;
/**
* The PurchaseUnit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/** /**
* The order endpoint. * The order endpoint.
@ -86,17 +95,19 @@ class CreateOrderEndpoint implements EndpointInterface {
/** /**
* CreateOrderEndpoint constructor. * CreateOrderEndpoint constructor.
* *
* @param RequestData $request_data The RequestData object. * @param RequestData $request_data The RequestData object.
* @param CartRepository $repository The CartRepository object. * @param CartRepository $cart_repository The CartRepository object.
* @param OrderEndpoint $order_endpoint The OrderEndpoint object. * @param PurchaseUnitFactory $purchase_unit_factory The Purchaseunit factory.
* @param PayerFactory $payer_factory The PayerFactory object. * @param OrderEndpoint $order_endpoint The OrderEndpoint object.
* @param SessionHandler $session_handler The SessionHandler object. * @param PayerFactory $payer_factory The PayerFactory object.
* @param Settings $settings The Settings object. * @param SessionHandler $session_handler The SessionHandler object.
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object. * @param Settings $settings The Settings object.
* @param EarlyOrderHandler $early_order_handler The EarlyOrderHandler object.
*/ */
public function __construct( public function __construct(
RequestData $request_data, RequestData $request_data,
CartRepository $repository, CartRepository $cart_repository,
PurchaseUnitFactory $purchase_unit_factory,
OrderEndpoint $order_endpoint, OrderEndpoint $order_endpoint,
PayerFactory $payer_factory, PayerFactory $payer_factory,
SessionHandler $session_handler, SessionHandler $session_handler,
@ -104,13 +115,14 @@ class CreateOrderEndpoint implements EndpointInterface {
EarlyOrderHandler $early_order_handler EarlyOrderHandler $early_order_handler
) { ) {
$this->request_data = $request_data; $this->request_data = $request_data;
$this->repository = $repository; $this->cart_repository = $cart_repository;
$this->api_endpoint = $order_endpoint; $this->purchase_unit_factory = $purchase_unit_factory;
$this->payer_factory = $payer_factory; $this->api_endpoint = $order_endpoint;
$this->session_handler = $session_handler; $this->payer_factory = $payer_factory;
$this->settings = $settings; $this->session_handler = $session_handler;
$this->early_order_handler = $early_order_handler; $this->settings = $settings;
$this->early_order_handler = $early_order_handler;
} }
/** /**
@ -130,37 +142,37 @@ class CreateOrderEndpoint implements EndpointInterface {
*/ */
public function handle_request(): bool { public function handle_request(): bool {
try { try {
$data = $this->request_data->read_request( $this->nonce() ); $data = $this->request_data->read_request( $this->nonce() );
$purchase_units = $this->repository->all(); $wc_order = null;
$payer = null; if ( 'pay-now' === $data['context'] ) {
if ( isset( $data['payer'] ) && $data['payer'] ) { $wc_order = wc_get_order( (int) $data['order_id'] );
if ( isset( $data['payer']['phone']['phone_number']['national_number'] ) ) { if ( ! is_a( $wc_order, \WC_Order::class ) ) {
// make sure the phone number contains only numbers and is max 14. chars long. wp_send_json_error(
$number = $data['payer']['phone']['phone_number']['national_number']; array(
$number = preg_replace( '/[^0-9]/', '', $number ); 'name' => 'order-not-found',
$number = substr( $number, 0, 14 ); 'message' => __( 'Order not found', 'paypal-payments-for-woocommerce' ),
$data['payer']['phone']['phone_number']['national_number'] = $number; 'code' => 0,
'details' => array(),
)
);
} }
$payer = $this->payer_factory->from_paypal_response( json_decode( wp_json_encode( $data['payer'] ) ) ); $purchase_units = array( $this->purchase_unit_factory->from_wc_order( $wc_order ) );
} else {
$purchase_units = $this->cart_repository->all();
} }
$bn_code = isset( $data['bn_code'] ) ? (string) $data['bn_code'] : '';
if ( $bn_code ) { $this->set_bn_code( $data );
$this->session_handler->replace_bn_code( $bn_code ); $order = $this->api_endpoint->create(
$this->api_endpoint->with_bn_code( $bn_code );
}
$payee_preferred = $this->settings->has( 'payee_preferred' )
&& $this->settings->get( 'payee_preferred' ) ?
PaymentMethod::PAYEE_PREFERRED_IMMEDIATE_PAYMENT_REQUIRED
: PaymentMethod::PAYEE_PREFERRED_UNRESTRICTED;
$payment_method = new PaymentMethod( $payee_preferred );
$order = $this->api_endpoint->create(
$purchase_units, $purchase_units,
$payer, $this->payer( $data, $wc_order ),
null, null,
$payment_method $this->payment_method()
); );
if ( 'checkout' === $data['context'] ) { if ( 'checkout' === $data['context'] ) {
$this->validateForm( $data['form'], $order ); $this->validate_checkout_form( $data['form'], $order );
}
if ( 'pay-now' === $data['context'] ) {
$this->validate_paynow_form( $data['form'] );
} }
wp_send_json_success( $order->to_array() ); wp_send_json_success( $order->to_array() );
return true; return true;
@ -177,6 +189,64 @@ class CreateOrderEndpoint implements EndpointInterface {
} }
} }
/**
* Returns the Payer entity based on the request data.
*
* @param array $data The request data.
* @param \WC_Order $wc_order The order.
*
* @return Payer|null
*/
private function payer( array $data, \WC_Order $wc_order = null ) {
if ( 'pay-now' === $data['context'] ) {
$payer = $this->payer_factory->from_wc_order( $wc_order );
return $payer;
}
$payer = null;
if ( isset( $data['payer'] ) && $data['payer'] ) {
if ( isset( $data['payer']['phone']['phone_number']['national_number'] ) ) {
// make sure the phone number contains only numbers and is max 14. chars long.
$number = $data['payer']['phone']['phone_number']['national_number'];
$number = preg_replace( '/[^0-9]/', '', $number );
$number = substr( $number, 0, 14 );
$data['payer']['phone']['phone_number']['national_number'] = $number;
}
$payer = $this->payer_factory->from_paypal_response( json_decode( wp_json_encode( $data['payer'] ) ) );
}
return $payer;
}
/**
* Sets the BN Code for the following request.
*
* @param array $data The request data.
*/
private function set_bn_code( array $data ) {
$bn_code = isset( $data['bn_code'] ) ? (string) $data['bn_code'] : '';
if ( ! $bn_code ) {
return;
}
$this->session_handler->replace_bn_code( $bn_code );
$this->api_endpoint->with_bn_code( $bn_code );
}
/**
* Returns the PaymentMethod object for the order.
*
* @return PaymentMethod
* @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException In case a setting would not be found.
*/
private function payment_method() : PaymentMethod {
$payee_preferred = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ?
PaymentMethod::PAYEE_PREFERRED_IMMEDIATE_PAYMENT_REQUIRED
: PaymentMethod::PAYEE_PREFERRED_UNRESTRICTED;
$payment_method = new PaymentMethod( $payee_preferred );
return $payment_method;
}
/** /**
* Prepare the Request parameter and process the checkout form and validate it. * Prepare the Request parameter and process the checkout form and validate it.
* *
@ -185,7 +255,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* *
* @throws \Exception On Error. * @throws \Exception On Error.
*/ */
private function validateForm( string $form_values, Order $order ) { private function validate_checkout_form( string $form_values, Order $order ) {
$this->order = $order; $this->order = $order;
$parsed_values = wp_parse_args( $form_values ); $parsed_values = wp_parse_args( $form_values );
$_POST = $parsed_values; $_POST = $parsed_values;
@ -204,6 +274,21 @@ class CreateOrderEndpoint implements EndpointInterface {
$checkout->process_checkout(); $checkout->process_checkout();
} }
/**
* Checks whether the terms input field is checked.
*
* @param string $form_values The form values.
* @throws \RuntimeException When field is not checked.
*/
private function validate_paynow_form( string $form_values ) {
$parsed_values = wp_parse_args( $form_values );
if ( ! isset( $parsed_values['terms'] ) ) {
throw new \RuntimeException(
__( 'Please read and accept the terms and conditions to proceed with your order.', 'paypal-payments-for-woocommerce' )
);
}
}
/** /**
* Once the checkout has been validated we execute this method. * Once the checkout has been validated we execute this method.
* *

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Dhii\Data\Container\ContainerInterface; use Dhii\Data\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn; use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail; use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
@ -26,6 +27,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
@ -37,9 +39,11 @@ return array(
$order_processor = $container->get( 'wcgateway.order-processor' ); $order_processor = $container->get( 'wcgateway.order-processor' );
$settings_renderer = $container->get( 'wcgateway.settings.render' ); $settings_renderer = $container->get( 'wcgateway.settings.render' );
$authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' ); $authorized_payments = $container->get( 'wcgateway.processor.authorized-payments' );
$notice = $container->get( 'wcgateway.notice.authorize-order-action' ); $notice = $container->get( 'wcgateway.notice.authorize-order-action' );
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
$session_handler = $container->get( 'session.handler' ); $session_handler = $container->get( 'session.handler' );
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
$state = $container->get( 'onboarding.state' );
return new PayPalGateway( return new PayPalGateway(
$settings_renderer, $settings_renderer,
@ -47,7 +51,9 @@ return array(
$authorized_payments, $authorized_payments,
$notice, $notice,
$settings, $settings,
$session_handler $session_handler,
$refund_processor,
$state
); );
}, },
'wcgateway.credit-card-gateway' => static function ( $container ): CreditCardGateway { 'wcgateway.credit-card-gateway' => static function ( $container ): CreditCardGateway {
@ -58,6 +64,8 @@ return array(
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
$module_url = $container->get( 'wcgateway.url' ); $module_url = $container->get( 'wcgateway.url' );
$session_handler = $container->get( 'session.handler' ); $session_handler = $container->get( 'session.handler' );
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
$state = $container->get( 'onboarding.state' );
return new CreditCardGateway( return new CreditCardGateway(
$settings_renderer, $settings_renderer,
$order_processor, $order_processor,
@ -65,7 +73,9 @@ return array(
$notice, $notice,
$settings, $settings,
$module_url, $module_url,
$session_handler $session_handler,
$refund_processor,
$state
); );
}, },
'wcgateway.disabler' => static function ( $container ): DisableGateways { 'wcgateway.disabler' => static function ( $container ): DisableGateways {
@ -133,6 +143,11 @@ return array(
$settings $settings
); );
}, },
'wcgateway.processor.refunds' => static function ( $container ): RefundProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' );
return new RefundProcessor( $order_endpoint, $payments_endpoint );
},
'wcgateway.processor.authorized-payments' => static function ( $container ): AuthorizedPaymentsProcessor { 'wcgateway.processor.authorized-payments' => static function ( $container ): AuthorizedPaymentsProcessor {
$order_endpoint = $container->get( 'api.endpoint.order' ); $order_endpoint = $container->get( 'api.endpoint.order' );
$payments_endpoint = $container->get( 'api.endpoint.payments' ); $payments_endpoint = $container->get( 'api.endpoint.payments' );
@ -1714,6 +1729,25 @@ return array(
if ( 'GB' === $country ) { if ( 'GB' === $country ) {
unset( $fields['disable_funding']['options']['card'] ); unset( $fields['disable_funding']['options']['card'] );
} }
$dcc_applies = $container->get( 'api.helpers.dccapplies' );
/**
* Depending on your store location, some credit cards can't be used.
* Here, we filter them out.
*
* The DCC Applies object.
*
* @var DccApplies $dcc_applies
*/
$card_options = $fields['disable_cards']['options'];
foreach ( $card_options as $card => $label ) {
if ( $dcc_applies->can_process_card( $card ) ) {
continue;
}
unset( $card_options[ $card ] );
}
$fields['disable_cards']['options'] = $card_options;
$fields['card_icons']['options'] = $card_options;
return $fields; return $fields;
}, },

View file

@ -9,10 +9,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -32,6 +34,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/ */
private $module_url; private $module_url;
/**
* The refund processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/** /**
* CreditCardGateway constructor. * CreditCardGateway constructor.
* *
@ -42,6 +51,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param ContainerInterface $config The settings. * @param ContainerInterface $config The settings.
* @param string $module_url The URL to the module. * @param string $module_url The URL to the module.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The refund processor.
* @param State $state The state.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -50,7 +61,9 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
AuthorizeOrderActionNotice $notice, AuthorizeOrderActionNotice $notice,
ContainerInterface $config, ContainerInterface $config,
string $module_url, string $module_url,
SessionHandler $session_handler SessionHandler $session_handler,
RefundProcessor $refund_processor,
State $state
) { ) {
$this->id = self::ID; $this->id = self::ID;
@ -60,6 +73,11 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
$this->config = $config; $this->config = $config;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' );
}
if ( if (
defined( 'PPCP_FLAG_SUBSCRIPTION' ) defined( 'PPCP_FLAG_SUBSCRIPTION' )
&& PPCP_FLAG_SUBSCRIPTION && PPCP_FLAG_SUBSCRIPTION
@ -67,6 +85,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
&& $this->config->get( 'vault_enabled' ) && $this->config->get( 'vault_enabled' )
) { ) {
$this->supports = array( $this->supports = array(
'refunds',
'products', 'products',
'subscriptions', 'subscriptions',
'subscription_cancellation', 'subscription_cancellation',
@ -211,4 +230,24 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
public function is_available() : bool { public function is_available() : bool {
return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ); return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' );
} }
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
} }

View file

@ -10,10 +10,12 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -72,6 +74,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/ */
protected $session_handler; protected $session_handler;
/**
* The Refund Processor.
*
* @var RefundProcessor
*/
private $refund_processor;
/** /**
* PayPalGateway constructor. * PayPalGateway constructor.
* *
@ -81,6 +90,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param AuthorizeOrderActionNotice $notice The Order Action Notice object. * @param AuthorizeOrderActionNotice $notice The Order Action Notice object.
* @param ContainerInterface $config The settings. * @param ContainerInterface $config The settings.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The Refund Processor.
* @param State $state The state.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -88,7 +99,9 @@ class PayPalGateway extends \WC_Payment_Gateway {
AuthorizedPaymentsProcessor $authorized_payments_processor, AuthorizedPaymentsProcessor $authorized_payments_processor,
AuthorizeOrderActionNotice $notice, AuthorizeOrderActionNotice $notice,
ContainerInterface $config, ContainerInterface $config,
SessionHandler $session_handler SessionHandler $session_handler,
RefundProcessor $refund_processor,
State $state
) { ) {
$this->id = self::ID; $this->id = self::ID;
@ -98,6 +111,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
$this->config = $config; $this->config = $config;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' );
}
if ( if (
defined( 'PPCP_FLAG_SUBSCRIPTION' ) defined( 'PPCP_FLAG_SUBSCRIPTION' )
&& PPCP_FLAG_SUBSCRIPTION && PPCP_FLAG_SUBSCRIPTION
@ -105,6 +123,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
&& $this->config->get( 'vault_enabled' ) && $this->config->get( 'vault_enabled' )
) { ) {
$this->supports = array( $this->supports = array(
'refunds',
'products', 'products',
'subscriptions', 'subscriptions',
'subscription_cancellation', 'subscription_cancellation',
@ -302,4 +321,23 @@ class PayPalGateway extends \WC_Payment_Gateway {
&& self::ID === sanitize_text_field( wp_unslash( $_GET['section'] ) ); && self::ID === sanitize_text_field( wp_unslash( $_GET['section'] ) );
} }
// phpcs:enable WordPress.Security.NonceVerification.Recommended // phpcs:enable WordPress.Security.NonceVerification.Recommended
/**
* Process refund.
*
* If the gateway declares 'refunds' support, this will allow it to refund.
* a passed in amount.
*
* @param int $order_id Order ID.
* @param float $amount Refund amount.
* @param string $reason Refund reason.
* @return boolean True or false based on success, or a WP_Error object.
*/
public function process_refund( $order_id, $amount = null, $reason = '' ) {
$order = wc_get_order( $order_id );
if ( ! is_a( $order, \WC_Order::class ) ) {
return false;
}
return $this->refund_processor->process( $order, (float) $amount, (string) $reason );
}
} }

View file

@ -0,0 +1,99 @@
<?php
/**
* Processes refunds started in the WooCommerce environment.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Processor
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class RefundProcessor
*/
class RefundProcessor {
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The payments endpoint.
*
* @var PaymentsEndpoint
*/
private $payments_endpoint;
/**
* RefundProcessor constructor.
*
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
*/
public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint ) {
$this->order_endpoint = $order_endpoint;
$this->payments_endpoint = $payments_endpoint;
}
/**
* Processes a refund.
*
* @param \WC_Order $wc_order The WooCommerce order.
* @param float|null $amount The refund amount.
* @param string $reason The reason for the refund.
*
* @return bool
*/
public function process( \WC_Order $wc_order, float $amount = null, string $reason = '' ) : bool {
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) {
return false;
}
try {
$order = $this->order_endpoint->order( $order_id );
if ( ! $order ) {
return false;
}
$purchase_units = $order->purchase_units();
if ( ! $purchase_units ) {
return false;
}
$payments = $purchase_units[0]->payments();
if ( ! $payments ) {
return false;
}
$captures = $payments->captures();
if ( ! $captures ) {
return false;
}
$capture = $captures[0];
$refund = new Refund(
$capture,
$capture->invoice_id(),
$reason,
new Amount(
new Money( $amount, get_woocommerce_currency() )
)
);
return $this->payments_endpoint->refund( $refund );
} catch ( RuntimeException $error ) {
return false;
}
}
}

View file

@ -460,19 +460,6 @@ class SettingsRenderer {
'paypal-payments-for-woocommerce' 'paypal-payments-for-woocommerce'
); );
?> ?>
<a
href="https://developer.paypal.com/docs/platforms/checkout/reference/country-availability-advanced-cards/"
target="_blank"
rel="noreferrer noopener"
>
<?php
esc_html_e(
'Click here to see, in which countries this option is currently available.',
'paypal-payments-for-woocommerce'
);
?>
</a>
</p> </p>
</td> </td>
</tr> </tr>

View file

@ -8,15 +8,24 @@ use WooCommerce\PayPalCommerce\ApiClient\TestCase;
class PaymentsTest extends TestCase class PaymentsTest extends TestCase
{ {
public function testAuthorizations() public function testAuthorizations()
{ {
$authorization = \Mockery::mock(Authorization::class); $authorization = \Mockery::mock(Authorization::class);
$authorizations = [$authorization]; $authorizations = [$authorization];
$testee = new Payments(...$authorizations); $testee = new Payments($authorizations, []);
$this->assertEquals($authorizations, $testee->authorizations()); $this->assertEquals($authorizations, $testee->authorizations());
} }
public function testCaptures()
{
$capture = \Mockery::mock(Capture::class);
$captures = [$capture];
$testee = new Payments([], $captures);
$this->assertEquals($captures, $testee->captures());
}
public function testToArray() public function testToArray()
{ {
@ -27,9 +36,17 @@ class PaymentsTest extends TestCase
'status' => 'CREATED', 'status' => 'CREATED',
] ]
); );
$capture = \Mockery::mock(Capture::class);
$capture->shouldReceive('to_array')->andReturn(
[
'id' => 'capture',
'status' => 'CREATED',
]
);
$captures = [$capture];
$authorizations = [$authorization]; $authorizations = [$authorization];
$testee = new Payments(...$authorizations); $testee = new Payments($authorizations, $captures);
$this->assertEquals( $this->assertEquals(
[ [
@ -39,6 +56,12 @@ class PaymentsTest extends TestCase
'status' => 'CREATED', 'status' => 'CREATED',
], ],
], ],
'captures' => [
[
'id' => 'capture',
'status' => 'CREATED',
],
],
], ],
$testee->to_array() $testee->to_array()
); );

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory; namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization; use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use Woocommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\TestCase; use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery; use Mockery;
@ -13,27 +14,36 @@ class PaymentsFactoryTest extends TestCase
{ {
public function testFromPayPalResponse() public function testFromPayPalResponse()
{ {
$authorization = Mockery::mock(Authorization::class); $authorization = Mockery::mock(Authorization::class);
$authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']); $authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']);
$capture = Mockery::mock(Capture::class);
$capture->shouldReceive('to_array')->andReturn(['id' => 'capture', 'status' => 'CREATED']);
$authorizationsFactory = Mockery::mock(AuthorizationFactory::class); $authorizationsFactory = Mockery::mock(AuthorizationFactory::class);
$authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization); $authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization);
$captureFactory = Mockery::mock(CaptureFactory::class);
$captureFactory->shouldReceive('from_paypal_response')->andReturn($capture);
$response = (object)[ $response = (object)[
'authorizations' => [ 'authorizations' => [
(object)['id' => 'foo', 'status' => 'CREATED'], (object)['id' => 'foo', 'status' => 'CREATED'],
], ],
'captures' => [
(object)['id' => 'capture', 'status' => 'CREATED'],
],
]; ];
$testee = new PaymentsFactory($authorizationsFactory); $testee = new PaymentsFactory($authorizationsFactory, $captureFactory);
$result = $testee->from_paypal_response($response); $result = $testee->from_paypal_response($response);
$this->assertInstanceOf(Payments::class, $result); $this->assertInstanceOf(Payments::class, $result);
$expectedToArray = [ $expectedToArray = [
'authorizations' => [ 'authorizations' => [
['id' => 'foo', 'status' => 'CREATED'], ['id' => 'foo', 'status' => 'CREATED'],
], ],
'captures' => [
['id' => 'capture', 'status' => 'CREATED'],
],
]; ];
$this->assertEquals($expectedToArray, $result->to_array()); $this->assertEquals($expectedToArray, $result->to_array());
} }

View file

@ -5,11 +5,13 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\TestCase; use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice; use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsFields; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsFields;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
@ -42,13 +44,19 @@ class WcGatewayTest extends TestCase
->shouldReceive('destroy_session_data'); ->shouldReceive('destroy_session_data');
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
expect('wc_get_order') expect('wc_get_order')
@ -76,13 +84,19 @@ class WcGatewayTest extends TestCase
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler = Mockery::mock(SessionHandler::class);
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
expect('wc_get_order') expect('wc_get_order')
@ -116,13 +130,19 @@ class WcGatewayTest extends TestCase
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler = Mockery::mock(SessionHandler::class);
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
expect('wc_get_order') expect('wc_get_order')
@ -171,13 +191,19 @@ class WcGatewayTest extends TestCase
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler = Mockery::mock(SessionHandler::class);
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
$this->assertTrue($testee->capture_authorized_payment($wcOrder)); $this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -218,13 +244,19 @@ class WcGatewayTest extends TestCase
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler = Mockery::mock(SessionHandler::class);
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
$this->assertTrue($testee->capture_authorized_payment($wcOrder)); $this->assertTrue($testee->capture_authorized_payment($wcOrder));
@ -258,13 +290,19 @@ class WcGatewayTest extends TestCase
$settings $settings
->shouldReceive('has')->andReturnFalse(); ->shouldReceive('has')->andReturnFalse();
$sessionHandler = Mockery::mock(SessionHandler::class); $sessionHandler = Mockery::mock(SessionHandler::class);
$refundProcessor = Mockery::mock(RefundProcessor::class);
$state = Mockery::mock(State::class);
$state
->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED);
$testee = new PayPalGateway( $testee = new PayPalGateway(
$settingsRenderer, $settingsRenderer,
$orderProcessor, $orderProcessor,
$authorizedPaymentsProcessor, $authorizedPaymentsProcessor,
$authorizedOrderActionNotice, $authorizedOrderActionNotice,
$settings, $settings,
$sessionHandler $sessionHandler,
$refundProcessor,
$state
); );
$this->assertFalse($testee->capture_authorized_payment($wcOrder)); $this->assertFalse($testee->capture_authorized_payment($wcOrder));

View file

@ -26,8 +26,8 @@ define( 'PAYPAL_API_URL', 'https://api.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' );
// @ToDo: Real connect.woocommerce.com production link. // @ToDo: Real connect.woocommerce.com production link.
define( 'CONNECT_WOO_URL', 'http://connect-woo.wpcust.com' ); define( 'CONNECT_WOO_URL', 'http://connect-woo.wpcust.com/ppc' );
define( 'CONNECT_WOO_SANDBOX_URL', 'http://connect-woo.wpcust.com' ); define( 'CONNECT_WOO_SANDBOX_URL', 'http://connect-woo.wpcust.com/ppcsandbox' );
( function () { ( function () {
include __DIR__ . '/vendor/autoload.php'; include __DIR__ . '/vendor/autoload.php';