Merge branch 'trunk' into pcp-568-onboarding

This commit is contained in:
dinamiko 2022-02-23 12:06:25 +01:00
commit e0880670ca
55 changed files with 1249 additions and 154 deletions

View file

@ -4,6 +4,7 @@
* Fix - DCC orders randomly failing #503
* Fix - Multi-currency broke #481
* Fix - Address information from PayPal shortcut flow not loaded #451
* Fix - WooCommerce as mu-plugin is not detected as active #461
* Enhancement - Improve onboarding flow, allow no card processing #443
* Enhancement - Add Germany to supported ACDC countries #459
* Enhancement - Add filters to allow ACDC for countries #437
@ -14,6 +15,9 @@
* Enhancement - Improve onboarding notice #465
* Enhancement - Add transaction ID to WC order and order note when refund is received #473
* Enhancement - Asset caching may cause bugs on upgrades #501
* Enhancement - Allow partial capture #483
* Enhancement - PayPal Payments doesn't set transaction fee metadata #467
* Enhancement - Show PayPal fee information in order #489
= 1.6.5 - 2022-01-31 =
* Fix - Allow guest users to purchase subscription products from checkout page #422

View file

@ -26,7 +26,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ApplicationContextFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\MoneyFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory;
@ -34,7 +36,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentSourceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
@ -239,7 +243,10 @@ return array(
'api.factory.capture' => static function ( ContainerInterface $container ): CaptureFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new CaptureFactory( $amount_factory );
return new CaptureFactory(
$amount_factory,
$container->get( 'api.factory.seller-receivable-breakdown' )
);
},
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
@ -280,9 +287,13 @@ return array(
$item_factory = $container->get( 'api.factory.item' );
return new AmountFactory(
$item_factory,
$container->get( 'api.factory.money' ),
$container->get( 'api.shop.currency' )
);
},
'api.factory.money' => static function ( ContainerInterface $container ): MoneyFactory {
return new MoneyFactory();
},
'api.factory.payer' => static function ( ContainerInterface $container ): PayerFactory {
$address_factory = $container->get( 'api.factory.address' );
return new PayerFactory( $address_factory );
@ -315,6 +326,22 @@ return array(
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
},
'api.factory.exchange-rate' => static function ( ContainerInterface $container ): ExchangeRateFactory {
return new ExchangeRateFactory();
},
'api.factory.platform-fee' => static function ( ContainerInterface $container ): PlatformFeeFactory {
return new PlatformFeeFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.payee' )
);
},
'api.factory.seller-receivable-breakdown' => static function ( ContainerInterface $container ): SellerReceivableBreakdownFactory {
return new SellerReceivableBreakdownFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.exchange-rate' ),
$container->get( 'api.factory.platform-fee' )
);
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ),

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
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\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -146,22 +147,32 @@ class PaymentsEndpoint {
/**
* Capture an authorization by a given ID.
*
* @param string $authorization_id The id.
* @param string $authorization_id The id.
* @param Money|null $amount The amount to capture. If not specified, the whole authorized amount is captured.
*
* @return Capture
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function capture( string $authorization_id ): Capture {
public function capture( string $authorization_id, ?Money $amount = null ): Capture {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/authorizations/' . $authorization_id . '/capture';
$args = array(
$data = array(
'final_capture' => true,
);
if ( $amount ) {
$data['amount'] = $amount->to_array();
}
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data, JSON_FORCE_OBJECT ),
);
$response = $this->request( $url, $args );

View file

@ -51,6 +51,13 @@ class Capture {
*/
private $seller_protection;
/**
* The detailed breakdown of the capture activity (fees, ...).
*
* @var SellerReceivableBreakdown|null
*/
private $seller_receivable_breakdown;
/**
* The invoice id.
*
@ -68,13 +75,14 @@ class Capture {
/**
* Capture constructor.
*
* @param string $id The ID.
* @param CaptureStatus $status The status.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param string $id The ID.
* @param CaptureStatus $status The status.
* @param Amount $amount The amount.
* @param bool $final_capture The final capture.
* @param string $seller_protection The seller protection.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param SellerReceivableBreakdown|null $seller_receivable_breakdown The detailed breakdown of the capture activity (fees, ...).
*/
public function __construct(
string $id,
@ -83,16 +91,18 @@ class Capture {
bool $final_capture,
string $seller_protection,
string $invoice_id,
string $custom_id
string $custom_id,
?SellerReceivableBreakdown $seller_receivable_breakdown
) {
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->final_capture = $final_capture;
$this->seller_protection = $seller_protection;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->seller_receivable_breakdown = $seller_receivable_breakdown;
}
/**
@ -158,6 +168,15 @@ class Capture {
return $this->custom_id;
}
/**
* Returns the detailed breakdown of the capture activity (fees, ...).
*
* @return SellerReceivableBreakdown|null
*/
public function seller_receivable_breakdown() : ?SellerReceivableBreakdown {
return $this->seller_receivable_breakdown;
}
/**
* Returns the entity as array.
*
@ -177,6 +196,9 @@ class Capture {
if ( $details ) {
$data['status_details'] = array( 'reason' => $details->reason() );
}
if ( $this->seller_receivable_breakdown ) {
$data['seller_receivable_breakdown'] = $this->seller_receivable_breakdown->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* The exchange rate object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class ExchangeRate.
*/
class ExchangeRate {
/**
* The source currency from which to convert an amount.
*
* @var string
*/
private $source_currency;
/**
* The target currency to which to convert an amount.
*
* @var string
*/
private $target_currency;
/**
* The target currency amount. Equivalent to one unit of the source currency.
*
* @var string
*/
private $value;
/**
* ExchangeRate constructor.
*
* @param string $source_currency The source currency from which to convert an amount.
* @param string $target_currency The target currency to which to convert an amount.
* @param string $value The target currency amount. Equivalent to one unit of the source currency.
*/
public function __construct( string $source_currency, string $target_currency, string $value ) {
$this->source_currency = $source_currency;
$this->target_currency = $target_currency;
$this->value = $value;
}
/**
* The source currency from which to convert an amount.
*
* @return string
*/
public function source_currency(): string {
return $this->source_currency;
}
/**
* The target currency to which to convert an amount.
*
* @return string
*/
public function target_currency(): string {
return $this->target_currency;
}
/**
* The target currency amount. Equivalent to one unit of the source currency.
*
* @return string
*/
public function value(): string {
return $this->value;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'source_currency' => $this->source_currency,
'target_currency' => $this->target_currency,
'value' => $this->value,
);
}
}

View file

@ -0,0 +1,74 @@
<?php
/**
* The platform fee object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PlatformFee.
*/
class PlatformFee {
/**
* The fee.
*
* @var Money
*/
private $amount;
/**
* The recipient of the fee.
*
* @var Payee|null
*/
private $payee;
/**
* PlatformFee constructor.
*
* @param Money $amount The fee.
* @param Payee|null $payee The recipient of the fee.
*/
public function __construct( Money $amount, ?Payee $payee ) {
$this->amount = $amount;
$this->payee = $payee;
}
/**
* The fee.
*
* @return Money
*/
public function amount(): Money {
return $this->amount;
}
/**
* The recipient of the fee.
*
* @return Payee|null
*/
public function payee(): ?Payee {
return $this->payee;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array(
'amount' => $this->amount->to_array(),
);
if ( $this->payee ) {
$data['payee'] = $this->payee->to_array();
}
return $data;
}
}

View file

@ -0,0 +1,211 @@
<?php
/**
* The info about fees and amount that will be received by the seller.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class SellerReceivableBreakdown
*/
class SellerReceivableBreakdown {
/**
* The amount for this captured payment in the currency of the transaction.
*
* @var Money
*/
private $gross_amount;
/**
* The applicable fee for this captured 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 captured 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 that is credited to the payee's PayPal account.
*
* Present only when the currency of the captured payment is different from the currency
* of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate.
*
* @var Money|null
*/
private $receivable_amount;
/**
* The exchange rate that determines the amount that is credited to the payee's PayPal account.
*
* Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds.
*
* @var ExchangeRate|null
*/
private $exchange_rate;
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*
* @var PlatformFee[]
*/
private $platform_fees;
/**
* SellerReceivableBreakdown constructor.
*
* @param Money $gross_amount The amount for this captured payment in the currency of the transaction.
* @param Money|null $paypal_fee The applicable fee for this captured payment in the currency of the transaction.
* @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this captured payment in the receivable currency.
* @param Money|null $net_amount The net amount that the payee receives for this captured payment in their PayPal account.
* @param Money|null $receivable_amount The net amount that is credited to the payee's PayPal account.
* @param ExchangeRate|null $exchange_rate The exchange rate that determines the amount that is credited to the payee's PayPal account.
* @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 $receivable_amount,
?ExchangeRate $exchange_rate,
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->receivable_amount = $receivable_amount;
$this->exchange_rate = $exchange_rate;
$this->platform_fees = $platform_fees;
}
/**
* The amount for this captured payment in the currency of the transaction.
*
* @return Money
*/
public function gross_amount(): ?Money {
return $this->gross_amount;
}
/**
* The applicable fee for this captured payment in the currency of the transaction.
*
* @return Money|null
*/
public function paypal_fee(): ?Money {
return $this->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.
*
* @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 captured 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 that is credited to the payee's PayPal account.
*
* Present only when the currency of the captured payment is different from the currency
* of the PayPal account where the payee wants to credit the funds. Computed as net_amount times exchange_rate.
*
* @return Money|null
*/
public function receivable_amount(): ?Money {
return $this->receivable_amount;
}
/**
* The exchange rate that determines the amount that is credited to the payee's PayPal account.
*
* Present when the currency of the captured payment is different from the currency of the PayPal account where the payee wants to credit the funds.
*
* @return ExchangeRate|null
*/
public function exchange_rate(): ?ExchangeRate {
return $this->exchange_rate;
}
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured 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(
'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->receivable_amount ) {
$data['receivable_amount'] = $this->receivable_amount->to_array();
}
if ( $this->exchange_rate ) {
$data['exchange_rate'] = $this->exchange_rate->to_array();
}
if ( $this->platform_fees ) {
$data['platform_fees'] = array_map(
function ( PlatformFee $fee ) {
return $fee->to_array();
},
$this->platform_fees
);
}
return $data;
}
}

View file

@ -28,6 +28,13 @@ class AmountFactory {
*/
private $item_factory;
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* 3-letter currency code of the shop.
*
@ -38,12 +45,14 @@ class AmountFactory {
/**
* AmountFactory constructor.
*
* @param ItemFactory $item_factory The Item factory.
* @param string $currency 3-letter currency code of the shop.
* @param ItemFactory $item_factory The Item factory.
* @param MoneyFactory $money_factory The Money factory.
* @param string $currency 3-letter currency code of the shop.
*/
public function __construct( ItemFactory $item_factory, string $currency ) {
$this->item_factory = $item_factory;
$this->currency = $currency;
public function __construct( ItemFactory $item_factory, MoneyFactory $money_factory, string $currency ) {
$this->item_factory = $item_factory;
$this->money_factory = $money_factory;
$this->currency = $currency;
}
/**
@ -169,16 +178,7 @@ class AmountFactory {
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ): Amount {
if ( ! isset( $data->value ) || ! is_numeric( $data->value ) ) {
throw new RuntimeException( __( 'No value given', 'woocommerce-paypal-payments' ) );
}
if ( ! isset( $data->currency_code ) ) {
throw new RuntimeException(
__( 'No currency given', 'woocommerce-paypal-payments' )
);
}
$money = new Money( (float) $data->value, $data->currency_code );
$money = $this->money_factory->from_paypal_response( $data );
$breakdown = ( isset( $data->breakdown ) ) ? $this->break_down( $data->breakdown ) : null;
return new Amount( $money, $breakdown );
}

View file

@ -25,14 +25,26 @@ class CaptureFactory {
*/
private $amount_factory;
/**
* The SellerReceivableBreakdown factory.
*
* @var SellerReceivableBreakdownFactory
*/
private $seller_receivable_breakdown_factory;
/**
* CaptureFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param AmountFactory $amount_factory The amount factory.
* @param SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory The SellerReceivableBreakdown factory.
*/
public function __construct( AmountFactory $amount_factory ) {
public function __construct(
AmountFactory $amount_factory,
SellerReceivableBreakdownFactory $seller_receivable_breakdown_factory
) {
$this->amount_factory = $amount_factory;
$this->amount_factory = $amount_factory;
$this->seller_receivable_breakdown_factory = $seller_receivable_breakdown_factory;
}
/**
@ -44,7 +56,10 @@ class CaptureFactory {
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = $data->status_details->reason ?? null;
$reason = $data->status_details->reason ?? null;
$seller_receivable_breakdown = isset( $data->seller_receivable_breakdown ) ?
$this->seller_receivable_breakdown_factory->from_paypal_response( $data->seller_receivable_breakdown )
: null;
return new Capture(
(string) $data->id,
@ -56,7 +71,8 @@ class CaptureFactory {
(bool) $data->final_capture,
(string) $data->seller_protection->status,
(string) $data->invoice_id,
(string) $data->custom_id
(string) $data->custom_id,
$seller_receivable_breakdown
);
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* The ExchangeRateFactory Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ExchangeRate;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ExchangeRateFactory
*/
class ExchangeRateFactory {
/**
* Returns an ExchangeRate object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return ExchangeRate|null
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): ?ExchangeRate {
// Looks like all fields in this object are optional, according to the docs,
// and sometimes we get an empty object.
$source_currency = $data->source_currency ?? '';
$target_currency = $data->target_currency ?? '';
$value = $data->value ?? '';
if ( ! $source_currency && ! $target_currency && ! $value ) {
// Do not return empty object.
return null;
}
return new ExchangeRate( $source_currency, $target_currency, $value );
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* The Money factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class MoneyFactory
*/
class MoneyFactory {
/**
* Returns a Money object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return Money
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): Money {
if ( ! isset( $data->value ) || ! is_numeric( $data->value ) ) {
throw new RuntimeException( 'No money value given' );
}
if ( ! isset( $data->currency_code ) ) {
throw new RuntimeException( 'No currency given' );
}
return new Money( (float) $data->value, $data->currency_code );
}
}

View file

@ -0,0 +1,64 @@
<?php
/**
* The PlatformFee 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\Exception\RuntimeException;
/**
* Class PayeeFactory
*/
class PlatformFeeFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The Payee factory.
*
* @var PayeeFactory
*/
private $payee_factory;
/**
* PlatformFeeFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param PayeeFactory $payee_factory The Payee factory.
*/
public function __construct( MoneyFactory $money_factory, PayeeFactory $payee_factory ) {
$this->money_factory = $money_factory;
$this->payee_factory = $payee_factory;
}
/**
* Returns a Payee object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return PlatformFee
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): PlatformFee {
if ( ! isset( $data->amount ) ) {
throw new RuntimeException( 'Platform fee amount not found' );
}
$amount = $this->money_factory->from_paypal_response( $data->amount );
$payee = ( isset( $data->payee ) ) ? $this->payee_factory->from_paypal_response( $data->payee ) : null;
return new PlatformFee( $amount, $payee );
}
}

View file

@ -0,0 +1,97 @@
<?php
/**
* The SellerReceivableBreakdown 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\SellerReceivableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class SellerReceivableBreakdownFactory
*/
class SellerReceivableBreakdownFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The ExchangeRate factory.
*
* @var ExchangeRateFactory
*/
private $exchange_rate_factory;
/**
* The PlatformFee factory.
*
* @var PlatformFeeFactory
*/
private $platform_fee_factory;
/**
* SellerReceivableBreakdownFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param ExchangeRateFactory $exchange_rate_factory The ExchangeRate factory.
* @param PlatformFeeFactory $platform_fee_factory The PlatformFee factory.
*/
public function __construct(
MoneyFactory $money_factory,
ExchangeRateFactory $exchange_rate_factory,
PlatformFeeFactory $platform_fee_factory
) {
$this->money_factory = $money_factory;
$this->exchange_rate_factory = $exchange_rate_factory;
$this->platform_fee_factory = $platform_fee_factory;
}
/**
* Returns a SellerReceivableBreakdown object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return SellerReceivableBreakdown
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): SellerReceivableBreakdown {
if ( ! isset( $data->gross_amount ) ) {
throw new RuntimeException( 'Seller breakdown gross amount not found' );
}
$gross_amount = $this->money_factory->from_paypal_response( $data->gross_amount );
$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;
$receivable_amount = ( isset( $data->receivable_amount ) ) ? $this->money_factory->from_paypal_response( $data->receivable_amount ) : null;
$exchange_rate = ( isset( $data->exchange_rate ) ) ? $this->exchange_rate_factory->from_paypal_response( $data->exchange_rate ) : 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 SellerReceivableBreakdown(
$gross_amount,
$paypal_fee,
$paypal_fee_in_receivable_currency,
$net_amount,
$receivable_amount,
$exchange_rate,
$platform_fees
);
}
}

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesDisclaimers;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction;
@ -251,6 +252,9 @@ return array(
$settings = $container->get( 'wcgateway.settings' );
return new OrderTablePaymentStatusColumn( $settings );
},
'wcgateway.admin.fees-renderer' => static function ( ContainerInterface $container ): FeesRenderer {
return new FeesRenderer();
},
'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array {

View file

@ -0,0 +1,86 @@
<?php
/**
* Renders the PayPal fees in the order details.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Admin
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Admin;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
/**
* Class FeesRenderer
*/
class FeesRenderer {
/**
* Renders the PayPal fees in the order details.
*
* @param WC_Order $wc_order The order for which to render the fees.
*
* @return string
*/
public function render( WC_Order $wc_order ) : string {
$breakdown = $wc_order->get_meta( PayPalGateway::FEES_META_KEY );
if ( ! is_array( $breakdown ) ) {
return '';
}
$html = '';
$fee = $breakdown['paypal_fee'] ?? null;
if ( is_array( $fee ) ) {
$html .= $this->render_money_row(
__( 'PayPal Fee:', 'woocommerce-paypal-payments' ),
__( 'The fee PayPal collects for the transaction.', 'woocommerce-paypal-payments' ),
$fee['value'],
$fee['currency_code'],
true
);
}
$net = $breakdown['net_amount'] ?? null;
if ( is_array( $net ) ) {
$html .= $this->render_money_row(
__( 'PayPal Payout:', 'woocommerce-paypal-payments' ),
__( 'The net total that will be credited to your PayPal account.', 'woocommerce-paypal-payments' ),
$net['value'],
$net['currency_code']
);
}
return $html;
}
/**
* Renders a row in the order price breakdown table.
*
* @param string $title The row title.
* @param string $tooltip The title tooltip.
* @param string|float $value The money value.
* @param string $currency The currency code.
* @param bool $negative Whether to add the minus sign.
* @return string
*/
private function render_money_row( string $title, string $tooltip, $value, string $currency, bool $negative = false ): string {
/**
* Bad type hint in WC phpdoc.
*
* @psalm-suppress InvalidScalarArgument
*/
return '
<tr>
<td class="label">' . wc_help_tip( $tooltip ) . ' ' . esc_html( $title ) . '
</td>
<td width="1%"></td>
<td class="total">
' .
( $negative ? ' - ' : '' ) .
wc_price( $value, array( 'currency' => $currency ) ) . '
</td>
</tr>';
}
}

View file

@ -37,6 +37,7 @@ class PayPalGateway extends \WC_Payment_Gateway {
const INTENT_META_KEY = '_ppcp_paypal_intent';
const ORDER_ID_META_KEY = '_ppcp_paypal_order_id';
const ORDER_PAYMENT_MODE_META_KEY = '_ppcp_paypal_payment_mode';
const FEES_META_KEY = '_ppcp_paypal_fees';
/**
* The Settings Renderer.

View file

@ -11,12 +11,14 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Notice\AuthorizeOrderActionNotice;
@ -96,11 +98,11 @@ class AuthorizedPaymentsProcessor {
/**
* Process a WooCommerce order.
*
* @param \WC_Order $wc_order The WooCommerce order.
* @param WC_Order $wc_order The WooCommerce order.
*
* @return string One of the AuthorizedPaymentsProcessor status constants.
*/
public function process( \WC_Order $wc_order ): string {
public function process( WC_Order $wc_order ): string {
$this->captures = array();
try {
@ -123,7 +125,7 @@ class AuthorizedPaymentsProcessor {
}
try {
$this->capture_authorizations( ...$authorizations );
$this->captures[] = $this->capture_authorization( $wc_order, ...$authorizations );
} catch ( Exception $exception ) {
$this->logger->error( 'Failed to capture authorization: ' . $exception->getMessage() );
return self::FAILED;
@ -144,11 +146,11 @@ class AuthorizedPaymentsProcessor {
/**
* Captures an authorized payment for an WooCommerce order.
*
* @param \WC_Order $wc_order The WooCommerce order.
* @param WC_Order $wc_order The WooCommerce order.
*
* @return bool
*/
public function capture_authorized_payment( \WC_Order $wc_order ): bool {
public function capture_authorized_payment( WC_Order $wc_order ): bool {
$result_status = $this->process( $wc_order );
$this->render_authorization_message_for_status( $result_status );
@ -211,11 +213,11 @@ class AuthorizedPaymentsProcessor {
/**
* Returns the PayPal order from a given WooCommerce order.
*
* @param \WC_Order $wc_order The WooCommerce order.
* @param WC_Order $wc_order The WooCommerce order.
*
* @return Order
*/
private function paypal_order_from_wc_order( \WC_Order $wc_order ): Order {
private function paypal_order_from_wc_order( WC_Order $wc_order ): Order {
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
return $this->order_endpoint->order( $order_id );
}
@ -239,15 +241,21 @@ class AuthorizedPaymentsProcessor {
}
/**
* Captures the authorizations.
* Captures the authorization.
*
* @param WC_Order $order The order.
* @param Authorization ...$authorizations All authorizations.
* @throws Exception If capture failed.
*/
private function capture_authorizations( Authorization ...$authorizations ) {
private function capture_authorization( WC_Order $order, Authorization ...$authorizations ): Capture {
$uncaptured_authorizations = $this->authorizations_to_capture( ...$authorizations );
foreach ( $uncaptured_authorizations as $authorization ) {
$this->captures[] = $this->payments_endpoint->capture( $authorization->id() );
if ( ! $uncaptured_authorizations ) {
throw new Exception( 'No authorizations to capture.' );
}
$authorization = end( $uncaptured_authorizations );
return $this->payments_endpoint->capture( $authorization->id(), new Money( (float) $order->get_total(), $order->get_currency() ) );
}
/**

View file

@ -63,6 +63,7 @@ trait PaymentsStatusHandlingTrait {
switch ( $status->name() ) {
case CaptureStatus::COMPLETED:
$wc_order->payment_complete();
do_action( 'woocommerce_paypal_payments_order_captured', $wc_order, $capture );
break;
// It is checked in the capture endpoint already, but there are other ways to capture,
// such as when paid via saved card.

View file

@ -11,9 +11,12 @@ namespace WooCommerce\PayPalCommerce\WcGateway;
use Dhii\Container\ServiceProvider;
use Dhii\Modular\Module\ModuleInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Admin\OrderTablePaymentStatusColumn;
use WooCommerce\PayPalCommerce\WcGateway\Admin\PaymentStatusOrderDetail;
use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction;
@ -71,6 +74,35 @@ class WCGatewayModule implements ModuleInterface {
}
);
add_action(
'woocommerce_paypal_payments_order_captured',
function ( WC_Order $wc_order, Capture $capture ) {
$breakdown = $capture->seller_receivable_breakdown();
if ( $breakdown ) {
$wc_order->update_meta_data( PayPalGateway::FEES_META_KEY, $breakdown->to_array() );
$wc_order->save_meta_data();
}
},
10,
2
);
$fees_renderer = $c->get( 'wcgateway.admin.fees-renderer' );
assert( $fees_renderer instanceof FeesRenderer );
add_action(
'woocommerce_admin_order_totals_after_total',
function ( int $order_id ) use ( $fees_renderer ) {
$wc_order = wc_get_order( $order_id );
if ( ! $wc_order instanceof WC_Order ) {
return;
}
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $fees_renderer->render( $wc_order );
}
);
if ( $c->has( 'wcgateway.url' ) ) {
$assets = new SettingsPageAssets(
$c->get( 'wcgateway.url' ),
@ -249,7 +281,7 @@ class WCGatewayModule implements ModuleInterface {
static function ( $order_actions ) use ( $container ): array {
global $theorder;
if ( ! is_a( $theorder, \WC_Order::class ) ) {
if ( ! is_a( $theorder, WC_Order::class ) ) {
return $order_actions;
}
@ -265,7 +297,7 @@ class WCGatewayModule implements ModuleInterface {
add_action(
'woocommerce_order_action_ppcp_authorize_order',
static function ( \WC_Order $wc_order ) use ( $container ) {
static function ( WC_Order $wc_order ) use ( $container ) {
/**
* The authorized payments processor.

View file

@ -149,5 +149,6 @@
<MixedReturnTypeCoercion errorLevel="info"/>
<MixedStringOffsetAssignment errorLevel="info"/>
<ParamNameMismatch errorLevel="info"/>
<RedundantCastGivenDocblockType errorLevel="info"/>
</issueHandlers>
</psalm>

View file

@ -85,6 +85,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - DCC orders randomly failing #503
* Fix - Multi-currency broke #481
* Fix - Address information from PayPal shortcut flow not loaded #451
* Fix - WooCommerce as mu-plugin is not detected as active #461
* Enhancement - Improve onboarding flow, allow no card processing #443
* Enhancement - Add Germany to supported ACDC countries #459
* Enhancement - Add filters to allow ACDC for countries #437
@ -95,6 +96,9 @@ Follow the steps below to connect the plugin to your PayPal account:
* Enhancement - Improve onboarding notice #465
* Enhancement - Add transaction ID to WC order and order note when refund is received #473
* Enhancement - Asset caching may cause bugs on upgrades #501
* Enhancement - Allow partial capture #483
* Enhancement - PayPal Payments doesn't set transaction fee metadata #467
* Enhancement - Show PayPal fee information in order #489
= 1.6.5 =
* Fix - Allow guest users to purchase subscription products from checkout page #422

View file

@ -6,9 +6,9 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Authentication;
use Requests_Utility_CaseInsensitiveDictionary;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Psr\Log\LoggerInterface;
use Mockery;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use function Brain\Monkey\Functions\expect;

View file

@ -10,8 +10,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
@ -89,7 +89,6 @@ class IdentityTokenTest extends TestCase
expect('is_wp_error')->with($rawResponse)->andReturn(false);
expect('wp_remote_retrieve_response_code')->with($rawResponse)->andReturn(200);
when('wc_print_r')->returnArg();
when('get_user_meta')->justReturn('');
$result = $this->sut->generate_for_user(1);
@ -108,7 +107,6 @@ class IdentityTokenTest extends TestCase
$headers->shouldReceive('getAll');
expect('wp_remote_get')->andReturn(['headers' => $headers,]);
expect('is_wp_error')->andReturn(true);
when('wc_print_r')->returnArg();
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->settings->shouldReceive('has')->andReturn(true);
@ -135,7 +133,6 @@ class IdentityTokenTest extends TestCase
]);
expect('is_wp_error')->andReturn(false);
expect('wp_remote_retrieve_response_code')->andReturn(500);
when('wc_print_r')->returnArg();
$this->logger->shouldReceive('log');
$this->logger->shouldReceive('debug');
$this->settings->shouldReceive('has')->andReturn(true);

View file

@ -24,10 +24,10 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
@ -38,7 +38,6 @@ class OrderEndpointTest extends TestCase
public function setUp(): void
{
parent::setUp();
when('wc_print_r')->returnArg();
$this->shipping = new Shipping('shipping', new Address('US', 'street', '', 'CA', '', '12345'));
}

View file

@ -9,8 +9,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use WooCommerce\PayPalCommerce\TestCase;
class OrderTest extends TestCase
{

View file

@ -12,9 +12,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
class PaymentTokenEndpointTest extends TestCase

View file

@ -13,10 +13,9 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
class PaymentsEndpointTest extends TestCase

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class AddressTest extends TestCase
{

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class AmountBreakdownTest extends TestCase

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class AmountTest extends TestCase

View file

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class AuthorizationStatusTest extends TestCase
{

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class AuthorizationTest extends TestCase
{

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class ItemTest extends TestCase

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class MoneyTest extends TestCase
{

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class PayerTest extends TestCase

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class PaymentsTest extends TestCase
{

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class PurchaseUnitTest extends TestCase

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class AddressFactoryTest extends TestCase

View file

@ -6,7 +6,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
@ -15,11 +15,21 @@ class AmountFactoryTest extends TestCase
{
private $currency = 'EUR';
public function testFromWcCartDefault()
{
$itemFactory = Mockery::mock(ItemFactory::class);
$testee = new AmountFactory($itemFactory, $this->currency);
private $itemFactory;
private $moneyFactory;
private $testee;
public function setUp(): void
{
parent::setUp();
$this->itemFactory = Mockery::mock(ItemFactory::class);
$this->moneyFactory = new MoneyFactory();
$this->testee = new AmountFactory($this->itemFactory, $this->moneyFactory, $this->currency);
}
public function testFromWcCartDefault()
{
$cart = Mockery::mock(\WC_Cart::class);
$cart
->shouldReceive('get_total')
@ -53,7 +63,7 @@ class AmountFactoryTest extends TestCase
$woocommerce->session = $session;
$session->shouldReceive('get')->andReturn([]);
$result = $testee->from_wc_cart($cart);
$result = $this->testee->from_wc_cart($cart);
$this->assertEquals($this->currency, $result->currency_code());
$this->assertEquals((float) 1, $result->value());
$this->assertEquals((float) 10, $result->breakdown()->discount()->value());
@ -68,10 +78,6 @@ class AmountFactoryTest extends TestCase
public function testFromWcCartNoDiscount()
{
$itemFactory = Mockery::mock(ItemFactory::class);
$testee = new AmountFactory($itemFactory, $this->currency);
$expectedCurrency = 'EUR';
$expectedTotal = 1;
$cart = Mockery::mock(\WC_Cart::class);
$cart
@ -105,13 +111,12 @@ class AmountFactoryTest extends TestCase
when('WC')->justReturn($woocommerce);
$woocommerce->session = $session;
$session->shouldReceive('get')->andReturn([]);
$result = $testee->from_wc_cart($cart);
$result = $this->testee->from_wc_cart($cart);
$this->assertNull($result->breakdown()->discount());
}
public function testFromWcOrderDefault()
{
$itemFactory = Mockery::mock(ItemFactory::class);
$order = Mockery::mock(\WC_Order::class);
$unitAmount = Mockery::mock(Money::class);
$unitAmount
@ -131,11 +136,10 @@ class AmountFactoryTest extends TestCase
$item
->shouldReceive('tax')
->andReturn($tax);
$itemFactory
$this->itemFactory
->expects('from_wc_order')
->with($order)
->andReturn([$item]);
$testee = new AmountFactory($itemFactory, $this->currency);
$order
->shouldReceive('get_total')
@ -154,7 +158,7 @@ class AmountFactoryTest extends TestCase
->with(false)
->andReturn(3);
$result = $testee->from_wc_order($order);
$result = $this->testee->from_wc_order($order);
$this->assertEquals((float) 3, $result->breakdown()->discount()->value());
$this->assertEquals((float) 6, $result->breakdown()->item_total()->value());
$this->assertEquals((float) 1.5, $result->breakdown()->shipping()->value());
@ -169,7 +173,6 @@ class AmountFactoryTest extends TestCase
public function testFromWcOrderDiscountIsNull()
{
$itemFactory = Mockery::mock(ItemFactory::class);
$order = Mockery::mock(\WC_Order::class);
$unitAmount = Mockery::mock(Money::class);
$unitAmount
@ -189,11 +192,10 @@ class AmountFactoryTest extends TestCase
$item
->shouldReceive('tax')
->andReturn($tax);
$itemFactory
$this->itemFactory
->expects('from_wc_order')
->with($order)
->andReturn([$item]);
$testee = new AmountFactory($itemFactory, $this->currency);
$order
->shouldReceive('get_total')
@ -212,7 +214,7 @@ class AmountFactoryTest extends TestCase
->with(false)
->andReturn(0);
$result = $testee->from_wc_order($order);
$result = $this->testee->from_wc_order($order);
$this->assertNull($result->breakdown()->discount());
}
@ -222,12 +224,10 @@ class AmountFactoryTest extends TestCase
*/
public function testFromPayPalResponse($response, $expectsException)
{
$itemFactory = Mockery::mock(ItemFactory::class);
$testee = new AmountFactory($itemFactory, $this->currency);
if ($expectsException) {
$this->expectException(RuntimeException::class);
}
$result = $testee->from_paypal_response($response);
$result = $this->testee->from_paypal_response($response);
if ($expectsException) {
return;
}

View file

@ -7,7 +7,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
class AuthorizationFactoryTest extends TestCase
{

View file

@ -5,7 +5,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\expect;
use function Brain\Monkey\Functions\when;
use Mockery;

View file

@ -10,7 +10,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class OrderFactoryTest extends TestCase

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class PayerFactoryTest extends TestCase

View file

@ -7,7 +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\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
class PaymentsFactoryTest extends TestCase

View file

@ -11,7 +11,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
use function Brain\Monkey\Functions\expect;

View file

@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\TestCase;
class SellerReceivableBreakdownFactoryTest extends TestCase
{
private $testee;
public function setUp(): void
{
parent::setUp();
$this->testee = new SellerReceivableBreakdownFactory(
new MoneyFactory(),
new ExchangeRateFactory(),
new PlatformFeeFactory(new MoneyFactory(), new PayeeFactory())
);
}
/**
* @dataProvider dataForTestFromPayPalResponse
*/
public function testFromPayPalResponse(string $json, array $expected_result)
{
$obj = json_decode($json);
$result = $this->testee->from_paypal_response($obj);
self::assertEquals($expected_result, $result->to_array());
}
public function dataForTestFromPayPalResponse() : array
{
return [
'fee' => [
'
{
"gross_amount": {
"currency_code": "USD",
"value": "10.42"
},
"paypal_fee": {
"currency_code": "USD",
"value": "0.41"
},
"net_amount": {
"currency_code": "USD",
"value": "10.01"
}
}',
[
'gross_amount' => [
'currency_code' => 'USD',
'value' => '10.42',
],
'paypal_fee' => [
'currency_code' => 'USD',
'value' => '0.41',
],
'net_amount' => [
'currency_code' => 'USD',
'value' => '10.01',
],
],
],
'min' => [
'
{
"gross_amount": {
"currency_code": "USD",
"value": "10.42"
}
}',
[
'gross_amount' => [
'currency_code' => 'USD',
'value' => '10.42',
],
],
],
'exchange' => [
'
{
"gross_amount": {
"value": "10.99",
"currency_code": "USD"
},
"paypal_fee": {
"value": "0.33",
"currency_code": "USD"
},
"net_amount": {
"value": "10.66",
"currency_code": "USD"
},
"receivable_amount": {
"currency_code": "CNY",
"value": "59.26"
},
"paypal_fee_in_receivable_currency": {
"currency_code": "CNY",
"value": "1.13"
},
"exchange_rate": {
"source_currency": "USD",
"target_currency": "CNY",
"value": "5.9483297432325"
}
}',
[
'gross_amount' => [
'currency_code' => 'USD',
'value' => '10.99',
],
'paypal_fee' => [
'currency_code' => 'USD',
'value' => '0.33',
],
'net_amount' => [
'currency_code' => 'USD',
'value' => '10.66',
],
'receivable_amount' => [
'currency_code' => 'CNY',
'value' => '59.26',
],
'paypal_fee_in_receivable_currency' => [
'currency_code' => 'CNY',
'value' => '1.13',
],
'exchange_rate' => [
'source_currency' => 'USD',
'target_currency' => 'CNY',
'value' => '5.9483297432325',
],
],
],
'platform_fees' => [
'
{
"gross_amount": {
"currency_code": "USD",
"value": "10.42"
},
"platform_fees": [
{
"amount": {
"currency_code": "USD",
"value": "0.06"
}
},
{
"amount": {
"currency_code": "USD",
"value": "0.08"
},
"payee": {
"email_address": "example@gmail.com"
}
}
]
}',
[
'gross_amount' => [
'currency_code' => 'USD',
'value' => '10.42',
],
'platform_fees' => [
[
'amount' => [
'currency_code' => 'USD',
'value' => '0.06',
],
],
[
'amount' => [
'currency_code' => 'USD',
'value' => '0.08',
],
'payee' => [
'email_address' => 'example@gmail.com',
],
],
],
],
],
];
}
}

View file

@ -3,8 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
use WooCommerce\PayPalCommerce\TestCase;
use function Brain\Monkey\Functions\when;
class ApplicationContextRepositoryTest extends TestCase

View file

@ -3,9 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Repository;
use WooCommerce\PayPalCommerce\ApiClient\Config\Config;
use WooCommerce\PayPalCommerce\ApiClient\TestCase;
use Mockery;
use WooCommerce\PayPalCommerce\TestCase;
class PayeeRepositoryTest extends TestCase
{

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use function Brain\Monkey\setUp;
use function Brain\Monkey\tearDown;
use function Brain\Monkey\Functions\expect;
use Mockery;
class TestCase extends \PHPUnit\Framework\TestCase
{
public function setUp(): void
{
parent::setUp();
expect('__')->andReturnUsing(function (string $text) {
return $text;
});
setUp();
}
public function tearDown(): void
{
tearDown();
Mockery::close();
parent::tearDown();
}
}

View file

@ -36,7 +36,6 @@ class ThreeDSecureTest extends TestCase
$order->shouldReceive('payment_source')->andReturn($source);
$logger = \Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('info');
when('wc_print_r')->justReturn();
$testee = new ThreeDSecure($logger);
$result = $testee->proceed_with_order($order);

View file

@ -25,6 +25,9 @@ class TestCase extends \PHPUnit\Framework\TestCase
when('sanitize_text_field')->returnArg();
when('wp_kses_post')->returnArg();
when('wp_unslash')->returnArg();
when('wc_print_r')->alias(function ($value, bool $return = false) {
return print_r($value, $return);
});
when('get_plugin_data')->justReturn(['Version' => '1.0']);
when('plugin_basename')->justReturn('woocommerce-paypal-payments/woocommerce-paypal-payments.php');

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Mockery;
use WC_Order;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\WcGateway\Admin\FeesRenderer;
use function Brain\Monkey\Functions\when;
class FeesRendererTest extends TestCase
{
private $renderer;
public function setUp(): void
{
parent::setUp();
$this->renderer = new FeesRenderer();
when('wc_help_tip')->returnArg();
when('wc_price')->returnArg();
}
public function testRender() {
$wcOrder = Mockery::mock(WC_Order::class);
$wcOrder->expects('get_meta')
->with(PayPalGateway::FEES_META_KEY)
->andReturn([
'gross_amount' => [
'currency_code' => 'USD',
'value' => '10.42',
],
'paypal_fee' => [
'currency_code' => 'USD',
'value' => '0.41',
],
'net_amount' => [
'currency_code' => 'USD',
'value' => '10.01',
],
]);
$result = $this->renderer->render($wcOrder);
$this->assertStringContainsString('Fee', $result);
$this->assertStringContainsString('0.41', $result);
$this->assertStringContainsString('Payout', $result);
$this->assertStringContainsString('10.01', $result);
}
public function testRenderWithoutNet() {
$wcOrder = Mockery::mock(WC_Order::class);
$wcOrder->expects('get_meta')
->with(PayPalGateway::FEES_META_KEY)
->andReturn([
'paypal_fee' => [
'currency_code' => 'USD',
'value' => '0.41',
],
]);
$result = $this->renderer->render($wcOrder);
$this->assertStringContainsString('Fee', $result);
$this->assertStringContainsString('0.41', $result);
$this->assertStringNotContainsString('Payout', $result);
}
/**
* @dataProvider noFeesDataProvider
*/
public function testNoFees($meta) {
$wcOrder = Mockery::mock(WC_Order::class);
$wcOrder->expects('get_meta')
->with(PayPalGateway::FEES_META_KEY)
->andReturn($meta);
$this->assertSame('', $this->renderer->render($wcOrder));
}
function noFeesDataProvider(): array
{
return [
['hello'],
[[]],
[['paypal_fee' => 'hello']],
];
}
}

View file

@ -12,6 +12,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
@ -26,6 +27,8 @@ class AuthorizedPaymentsProcessorTest extends TestCase
private $wcOrder;
private $paypalOrderId = 'abc';
private $authorizationId = 'qwe';
private $amount = 42.0;
private $currency = 'EUR';
private $paypalOrder;
private $orderEndpoint;
private $paymentsEndpoint;
@ -64,13 +67,13 @@ class AuthorizedPaymentsProcessorTest extends TestCase
public function testSuccess() {
$this->paymentsEndpoint
->expects('capture')
->with($this->authorizationId)
->with($this->authorizationId, equalTo(new Money($this->amount, $this->currency)))
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
$this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder));
}
public function testCapturesAllCaptureable() {
public function testCapturesLastCaptureable() {
$authorizations = [
$this->createAuthorization('id1', AuthorizationStatus::CREATED),
$this->createAuthorization('id2', AuthorizationStatus::VOIDED),
@ -82,12 +85,10 @@ class AuthorizedPaymentsProcessorTest extends TestCase
];
$this->paypalOrder = $this->createPaypalOrder($authorizations);
foreach ([$authorizations[0], $authorizations[2]] as $authorization) {
$this->paymentsEndpoint
->expects('capture')
->with($authorization->id())
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
}
$this->paymentsEndpoint
->expects('capture')
->with($authorizations[2]->id(), equalTo(new Money($this->amount, $this->currency)))
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
$this->assertEquals(AuthorizedPaymentsProcessor::SUCCESSFUL, $this->testee->process($this->wcOrder));
}
@ -113,7 +114,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase
public function testCaptureFails() {
$this->paymentsEndpoint
->expects('capture')
->with($this->authorizationId)
->with($this->authorizationId, equalTo(new Money($this->amount, $this->currency)))
->andThrow(RuntimeException::class);
$this->assertEquals(AuthorizedPaymentsProcessor::FAILED, $this->testee->process($this->wcOrder));
@ -137,7 +138,7 @@ class AuthorizedPaymentsProcessorTest extends TestCase
$this->paymentsEndpoint
->expects('capture')
->with($this->authorizationId)
->with($this->authorizationId, equalTo(new Money($this->amount, $this->currency)))
->andReturn($this->createCapture(CaptureStatus::COMPLETED));
$this->wcOrder->shouldReceive('payment_complete')->andReturn(true);
@ -172,6 +173,12 @@ class AuthorizedPaymentsProcessorTest extends TestCase
->shouldReceive('get_meta')
->with(PayPalGateway::ORDER_ID_META_KEY)
->andReturn($paypalOrderId);
$wcOrder
->shouldReceive('get_total')
->andReturn($this->amount);
$wcOrder
->shouldReceive('get_currency')
->andReturn($this->currency);
return $wcOrder;
}

View file

@ -8,3 +8,5 @@ require_once ROOT_DIR . '/vendor/autoload.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Payment_Gateway_CC.php';
require_once TESTS_ROOT_DIR . '/stubs/WC_Ajax.php';
Hamcrest\Util::registerGlobalFunctions();

View file

@ -41,10 +41,7 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true );
function init() {
$root_dir = __DIR__;
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . '/wp-admin/includes/plugin.php';
}
if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
if ( ! is_woocommerce_activated() ) {
add_action(
'admin_notices',
function() {
@ -124,10 +121,7 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true );
add_filter(
'plugin_action_links_' . plugin_basename( __FILE__ ),
function( $links ) {
if ( ! function_exists( 'is_plugin_active' ) ) {
require_once ABSPATH . '/wp-admin/includes/plugin.php';
}
if ( ! is_plugin_active( 'woocommerce/woocommerce.php' ) ) {
if ( ! is_woocommerce_activated() ) {
return $links;
}
@ -144,4 +138,13 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true );
}
);
/**
* Check if WooCommerce is active.
*
* @return bool true if WooCommerce is active, otherwise false.
*/
function is_woocommerce_activated(): bool {
return class_exists( 'woocommerce' );
}
} )();