mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Merge branch 'trunk' into feat/PCP-154-apple-pay-payment
This commit is contained in:
commit
614a37f234
72 changed files with 3030 additions and 277 deletions
|
@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\PPCP;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
|
||||
|
||||
|
@ -107,3 +108,14 @@ function ppcp_void_order( WC_Order $wc_order ): void {
|
|||
|
||||
$refund_processor->void( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the PayPal refund fees totals on an order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WC order.
|
||||
*/
|
||||
function ppcp_update_order_refund_fees( WC_Order $wc_order ): void {
|
||||
$updater = PPCP::container()->get( 'wcgateway.helper.refund-fees-updater' );
|
||||
assert( $updater instanceof RefundFeesUpdater );
|
||||
$updater->update( $wc_order );
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
*** Changelog ***
|
||||
|
||||
= 2.2.1 - xxxx-xx-xx =
|
||||
= 2.2.2 - 2023-08-29 =
|
||||
* Fix - High rate of auth voids on vaulted subscriptions for guest users #1529
|
||||
* Enhancement - HPOS compatibility issues #1594
|
||||
* Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607
|
||||
|
||||
= 2.2.1 - 2023-08-24 =
|
||||
* Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536
|
||||
* Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565
|
||||
* Fix - PayPal button doesn't work for variable products on product page after recent 2.2.0 release #1533
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"autoload": {
|
||||
"psr-4": {
|
||||
"WooCommerce\\PayPalCommerce\\": "src",
|
||||
"WooCommerce\\PayPalCommerce\\Common\\": "lib/common/",
|
||||
"WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/"
|
||||
},
|
||||
"files": [
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
## packages
|
||||
The packages that are likely to cause conflicts with other plugins (by loading multiple incompatible versions).
|
||||
Their namespaces are isolated by [Mozart](https://github.com/coenjacobs/mozart).
|
||||
|
||||
Currently, the packages are simply added in the repo to avoid making the build process more complex (Mozart has different PHP requirements).
|
||||
We need to isolate only PSR-11 containers and Dhii modularity packages, which are not supposed to change often.
|
||||
|
||||
## common
|
||||
This folder contains reusable classes or components that do not fit into any specific module.
|
||||
They are designed to be versatile and can be used by any module within the plugin.
|
||||
|
|
68
lib/common/Pattern/SingletonDecorator.php
Normal file
68
lib/common/Pattern/SingletonDecorator.php
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
/**
|
||||
* The Singleton Trait can be used to wrap an execution block, so it behaves like a Singleton.
|
||||
* It executes the callable once, on subsequent calls returns the same result.
|
||||
*/
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Common\Pattern;
|
||||
|
||||
/**
|
||||
* Class SingletonDecorator.
|
||||
*/
|
||||
class SingletonDecorator {
|
||||
|
||||
/**
|
||||
* The callable with the executing code
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
private $callable;
|
||||
|
||||
/**
|
||||
* The execution result
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
private $result;
|
||||
|
||||
/**
|
||||
* Indicates if the callable is resolved
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $executed = false;
|
||||
|
||||
/**
|
||||
* SingletonDecorator constructor.
|
||||
*
|
||||
* @param callable $callable
|
||||
*/
|
||||
public function __construct( callable $callable ) {
|
||||
$this->callable = $callable;
|
||||
}
|
||||
|
||||
/**
|
||||
* The make constructor.
|
||||
*
|
||||
* @param callable $callable
|
||||
* @return self
|
||||
*/
|
||||
public static function make( callable $callable ): self {
|
||||
return new static( $callable );
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes a callable once and returns the same result on subsequent invokes.
|
||||
*
|
||||
* @param mixed ...$args Arguments to be passed to the callable.
|
||||
* @return mixed
|
||||
*/
|
||||
public function __invoke( ...$args ) {
|
||||
if ( ! $this->executed ) {
|
||||
$this->result = call_user_func_array( $this->callable, $args );
|
||||
$this->executed = true;
|
||||
}
|
||||
|
||||
return $this->result;
|
||||
}
|
||||
}
|
41
lib/common/Pattern/SingletonTrait.php
Normal file
41
lib/common/Pattern/SingletonTrait.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
/**
|
||||
* The Singleton Trait can be used to add singleton behaviour to a class.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Common\Pattern
|
||||
*/
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Common\Pattern;
|
||||
|
||||
/**
|
||||
* Class SingletonTrait.
|
||||
*/
|
||||
trait SingletonTrait {
|
||||
/**
|
||||
* The single instance of the class.
|
||||
*
|
||||
* @var self
|
||||
*/
|
||||
protected static $instance = null;
|
||||
|
||||
/**
|
||||
* Static method to get the instance of the Singleton class
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public static function get_instance(): ?self {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to get the instance of the Singleton class
|
||||
*
|
||||
* @param self $instance
|
||||
* @return self
|
||||
*/
|
||||
protected static function set_instance( self $instance ): self {
|
||||
self::$instance = $instance;
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
}
|
|
@ -35,17 +35,26 @@ class Message {
|
|||
*/
|
||||
private $dismissable;
|
||||
|
||||
/**
|
||||
* The wrapper selector that will contain the notice.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $wrapper;
|
||||
|
||||
/**
|
||||
* Message constructor.
|
||||
*
|
||||
* @param string $message The message text.
|
||||
* @param string $type The message type.
|
||||
* @param bool $dismissable Whether the message is dismissable.
|
||||
* @param string $wrapper The wrapper selector that will contain the notice.
|
||||
*/
|
||||
public function __construct( string $message, string $type, bool $dismissable = true ) {
|
||||
public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) {
|
||||
$this->type = $type;
|
||||
$this->message = $message;
|
||||
$this->dismissable = $dismissable;
|
||||
$this->wrapper = $wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -74,4 +83,13 @@ class Message {
|
|||
public function is_dismissable(): bool {
|
||||
return $this->dismissable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the wrapper selector that will contain the notice.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function wrapper(): string {
|
||||
return $this->wrapper;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,9 +41,10 @@ class Renderer implements RendererInterface {
|
|||
$messages = $this->repository->current_message();
|
||||
foreach ( $messages as $message ) {
|
||||
printf(
|
||||
'<div class="notice notice-%s %s"><p>%s</p></div>',
|
||||
'<div class="notice notice-%s %s" %s><p>%s</p></div>',
|
||||
$message->type(),
|
||||
( $message->is_dismissable() ) ? 'is-dismissible' : '',
|
||||
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
|
||||
wp_kses_post( $message->message() )
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,13 +9,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
@ -58,6 +63,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
|
||||
|
@ -289,6 +296,14 @@ return array(
|
|||
$container->get( 'api.factory.fraud-processor-response' )
|
||||
);
|
||||
},
|
||||
'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory {
|
||||
$amount_factory = $container->get( 'api.factory.amount' );
|
||||
return new RefundFactory(
|
||||
$amount_factory,
|
||||
$container->get( 'api.factory.seller-payable-breakdown' ),
|
||||
$container->get( 'api.factory.refund_payer' )
|
||||
);
|
||||
},
|
||||
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
|
||||
|
||||
$amount_factory = $container->get( 'api.factory.amount' );
|
||||
|
@ -299,6 +314,7 @@ return array(
|
|||
$payments_factory = $container->get( 'api.factory.payments' );
|
||||
$prefix = $container->get( 'api.prefix' );
|
||||
$soft_descriptor = $container->get( 'wcgateway.soft-descriptor' );
|
||||
$sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
|
||||
|
||||
return new PurchaseUnitFactory(
|
||||
$amount_factory,
|
||||
|
@ -308,7 +324,8 @@ return array(
|
|||
$shipping_factory,
|
||||
$payments_factory,
|
||||
$prefix,
|
||||
$soft_descriptor
|
||||
$soft_descriptor,
|
||||
$sanitizer
|
||||
);
|
||||
},
|
||||
'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory {
|
||||
|
@ -351,6 +368,9 @@ return array(
|
|||
$address_factory = $container->get( 'api.factory.address' );
|
||||
return new PayerFactory( $address_factory );
|
||||
},
|
||||
'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory {
|
||||
return new RefundPayerFactory();
|
||||
},
|
||||
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
|
||||
return new AddressFactory();
|
||||
},
|
||||
|
@ -374,7 +394,8 @@ return array(
|
|||
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
|
||||
$authorizations_factory = $container->get( 'api.factory.authorization' );
|
||||
$capture_factory = $container->get( 'api.factory.capture' );
|
||||
return new PaymentsFactory( $authorizations_factory, $capture_factory );
|
||||
$refund_factory = $container->get( 'api.factory.refund' );
|
||||
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
|
||||
},
|
||||
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
|
||||
return new AuthorizationFactory();
|
||||
|
@ -395,6 +416,12 @@ return array(
|
|||
$container->get( 'api.factory.platform-fee' )
|
||||
);
|
||||
},
|
||||
'api.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory {
|
||||
return new SellerPayableBreakdownFactory(
|
||||
$container->get( 'api.factory.money' ),
|
||||
$container->get( 'api.factory.platform-fee' )
|
||||
);
|
||||
},
|
||||
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
|
||||
return new FraudProcessorResponseFactory();
|
||||
},
|
||||
|
@ -814,4 +841,19 @@ return array(
|
|||
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
|
||||
return new OrderHelper();
|
||||
},
|
||||
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
|
||||
$cache = new Cache( 'ppcp-paypal-bearer' );
|
||||
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
|
||||
return new OrderTransient( $cache, $purchase_unit_sanitizer );
|
||||
},
|
||||
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
|
||||
static function( ContainerInterface $container ): PurchaseUnitSanitizer {
|
||||
$settings = $container->get( 'wcgateway.settings' );
|
||||
assert( $settings instanceof Settings );
|
||||
|
||||
$behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null;
|
||||
$line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null;
|
||||
return new PurchaseUnitSanitizer( $behavior, $line_name );
|
||||
}
|
||||
),
|
||||
);
|
||||
|
|
|
@ -9,10 +9,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient;
|
||||
|
||||
use WC_Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
|
||||
/**
|
||||
* Class ApiModule
|
||||
|
@ -40,6 +43,30 @@ class ApiModule implements ModuleInterface {
|
|||
WC()->session->set( 'ppcp_fees', $fees );
|
||||
}
|
||||
);
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_paypal_order_created',
|
||||
function ( Order $order ) use ( $c ) {
|
||||
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
|
||||
|
||||
if ( $transient instanceof OrderTransient ) {
|
||||
$transient->on_order_created( $order );
|
||||
}
|
||||
},
|
||||
10,
|
||||
1
|
||||
);
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_woocommerce_order_created',
|
||||
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
|
||||
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
|
||||
|
||||
if ( $transient instanceof OrderTransient ) {
|
||||
$transient->on_woocommerce_order_created( $wc_order, $order );
|
||||
}
|
||||
},
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -54,8 +54,6 @@ class BillingPlans {
|
|||
private $plan_factory;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
|
|
|
@ -281,6 +281,9 @@ class OrderEndpoint {
|
|||
throw $error;
|
||||
}
|
||||
$order = $this->order_factory->from_paypal_response( $json );
|
||||
|
||||
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ class PayUponInvoiceOrderEndpoint {
|
|||
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
|
||||
'purchase_units' => array_map(
|
||||
static function ( PurchaseUnit $item ): array {
|
||||
return $item->to_array( false );
|
||||
return $item->to_array( true, false );
|
||||
},
|
||||
$items
|
||||
),
|
||||
|
@ -166,8 +166,11 @@ class PayUponInvoiceOrderEndpoint {
|
|||
|
||||
throw new PayPalApiException( $json, $status_code );
|
||||
}
|
||||
$order = $this->order_factory->from_paypal_response( $json );
|
||||
|
||||
return $this->order_factory->from_paypal_response( $json );
|
||||
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
|
||||
|
||||
return $order;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
|
||||
|
@ -196,13 +196,13 @@ class PaymentsEndpoint {
|
|||
/**
|
||||
* Refunds a payment.
|
||||
*
|
||||
* @param Refund $refund The refund to be processed.
|
||||
* @param RefundCapture $refund The refund to be processed.
|
||||
*
|
||||
* @return string Refund ID.
|
||||
* @throws RuntimeException If the request fails.
|
||||
* @throws PayPalApiException If the request fails.
|
||||
*/
|
||||
public function refund( Refund $refund ) : string {
|
||||
public function refund( RefundCapture $refund ) : string {
|
||||
$bearer = $this->bearer->bearer();
|
||||
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
|
||||
$args = array(
|
||||
|
|
|
@ -203,7 +203,7 @@ class Item {
|
|||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() {
|
||||
public function to_array(): array {
|
||||
$item = array(
|
||||
'name' => $this->name(),
|
||||
'unit_amount' => $this->unit_amount()->to_array(),
|
||||
|
|
|
@ -28,13 +28,21 @@ class Payments {
|
|||
*/
|
||||
private $captures;
|
||||
|
||||
/**
|
||||
* The Refunds.
|
||||
*
|
||||
* @var Refund[]
|
||||
*/
|
||||
private $refunds;
|
||||
|
||||
/**
|
||||
* Payments constructor.
|
||||
*
|
||||
* @param array $authorizations The Authorizations.
|
||||
* @param array $captures The Captures.
|
||||
* @param array $refunds The Refunds.
|
||||
*/
|
||||
public function __construct( array $authorizations, array $captures ) {
|
||||
public function __construct( array $authorizations, array $captures, array $refunds = array() ) {
|
||||
foreach ( $authorizations as $key => $authorization ) {
|
||||
if ( is_a( $authorization, Authorization::class ) ) {
|
||||
continue;
|
||||
|
@ -47,8 +55,15 @@ class Payments {
|
|||
}
|
||||
unset( $captures[ $key ] );
|
||||
}
|
||||
foreach ( $refunds as $key => $refund ) {
|
||||
if ( is_a( $refund, Refund::class ) ) {
|
||||
continue;
|
||||
}
|
||||
unset( $refunds[ $key ] );
|
||||
}
|
||||
$this->authorizations = $authorizations;
|
||||
$this->captures = $captures;
|
||||
$this->refunds = $refunds;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -70,6 +85,12 @@ class Payments {
|
|||
},
|
||||
$this->captures()
|
||||
),
|
||||
'refunds' => array_map(
|
||||
static function ( Refund $refund ): array {
|
||||
return $refund->to_array();
|
||||
},
|
||||
$this->refunds()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -90,4 +111,13 @@ class Payments {
|
|||
public function captures(): array {
|
||||
return $this->captures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Refunds.
|
||||
*
|
||||
* @return Refund[]
|
||||
**/
|
||||
public function refunds(): array {
|
||||
return $this->refunds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
|
||||
/**
|
||||
* Class PurchaseUnit
|
||||
*/
|
||||
|
@ -91,6 +93,13 @@ class PurchaseUnit {
|
|||
*/
|
||||
private $contains_physical_goods = false;
|
||||
|
||||
/**
|
||||
* The sanitizer for this purchase unit output.
|
||||
*
|
||||
* @var PurchaseUnitSanitizer|null
|
||||
*/
|
||||
private $sanitizer;
|
||||
|
||||
/**
|
||||
* PurchaseUnit constructor.
|
||||
*
|
||||
|
@ -220,6 +229,16 @@ class PurchaseUnit {
|
|||
$this->custom_id = $custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sanitizer for this purchase unit output.
|
||||
*
|
||||
* @param PurchaseUnitSanitizer|null $sanitizer The sanitizer.
|
||||
* @return void
|
||||
*/
|
||||
public function set_sanitizer( ?PurchaseUnitSanitizer $sanitizer ) {
|
||||
$this->sanitizer = $sanitizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the invoice id.
|
||||
*
|
||||
|
@ -277,11 +296,12 @@ class PurchaseUnit {
|
|||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @param bool $ditch_items_when_mismatch Whether ditch items when mismatch or not.
|
||||
* @param bool $sanitize_output Whether output should be sanitized for PayPal consumption.
|
||||
* @param bool $allow_ditch_items Whether to allow items to be ditched.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array( bool $ditch_items_when_mismatch = true ): array {
|
||||
public function to_array( bool $sanitize_output = true, bool $allow_ditch_items = true ): array {
|
||||
$purchase_unit = array(
|
||||
'reference_id' => $this->reference_id(),
|
||||
'amount' => $this->amount()->to_array(),
|
||||
|
@ -294,17 +314,6 @@ class PurchaseUnit {
|
|||
),
|
||||
);
|
||||
|
||||
$ditch = $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() );
|
||||
/**
|
||||
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
|
||||
*/
|
||||
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditch, $this );
|
||||
|
||||
if ( $ditch ) {
|
||||
unset( $purchase_unit['items'] );
|
||||
unset( $purchase_unit['amount']['breakdown'] );
|
||||
}
|
||||
|
||||
if ( $this->payee() ) {
|
||||
$purchase_unit['payee'] = $this->payee()->to_array();
|
||||
}
|
||||
|
@ -325,101 +334,45 @@ class PurchaseUnit {
|
|||
if ( $this->soft_descriptor() ) {
|
||||
$purchase_unit['soft_descriptor'] = $this->soft_descriptor();
|
||||
}
|
||||
return $purchase_unit;
|
||||
|
||||
$has_ditched_items_breakdown = false;
|
||||
|
||||
if ( $sanitize_output && isset( $this->sanitizer ) ) {
|
||||
$purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) );
|
||||
$has_ditched_items_breakdown = $this->sanitizer->has_ditched_items_breakdown();
|
||||
}
|
||||
|
||||
return $this->apply_ditch_items_mismatch_filter(
|
||||
$has_ditched_items_breakdown,
|
||||
$purchase_unit
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
|
||||
* not have this restriction. Therefore the totals of the cart in WooCommerce and the totals
|
||||
* of the rounded money values of the items, we send to PayPal, can differ. In those cases,
|
||||
* we can not send the line items.
|
||||
* Applies the ppcp_ditch_items_breakdown filter.
|
||||
* If true purchase_unit items and breakdown are ditched from PayPal.
|
||||
*
|
||||
* @param Amount $amount The amount.
|
||||
* @param Item ...$items The items.
|
||||
* @return bool
|
||||
* @param bool $ditched_items_breakdown If the breakdown and items were already ditched.
|
||||
* @param array $purchase_unit The purchase_unit array.
|
||||
* @return array
|
||||
*/
|
||||
private function ditch_items_when_mismatch( Amount $amount, Item ...$items ): bool {
|
||||
$breakdown = $amount->breakdown();
|
||||
if ( ! $breakdown ) {
|
||||
return false;
|
||||
}
|
||||
public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array {
|
||||
/**
|
||||
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
|
||||
*/
|
||||
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditched_items_breakdown, $this );
|
||||
|
||||
$item_total = $breakdown->item_total();
|
||||
if ( $item_total ) {
|
||||
$remaining_item_total = array_reduce(
|
||||
$items,
|
||||
function ( float $total, Item $item ): float {
|
||||
return $total - (float) $item->unit_amount()->value_str() * (float) $item->quantity();
|
||||
},
|
||||
(float) $item_total->value_str()
|
||||
);
|
||||
if ( $ditch ) {
|
||||
unset( $purchase_unit['items'] );
|
||||
unset( $purchase_unit['amount']['breakdown'] );
|
||||
|
||||
$remaining_item_total = round( $remaining_item_total, 2 );
|
||||
|
||||
if ( 0.0 !== $remaining_item_total ) {
|
||||
return true;
|
||||
if ( isset( $this->sanitizer ) && ( $ditch !== $ditched_items_breakdown ) ) {
|
||||
$this->sanitizer->set_last_message(
|
||||
__( 'Ditch items breakdown filter. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$tax_total = $breakdown->tax_total();
|
||||
$items_with_tax = array_filter(
|
||||
$this->items,
|
||||
function ( Item $item ): bool {
|
||||
return null !== $item->tax();
|
||||
}
|
||||
);
|
||||
if ( $tax_total && ! empty( $items_with_tax ) ) {
|
||||
$remaining_tax_total = array_reduce(
|
||||
$items,
|
||||
function ( float $total, Item $item ): float {
|
||||
$tax = $item->tax();
|
||||
if ( $tax ) {
|
||||
$total -= (float) $tax->value_str() * (float) $item->quantity();
|
||||
}
|
||||
return $total;
|
||||
},
|
||||
(float) $tax_total->value_str()
|
||||
);
|
||||
|
||||
$remaining_tax_total = round( $remaining_tax_total, 2 );
|
||||
|
||||
if ( 0.0 !== $remaining_tax_total ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$shipping = $breakdown->shipping();
|
||||
$discount = $breakdown->discount();
|
||||
$shipping_discount = $breakdown->shipping_discount();
|
||||
$handling = $breakdown->handling();
|
||||
$insurance = $breakdown->insurance();
|
||||
|
||||
$amount_total = 0.0;
|
||||
if ( $shipping ) {
|
||||
$amount_total += (float) $shipping->value_str();
|
||||
}
|
||||
if ( $item_total ) {
|
||||
$amount_total += (float) $item_total->value_str();
|
||||
}
|
||||
if ( $discount ) {
|
||||
$amount_total -= (float) $discount->value_str();
|
||||
}
|
||||
if ( $tax_total ) {
|
||||
$amount_total += (float) $tax_total->value_str();
|
||||
}
|
||||
if ( $shipping_discount ) {
|
||||
$amount_total -= (float) $shipping_discount->value_str();
|
||||
}
|
||||
if ( $handling ) {
|
||||
$amount_total += (float) $handling->value_str();
|
||||
}
|
||||
if ( $insurance ) {
|
||||
$amount_total += (float) $insurance->value_str();
|
||||
}
|
||||
|
||||
$amount_str = $amount->value_str();
|
||||
$amount_total_str = ( new Money( $amount_total, $amount->currency_code() ) )->value_str();
|
||||
$needs_to_ditch = $amount_str !== $amount_total_str;
|
||||
return $needs_to_ditch;
|
||||
return $purchase_unit;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
/**
|
||||
* The refund object.
|
||||
* The refund entity.
|
||||
*
|
||||
* @link https://developer.paypal.com/docs/api/orders/v2/#definition-refund
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
@ -15,11 +17,32 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
|||
class Refund {
|
||||
|
||||
/**
|
||||
* The Capture.
|
||||
* The ID.
|
||||
*
|
||||
* @var Capture
|
||||
* @var string
|
||||
*/
|
||||
private $capture;
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* The status.
|
||||
*
|
||||
* @var RefundStatus
|
||||
*/
|
||||
private $status;
|
||||
|
||||
/**
|
||||
* The amount.
|
||||
*
|
||||
* @var Amount
|
||||
*/
|
||||
private $amount;
|
||||
|
||||
/**
|
||||
* The detailed breakdown of the refund activity (fees, ...).
|
||||
*
|
||||
* @var SellerPayableBreakdown|null
|
||||
*/
|
||||
private $seller_payable_breakdown;
|
||||
|
||||
/**
|
||||
* The invoice id.
|
||||
|
@ -29,50 +52,97 @@ class Refund {
|
|||
private $invoice_id;
|
||||
|
||||
/**
|
||||
* The note to the payer.
|
||||
* The custom id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $custom_id;
|
||||
|
||||
/**
|
||||
* The acquirer reference number.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $acquirer_reference_number;
|
||||
|
||||
/**
|
||||
* The acquirer reference number.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $note_to_payer;
|
||||
|
||||
/**
|
||||
* The Amount.
|
||||
* The payer of the refund.
|
||||
*
|
||||
* @var Amount|null
|
||||
* @var ?RefundPayer
|
||||
*/
|
||||
private $amount;
|
||||
private $payer;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param string $id The ID.
|
||||
* @param RefundStatus $status The status.
|
||||
* @param Amount $amount The amount.
|
||||
* @param string $invoice_id The invoice id.
|
||||
* @param string $custom_id The custom id.
|
||||
* @param SellerPayableBreakdown|null $seller_payable_breakdown The detailed breakdown of the refund activity (fees, ...).
|
||||
* @param string $acquirer_reference_number The acquirer reference number.
|
||||
* @param string $note_to_payer The note to payer.
|
||||
* @param RefundPayer|null $payer The payer.
|
||||
*/
|
||||
public function __construct(
|
||||
Capture $capture,
|
||||
string $id,
|
||||
RefundStatus $status,
|
||||
Amount $amount,
|
||||
string $invoice_id,
|
||||
string $note_to_payer = '',
|
||||
Amount $amount = null
|
||||
string $custom_id,
|
||||
?SellerPayableBreakdown $seller_payable_breakdown,
|
||||
string $acquirer_reference_number,
|
||||
string $note_to_payer,
|
||||
?RefundPayer $payer
|
||||
) {
|
||||
$this->capture = $capture;
|
||||
$this->invoice_id = $invoice_id;
|
||||
$this->note_to_payer = $note_to_payer;
|
||||
$this->amount = $amount;
|
||||
$this->id = $id;
|
||||
$this->status = $status;
|
||||
$this->amount = $amount;
|
||||
$this->invoice_id = $invoice_id;
|
||||
$this->custom_id = $custom_id;
|
||||
$this->seller_payable_breakdown = $seller_payable_breakdown;
|
||||
$this->acquirer_reference_number = $acquirer_reference_number;
|
||||
$this->note_to_payer = $note_to_payer;
|
||||
$this->payer = $payer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the capture for the refund.
|
||||
* Returns the ID.
|
||||
*
|
||||
* @return Capture
|
||||
* @return string
|
||||
*/
|
||||
public function for_capture() : Capture {
|
||||
return $this->capture;
|
||||
public function id() : string {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the invoice id.
|
||||
* Returns the status.
|
||||
*
|
||||
* @return RefundStatus
|
||||
*/
|
||||
public function status() : RefundStatus {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the amount.
|
||||
*
|
||||
* @return Amount
|
||||
*/
|
||||
public function amount() : Amount {
|
||||
return $this->amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the invoice id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
@ -81,7 +151,34 @@ class Refund {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the note to the payer.
|
||||
* Returns the custom id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function custom_id() : string {
|
||||
return $this->custom_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the detailed breakdown of the refund activity (fees, ...).
|
||||
*
|
||||
* @return SellerPayableBreakdown|null
|
||||
*/
|
||||
public function seller_payable_breakdown() : ?SellerPayableBreakdown {
|
||||
return $this->seller_payable_breakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* The acquirer reference number.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function acquirer_reference_number() : string {
|
||||
return $this->acquirer_reference_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The note to payer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
|
@ -90,28 +187,38 @@ class Refund {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the Amount.
|
||||
* Returns the refund payer.
|
||||
*
|
||||
* @return Amount|null
|
||||
* @return RefundPayer|null
|
||||
*/
|
||||
public function amount() {
|
||||
return $this->amount;
|
||||
public function payer() : ?RefundPayer {
|
||||
return $this->payer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
* Returns the entity as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() : array {
|
||||
$data = array(
|
||||
'invoice_id' => $this->invoice_id(),
|
||||
$data = array(
|
||||
'id' => $this->id(),
|
||||
'status' => $this->status()->name(),
|
||||
'amount' => $this->amount()->to_array(),
|
||||
'invoice_id' => $this->invoice_id(),
|
||||
'custom_id' => $this->custom_id(),
|
||||
'acquirer_reference_number' => $this->acquirer_reference_number(),
|
||||
'note_to_payer' => (array) $this->note_to_payer(),
|
||||
);
|
||||
if ( $this->note_to_payer() ) {
|
||||
$data['note_to_payer'] = $this->note_to_payer();
|
||||
$details = $this->status()->details();
|
||||
if ( $details ) {
|
||||
$data['status_details'] = array( 'reason' => $details->reason() );
|
||||
}
|
||||
if ( $this->amount() ) {
|
||||
$data['amount'] = $this->amount()->to_array();
|
||||
if ( $this->seller_payable_breakdown ) {
|
||||
$data['seller_payable_breakdown'] = $this->seller_payable_breakdown->to_array();
|
||||
}
|
||||
if ( $this->payer ) {
|
||||
$data['payer'] = $this->payer->to_array();
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
|
118
modules/ppcp-api-client/src/Entity/RefundCapture.php
Normal file
118
modules/ppcp-api-client/src/Entity/RefundCapture.php
Normal file
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
/**
|
||||
* The refund capture object.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class RefundCapture
|
||||
*/
|
||||
class RefundCapture {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
79
modules/ppcp-api-client/src/Entity/RefundPayer.php
Normal file
79
modules/ppcp-api-client/src/Entity/RefundPayer.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
/**
|
||||
* The refund payer object.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class RefundPayer
|
||||
* The customer who sends the money.
|
||||
*/
|
||||
class RefundPayer {
|
||||
|
||||
/**
|
||||
* The email address.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $email_address;
|
||||
|
||||
/**
|
||||
* The merchant id.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $merchant_id;
|
||||
|
||||
/**
|
||||
* RefundPayer constructor.
|
||||
*
|
||||
* @param string $email_address The email.
|
||||
* @param string $merchant_id The merchant id.
|
||||
*/
|
||||
public function __construct(
|
||||
string $email_address,
|
||||
string $merchant_id
|
||||
) {
|
||||
|
||||
$this->email_address = $email_address;
|
||||
$this->merchant_id = $merchant_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the email address.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function email_address(): string {
|
||||
return $this->email_address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the merchant id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function merchant_id(): string {
|
||||
return $this->merchant_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array() {
|
||||
$payer = array(
|
||||
'email_address' => $this->email_address(),
|
||||
);
|
||||
if ( $this->merchant_id ) {
|
||||
$payer['merchant_id'] = $this->merchant_id();
|
||||
}
|
||||
return $payer;
|
||||
}
|
||||
}
|
77
modules/ppcp-api-client/src/Entity/RefundStatus.php
Normal file
77
modules/ppcp-api-client/src/Entity/RefundStatus.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
/**
|
||||
* The RefundStatus object.
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/orders/v2/#definition-refund_status
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class RefundStatus
|
||||
*/
|
||||
class RefundStatus {
|
||||
|
||||
const COMPLETED = 'COMPLETED';
|
||||
const CANCELLED = 'CANCELLED';
|
||||
const FAILED = 'FAILED';
|
||||
const PENDING = 'PENDING';
|
||||
|
||||
/**
|
||||
* The status.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $status;
|
||||
|
||||
/**
|
||||
* The details.
|
||||
*
|
||||
* @var RefundStatusDetails|null
|
||||
*/
|
||||
private $details;
|
||||
|
||||
/**
|
||||
* RefundStatus constructor.
|
||||
*
|
||||
* @param string $status The status.
|
||||
* @param RefundStatusDetails|null $details The details.
|
||||
*/
|
||||
public function __construct( string $status, ?RefundStatusDetails $details = null ) {
|
||||
$this->status = $status;
|
||||
$this->details = $details;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current status with a given one.
|
||||
*
|
||||
* @param string $status The status to compare with.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is( string $status ): bool {
|
||||
return $this->status === $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function name(): string {
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the details.
|
||||
*
|
||||
* @return RefundStatusDetails|null
|
||||
*/
|
||||
public function details(): ?RefundStatusDetails {
|
||||
return $this->details;
|
||||
}
|
||||
}
|
71
modules/ppcp-api-client/src/Entity/RefundStatusDetails.php
Normal file
71
modules/ppcp-api-client/src/Entity/RefundStatusDetails.php
Normal file
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
/**
|
||||
* The RefundStatusDetails object.
|
||||
*
|
||||
* @see https://developer.paypal.com/docs/api/payments/v2/#definition-refund_status_details
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class RefundStatusDetails
|
||||
*/
|
||||
class RefundStatusDetails {
|
||||
|
||||
const ECHECK = 'ECHECK';
|
||||
|
||||
/**
|
||||
* The reason.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $reason;
|
||||
|
||||
/**
|
||||
* RefundStatusDetails constructor.
|
||||
*
|
||||
* @param string $reason The reason explaining refund status.
|
||||
*/
|
||||
public function __construct( string $reason ) {
|
||||
$this->reason = $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the current reason with a given one.
|
||||
*
|
||||
* @param string $reason The reason to compare with.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is( string $reason ): bool {
|
||||
return $this->reason === $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the reason explaining refund status.
|
||||
* One of RefundStatusDetails constants.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function reason(): string {
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the human-readable reason text explaining refund status.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function text(): string {
|
||||
switch ( $this->reason ) {
|
||||
case self::ECHECK:
|
||||
return __( 'The payer paid by an eCheck that has not yet cleared.', 'woocommerce-paypal-payments' );
|
||||
default:
|
||||
return $this->reason;
|
||||
}
|
||||
}
|
||||
}
|
202
modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php
Normal file
202
modules/ppcp-api-client/src/Entity/SellerPayableBreakdown.php
Normal file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
/**
|
||||
* The info about fees and amount that will be paid by the seller.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
/**
|
||||
* Class SellerPayableBreakdown
|
||||
*/
|
||||
class SellerPayableBreakdown {
|
||||
|
||||
/**
|
||||
* The amount for this refunded payment in the currency of the transaction.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $gross_amount;
|
||||
|
||||
/**
|
||||
* The applicable fee for this refunded payment in the currency of the transaction.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $paypal_fee;
|
||||
|
||||
/**
|
||||
* The applicable fee for this captured payment in the receivable currency.
|
||||
*
|
||||
* Present only in cases the fee is charged in the receivable currency.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $paypal_fee_in_receivable_currency;
|
||||
|
||||
/**
|
||||
* The net amount that the payee receives for this refunded payment in their PayPal account.
|
||||
*
|
||||
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $net_amount;
|
||||
|
||||
/**
|
||||
* The net amount for this refunded payment in the receivable currency.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $net_amount_in_receivable_currency;
|
||||
|
||||
/**
|
||||
* The total amount for this refund.
|
||||
*
|
||||
* @var Money|null
|
||||
*/
|
||||
private $total_refunded_amount;
|
||||
|
||||
/**
|
||||
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
|
||||
*
|
||||
* @var PlatformFee[]
|
||||
*/
|
||||
private $platform_fees;
|
||||
|
||||
/**
|
||||
* SellerPayableBreakdown constructor.
|
||||
*
|
||||
* @param Money|null $gross_amount The amount for this refunded payment in the currency of the transaction.
|
||||
* @param Money|null $paypal_fee The applicable fee for this refunded payment in the currency of the transaction.
|
||||
* @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this refunded payment in the receivable currency.
|
||||
* @param Money|null $net_amount The net amount that the payee receives for this refunded payment in their PayPal account.
|
||||
* @param Money|null $net_amount_in_receivable_currency The net amount for this refunded payment in the receivable currency.
|
||||
* @param Money|null $total_refunded_amount The total amount for this refund.
|
||||
* @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
|
||||
*/
|
||||
public function __construct(
|
||||
?Money $gross_amount,
|
||||
?Money $paypal_fee,
|
||||
?Money $paypal_fee_in_receivable_currency,
|
||||
?Money $net_amount,
|
||||
?Money $net_amount_in_receivable_currency,
|
||||
?Money $total_refunded_amount,
|
||||
array $platform_fees
|
||||
) {
|
||||
$this->gross_amount = $gross_amount;
|
||||
$this->paypal_fee = $paypal_fee;
|
||||
$this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency;
|
||||
$this->net_amount = $net_amount;
|
||||
$this->net_amount_in_receivable_currency = $net_amount_in_receivable_currency;
|
||||
$this->total_refunded_amount = $total_refunded_amount;
|
||||
$this->platform_fees = $platform_fees;
|
||||
}
|
||||
|
||||
/**
|
||||
* The amount for this refunded payment in the currency of the transaction.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function gross_amount(): ?Money {
|
||||
return $this->gross_amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* The applicable fee for this refunded payment in the currency of the transaction.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function paypal_fee(): ?Money {
|
||||
return $this->paypal_fee;
|
||||
}
|
||||
|
||||
/**
|
||||
* The applicable fee for this refunded payment in the receivable currency.
|
||||
*
|
||||
* Present only in cases the fee is charged in the receivable currency.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function paypal_fee_in_receivable_currency(): ?Money {
|
||||
return $this->paypal_fee_in_receivable_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* The net amount that the payee receives for this refunded payment in their PayPal account.
|
||||
*
|
||||
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function net_amount(): ?Money {
|
||||
return $this->net_amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* The net amount for this refunded payment in the receivable currency.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function net_amount_in_receivable_currency(): ?Money {
|
||||
return $this->net_amount_in_receivable_currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total amount for this refund.
|
||||
*
|
||||
* @return Money|null
|
||||
*/
|
||||
public function total_refunded_amount(): ?Money {
|
||||
return $this->total_refunded_amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of platform or partner fees, commissions, or brokerage fees that associated with the refunded payment.
|
||||
*
|
||||
* @return PlatformFee[]
|
||||
*/
|
||||
public function platform_fees(): array {
|
||||
return $this->platform_fees;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$data = array();
|
||||
if ( $this->gross_amount ) {
|
||||
$data['gross_amount'] = $this->gross_amount->to_array();
|
||||
}
|
||||
if ( $this->paypal_fee ) {
|
||||
$data['paypal_fee'] = $this->paypal_fee->to_array();
|
||||
}
|
||||
if ( $this->paypal_fee_in_receivable_currency ) {
|
||||
$data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array();
|
||||
}
|
||||
if ( $this->net_amount ) {
|
||||
$data['net_amount'] = $this->net_amount->to_array();
|
||||
}
|
||||
if ( $this->net_amount_in_receivable_currency ) {
|
||||
$data['net_amount_in_receivable_currency'] = $this->net_amount_in_receivable_currency->to_array();
|
||||
}
|
||||
if ( $this->total_refunded_amount ) {
|
||||
$data['total_refunded_amount'] = $this->total_refunded_amount->to_array();
|
||||
}
|
||||
if ( $this->platform_fees ) {
|
||||
$data['platform_fees'] = array_map(
|
||||
function ( PlatformFee $fee ) {
|
||||
return $fee->to_array();
|
||||
},
|
||||
$this->platform_fees
|
||||
);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
|
||||
|
||||
/**
|
||||
|
@ -32,19 +33,29 @@ class PaymentsFactory {
|
|||
*/
|
||||
private $capture_factory;
|
||||
|
||||
/**
|
||||
* The Refund factory.
|
||||
*
|
||||
* @var RefundFactory
|
||||
*/
|
||||
private $refund_factory;
|
||||
|
||||
/**
|
||||
* PaymentsFactory constructor.
|
||||
*
|
||||
* @param AuthorizationFactory $authorization_factory The Authorization factory.
|
||||
* @param CaptureFactory $capture_factory The Capture factory.
|
||||
* @param RefundFactory $refund_factory The Refund factory.
|
||||
*/
|
||||
public function __construct(
|
||||
AuthorizationFactory $authorization_factory,
|
||||
CaptureFactory $capture_factory
|
||||
CaptureFactory $capture_factory,
|
||||
RefundFactory $refund_factory
|
||||
) {
|
||||
|
||||
$this->authorization_factory = $authorization_factory;
|
||||
$this->capture_factory = $capture_factory;
|
||||
$this->refund_factory = $refund_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -62,12 +73,18 @@ class PaymentsFactory {
|
|||
isset( $data->authorizations ) ? $data->authorizations : array()
|
||||
);
|
||||
$captures = array_map(
|
||||
function ( \stdClass $authorization ): Capture {
|
||||
return $this->capture_factory->from_paypal_response( $authorization );
|
||||
function ( \stdClass $capture ): Capture {
|
||||
return $this->capture_factory->from_paypal_response( $capture );
|
||||
},
|
||||
isset( $data->captures ) ? $data->captures : array()
|
||||
);
|
||||
$payments = new Payments( $authorizations, $captures );
|
||||
$refunds = array_map(
|
||||
function ( \stdClass $refund ): Refund {
|
||||
return $this->refund_factory->from_paypal_response( $refund );
|
||||
},
|
||||
isset( $data->refunds ) ? $data->refunds : array()
|
||||
);
|
||||
$payments = new Payments( $authorizations, $captures, $refunds );
|
||||
return $payments;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use WC_Session_Handler;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
|
||||
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
|
||||
|
||||
|
@ -77,17 +78,25 @@ class PurchaseUnitFactory {
|
|||
*/
|
||||
private $soft_descriptor;
|
||||
|
||||
/**
|
||||
* The sanitizer for purchase unit output data.
|
||||
*
|
||||
* @var PurchaseUnitSanitizer|null
|
||||
*/
|
||||
private $sanitizer;
|
||||
|
||||
/**
|
||||
* PurchaseUnitFactory constructor.
|
||||
*
|
||||
* @param AmountFactory $amount_factory The amount factory.
|
||||
* @param PayeeRepository $payee_repository The Payee repository.
|
||||
* @param PayeeFactory $payee_factory The Payee factory.
|
||||
* @param ItemFactory $item_factory The item factory.
|
||||
* @param ShippingFactory $shipping_factory The shipping factory.
|
||||
* @param PaymentsFactory $payments_factory The payments factory.
|
||||
* @param string $prefix The prefix.
|
||||
* @param string $soft_descriptor The soft descriptor.
|
||||
* @param AmountFactory $amount_factory The amount factory.
|
||||
* @param PayeeRepository $payee_repository The Payee repository.
|
||||
* @param PayeeFactory $payee_factory The Payee factory.
|
||||
* @param ItemFactory $item_factory The item factory.
|
||||
* @param ShippingFactory $shipping_factory The shipping factory.
|
||||
* @param PaymentsFactory $payments_factory The payments factory.
|
||||
* @param string $prefix The prefix.
|
||||
* @param string $soft_descriptor The soft descriptor.
|
||||
* @param ?PurchaseUnitSanitizer $sanitizer The purchase unit to_array sanitizer.
|
||||
*/
|
||||
public function __construct(
|
||||
AmountFactory $amount_factory,
|
||||
|
@ -97,7 +106,8 @@ class PurchaseUnitFactory {
|
|||
ShippingFactory $shipping_factory,
|
||||
PaymentsFactory $payments_factory,
|
||||
string $prefix = 'WC-',
|
||||
string $soft_descriptor = ''
|
||||
string $soft_descriptor = '',
|
||||
PurchaseUnitSanitizer $sanitizer = null
|
||||
) {
|
||||
|
||||
$this->amount_factory = $amount_factory;
|
||||
|
@ -108,6 +118,7 @@ class PurchaseUnitFactory {
|
|||
$this->payments_factory = $payments_factory;
|
||||
$this->prefix = $prefix;
|
||||
$this->soft_descriptor = $soft_descriptor;
|
||||
$this->sanitizer = $sanitizer;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,6 +162,9 @@ class PurchaseUnitFactory {
|
|||
$invoice_id,
|
||||
$soft_descriptor
|
||||
);
|
||||
|
||||
$this->init_purchase_unit( $purchase_unit );
|
||||
|
||||
/**
|
||||
* Returns PurchaseUnit for the WC order.
|
||||
*/
|
||||
|
@ -221,6 +235,8 @@ class PurchaseUnitFactory {
|
|||
$soft_descriptor
|
||||
);
|
||||
|
||||
$this->init_purchase_unit( $purchase_unit );
|
||||
|
||||
return $purchase_unit;
|
||||
}
|
||||
|
||||
|
@ -283,6 +299,9 @@ class PurchaseUnitFactory {
|
|||
$soft_descriptor,
|
||||
$payments
|
||||
);
|
||||
|
||||
$this->init_purchase_unit( $purchase_unit );
|
||||
|
||||
return $purchase_unit;
|
||||
}
|
||||
|
||||
|
@ -313,4 +332,16 @@ class PurchaseUnitFactory {
|
|||
$countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' );
|
||||
return in_array( $country_code, $countries, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a purchase unit object.
|
||||
*
|
||||
* @param PurchaseUnit $purchase_unit The purchase unit.
|
||||
* @return void
|
||||
*/
|
||||
private function init_purchase_unit( PurchaseUnit $purchase_unit ): void {
|
||||
if ( $this->sanitizer instanceof PurchaseUnitSanitizer ) {
|
||||
$purchase_unit->set_sanitizer( $this->sanitizer );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
91
modules/ppcp-api-client/src/Factory/RefundFactory.php
Normal file
91
modules/ppcp-api-client/src/Factory/RefundFactory.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
/**
|
||||
* The refund factory.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
|
||||
*/
|
||||
|
||||
declare( strict_types=1 );
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails;
|
||||
|
||||
/**
|
||||
* Class RefundFactory
|
||||
*/
|
||||
class RefundFactory {
|
||||
|
||||
/**
|
||||
* The Amount factory.
|
||||
*
|
||||
* @var AmountFactory
|
||||
*/
|
||||
private $amount_factory;
|
||||
|
||||
/**
|
||||
* The SellerPayableBreakdownFactory factory.
|
||||
*
|
||||
* @var SellerPayableBreakdownFactory
|
||||
*/
|
||||
private $seller_payable_breakdown_factory;
|
||||
|
||||
/**
|
||||
* The RefundPayerFactory factory.
|
||||
*
|
||||
* @var RefundPayerFactory
|
||||
*/
|
||||
private $refund_payer_factory;
|
||||
|
||||
/**
|
||||
* RefundFactory constructor.
|
||||
*
|
||||
* @param AmountFactory $amount_factory The amount factory.
|
||||
* @param SellerPayableBreakdownFactory $seller_payable_breakdown_factory The payable breakdown factory.
|
||||
* @param RefundPayerFactory $refund_payer_factory The payer breakdown factory.
|
||||
*/
|
||||
public function __construct(
|
||||
AmountFactory $amount_factory,
|
||||
SellerPayableBreakdownFactory $seller_payable_breakdown_factory,
|
||||
RefundPayerFactory $refund_payer_factory
|
||||
) {
|
||||
$this->amount_factory = $amount_factory;
|
||||
$this->seller_payable_breakdown_factory = $seller_payable_breakdown_factory;
|
||||
$this->refund_payer_factory = $refund_payer_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the refund object based off the PayPal response.
|
||||
*
|
||||
* @param \stdClass $data The PayPal response.
|
||||
*
|
||||
* @return Refund
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ) : Refund {
|
||||
$reason = $data->status_details->reason ?? null;
|
||||
$seller_payable_breakdown = isset( $data->seller_payable_breakdown ) ?
|
||||
$this->seller_payable_breakdown_factory->from_paypal_response( $data->seller_payable_breakdown )
|
||||
: null;
|
||||
|
||||
$payer = isset( $data->payer ) ?
|
||||
$this->refund_payer_factory->from_paypal_response( $data->payer )
|
||||
: null;
|
||||
|
||||
return new Refund(
|
||||
(string) $data->id,
|
||||
new RefundStatus(
|
||||
(string) $data->status,
|
||||
$reason ? new RefundStatusDetails( $reason ) : null
|
||||
),
|
||||
$this->amount_factory->from_paypal_response( $data->amount ),
|
||||
(string) ( $data->invoice_id ?? '' ),
|
||||
(string) ( $data->custom_id ?? '' ),
|
||||
$seller_payable_breakdown,
|
||||
(string) ( $data->acquirer_reference_number ?? '' ),
|
||||
(string) ( $data->note_to_payer ?? '' ),
|
||||
$payer
|
||||
);
|
||||
}
|
||||
}
|
39
modules/ppcp-api-client/src/Factory/RefundPayerFactory.php
Normal file
39
modules/ppcp-api-client/src/Factory/RefundPayerFactory.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
/**
|
||||
* The RefundPayerFactory factory.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerName;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerTaxInfo;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Phone;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PhoneWithType;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundPayer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class RefundPayerFactory
|
||||
*/
|
||||
class RefundPayerFactory {
|
||||
|
||||
/**
|
||||
* Returns a Refund Payer object based off a PayPal Response.
|
||||
*
|
||||
* @param \stdClass $data The JSON object.
|
||||
*
|
||||
* @return RefundPayer
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ): RefundPayer {
|
||||
return new RefundPayer(
|
||||
isset( $data->email_address ) ? $data->email_address : '',
|
||||
isset( $data->merchant_id ) ? $data->merchant_id : ''
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
/**
|
||||
* The SellerPayableBreakdownFactory Factory.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use stdClass;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PlatformFee;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
|
||||
|
||||
/**
|
||||
* Class SellerPayableBreakdownFactory
|
||||
*/
|
||||
class SellerPayableBreakdownFactory {
|
||||
|
||||
/**
|
||||
* The Money factory.
|
||||
*
|
||||
* @var MoneyFactory
|
||||
*/
|
||||
private $money_factory;
|
||||
|
||||
/**
|
||||
* The PlatformFee factory.
|
||||
*
|
||||
* @var PlatformFeeFactory
|
||||
*/
|
||||
private $platform_fee_factory;
|
||||
|
||||
/**
|
||||
* SellerPayableBreakdownFactory constructor.
|
||||
*
|
||||
* @param MoneyFactory $money_factory The Money factory.
|
||||
* @param PlatformFeeFactory $platform_fee_factory The PlatformFee factory.
|
||||
*/
|
||||
public function __construct(
|
||||
MoneyFactory $money_factory,
|
||||
PlatformFeeFactory $platform_fee_factory
|
||||
) {
|
||||
$this->money_factory = $money_factory;
|
||||
$this->platform_fee_factory = $platform_fee_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SellerPayableBreakdownFactory object based off a PayPal Response.
|
||||
*
|
||||
* @param stdClass $data The JSON object.
|
||||
*
|
||||
* @return SellerPayableBreakdown
|
||||
*/
|
||||
public function from_paypal_response( stdClass $data ): SellerPayableBreakdown {
|
||||
|
||||
$gross_amount = ( isset( $data->gross_amount ) ) ? $this->money_factory->from_paypal_response( $data->gross_amount ) : null;
|
||||
$paypal_fee = ( isset( $data->paypal_fee ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee ) : null;
|
||||
$paypal_fee_in_receivable_currency = ( isset( $data->paypal_fee_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee_in_receivable_currency ) : null;
|
||||
$net_amount = ( isset( $data->net_amount ) ) ? $this->money_factory->from_paypal_response( $data->net_amount ) : null;
|
||||
$net_amount_in_receivable_currency = ( isset( $data->net_amount_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->net_amount_in_receivable_currency ) : null;
|
||||
$total_refunded_amount = ( isset( $data->total_refunded_amount ) ) ? $this->money_factory->from_paypal_response( $data->total_refunded_amount ) : null;
|
||||
$platform_fees = ( isset( $data->platform_fees ) ) ? array_map(
|
||||
function ( stdClass $fee_data ): PlatformFee {
|
||||
return $this->platform_fee_factory->from_paypal_response( $fee_data );
|
||||
},
|
||||
$data->platform_fees
|
||||
) : array();
|
||||
|
||||
return new SellerPayableBreakdown(
|
||||
$gross_amount,
|
||||
$paypal_fee,
|
||||
$paypal_fee_in_receivable_currency,
|
||||
$net_amount,
|
||||
$net_amount_in_receivable_currency,
|
||||
$total_refunded_amount,
|
||||
$platform_fees
|
||||
);
|
||||
}
|
||||
}
|
|
@ -33,4 +33,16 @@ class MoneyFormatter {
|
|||
? (string) round( $value, 0 )
|
||||
: number_format( $value, 2, '.', '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum amount a currency can be incremented or decremented.
|
||||
*
|
||||
* @param string $currency The 3-letter currency code.
|
||||
* @return float
|
||||
*/
|
||||
public function minimum_increment( string $currency ): float {
|
||||
return (float) in_array( $currency, $this->currencies_without_decimals, true )
|
||||
? 1.00
|
||||
: 0.01;
|
||||
}
|
||||
}
|
||||
|
|
160
modules/ppcp-api-client/src/Helper/OrderTransient.php
Normal file
160
modules/ppcp-api-client/src/Helper/OrderTransient.php
Normal file
|
@ -0,0 +1,160 @@
|
|||
<?php
|
||||
/**
|
||||
* PayPal order transient helper.
|
||||
*
|
||||
* This class is used to pass transient data between the PayPal order and the WooCommerce order.
|
||||
* These two orders can be created on different requests and at different times so this transient
|
||||
* data must be persisted between requests.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
|
||||
|
||||
use WC_Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
|
||||
/**
|
||||
* Class OrderHelper
|
||||
*/
|
||||
class OrderTransient {
|
||||
const CACHE_KEY = 'order_transient';
|
||||
const CACHE_TIMEOUT = DAY_IN_SECONDS; // If necessary we can increase this.
|
||||
|
||||
/**
|
||||
* The Cache.
|
||||
*
|
||||
* @var Cache
|
||||
*/
|
||||
private $cache;
|
||||
|
||||
/**
|
||||
* The purchase unit sanitizer.
|
||||
*
|
||||
* @var PurchaseUnitSanitizer
|
||||
*/
|
||||
private $purchase_unit_sanitizer;
|
||||
|
||||
/**
|
||||
* OrderTransient constructor.
|
||||
*
|
||||
* @param Cache $cache The Cache.
|
||||
* @param PurchaseUnitSanitizer $purchase_unit_sanitizer The purchase unit sanitizer.
|
||||
*/
|
||||
public function __construct( Cache $cache, PurchaseUnitSanitizer $purchase_unit_sanitizer ) {
|
||||
$this->cache = $cache;
|
||||
$this->purchase_unit_sanitizer = $purchase_unit_sanitizer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the created PayPal order.
|
||||
*
|
||||
* @param Order $order The PayPal order.
|
||||
* @return void
|
||||
*/
|
||||
public function on_order_created( Order $order ): void {
|
||||
$message = $this->purchase_unit_sanitizer->get_last_message();
|
||||
$this->add_order_note( $order, $message );
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the created WooCommerce order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WooCommerce order.
|
||||
* @param Order $order The PayPal order.
|
||||
* @return void
|
||||
*/
|
||||
public function on_woocommerce_order_created( WC_Order $wc_order, Order $order ): void {
|
||||
$cache_key = $this->cache_key( $order );
|
||||
|
||||
if ( ! $cache_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->apply_order_notes( $order, $wc_order );
|
||||
$this->cache->delete( $cache_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an order note associated with a PayPal order.
|
||||
* It can be added to a WooCommerce order associated with this PayPal order in the future.
|
||||
*
|
||||
* @param Order $order The PayPal order.
|
||||
* @param string $message The message to be added to order notes.
|
||||
* @return void
|
||||
*/
|
||||
private function add_order_note( Order $order, string $message ): void {
|
||||
if ( ! $message ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cache_key = $this->cache_key( $order );
|
||||
|
||||
if ( ! $cache_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transient = $this->cache->get( $cache_key );
|
||||
|
||||
if ( ! is_array( $transient ) ) {
|
||||
$transient = array();
|
||||
}
|
||||
|
||||
if ( ! is_array( $transient['notes'] ) ) {
|
||||
$transient['notes'] = array();
|
||||
}
|
||||
|
||||
$transient['notes'][] = $message;
|
||||
|
||||
$this->cache->set( $cache_key, $transient, self::CACHE_TIMEOUT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an order note associated with a PayPal order.
|
||||
* It can be added to a WooCommerce order associated with this PayPal order in the future.
|
||||
*
|
||||
* @param Order $order The PayPal order.
|
||||
* @param WC_Order $wc_order The WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
private function apply_order_notes( Order $order, WC_Order $wc_order ): void {
|
||||
$cache_key = $this->cache_key( $order );
|
||||
|
||||
if ( ! $cache_key ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transient = $this->cache->get( $cache_key );
|
||||
|
||||
if ( ! is_array( $transient ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_array( $transient['notes'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( $transient['notes'] as $note ) {
|
||||
if ( ! is_string( $note ) ) {
|
||||
continue;
|
||||
}
|
||||
$wc_order->add_order_note( $note );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cache key.
|
||||
*
|
||||
* @param Order $order The PayPal order.
|
||||
* @return string|null
|
||||
*/
|
||||
private function cache_key( Order $order ): ?string {
|
||||
if ( ! $order->id() ) {
|
||||
return null;
|
||||
}
|
||||
return implode( '_', array( self::CACHE_KEY . $order->id() ) );
|
||||
}
|
||||
|
||||
}
|
368
modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php
Normal file
368
modules/ppcp-api-client/src/Helper/PurchaseUnitSanitizer.php
Normal file
|
@ -0,0 +1,368 @@
|
|||
<?php
|
||||
/**
|
||||
* Class PurchaseUnitSanitizer.
|
||||
*
|
||||
* Sanitizes a purchase_unit array to be consumed by PayPal.
|
||||
*
|
||||
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
|
||||
* not have this restriction. Therefore, the totals of the cart in WooCommerce and the totals
|
||||
* of the rounded money values of the items, we send to PayPal, can differ. In those case we either:
|
||||
* - Add an extra line with roundings.
|
||||
* - Don't send the line items.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
|
||||
/**
|
||||
* Class PurchaseUnitSanitizer
|
||||
*/
|
||||
class PurchaseUnitSanitizer {
|
||||
const MODE_DITCH = 'ditch';
|
||||
const MODE_EXTRA_LINE = 'extra_line';
|
||||
const VALID_MODES = array(
|
||||
self::MODE_DITCH,
|
||||
self::MODE_EXTRA_LINE,
|
||||
);
|
||||
|
||||
const EXTRA_LINE_NAME = 'Subtotal mismatch';
|
||||
|
||||
/**
|
||||
* The purchase unit data
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $purchase_unit = array();
|
||||
|
||||
/**
|
||||
* Whether to allow items to be ditched.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $allow_ditch_items = true;
|
||||
|
||||
/**
|
||||
* The working mode
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $mode;
|
||||
|
||||
/**
|
||||
* The name for the extra line
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $extra_line_name;
|
||||
|
||||
/**
|
||||
* The last message. To be added to order notes.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $last_message = '';
|
||||
|
||||
/**
|
||||
* If the items and breakdown has been ditched.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $has_ditched_items_breakdown = false;
|
||||
|
||||
/**
|
||||
* PurchaseUnitSanitizer constructor.
|
||||
*
|
||||
* @param string|null $mode The mismatch handling mode, ditch or extra_line.
|
||||
* @param string|null $extra_line_name The name of the extra line.
|
||||
*/
|
||||
public function __construct( string $mode = null, string $extra_line_name = null ) {
|
||||
|
||||
if ( ! in_array( $mode, self::VALID_MODES, true ) ) {
|
||||
$mode = self::MODE_DITCH;
|
||||
}
|
||||
|
||||
if ( ! $extra_line_name ) {
|
||||
$extra_line_name = self::EXTRA_LINE_NAME;
|
||||
}
|
||||
|
||||
$this->mode = $mode;
|
||||
$this->extra_line_name = $extra_line_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* The purchase_unit amount.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function amount(): array {
|
||||
return $this->purchase_unit['amount'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* The purchase_unit currency code.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function currency_code(): string {
|
||||
return (string) ( $this->amount()['currency_code'] ?? '' );
|
||||
}
|
||||
|
||||
/**
|
||||
* The purchase_unit breakdown.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function breakdown(): array {
|
||||
return $this->amount()['breakdown'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* The purchase_unit breakdown.
|
||||
*
|
||||
* @param string $key The breakdown element to get the value from.
|
||||
* @return float
|
||||
*/
|
||||
private function breakdown_value( string $key ): float {
|
||||
if ( ! isset( $this->breakdown()[ $key ] ) ) {
|
||||
return 0.0;
|
||||
}
|
||||
return (float) ( $this->breakdown()[ $key ]['value'] ?? 0.0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The purchase_unit items array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function items(): array {
|
||||
return $this->purchase_unit['items'] ?? array();
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizes the purchase_unit array.
|
||||
*
|
||||
* @param array $purchase_unit The purchase_unit array that should be sanitized.
|
||||
* @param bool $allow_ditch_items Whether to allow items to be ditched.
|
||||
* @return array
|
||||
*/
|
||||
public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array {
|
||||
$this->purchase_unit = $purchase_unit;
|
||||
$this->allow_ditch_items = $allow_ditch_items;
|
||||
$this->has_ditched_items_breakdown = false;
|
||||
|
||||
$this->sanitize_item_amount_mismatch();
|
||||
$this->sanitize_item_tax_mismatch();
|
||||
$this->sanitize_breakdown_mismatch();
|
||||
return $this->purchase_unit;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizes the purchase_unit items amount.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sanitize_item_amount_mismatch(): void {
|
||||
$item_mismatch = $this->calculate_item_mismatch();
|
||||
|
||||
if ( $this->mode === self::MODE_EXTRA_LINE ) {
|
||||
if ( $item_mismatch < 0 ) {
|
||||
|
||||
// Do floors on item amounts so item_mismatch is a positive value.
|
||||
foreach ( $this->purchase_unit['items'] as $index => $item ) {
|
||||
// Get a more intelligent adjustment mechanism.
|
||||
$increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] );
|
||||
|
||||
$this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money(
|
||||
( (float) $item['unit_amount']['value'] ) - $increment,
|
||||
$item['unit_amount']['currency_code']
|
||||
) )->to_array();
|
||||
}
|
||||
}
|
||||
|
||||
$item_mismatch = $this->calculate_item_mismatch();
|
||||
|
||||
if ( $item_mismatch > 0 ) {
|
||||
// Add extra line item with roundings.
|
||||
$line_name = $this->extra_line_name;
|
||||
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
|
||||
$this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array();
|
||||
|
||||
$this->set_last_message(
|
||||
__( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
|
||||
$item_mismatch = $this->calculate_item_mismatch();
|
||||
}
|
||||
|
||||
if ( $item_mismatch !== 0.0 ) {
|
||||
// Ditch items.
|
||||
if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) {
|
||||
unset( $this->purchase_unit['items'] );
|
||||
$this->set_last_message(
|
||||
__( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizes the purchase_unit items tax.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sanitize_item_tax_mismatch(): void {
|
||||
$tax_mismatch = $this->calculate_tax_mismatch();
|
||||
|
||||
if ( $this->allow_ditch_items && $tax_mismatch !== 0.0 ) {
|
||||
// Unset tax in items.
|
||||
foreach ( $this->purchase_unit['items'] as $index => $item ) {
|
||||
if ( isset( $this->purchase_unit['items'][ $index ]['tax'] ) ) {
|
||||
unset( $this->purchase_unit['items'][ $index ]['tax'] );
|
||||
}
|
||||
if ( isset( $this->purchase_unit['items'][ $index ]['tax_rate'] ) ) {
|
||||
unset( $this->purchase_unit['items'][ $index ]['tax_rate'] );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizes the purchase_unit breakdown.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function sanitize_breakdown_mismatch(): void {
|
||||
$breakdown_mismatch = $this->calculate_breakdown_mismatch();
|
||||
|
||||
if ( $this->allow_ditch_items && $breakdown_mismatch !== 0.0 ) {
|
||||
// Ditch breakdowns and items.
|
||||
if ( isset( $this->purchase_unit['items'] ) ) {
|
||||
unset( $this->purchase_unit['items'] );
|
||||
}
|
||||
if ( isset( $this->purchase_unit['amount']['breakdown'] ) ) {
|
||||
unset( $this->purchase_unit['amount']['breakdown'] );
|
||||
}
|
||||
|
||||
$this->has_ditched_items_breakdown = true;
|
||||
$this->set_last_message(
|
||||
__( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The calculates amount mismatch of items sums with breakdown.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function calculate_item_mismatch(): float {
|
||||
$item_total = $this->breakdown_value( 'item_total' );
|
||||
if ( ! $item_total ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remaining_item_total = array_reduce(
|
||||
$this->items(),
|
||||
function ( float $total, array $item ): float {
|
||||
return $total - (float) $item['unit_amount']['value'] * (float) $item['quantity'];
|
||||
},
|
||||
$item_total
|
||||
);
|
||||
|
||||
return round( $remaining_item_total, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The calculates tax mismatch of items sums with breakdown.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function calculate_tax_mismatch(): float {
|
||||
$tax_total = $this->breakdown_value( 'tax_total' );
|
||||
$items_with_tax = array_filter(
|
||||
$this->items(),
|
||||
function ( array $item ): bool {
|
||||
return isset( $item['tax'] );
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! $tax_total || empty( $items_with_tax ) ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$remaining_tax_total = array_reduce(
|
||||
$this->items(),
|
||||
function ( float $total, array $item ): float {
|
||||
$tax = $item['tax'] ?? false;
|
||||
if ( $tax ) {
|
||||
$total -= (float) $tax['value'] * (float) $item['quantity'];
|
||||
}
|
||||
return $total;
|
||||
},
|
||||
$tax_total
|
||||
);
|
||||
|
||||
return round( $remaining_tax_total, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The calculates mismatch of breakdown sums with total amount.
|
||||
*
|
||||
* @return float
|
||||
*/
|
||||
private function calculate_breakdown_mismatch(): float {
|
||||
$breakdown = $this->breakdown();
|
||||
if ( ! $breakdown ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$amount_total = 0.0;
|
||||
$amount_total += $this->breakdown_value( 'item_total' );
|
||||
$amount_total += $this->breakdown_value( 'tax_total' );
|
||||
$amount_total += $this->breakdown_value( 'shipping' );
|
||||
$amount_total -= $this->breakdown_value( 'discount' );
|
||||
$amount_total -= $this->breakdown_value( 'shipping_discount' );
|
||||
$amount_total += $this->breakdown_value( 'handling' );
|
||||
$amount_total += $this->breakdown_value( 'insurance' );
|
||||
|
||||
$amount_str = $this->amount()['value'] ?? 0;
|
||||
$amount_total_str = ( new Money( $amount_total, $this->currency_code() ) )->value_str();
|
||||
|
||||
return $amount_str - $amount_total_str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the items and breakdown were ditched.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has_ditched_items_breakdown(): bool {
|
||||
return $this->has_ditched_items_breakdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last sanitization message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_last_message(): string {
|
||||
return $this->last_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the last sanitization message.
|
||||
*
|
||||
* @param string $message The message.
|
||||
*/
|
||||
public function set_last_message( string $message ): void {
|
||||
$this->last_message = $message;
|
||||
}
|
||||
|
||||
}
|
|
@ -15,3 +15,7 @@
|
|||
.ppcp-dcc-order-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
iframe[id^="hosted-fields-tokenization-frame_"] {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ class SingleProductActionHandler {
|
|||
getSubscriptionProducts()
|
||||
{
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
return [new Product(id, 1, this.variations())];
|
||||
return [new Product(id, 1, this.variations(), this.extraFields())];
|
||||
}
|
||||
|
||||
configuration()
|
||||
|
@ -107,7 +107,7 @@ class SingleProductActionHandler {
|
|||
{
|
||||
if ( this.isBookingProduct() ) {
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))];
|
||||
return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"), this.extraFields())];
|
||||
} else if ( this.isGroupedProduct() ) {
|
||||
const products = [];
|
||||
this.formElement.querySelectorAll('input[type="number"]').forEach((element) => {
|
||||
|
@ -120,17 +120,25 @@ class SingleProductActionHandler {
|
|||
}
|
||||
const id = parseInt(elementName[1]);
|
||||
const quantity = parseInt(element.value);
|
||||
products.push(new Product(id, quantity, null));
|
||||
products.push(new Product(id, quantity, null, this.extraFields()));
|
||||
})
|
||||
return products;
|
||||
} else {
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
const qty = document.querySelector('[name="quantity"]').value;
|
||||
const variations = this.variations();
|
||||
return [new Product(id, qty, variations)];
|
||||
return [new Product(id, qty, variations, this.extraFields())];
|
||||
}
|
||||
}
|
||||
|
||||
extraFields() {
|
||||
return FormHelper.getFilteredFields(
|
||||
this.formElement,
|
||||
['add-to-cart', 'quantity', 'product_id', 'variation_id'],
|
||||
['attribute_', 'wc_bookings_field']
|
||||
);
|
||||
}
|
||||
|
||||
createOrder()
|
||||
{
|
||||
this.cartHelper = null;
|
||||
|
|
|
@ -2,8 +2,8 @@ import Product from "./Product";
|
|||
|
||||
class BookingProduct extends Product {
|
||||
|
||||
constructor(id, quantity, booking) {
|
||||
super(id, quantity, null);
|
||||
constructor(id, quantity, booking, extra) {
|
||||
super(id, quantity, null, extra);
|
||||
this.booking = booking;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
class Product {
|
||||
|
||||
constructor(id, quantity, variations) {
|
||||
constructor(id, quantity, variations, extra) {
|
||||
this.id = id;
|
||||
this.quantity = quantity;
|
||||
this.variations = variations;
|
||||
this.extra = extra;
|
||||
}
|
||||
|
||||
data() {
|
||||
return {
|
||||
id:this.id,
|
||||
quantity:this.quantity,
|
||||
variations:this.variations
|
||||
quantity: this.quantity,
|
||||
variations: this.variations,
|
||||
extra: this.extra,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Product;
|
||||
export default Product;
|
||||
|
|
|
@ -5,13 +5,46 @@
|
|||
export default class FormHelper {
|
||||
|
||||
static getPrefixedFields(formElement, prefix) {
|
||||
const formData = new FormData(formElement);
|
||||
let fields = {};
|
||||
for(const element of formElement.elements) {
|
||||
if( element.name.startsWith(prefix) ) {
|
||||
fields[element.name] = element.value;
|
||||
|
||||
for (const [name, value] of formData.entries()) {
|
||||
if (!prefix || name.startsWith(prefix)) {
|
||||
fields[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
static getFilteredFields(formElement, exactFilters, prefixFilters) {
|
||||
const formData = new FormData(formElement);
|
||||
let fields = {};
|
||||
let counters = {};
|
||||
|
||||
for (let [name, value] of formData.entries()) {
|
||||
|
||||
// Handle array format
|
||||
if (name.indexOf('[]') !== -1) {
|
||||
const k = name;
|
||||
counters[k] = counters[k] || 0;
|
||||
name = name.replace('[]', `[${counters[k]}]`);
|
||||
counters[k]++;
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
if (exactFilters && (exactFilters.indexOf(name) !== -1)) {
|
||||
continue;
|
||||
}
|
||||
if (prefixFilters && prefixFilters.some(prefixFilter => name.startsWith(prefixFilter))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fields[name] = value;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ return array(
|
|||
$session_handler = $container->get( 'session.handler' );
|
||||
$settings = $container->get( 'wcgateway.settings' );
|
||||
$early_order_handler = $container->get( 'button.helper.early-order-handler' );
|
||||
$registration_needed = $container->get( 'button.current-user-must-register' );
|
||||
$registration_needed = $container->get( 'button.current-user-must-register' );
|
||||
$logger = $container->get( 'woocommerce.logger.woocommerce' );
|
||||
return new CreateOrderEndpoint(
|
||||
$request_data,
|
||||
|
|
|
@ -112,6 +112,18 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
|
|||
|
||||
$success = true;
|
||||
foreach ( $products as $product ) {
|
||||
|
||||
// Add extras to POST, they are usually added by custom plugins.
|
||||
if ( $product['extra'] && is_array( $product['extra'] ) ) {
|
||||
// Handle cases like field[].
|
||||
$query = http_build_query( $product['extra'] );
|
||||
parse_str( $query, $extra );
|
||||
|
||||
foreach ( $extra as $key => $value ) {
|
||||
$_POST[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $product['product']->is_type( 'booking' ) ) {
|
||||
$success = $success && $this->add_booking_product(
|
||||
$product['product'],
|
||||
|
@ -229,6 +241,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
|
|||
'quantity' => (int) $product['quantity'],
|
||||
'variations' => $product['variations'] ?? null,
|
||||
'booking' => $product['booking'] ?? null,
|
||||
'extra' => $product['extra'] ?? null,
|
||||
);
|
||||
}
|
||||
return $products;
|
||||
|
|
|
@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
|
||||
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
|
||||
|
@ -330,6 +331,8 @@ class CreateOrderEndpoint implements EndpointInterface {
|
|||
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
|
||||
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
|
||||
$wc_order->save_meta_data();
|
||||
|
||||
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
|
||||
}
|
||||
|
||||
wp_send_json_success( $this->make_response( $order ) );
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
|
|||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\State;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
|
@ -163,6 +164,10 @@ class EarlyOrderHandler {
|
|||
/**
|
||||
* Patch Order so we have the \WC_Order id added.
|
||||
*/
|
||||
return $this->order_processor->patch_order( $wc_order, $order );
|
||||
$order = $this->order_processor->patch_order( $wc_order, $order );
|
||||
|
||||
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,6 +213,12 @@ class RenewalHandler {
|
|||
$transaction_id = $this->get_paypal_order_transaction_id( $order );
|
||||
if ( $transaction_id ) {
|
||||
$this->update_transaction_id( $transaction_id, $wc_order );
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_order( $wc_order->get_id(), array( 'order_type' => 'any' ) );
|
||||
foreach ( $subscriptions as $id => $subscription ) {
|
||||
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
|
||||
$this->handle_new_order_status( $order, $wc_order );
|
||||
|
|
|
@ -17,6 +17,7 @@ use WC_Product_Variable_Subscription;
|
|||
use WC_Subscriptions_Product;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Environment;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
|
||||
|
@ -30,6 +31,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
|
||||
use WP_Post;
|
||||
|
@ -39,6 +41,8 @@ use WP_Post;
|
|||
*/
|
||||
class SubscriptionModule implements ModuleInterface {
|
||||
|
||||
use TransactionIdHandlingTrait;
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
|
@ -83,6 +87,19 @@ class SubscriptionModule implements ModuleInterface {
|
|||
$logger = $c->get( 'woocommerce.logger.woocommerce' );
|
||||
|
||||
$this->add_payment_token_id( $subscription, $payment_token_repository, $logger );
|
||||
|
||||
if ( count( $subscription->get_related_orders() ) === 1 ) {
|
||||
$parent_order = $subscription->get_parent();
|
||||
if ( is_a( $parent_order, WC_Order::class ) ) {
|
||||
$order_repository = $c->get( 'api.repository.order' );
|
||||
$order = $order_repository->for_wc_order( $parent_order );
|
||||
$transaction_id = $this->get_paypal_order_transaction_id( $order );
|
||||
if ( $transaction_id ) {
|
||||
$subscription->update_meta_data( 'ppcp_previous_transaction_reference', $transaction_id );
|
||||
$subscription->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -133,28 +150,20 @@ class SubscriptionModule implements ModuleInterface {
|
|||
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
|
||||
&& isset( $data['payment_source']['token']['source']->card )
|
||||
) {
|
||||
$renewal_order_id = absint( $data['purchase_units'][0]['custom_id'] );
|
||||
$subscriptions = wcs_get_subscriptions_for_renewal_order( $renewal_order_id );
|
||||
$subscriptions_values = array_values( $subscriptions );
|
||||
$latest_subscription = array_shift( $subscriptions_values );
|
||||
if ( is_a( $latest_subscription, WC_Subscription::class ) ) {
|
||||
$related_renewal_orders = $latest_subscription->get_related_orders( 'ids', 'renewal' );
|
||||
$latest_order_id_with_transaction = array_slice( $related_renewal_orders, 1, 1, false );
|
||||
$order_id = ! empty( $latest_order_id_with_transaction ) ? $latest_order_id_with_transaction[0] : 0;
|
||||
if ( count( $related_renewal_orders ) === 1 ) {
|
||||
$order_id = $latest_subscription->get_parent_id();
|
||||
}
|
||||
|
||||
$wc_order = wc_get_order( $order_id );
|
||||
if ( is_a( $wc_order, WC_Order::class ) ) {
|
||||
$transaction_id = $wc_order->get_transaction_id();
|
||||
$data['application_context']['stored_payment_source'] = array(
|
||||
$data['payment_source'] = array(
|
||||
'card' => array(
|
||||
'vault_id' => $data['payment_source']['token']['id'],
|
||||
'stored_credential' => array(
|
||||
'payment_initiator' => 'MERCHANT',
|
||||
'payment_type' => 'RECURRING',
|
||||
'usage' => 'SUBSEQUENT',
|
||||
'previous_transaction_reference' => $transaction_id,
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$previous_transaction_reference = $subscription->get_meta( 'ppcp_previous_transaction_reference' );
|
||||
if ( $previous_transaction_reference ) {
|
||||
$data['payment_source']['card']['stored_credential']['previous_transaction_reference'] = $previous_transaction_reference;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
10
modules/ppcp-wc-gateway/resources/js/common.js
Normal file
10
modules/ppcp-wc-gateway/resources/js/common.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import moveWrappedElements from "./common/wrapped-elements";
|
||||
document.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
() => {
|
||||
// Wait for current execution context to end.
|
||||
setTimeout(function () {
|
||||
moveWrappedElements();
|
||||
}, 0);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
// This function is needed because WordPress moves our custom notices to the global placeholder.
|
||||
function moveWrappedElements() {
|
||||
(($) => {
|
||||
$('*[data-ppcp-wrapper]').each(function() {
|
||||
let $wrapper = $('.' + $(this).data('ppcpWrapper'));
|
||||
if ($wrapper.length) {
|
||||
$wrapper.append(this);
|
||||
}
|
||||
});
|
||||
})(jQuery)
|
||||
}
|
||||
|
||||
export default moveWrappedElements;
|
|
@ -53,6 +53,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\CheckoutHelper;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Notice\ConnectAdminNotice;
|
||||
|
@ -211,10 +212,18 @@ return array(
|
|||
return new ConnectAdminNotice( $state, $settings );
|
||||
},
|
||||
'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice {
|
||||
$state = $container->get( 'onboarding.state' );
|
||||
$shop_currency = $container->get( 'api.shop.currency' );
|
||||
$supported_currencies = $container->get( 'api.supported-currencies' );
|
||||
return new UnsupportedCurrencyAdminNotice( $state, $shop_currency, $supported_currencies );
|
||||
$state = $container->get( 'onboarding.state' );
|
||||
$shop_currency = $container->get( 'api.shop.currency' );
|
||||
$supported_currencies = $container->get( 'api.supported-currencies' );
|
||||
$is_wc_gateways_list_page = $container->get( 'wcgateway.is-wc-gateways-list-page' );
|
||||
$is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' );
|
||||
return new UnsupportedCurrencyAdminNotice(
|
||||
$state,
|
||||
$shop_currency,
|
||||
$supported_currencies,
|
||||
$is_wc_gateways_list_page,
|
||||
$is_ppcp_settings_page
|
||||
);
|
||||
},
|
||||
'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice {
|
||||
return new GatewayWithoutPayPalAdminNotice(
|
||||
|
@ -336,10 +345,11 @@ return array(
|
|||
);
|
||||
},
|
||||
'wcgateway.processor.refunds' => static function ( ContainerInterface $container ): RefundProcessor {
|
||||
$order_endpoint = $container->get( 'api.endpoint.order' );
|
||||
$payments_endpoint = $container->get( 'api.endpoint.payments' );
|
||||
$logger = $container->get( 'woocommerce.logger.woocommerce' );
|
||||
return new RefundProcessor( $order_endpoint, $payments_endpoint, $logger );
|
||||
$order_endpoint = $container->get( 'api.endpoint.order' );
|
||||
$payments_endpoint = $container->get( 'api.endpoint.payments' );
|
||||
$refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' );
|
||||
$logger = $container->get( 'woocommerce.logger.woocommerce' );
|
||||
return new RefundProcessor( $order_endpoint, $payments_endpoint, $refund_fees_updater, $logger );
|
||||
},
|
||||
'wcgateway.processor.authorized-payments' => static function ( ContainerInterface $container ): AuthorizedPaymentsProcessor {
|
||||
$order_endpoint = $container->get( 'api.endpoint.order' );
|
||||
|
@ -1026,6 +1036,12 @@ return array(
|
|||
);
|
||||
},
|
||||
|
||||
'wcgateway.helper.refund-fees-updater' => static function ( ContainerInterface $container ): RefundFeesUpdater {
|
||||
$order_endpoint = $container->get( 'api.endpoint.order' );
|
||||
$logger = $container->get( 'woocommerce.logger.woocommerce' );
|
||||
return new RefundFeesUpdater( $order_endpoint, $logger );
|
||||
},
|
||||
|
||||
'button.helper.messages-disclaimers' => static function ( ContainerInterface $container ): MessagesDisclaimers {
|
||||
return new MessagesDisclaimers(
|
||||
$container->get( 'api.shop.country' )
|
||||
|
|
|
@ -24,11 +24,18 @@ class FeesRenderer {
|
|||
* @return string
|
||||
*/
|
||||
public function render( WC_Order $wc_order ) : string {
|
||||
$breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY );
|
||||
$breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY );
|
||||
$refund_breakdown = $wc_order->get_meta( PayPalGateway::REFUND_FEES_META_KEY ) ?: array();
|
||||
|
||||
if ( ! is_array( $breakdown ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$refund_fee = $refund_breakdown['paypal_fee'] ?? array();
|
||||
$refund_amount = $refund_breakdown['net_amount'] ?? array();
|
||||
$refund_total = ( $refund_fee['value'] ?? 0 ) + ( $refund_amount['value'] ?? 0 );
|
||||
$refund_currency = ( ( $refund_amount['currency_code'] ?? '' ) === ( $refund_fee['currency_code'] ?? '' ) ) ? ( $refund_amount['currency_code'] ?? '' ) : '';
|
||||
|
||||
$html = '';
|
||||
|
||||
$fee = $breakdown['paypal_fee'] ?? null;
|
||||
|
@ -42,6 +49,28 @@ class FeesRenderer {
|
|||
);
|
||||
}
|
||||
|
||||
if ( $refund_fee ) {
|
||||
$html .= $this->render_money_row(
|
||||
__( 'PayPal Refund Fee:', 'woocommerce-paypal-payments' ),
|
||||
__( 'The fee PayPal collects for the refund transactions.', 'woocommerce-paypal-payments' ),
|
||||
$refund_fee['value'],
|
||||
$refund_fee['currency_code'],
|
||||
true,
|
||||
'refunded-total'
|
||||
);
|
||||
}
|
||||
|
||||
if ( $refund_amount ) {
|
||||
$html .= $this->render_money_row(
|
||||
__( 'PayPal Refunded:', 'woocommerce-paypal-payments' ),
|
||||
__( 'The net amount that was refunded.', 'woocommerce-paypal-payments' ),
|
||||
$refund_amount['value'],
|
||||
$refund_amount['currency_code'],
|
||||
true,
|
||||
'refunded-total'
|
||||
);
|
||||
}
|
||||
|
||||
$net = $breakdown['net_amount'] ?? null;
|
||||
if ( is_array( $net ) ) {
|
||||
$html .= $this->render_money_row(
|
||||
|
@ -50,6 +79,15 @@ class FeesRenderer {
|
|||
$net['value'],
|
||||
$net['currency_code']
|
||||
);
|
||||
|
||||
if ( ( $refund_total > 0.0 && $refund_currency === $net['currency_code'] ) ) {
|
||||
$html .= $this->render_money_row(
|
||||
__( 'PayPal Net Total:', 'woocommerce-paypal-payments' ),
|
||||
__( 'The net total that will be credited to your PayPal account minus the refunds.', 'woocommerce-paypal-payments' ),
|
||||
$net['value'] - $refund_total,
|
||||
$net['currency_code']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $html;
|
||||
|
@ -63,9 +101,10 @@ class FeesRenderer {
|
|||
* @param string|float $value The money value.
|
||||
* @param string $currency The currency code.
|
||||
* @param bool $negative Whether to add the minus sign.
|
||||
* @param string $html_class Html class to add to the elements.
|
||||
* @return string
|
||||
*/
|
||||
private function render_money_row( string $title, string $tooltip, $value, string $currency, bool $negative = false ): string {
|
||||
private function render_money_row( string $title, string $tooltip, $value, string $currency, bool $negative = false, string $html_class = '' ): string {
|
||||
/**
|
||||
* Bad type hint in WC phpdoc.
|
||||
*
|
||||
|
@ -73,10 +112,10 @@ class FeesRenderer {
|
|||
*/
|
||||
return '
|
||||
<tr>
|
||||
<td class="label">' . wc_help_tip( $tooltip ) . ' ' . esc_html( $title ) . '
|
||||
<td class="' . trim( 'label ' . $html_class ) . '">' . wc_help_tip( $tooltip ) . ' ' . esc_html( $title ) . '
|
||||
</td>
|
||||
<td width="1%"></td>
|
||||
<td class="total">
|
||||
<td class="' . trim( 'total ' . $html_class ) . '">
|
||||
' .
|
||||
( $negative ? ' - ' : '' ) .
|
||||
wc_price( $value, array( 'currency' => $currency ) ) . '
|
||||
|
|
|
@ -87,6 +87,13 @@ class SettingsPageAssets {
|
|||
*/
|
||||
protected $all_funding_sources;
|
||||
|
||||
/**
|
||||
* Whether it's a settings page of this plugin.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_settings_page;
|
||||
|
||||
/**
|
||||
* Assets constructor.
|
||||
*
|
||||
|
@ -100,6 +107,7 @@ class SettingsPageAssets {
|
|||
* @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page.
|
||||
* @param array $disabled_sources The list of disabled funding sources.
|
||||
* @param array $all_funding_sources The list of all existing funding sources.
|
||||
* @param bool $is_settings_page Whether it's a settings page of this plugin.
|
||||
*/
|
||||
public function __construct(
|
||||
string $module_url,
|
||||
|
@ -111,7 +119,8 @@ class SettingsPageAssets {
|
|||
Environment $environment,
|
||||
bool $is_pay_later_button_enabled,
|
||||
array $disabled_sources,
|
||||
array $all_funding_sources
|
||||
array $all_funding_sources,
|
||||
bool $is_settings_page
|
||||
) {
|
||||
$this->module_url = $module_url;
|
||||
$this->version = $version;
|
||||
|
@ -123,12 +132,15 @@ class SettingsPageAssets {
|
|||
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
|
||||
$this->disabled_sources = $disabled_sources;
|
||||
$this->all_funding_sources = $all_funding_sources;
|
||||
$this->is_settings_page = $is_settings_page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register assets provided by this module.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register_assets() {
|
||||
public function register_assets(): void {
|
||||
add_action(
|
||||
'admin_enqueue_scripts',
|
||||
function() {
|
||||
|
@ -136,11 +148,13 @@ class SettingsPageAssets {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( ! $this->is_paypal_payment_method_page() ) {
|
||||
return;
|
||||
if ( $this->is_settings_page ) {
|
||||
$this->register_admin_assets();
|
||||
}
|
||||
|
||||
$this->register_admin_assets();
|
||||
if ( $this->is_paypal_payment_method_page() ) {
|
||||
$this->register_paypal_admin_assets();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -171,9 +185,9 @@ class SettingsPageAssets {
|
|||
}
|
||||
|
||||
/**
|
||||
* Register assets for admin pages.
|
||||
* Register assets for PayPal admin pages.
|
||||
*/
|
||||
private function register_admin_assets(): void {
|
||||
private function register_paypal_admin_assets(): void {
|
||||
wp_enqueue_style(
|
||||
'ppcp-gateway-settings',
|
||||
trailingslashit( $this->module_url ) . 'assets/css/gateway-settings.css',
|
||||
|
@ -210,4 +224,18 @@ class SettingsPageAssets {
|
|||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register assets for PayPal admin pages.
|
||||
*/
|
||||
private function register_admin_assets(): void {
|
||||
wp_enqueue_script(
|
||||
'ppcp-admin-common',
|
||||
trailingslashit( $this->module_url ) . 'assets/js/common.js',
|
||||
array(),
|
||||
$this->version,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
|
|||
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
|
||||
const ORDER_PAYMENT_SOURCE_META_KEY = '_ppcp_paypal_payment_source';
|
||||
const FEES_META_KEY = '_ppcp_paypal_fees';
|
||||
const REFUND_FEES_META_KEY = '_ppcp_paypal_refund_fees';
|
||||
const REFUNDS_META_KEY = '_ppcp_refunds';
|
||||
|
||||
/**
|
||||
|
@ -289,9 +290,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
|
|||
// in the constructor, so must do it here.
|
||||
global $theorder;
|
||||
if ( $theorder instanceof WC_Order ) {
|
||||
$payment_method_title = $theorder->get_payment_method_title();
|
||||
if ( $payment_method_title ) {
|
||||
$this->title = $payment_method_title;
|
||||
if ( $theorder->get_payment_method() === self::ID ) {
|
||||
$payment_method_title = $theorder->get_payment_method_title();
|
||||
if ( $payment_method_title ) {
|
||||
$this->title = $payment_method_title;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
195
modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php
Normal file
195
modules/ppcp-wc-gateway/src/Helper/RefundFeesUpdater.php
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
/**
|
||||
* The RefundFeesUpdater helper.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\WcGateway\Helper;
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WC_Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WP_Comment;
|
||||
|
||||
/**
|
||||
* CheckoutHelper class.
|
||||
*/
|
||||
class RefundFeesUpdater {
|
||||
|
||||
/**
|
||||
* The Order Endpoint.
|
||||
*
|
||||
* @var OrderEndpoint
|
||||
*/
|
||||
private $order_endpoint;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* RefundFeesUpdater constructor.
|
||||
*
|
||||
* @param OrderEndpoint $order_endpoint The Order Endpoint.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct( OrderEndpoint $order_endpoint, LoggerInterface $logger ) {
|
||||
$this->order_endpoint = $order_endpoint;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the fees meta for a given order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WooCommerce order.
|
||||
* @return void
|
||||
*/
|
||||
public function update( WC_Order $wc_order ): void {
|
||||
$paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
|
||||
|
||||
if ( ! $paypal_order_id ) {
|
||||
$this->logger->error(
|
||||
sprintf( 'Failed to update order paypal refund fees. No PayPal order_id. [wc_order: %s]', $wc_order->get_id() )
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf( 'Updating order paypal refund fees. [wc_order: %s, paypal_order: %s]', $wc_order->get_id(), $paypal_order_id )
|
||||
);
|
||||
|
||||
$paypal_order = $this->order_endpoint->order( $paypal_order_id );
|
||||
$purchase_units = $paypal_order->purchase_units();
|
||||
|
||||
$gross_amount_total = 0.0;
|
||||
$fee_total = 0.0;
|
||||
$net_amount_total = 0.0;
|
||||
$currency_codes = array();
|
||||
$refunds_ids = array();
|
||||
|
||||
foreach ( $purchase_units as $purchase_unit ) {
|
||||
$payments = $purchase_unit->payments();
|
||||
|
||||
if ( ! $payments ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$refunds = $payments->refunds();
|
||||
|
||||
foreach ( $refunds as $refund ) {
|
||||
$breakdown = $refund->seller_payable_breakdown();
|
||||
$refunds_ids[] = $refund->id();
|
||||
|
||||
if ( ! $breakdown ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$gross_amount = $breakdown->gross_amount();
|
||||
if ( $gross_amount ) {
|
||||
$gross_amount_total += $gross_amount->value();
|
||||
$currency_codes[] = $gross_amount->currency_code();
|
||||
}
|
||||
|
||||
$paypal_fee = $breakdown->paypal_fee();
|
||||
if ( $paypal_fee ) {
|
||||
$fee_total += $paypal_fee->value();
|
||||
$currency_codes[] = $paypal_fee->currency_code();
|
||||
}
|
||||
|
||||
$net_amount = $breakdown->net_amount();
|
||||
if ( $net_amount ) {
|
||||
$net_amount_total += $net_amount->value();
|
||||
$currency_codes[] = $net_amount->currency_code();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$currency_codes = array_unique( $currency_codes );
|
||||
|
||||
if ( count( $currency_codes ) > 1 ) {
|
||||
// There are multiple different currencies codes in the refunds.
|
||||
|
||||
$this->logger->warning(
|
||||
sprintf(
|
||||
'Updating order paypal refund fees. Multiple currencies detected. [wc_order: %s, paypal_order: %s, currencies: %s]',
|
||||
$wc_order->get_id(),
|
||||
$paypal_order_id,
|
||||
implode( ',', $currency_codes )
|
||||
)
|
||||
);
|
||||
|
||||
$wc_order->update_meta_data( PayPalGateway::REFUND_FEES_META_KEY, array() );
|
||||
return;
|
||||
}
|
||||
|
||||
$currency_code = current( $currency_codes ) ?: '';
|
||||
|
||||
$meta_data = array(
|
||||
'gross_amount' => ( new Money( $gross_amount_total, $currency_code ) )->to_array(),
|
||||
'paypal_fee' => ( new Money( $fee_total, $currency_code ) )->to_array(),
|
||||
'net_amount' => ( new Money( $net_amount_total, $currency_code ) )->to_array(),
|
||||
);
|
||||
|
||||
$wc_order->update_meta_data( PayPalGateway::REFUND_FEES_META_KEY, $meta_data );
|
||||
$wc_order->save();
|
||||
|
||||
$order_notes = $this->get_order_notes( $wc_order );
|
||||
|
||||
foreach ( $refunds_ids as $refund_id ) {
|
||||
$has_note = false;
|
||||
foreach ( $order_notes as $order_note ) {
|
||||
if ( strpos( $order_note->comment_content, $refund_id ) !== false ) {
|
||||
$has_note = true;
|
||||
}
|
||||
}
|
||||
if ( ! $has_note ) {
|
||||
$wc_order->add_order_note( sprintf( 'PayPal refund ID: %s', $refund_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf( 'Updated order paypal refund fees. [wc_order: %s, paypal_order: %s]', $wc_order->get_id(), $paypal_order_id )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all order notes
|
||||
* Based on WC_Order::get_customer_order_notes
|
||||
*
|
||||
* @param WC_Order $wc_order The WooCommerce order.
|
||||
* @return WP_Comment[]
|
||||
*/
|
||||
private function get_order_notes( WC_Order $wc_order ): array {
|
||||
$notes = array();
|
||||
$args = array(
|
||||
'post_id' => $wc_order->get_id(),
|
||||
);
|
||||
|
||||
// By default, WooCommerce excludes comments of the comment_type order_note.
|
||||
// We need to remove this filter to get the order notes.
|
||||
remove_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) );
|
||||
|
||||
$comments = get_comments( $args );
|
||||
|
||||
if ( is_array( $comments ) ) {
|
||||
foreach ( $comments as $comment ) {
|
||||
if ( $comment instanceof WP_Comment ) {
|
||||
$notes[] = $comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add_filter( 'comments_clauses', array( 'WC_Comments', 'exclude_order_comments' ) );
|
||||
|
||||
return $notes;
|
||||
}
|
||||
|
||||
}
|
|
@ -40,17 +40,42 @@ class UnsupportedCurrencyAdminNotice {
|
|||
*/
|
||||
private $shop_currency;
|
||||
|
||||
/**
|
||||
* Indicates if we're on the WooCommerce gateways list page.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_wc_gateways_list_page;
|
||||
|
||||
/**
|
||||
* Indicates if we're on a PPCP Settings page.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_ppcp_settings_page;
|
||||
|
||||
/**
|
||||
* UnsupportedCurrencyAdminNotice constructor.
|
||||
*
|
||||
* @param State $state The state.
|
||||
* @param string $shop_currency The shop currency.
|
||||
* @param array $supported_currencies The supported currencies.
|
||||
* @param bool $is_wc_gateways_list_page Indicates if we're on the WooCommerce gateways list page.
|
||||
* @param bool $is_ppcp_settings_page Indicates if we're on a PPCP Settings page.
|
||||
*/
|
||||
public function __construct( State $state, string $shop_currency, array $supported_currencies ) {
|
||||
$this->state = $state;
|
||||
$this->shop_currency = $shop_currency;
|
||||
$this->supported_currencies = $supported_currencies;
|
||||
public function __construct(
|
||||
State $state,
|
||||
string $shop_currency,
|
||||
array $supported_currencies,
|
||||
bool $is_wc_gateways_list_page,
|
||||
bool $is_ppcp_settings_page
|
||||
) {
|
||||
$this->state = $state;
|
||||
$this->shop_currency = $shop_currency;
|
||||
$this->supported_currencies = $supported_currencies;
|
||||
$this->is_wc_gateways_list_page = $is_wc_gateways_list_page;
|
||||
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -63,16 +88,19 @@ class UnsupportedCurrencyAdminNotice {
|
|||
return null;
|
||||
}
|
||||
|
||||
$paypal_currency_support_url = 'https://developer.paypal.com/api/rest/reference/currency-codes/';
|
||||
|
||||
$message = sprintf(
|
||||
/* translators: %1$s the shop currency, 2$s the gateway name. */
|
||||
/* translators: %1$s the shop currency, %2$s the PayPal currency support page link opening HTML tag, %3$s the link ending HTML tag. */
|
||||
__(
|
||||
'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the <a href="%2$s">PayPal currency support page</a> for more information on supported currencies.',
|
||||
'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the %2$sPayPal currency support page%3$s for more information on supported currencies.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
$this->shop_currency,
|
||||
'https://developer.paypal.com/api/rest/reference/currency-codes/'
|
||||
'<a href="' . esc_url( $paypal_currency_support_url ) . '">',
|
||||
'</a>'
|
||||
);
|
||||
return new Message( $message, 'warning' );
|
||||
return new Message( $message, 'warning', true, 'ppcp-notice-wrapper' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -81,7 +109,9 @@ class UnsupportedCurrencyAdminNotice {
|
|||
* @return bool
|
||||
*/
|
||||
protected function should_display(): bool {
|
||||
return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported();
|
||||
return $this->state->current_state() === State::STATE_ONBOARDED
|
||||
&& ! $this->currency_supported()
|
||||
&& ( $this->is_wc_gateways_list_page || $this->is_ppcp_settings_page );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
|
|||
|
||||
use WC_Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Environment;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
|
||||
|
@ -22,14 +23,16 @@ trait OrderMetaTrait {
|
|||
/**
|
||||
* Adds common metadata to the order.
|
||||
*
|
||||
* @param WC_Order $wc_order The WC order to which metadata will be added.
|
||||
* @param Order $order The PayPal order.
|
||||
* @param Environment $environment The environment.
|
||||
* @param WC_Order $wc_order The WC order to which metadata will be added.
|
||||
* @param Order $order The PayPal order.
|
||||
* @param Environment $environment The environment.
|
||||
* @param OrderTransient|null $order_transient The order transient helper.
|
||||
*/
|
||||
protected function add_paypal_meta(
|
||||
WC_Order $wc_order,
|
||||
Order $order,
|
||||
Environment $environment
|
||||
Environment $environment,
|
||||
OrderTransient $order_transient = null
|
||||
): void {
|
||||
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
|
||||
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
|
||||
|
@ -43,6 +46,8 @@ trait OrderMetaTrait {
|
|||
}
|
||||
|
||||
$wc_order->save();
|
||||
|
||||
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -20,9 +20,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
|
||||
/**
|
||||
* Class RefundProcessor
|
||||
|
@ -55,18 +56,27 @@ class RefundProcessor {
|
|||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* The refund fees updater.
|
||||
*
|
||||
* @var RefundFeesUpdater
|
||||
*/
|
||||
private $refund_fees_updater;
|
||||
|
||||
/**
|
||||
* RefundProcessor constructor.
|
||||
*
|
||||
* @param OrderEndpoint $order_endpoint The order endpoint.
|
||||
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param OrderEndpoint $order_endpoint The order endpoint.
|
||||
* @param PaymentsEndpoint $payments_endpoint The payments endpoint.
|
||||
* @param RefundFeesUpdater $refund_fees_updater The refund fees updater.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, LoggerInterface $logger ) {
|
||||
public function __construct( OrderEndpoint $order_endpoint, PaymentsEndpoint $payments_endpoint, RefundFeesUpdater $refund_fees_updater, LoggerInterface $logger ) {
|
||||
|
||||
$this->order_endpoint = $order_endpoint;
|
||||
$this->payments_endpoint = $payments_endpoint;
|
||||
$this->logger = $logger;
|
||||
$this->order_endpoint = $order_endpoint;
|
||||
$this->payments_endpoint = $payments_endpoint;
|
||||
$this->refund_fees_updater = $refund_fees_updater;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -106,6 +116,7 @@ class RefundProcessor {
|
|||
$refund_id = $this->refund( $order, $wc_order, $amount, $reason );
|
||||
|
||||
$this->add_refund_to_meta( $wc_order, $refund_id );
|
||||
$this->refund_fees_updater->update( $wc_order );
|
||||
|
||||
break;
|
||||
case self::REFUND_MODE_VOID:
|
||||
|
@ -151,7 +162,7 @@ class RefundProcessor {
|
|||
}
|
||||
|
||||
$capture = $captures[0];
|
||||
$refund = new Refund(
|
||||
$refund = new RefundCapture(
|
||||
$capture,
|
||||
$capture->invoice_id(),
|
||||
$reason,
|
||||
|
|
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Environment;
|
||||
|
@ -496,6 +497,55 @@ return function ( ContainerInterface $container, array $fields ): array {
|
|||
'requirements' => array(),
|
||||
'gateway' => Settings::CONNECTION_TAB_ID,
|
||||
),
|
||||
'subtotal_mismatch_behavior' => array(
|
||||
'title' => __( 'Subtotal mismatch behavior', 'woocommerce-paypal-payments' ),
|
||||
'type' => 'select',
|
||||
'input_class' => array( 'wc-enhanced-select' ),
|
||||
'default' => 'vertical',
|
||||
'desc_tip' => true,
|
||||
'description' => __(
|
||||
'Differences between WooCommerce and PayPal roundings may cause mismatch in order items subtotal calculations. If not handled, these mismatches will cause the PayPal transaction to fail.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'options' => array(
|
||||
PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ),
|
||||
PurchaseUnitSanitizer::MODE_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ),
|
||||
),
|
||||
'screens' => array(
|
||||
State::STATE_START,
|
||||
State::STATE_ONBOARDED,
|
||||
),
|
||||
'requirements' => array(),
|
||||
'gateway' => Settings::CONNECTION_TAB_ID,
|
||||
'custom_attributes' => array(
|
||||
'data-ppcp-handlers' => wp_json_encode(
|
||||
array(
|
||||
array(
|
||||
'handler' => 'SubElementsHandler',
|
||||
'options' => array(
|
||||
'values' => array( PurchaseUnitSanitizer::MODE_EXTRA_LINE ),
|
||||
'elements' => array( '#field-subtotal_mismatch_line_name' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
'subtotal_mismatch_line_name' => array(
|
||||
'title' => __( 'Subtotal mismatch line name', 'woocommerce-paypal-payments' ),
|
||||
'type' => 'text',
|
||||
'desc_tip' => true,
|
||||
'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ),
|
||||
'maxlength' => 22,
|
||||
'default' => '',
|
||||
'screens' => array(
|
||||
State::STATE_START,
|
||||
State::STATE_ONBOARDED,
|
||||
),
|
||||
'requirements' => array(),
|
||||
'placeholder' => PurchaseUnitSanitizer::EXTRA_LINE_NAME,
|
||||
'gateway' => Settings::CONNECTION_TAB_ID,
|
||||
),
|
||||
);
|
||||
|
||||
return array_merge( $fields, $connection_fields );
|
||||
|
|
|
@ -77,6 +77,8 @@ class HeaderRenderer {
|
|||
'</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ppcp-notice-wrapper"></div>
|
||||
';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,7 +183,8 @@ class WCGatewayModule implements ModuleInterface {
|
|||
$c->get( 'onboarding.environment' ),
|
||||
$settings_status->is_pay_later_button_enabled(),
|
||||
$settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(),
|
||||
$c->get( 'wcgateway.settings.funding-sources' )
|
||||
$c->get( 'wcgateway.settings.funding-sources' ),
|
||||
$c->get( 'wcgateway.is-ppcp-settings-page' )
|
||||
);
|
||||
$assets->register_assets();
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ module.exports = {
|
|||
mode: isProduction ? 'production' : 'development',
|
||||
target: 'web',
|
||||
entry: {
|
||||
'common': path.resolve('./resources/js/common.js'),
|
||||
'gateway-settings': path.resolve('./resources/js/gateway-settings.js'),
|
||||
'fraudnet': path.resolve('./resources/js/fraudnet.js'),
|
||||
'oxxo': path.resolve('./resources/js/oxxo.js'),
|
||||
|
|
|
@ -80,6 +80,7 @@ return array(
|
|||
$order_endpoint = $container->get( 'api.endpoint.order' );
|
||||
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
|
||||
$payment_token_factory = $container->get( 'vaulting.payment-token-factory' );
|
||||
$refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' );
|
||||
|
||||
return array(
|
||||
new CheckoutOrderApproved(
|
||||
|
@ -91,14 +92,14 @@ return array(
|
|||
),
|
||||
new CheckoutOrderCompleted( $logger ),
|
||||
new CheckoutPaymentApprovalReversed( $logger ),
|
||||
new PaymentCaptureRefunded( $logger ),
|
||||
new PaymentCaptureRefunded( $logger, $refund_fees_updater ),
|
||||
new PaymentCaptureReversed( $logger ),
|
||||
new PaymentCaptureCompleted( $logger, $order_endpoint ),
|
||||
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ),
|
||||
new VaultPaymentTokenDeleted( $logger ),
|
||||
new PaymentCapturePending( $logger ),
|
||||
new PaymentSaleCompleted( $logger ),
|
||||
new PaymentSaleRefunded( $logger ),
|
||||
new PaymentSaleRefunded( $logger, $refund_fees_updater ),
|
||||
new BillingSubscriptionCancelled( $logger ),
|
||||
new BillingPlanPricingChangeActivated( $logger ),
|
||||
new CatalogProductUpdated( $logger ),
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
|
|||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WC_Order;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
|
||||
use WP_Error;
|
||||
|
@ -31,13 +32,22 @@ class PaymentCaptureRefunded implements RequestHandler {
|
|||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* The refund fees updater.
|
||||
*
|
||||
* @var RefundFeesUpdater
|
||||
*/
|
||||
private $refund_fees_updater;
|
||||
|
||||
/**
|
||||
* PaymentCaptureRefunded constructor.
|
||||
*
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param RefundFeesUpdater $refund_fees_updater The refund fees updater.
|
||||
*/
|
||||
public function __construct( LoggerInterface $logger ) {
|
||||
$this->logger = $logger;
|
||||
public function __construct( LoggerInterface $logger, RefundFeesUpdater $refund_fees_updater ) {
|
||||
$this->logger = $logger;
|
||||
$this->refund_fees_updater = $refund_fees_updater;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -68,9 +78,11 @@ class PaymentCaptureRefunded implements RequestHandler {
|
|||
* @return WP_REST_Response
|
||||
*/
|
||||
public function handle_request( WP_REST_Request $request ): WP_REST_Response {
|
||||
$order_id = isset( $request['resource']['custom_id'] ) ?
|
||||
$request['resource']['custom_id'] : 0;
|
||||
$refund_id = (string) ( $request['resource']['id'] ?? '' );
|
||||
$resource = ( $request['resource'] ?? array() ) ?: array();
|
||||
|
||||
$order_id = $resource['custom_id'] ?? 0;
|
||||
$refund_id = (string) ( $resource['id'] ?? '' );
|
||||
|
||||
if ( ! $order_id ) {
|
||||
$message = sprintf(
|
||||
'No order for webhook event %s was found.',
|
||||
|
@ -122,6 +134,7 @@ class PaymentCaptureRefunded implements RequestHandler {
|
|||
if ( $refund_id ) {
|
||||
$this->update_transaction_id( $refund_id, $wc_order, $this->logger );
|
||||
$this->add_refund_to_meta( $wc_order, $refund_id );
|
||||
$this->refund_fees_updater->update( $wc_order );
|
||||
}
|
||||
|
||||
return $this->success_response();
|
||||
|
|
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Webhooks\Handler;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundMetaTrait;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
|
||||
use WP_Error;
|
||||
|
@ -30,13 +31,22 @@ class PaymentSaleRefunded implements RequestHandler {
|
|||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* The refund fees updater.
|
||||
*
|
||||
* @var RefundFeesUpdater
|
||||
*/
|
||||
private $refund_fees_updater;
|
||||
|
||||
/**
|
||||
* PaymentSaleRefunded constructor.
|
||||
*
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param RefundFeesUpdater $refund_fees_updater The refund fees updater.
|
||||
*/
|
||||
public function __construct( LoggerInterface $logger ) {
|
||||
$this->logger = $logger;
|
||||
public function __construct( LoggerInterface $logger, RefundFeesUpdater $refund_fees_updater ) {
|
||||
$this->logger = $logger;
|
||||
$this->refund_fees_updater = $refund_fees_updater;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,6 +130,7 @@ class PaymentSaleRefunded implements RequestHandler {
|
|||
|
||||
$this->update_transaction_id( $refund_id, $wc_order, $this->logger );
|
||||
$this->add_refund_to_meta( $wc_order, $refund_id );
|
||||
$this->refund_fees_updater->update( $wc_order );
|
||||
}
|
||||
|
||||
return $this->success_response();
|
||||
|
|
|
@ -208,6 +208,15 @@ class IncomingWebhookEndpoint {
|
|||
public function handle_request( \WP_REST_Request $request ): \WP_REST_Response {
|
||||
$event = $this->event_from_request( $request );
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf(
|
||||
'Webhook %s received of type %s and by resource "%s"',
|
||||
$event->id(),
|
||||
$event->event_type(),
|
||||
$event->resource_type()
|
||||
)
|
||||
);
|
||||
|
||||
$this->last_webhook_event_storage->save( $event );
|
||||
|
||||
if ( $this->simulation->is_simulation_event( $event ) ) {
|
||||
|
@ -218,11 +227,21 @@ class IncomingWebhookEndpoint {
|
|||
|
||||
foreach ( $this->handlers as $handler ) {
|
||||
if ( $handler->responsible_for_request( $request ) ) {
|
||||
$event_type = ( $handler->event_types() ? current( $handler->event_types() ) : '' ) ?: '';
|
||||
|
||||
$this->logger->debug(
|
||||
sprintf(
|
||||
'Webhook is going to be handled by %s on %s',
|
||||
$event_type,
|
||||
get_class( $handler )
|
||||
)
|
||||
);
|
||||
$response = $handler->handle_request( $request );
|
||||
$this->logger->info(
|
||||
sprintf(
|
||||
'Webhook has been handled by %s',
|
||||
( $handler->event_types() ) ? current( $handler->event_types() ) : ''
|
||||
'Webhook has been handled by %s on %s',
|
||||
$event_type,
|
||||
get_class( $handler )
|
||||
)
|
||||
);
|
||||
return $response;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "woocommerce-paypal-payments",
|
||||
"version": "2.2.1",
|
||||
"version": "2.2.2",
|
||||
"description": "WooCommerce PayPal Payments",
|
||||
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
|
||||
"license": "GPL-2.0",
|
||||
|
|
|
@ -259,6 +259,11 @@
|
|||
<code>DAY_IN_SECONDS</code>
|
||||
</UndefinedConstant>
|
||||
</file>
|
||||
<file src="modules/ppcp-api-client/src/Helper/OrderTransient.php">
|
||||
<UndefinedConstant occurrences="1">
|
||||
<code>DAY_IN_SECONDS</code>
|
||||
</UndefinedConstant>
|
||||
</file>
|
||||
<file src="modules/ppcp-button/services.php">
|
||||
<PossiblyFalseArgument occurrences="1">
|
||||
<code>realpath( __FILE__ )</code>
|
||||
|
|
|
@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell,
|
|||
Requires at least: 5.3
|
||||
Tested up to: 6.3
|
||||
Requires PHP: 7.2
|
||||
Stable tag: 2.2.1
|
||||
Stable tag: 2.2.2
|
||||
License: GPLv2
|
||||
License URI: http://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
|
@ -81,7 +81,12 @@ Follow the steps below to connect the plugin to your PayPal account:
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 2.2.1 - xxxx-xx-xx =
|
||||
= 2.2.2 - 2023-08-29 =
|
||||
* Fix - High rate of auth voids on vaulted subscriptions for guest users #1529
|
||||
* Enhancement - HPOS compatibility issues #1594
|
||||
* Feature preview - PayPal Subscriptions API fixes and improvements #1600 #1607
|
||||
|
||||
= 2.2.1 - 2023-08-24 =
|
||||
* Fix - One-page checkout causes mini cart not showing the PP button on certain pages #1536
|
||||
* Fix - When onboarding loading the return_url too fast may cause the onboarding to fail #1565
|
||||
* Fix - PayPal button doesn't work for variable products on product page after recent 2.2.0 release #1533
|
||||
|
|
130
tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php
Normal file
130
tests/PHPUnit/Api/OrderRefundFeesUpdateTest.php
Normal file
|
@ -0,0 +1,130 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Api;
|
||||
|
||||
use Mockery;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
|
||||
use WooCommerce\PayPalCommerce\ModularTestCase;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\RefundFeesUpdater;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WC_Order;
|
||||
use function Brain\Monkey\Functions\when;
|
||||
|
||||
class OrderRefundFeesUpdateTest extends ModularTestCase
|
||||
{
|
||||
|
||||
private $order_endpoint;
|
||||
private $logger;
|
||||
private $refundFeesUpdater;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->order_endpoint = $this->createMock(OrderEndpoint::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->refundFeesUpdater = new RefundFeesUpdater($this->order_endpoint, $this->logger);
|
||||
}
|
||||
|
||||
public function testUpdateWithoutPaypalOrderId(): void
|
||||
{
|
||||
$wc_order_id = 123;
|
||||
|
||||
$wc_order = Mockery::mock(WC_Order::class);
|
||||
$wc_order->expects('get_meta')
|
||||
->with(PayPalGateway::ORDER_ID_META_KEY)
|
||||
->andReturn(null);
|
||||
|
||||
$wc_order->expects('get_id')->andReturn($wc_order_id);
|
||||
|
||||
$this->logger->expects($this->once())
|
||||
->method('error');
|
||||
|
||||
$this->refundFeesUpdater->update($wc_order);
|
||||
}
|
||||
|
||||
public function testUpdateWithValidData(): void
|
||||
{
|
||||
$wc_order_id = 123;
|
||||
$paypal_order_id = 'test_order_id';
|
||||
$refund_id = 'XYZ123';
|
||||
$meta_data = [
|
||||
'gross_amount' => ['value' => 10.0, 'currency_code' => 'USD'],
|
||||
'paypal_fee' => ['value' => 7.0, 'currency_code' => 'USD'],
|
||||
'net_amount' => ['value' => 3.0, 'currency_code' => 'USD'],
|
||||
];
|
||||
|
||||
when('get_comments')->justReturn([]);
|
||||
|
||||
$wc_order = Mockery::mock(WC_Order::class);
|
||||
$wc_order->expects('get_meta')
|
||||
->with(PayPalGateway::ORDER_ID_META_KEY)
|
||||
->andReturn($paypal_order_id);
|
||||
|
||||
$wc_order->expects('get_id')
|
||||
->times(3)
|
||||
->andReturn($wc_order_id);
|
||||
|
||||
$wc_order->expects('update_meta_data')
|
||||
->once()
|
||||
->with('_ppcp_paypal_refund_fees', $meta_data);
|
||||
|
||||
$wc_order->expects('add_order_note')
|
||||
->once()
|
||||
->withArgs(function ($arg) use ($refund_id) {
|
||||
return strpos($arg, $refund_id) !== false;
|
||||
});
|
||||
|
||||
$wc_order->expects('save')->once();
|
||||
|
||||
$moneyGross = Mockery::mock(Money::class);
|
||||
$moneyGross->expects('value')->once()->andReturn($meta_data['gross_amount']['value']);
|
||||
$moneyGross->expects('currency_code')->once()->andReturn($meta_data['gross_amount']['currency_code']);
|
||||
|
||||
$moneyFee = Mockery::mock(Money::class);
|
||||
$moneyFee->expects('value')->once()->andReturn($meta_data['paypal_fee']['value']);
|
||||
$moneyFee->expects('currency_code')->once()->andReturn($meta_data['paypal_fee']['currency_code']);
|
||||
|
||||
$moneyNet = Mockery::mock(Money::class);
|
||||
$moneyNet->expects('value')->once()->andReturn($meta_data['net_amount']['value']);
|
||||
$moneyNet->expects('currency_code')->once()->andReturn($meta_data['net_amount']['currency_code']);
|
||||
|
||||
$breakdown = $this->getMockBuilder(\stdClass::class)
|
||||
->addMethods(['gross_amount', 'paypal_fee', 'net_amount'])
|
||||
->getMock();
|
||||
$breakdown->method('gross_amount')->willReturn($moneyGross);
|
||||
$breakdown->method('paypal_fee')->willReturn($moneyFee);
|
||||
$breakdown->method('net_amount')->willReturn($moneyNet);
|
||||
|
||||
$refund = $this->getMockBuilder(\stdClass::class)
|
||||
->addMethods(['id', 'seller_payable_breakdown'])
|
||||
->getMock();
|
||||
$refund->method('id')->willReturn($refund_id);
|
||||
$refund->method('seller_payable_breakdown')->willReturn($breakdown);
|
||||
|
||||
$payments = $this->getMockBuilder(\stdClass::class)
|
||||
->addMethods(['refunds'])
|
||||
->getMock();
|
||||
$payments->method('refunds')->willReturn([$refund]);
|
||||
|
||||
$purchase_unit = $this->getMockBuilder(\stdClass::class)
|
||||
->addMethods(['payments'])
|
||||
->getMock();
|
||||
$purchase_unit->method('payments')->willReturn($payments);
|
||||
|
||||
$paypal_order = Mockery::mock(Order::class);
|
||||
$paypal_order->expects('purchase_units')->andReturn([$purchase_unit]);
|
||||
|
||||
$this->order_endpoint->method('order')->with($paypal_order_id)->willReturn($paypal_order);
|
||||
|
||||
$this->logger->expects($this->exactly(2))
|
||||
->method('debug')
|
||||
->withConsecutive(
|
||||
[$this->stringContains('Updating order paypal refund fees.')],
|
||||
[$this->stringContains('Updated order paypal refund fees.')]
|
||||
);
|
||||
|
||||
$this->refundFeesUpdater->update($wc_order);
|
||||
}
|
||||
}
|
|
@ -43,10 +43,19 @@ class PaymentsTest extends TestCase
|
|||
'status' => 'CREATED',
|
||||
]
|
||||
);
|
||||
$captures = [$capture];
|
||||
$authorizations = [$authorization];
|
||||
$refund = \Mockery::mock(Refund::class);
|
||||
$refund->shouldReceive('to_array')->andReturn(
|
||||
[
|
||||
'id' => 'refund',
|
||||
'status' => 'CREATED',
|
||||
]
|
||||
);
|
||||
|
||||
$testee = new Payments($authorizations, $captures);
|
||||
$authorizations = [$authorization];
|
||||
$captures = [$capture];
|
||||
$refunds = [$refund];
|
||||
|
||||
$testee = new Payments($authorizations, $captures, $refunds);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
|
@ -62,6 +71,12 @@ class PaymentsTest extends TestCase
|
|||
'status' => 'CREATED',
|
||||
],
|
||||
],
|
||||
'refunds' => [
|
||||
[
|
||||
'id' => 'refund',
|
||||
'status' => 'CREATED',
|
||||
],
|
||||
],
|
||||
],
|
||||
$testee->to_array()
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\TestCase;
|
||||
use Mockery;
|
||||
|
||||
|
@ -75,24 +76,43 @@ class PurchaseUnitTest extends TestCase
|
|||
$this->assertEquals($expected, $testee->to_array());
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataForDitchTests
|
||||
* @param array $items
|
||||
* @param Amount $amount
|
||||
* @param bool $doDitch
|
||||
*/
|
||||
public function testDitchMethod(array $items, Amount $amount, bool $doDitch, string $message)
|
||||
/**
|
||||
* @dataProvider dataForDitchTests
|
||||
* @param array $items
|
||||
* @param Amount $amount
|
||||
* @param bool|array $doDitch
|
||||
* @param string $message
|
||||
*/
|
||||
public function testDitchMethod(array $items, Amount $amount, $doDitch, string $message)
|
||||
{
|
||||
if (is_array($doDitch)) {
|
||||
$doDitchItems = $doDitch['items'];
|
||||
$doDitchBreakdown = $doDitch['breakdown'];
|
||||
$doDitchTax = $doDitch['tax'];
|
||||
} else {
|
||||
$doDitchItems = $doDitch;
|
||||
$doDitchBreakdown = $doDitch;
|
||||
$doDitchTax = $doDitch;
|
||||
}
|
||||
|
||||
$testee = new PurchaseUnit(
|
||||
$amount,
|
||||
$items
|
||||
);
|
||||
|
||||
$testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_DITCH));
|
||||
|
||||
$array = $testee->to_array();
|
||||
$resultItems = $doDitch === ! array_key_exists('items', $array);
|
||||
$resultBreakdown = $doDitch === ! array_key_exists('breakdown', $array['amount']);
|
||||
$resultItems = $doDitchItems === ! array_key_exists('items', $array);
|
||||
|
||||
$resultBreakdown = $doDitchBreakdown === ! array_key_exists('breakdown', $array['amount']);
|
||||
$this->assertTrue($resultItems, $message);
|
||||
$this->assertTrue($resultBreakdown, $message);
|
||||
|
||||
foreach ($array['items'] ?? [] as $item) {
|
||||
$resultTax = $doDitchTax === ! array_key_exists('tax', $item);
|
||||
$this->assertTrue($resultTax, $message);
|
||||
}
|
||||
}
|
||||
|
||||
public function dataForDitchTests() : array
|
||||
|
@ -406,6 +426,58 @@ class PurchaseUnitTest extends TestCase
|
|||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'ditch_items_total_but_not_breakdown' => [
|
||||
'message' => 'Items should be ditched because the item total does not add up. But not breakdown because it adds up.',
|
||||
'ditch' => [
|
||||
'items' => true,
|
||||
'breakdown' => false,
|
||||
'tax' => true,
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 11,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 26,
|
||||
'breakdown' => [
|
||||
'item_total' => 20,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'ditch_items_tax_with_incorrect_tax_total' => [
|
||||
'message' => 'Ditch tax from items. Items should not be ditched because the mismatch is on the tax.',
|
||||
'ditch' => [
|
||||
'items' => false,
|
||||
'breakdown' => false,
|
||||
'tax' => true,
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10,
|
||||
'quantity' => 2,
|
||||
'tax' => 4,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 26,
|
||||
'breakdown' => [
|
||||
'item_total' => 20,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$values = [];
|
||||
|
@ -421,10 +493,16 @@ class PurchaseUnitTest extends TestCase
|
|||
'tax' => $tax,
|
||||
'quantity'=> $item['quantity'],
|
||||
'category' => $item['category'],
|
||||
'to_array' => [],
|
||||
'to_array' => [
|
||||
'unit_amount' => $unitAmount->to_array(),
|
||||
'tax' => $tax->to_array(),
|
||||
'quantity'=> $item['quantity'],
|
||||
'category' => $item['category'],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$breakdown = null;
|
||||
if ($test['breakdown']) {
|
||||
$breakdown = Mockery::mock(AmountBreakdown::class);
|
||||
|
@ -438,10 +516,29 @@ class PurchaseUnitTest extends TestCase
|
|||
return $money;
|
||||
});
|
||||
}
|
||||
|
||||
$breakdown
|
||||
->shouldReceive('to_array')
|
||||
->andReturn(
|
||||
array_map(
|
||||
function ($value) {
|
||||
return $value ? (new Money($value, 'EUR'))->to_array() : null;
|
||||
},
|
||||
$test['breakdown']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$amountMoney = new Money($test['amount'], 'EUR');
|
||||
$amount = Mockery::mock(Amount::class);
|
||||
$amount->shouldReceive('to_array')->andReturn(['value' => number_format( $test['amount'], 2, '.', '' ), 'breakdown' => []]);
|
||||
$amount->shouldReceive('value_str')->andReturn(number_format( $test['amount'], 2, '.', '' ));
|
||||
$amount
|
||||
->shouldReceive('to_array')
|
||||
->andReturn([
|
||||
'value' => $amountMoney->value_str(),
|
||||
'currency_code' => $amountMoney->currency_code(),
|
||||
'breakdown' => $breakdown ? $breakdown->to_array() : [],
|
||||
]);
|
||||
$amount->shouldReceive('value_str')->andReturn($amountMoney->value_str());
|
||||
$amount->shouldReceive('currency_code')->andReturn('EUR');
|
||||
$amount->shouldReceive('breakdown')->andReturn($breakdown);
|
||||
|
||||
|
@ -456,6 +553,262 @@ class PurchaseUnitTest extends TestCase
|
|||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider dataForExtraLineTests
|
||||
* @param array $items
|
||||
* @param Amount $amount
|
||||
* @param array $expected
|
||||
* @param string $message
|
||||
*/
|
||||
public function testExtraLineMethod(array $items, Amount $amount, array $expected, string $message)
|
||||
{
|
||||
$testee = new PurchaseUnit(
|
||||
$amount,
|
||||
$items
|
||||
);
|
||||
|
||||
$testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_EXTRA_LINE, $expected['extra_line_name'] ?? null));
|
||||
|
||||
$countItemsBefore = count($items);
|
||||
$array = $testee->to_array();
|
||||
$countItemsAfter = count($array['items']);
|
||||
$extraItem = array_pop($array['items']);
|
||||
|
||||
$this->assertEquals($countItemsBefore + 1, $countItemsAfter, $message);
|
||||
$this->assertEquals($expected['extra_line_value'], $extraItem['unit_amount']['value'], $message);
|
||||
$this->assertEquals($expected['extra_line_name'] ?? PurchaseUnitSanitizer::EXTRA_LINE_NAME, $extraItem['name'], $message);
|
||||
|
||||
foreach ($array['items'] as $i => $item) {
|
||||
$this->assertEquals($expected['item_value'][$i], $item['unit_amount']['value'], $message);
|
||||
}
|
||||
}
|
||||
|
||||
public function dataForExtraLineTests() : array
|
||||
{
|
||||
$data = [
|
||||
'default' => [
|
||||
'message' => 'Extra line should be added with price 0.01 and line amount 10.',
|
||||
'expected' => [
|
||||
'item_value' => [10],
|
||||
'extra_line_value' => 0.01,
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 26.01,
|
||||
'breakdown' => [
|
||||
'item_total' => 20.01,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'with_custom_name' => [
|
||||
'message' => 'Extra line should be added with price 0.01 and line amount 10.',
|
||||
'expected' => [
|
||||
'item_value' => [10],
|
||||
'extra_line_value' => 0.01,
|
||||
'extra_line_name' => 'My custom line name',
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 26.01,
|
||||
'breakdown' => [
|
||||
'item_total' => 20.01,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'with_rounding_down' => [
|
||||
'message' => 'Extra line should be added with price 0.01 and line amount 10.00.',
|
||||
'expected' => [
|
||||
'item_value' => [10.00],
|
||||
'extra_line_value' => 0.01
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10.005,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 26.01,
|
||||
'breakdown' => [
|
||||
'item_total' => 20.01,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'with_two_rounding_down' => [
|
||||
'message' => 'Extra line should be added with price 0.03 and lines amount 10.00 and 4.99.',
|
||||
'expected' => [
|
||||
'item_value' => [10.00, 4.99],
|
||||
'extra_line_value' => 0.03
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10.005,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
[
|
||||
'value' => 5,
|
||||
'quantity' => 2,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 36.01,
|
||||
'breakdown' => [
|
||||
'item_total' => 30.01,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
],
|
||||
'with_many_roundings_down' => [
|
||||
'message' => 'Extra line should be added with price 0.01 and lines amount 10.00, 5.00 and 6.66.',
|
||||
'expected' => [
|
||||
'item_value' => [10.00, 4.99, 6.66],
|
||||
'extra_line_value' => 0.02
|
||||
],
|
||||
'items' => [
|
||||
[
|
||||
'value' => 10.005,
|
||||
'quantity' => 1,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
[
|
||||
'value' => 5.001,
|
||||
'quantity' => 1,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
[
|
||||
'value' => 6.666,
|
||||
'quantity' => 1,
|
||||
'tax' => 3,
|
||||
'category' => Item::PHYSICAL_GOODS,
|
||||
],
|
||||
],
|
||||
'amount' => 27.67,
|
||||
'breakdown' => [
|
||||
'item_total' => 21.67,
|
||||
'tax_total' => 6,
|
||||
'shipping' => null,
|
||||
'discount' => null,
|
||||
'shipping_discount' => null,
|
||||
'handling' => null,
|
||||
'insurance' => null,
|
||||
],
|
||||
]
|
||||
];
|
||||
|
||||
$values = [];
|
||||
foreach ($data as $testKey => $test) {
|
||||
$items = [];
|
||||
foreach ($test['items'] as $key => $item) {
|
||||
$unitAmount = new Money($item['value'], 'EUR');
|
||||
$tax = new Money($item['tax'], 'EUR');
|
||||
$items[$key] = Mockery::mock(
|
||||
Item::class,
|
||||
[
|
||||
'unit_amount' => $unitAmount,
|
||||
'tax' => $tax,
|
||||
'quantity'=> $item['quantity'],
|
||||
'category' => $item['category'],
|
||||
]
|
||||
);
|
||||
|
||||
$items[$key]->shouldReceive('to_array')->andReturnUsing(function (bool $roundToFloor = false) use ($unitAmount, $tax, $item) {
|
||||
return [
|
||||
'unit_amount' => $unitAmount->to_array($roundToFloor),
|
||||
'tax' => $tax->to_array(),
|
||||
'quantity'=> $item['quantity'],
|
||||
'category' => $item['category'],
|
||||
];
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
$breakdown = null;
|
||||
if ($test['breakdown']) {
|
||||
$breakdown = Mockery::mock(AmountBreakdown::class);
|
||||
foreach ($test['breakdown'] as $method => $value) {
|
||||
$breakdown->shouldReceive($method)->andReturnUsing(function () use ($value) {
|
||||
if (! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$money = new Money($value, 'EUR');
|
||||
return $money;
|
||||
});
|
||||
}
|
||||
|
||||
$breakdown
|
||||
->shouldReceive('to_array')
|
||||
->andReturn(
|
||||
array_map(
|
||||
function ($value) {
|
||||
return $value ? (new Money($value, 'EUR'))->to_array() : null;
|
||||
},
|
||||
$test['breakdown']
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$amountMoney = new Money($test['amount'], 'EUR');
|
||||
$amount = Mockery::mock(Amount::class);
|
||||
$amount
|
||||
->shouldReceive('to_array')
|
||||
->andReturn([
|
||||
'value' => $amountMoney->value_str(),
|
||||
'currency_code' => $amountMoney->currency_code(),
|
||||
'breakdown' => $breakdown ? $breakdown->to_array() : [],
|
||||
]);
|
||||
$amount->shouldReceive('value_str')->andReturn($amountMoney->value_str());
|
||||
$amount->shouldReceive('currency_code')->andReturn('EUR');
|
||||
$amount->shouldReceive('breakdown')->andReturn($breakdown);
|
||||
|
||||
$values[$testKey] = [
|
||||
$items,
|
||||
$amount,
|
||||
$test['expected'],
|
||||
$test['message'],
|
||||
];
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function testPayee()
|
||||
{
|
||||
$amount = Mockery::mock(Amount::class);
|
||||
|
|
|
@ -7,6 +7,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\TestCase;
|
||||
use Mockery;
|
||||
|
||||
|
@ -18,11 +19,15 @@ class PaymentsFactoryTest extends TestCase
|
|||
$authorization->shouldReceive('to_array')->andReturn(['id' => 'foo', 'status' => 'CREATED']);
|
||||
$capture = Mockery::mock(Capture::class);
|
||||
$capture->shouldReceive('to_array')->andReturn(['id' => 'capture', 'status' => 'CREATED']);
|
||||
$refund = Mockery::mock(Refund::class);
|
||||
$refund->shouldReceive('to_array')->andReturn(['id' => 'refund', 'status' => 'CREATED']);
|
||||
|
||||
$authorizationsFactory = Mockery::mock(AuthorizationFactory::class);
|
||||
$authorizationsFactory->shouldReceive('from_paypal_response')->andReturn($authorization);
|
||||
$captureFactory = Mockery::mock(CaptureFactory::class);
|
||||
$captureFactory->shouldReceive('from_paypal_response')->andReturn($capture);
|
||||
$refundFactory = Mockery::mock(RefundFactory::class);
|
||||
$refundFactory->shouldReceive('from_paypal_response')->andReturn($refund);
|
||||
$response = (object)[
|
||||
'authorizations' => [
|
||||
(object)['id' => 'foo', 'status' => 'CREATED'],
|
||||
|
@ -30,9 +35,12 @@ class PaymentsFactoryTest extends TestCase
|
|||
'captures' => [
|
||||
(object)['id' => 'capture', 'status' => 'CREATED'],
|
||||
],
|
||||
'refunds' => [
|
||||
(object)['id' => 'refund', 'status' => 'CREATED'],
|
||||
],
|
||||
];
|
||||
|
||||
$testee = new PaymentsFactory($authorizationsFactory, $captureFactory);
|
||||
$testee = new PaymentsFactory($authorizationsFactory, $captureFactory, $refundFactory);
|
||||
$result = $testee->from_paypal_response($response);
|
||||
|
||||
$this->assertInstanceOf(Payments::class, $result);
|
||||
|
@ -44,6 +52,9 @@ class PaymentsFactoryTest extends TestCase
|
|||
'captures' => [
|
||||
['id' => 'capture', 'status' => 'CREATED'],
|
||||
],
|
||||
'refunds' => [
|
||||
['id' => 'refund', 'status' => 'CREATED'],
|
||||
],
|
||||
];
|
||||
$this->assertEquals($expectedToArray, $result->to_array());
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
|
|||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Processor\AuthorizedPaymentsProcessor;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use function Brain\Monkey\Functions\when;
|
||||
|
||||
class RenewalHandlerTest extends TestCase
|
||||
{
|
||||
|
@ -157,6 +158,8 @@ class RenewalHandlerTest extends TestCase
|
|||
->with([$purchaseUnit], 'no_shipping', $payer, $token)
|
||||
->andReturn($order);
|
||||
|
||||
when('wcs_get_subscriptions_for_order')->justReturn(array());
|
||||
|
||||
$wcOrder->shouldReceive('update_status');
|
||||
$wcOrder->shouldReceive('save');
|
||||
|
||||
|
|
|
@ -43,11 +43,32 @@ class FeesRendererTest extends TestCase
|
|||
],
|
||||
]);
|
||||
|
||||
$wcOrder->expects('get_meta')
|
||||
->with(PayPalGateway::REFUND_FEES_META_KEY)
|
||||
->andReturn([
|
||||
'gross_amount' => [
|
||||
'currency_code' => 'USD',
|
||||
'value' => '20.52',
|
||||
],
|
||||
'paypal_fee' => [
|
||||
'currency_code' => 'USD',
|
||||
'value' => '0.51',
|
||||
],
|
||||
'net_amount' => [
|
||||
'currency_code' => 'USD',
|
||||
'value' => '50.01',
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->renderer->render($wcOrder);
|
||||
$this->assertStringContainsString('Fee', $result);
|
||||
$this->assertStringContainsString('0.41', $result);
|
||||
$this->assertStringContainsString('Payout', $result);
|
||||
$this->assertStringContainsString('10.01', $result);
|
||||
$this->assertStringContainsString('PayPal Refund Fee', $result);
|
||||
$this->assertStringContainsString('0.51', $result);
|
||||
$this->assertStringContainsString('PayPal Refund', $result);
|
||||
$this->assertStringContainsString('50.01', $result);
|
||||
}
|
||||
|
||||
public function testRenderWithoutNet() {
|
||||
|
@ -62,6 +83,10 @@ class FeesRendererTest extends TestCase
|
|||
],
|
||||
]);
|
||||
|
||||
$wcOrder->expects('get_meta')
|
||||
->with(PayPalGateway::REFUND_FEES_META_KEY)
|
||||
->andReturn([]);
|
||||
|
||||
$result = $this->renderer->render($wcOrder);
|
||||
$this->assertStringContainsString('Fee', $result);
|
||||
$this->assertStringContainsString('0.41', $result);
|
||||
|
@ -78,6 +103,10 @@ class FeesRendererTest extends TestCase
|
|||
->with(PayPalGateway::FEES_META_KEY)
|
||||
->andReturn($meta);
|
||||
|
||||
$wcOrder->expects('get_meta')
|
||||
->with(PayPalGateway::REFUND_FEES_META_KEY)
|
||||
->andReturn([]);
|
||||
|
||||
$this->assertSame('', $this->renderer->render($wcOrder));
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,9 @@ class SettingsPagesAssetsTest extends TestCase
|
|||
Mockery::mock(Environment::class),
|
||||
true,
|
||||
array(),
|
||||
array()
|
||||
);
|
||||
array(),
|
||||
true
|
||||
);
|
||||
|
||||
when('is_admin')
|
||||
->justReturn(true);
|
||||
|
|
|
@ -364,6 +364,11 @@ class PurchaseUnitTest extends TestCase
|
|||
],
|
||||
self::adaptAmountFormat([
|
||||
'value' => 10.69,
|
||||
'breakdown' => [
|
||||
'item_total' => 10.69,
|
||||
'tax_total' => 0,
|
||||
'shipping' => 0,
|
||||
],
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
@ -432,6 +437,11 @@ class PurchaseUnitTest extends TestCase
|
|||
],
|
||||
self::adaptAmountFormat([
|
||||
'value' => 10.69,
|
||||
'breakdown' => [
|
||||
'item_total' => 10.69,
|
||||
'tax_total' => 0,
|
||||
'shipping' => 0,
|
||||
],
|
||||
], get_woocommerce_currency()),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: WooCommerce PayPal Payments
|
||||
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
|
||||
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
|
||||
* Version: 2.2.1
|
||||
* Version: 2.2.2
|
||||
* Author: WooCommerce
|
||||
* Author URI: https://woocommerce.com/
|
||||
* License: GPL-2.0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue