Merge pull request #490 from woocommerce/pcp-307-fee

Add fees to meta and show on the order
This commit is contained in:
Emili Castells 2022-02-23 11:28:17 +01:00 committed by GitHub
commit df98a7d017
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1152 additions and 59 deletions

View file

@ -16,6 +16,8 @@
* 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

@ -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

@ -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

@ -97,6 +97,8 @@ Follow the steps below to connect the plugin to your PayPal account:
* 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

@ -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

@ -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

@ -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']],
];
}
}