Merge branch 'trunk' into PCP-881-compatibility-with-third-party-product-add-ons-plugins

This commit is contained in:
Pedro Silva 2023-09-07 08:46:06 +01:00 committed by GitHub
commit 3a8de5f3de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
83 changed files with 3281 additions and 293 deletions

View file

@ -9,13 +9,18 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -58,6 +63,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
@ -289,6 +296,14 @@ return array(
$container->get( 'api.factory.fraud-processor-response' )
);
},
'api.factory.refund' => static function ( ContainerInterface $container ): RefundFactory {
$amount_factory = $container->get( 'api.factory.amount' );
return new RefundFactory(
$amount_factory,
$container->get( 'api.factory.seller-payable-breakdown' ),
$container->get( 'api.factory.refund_payer' )
);
},
'api.factory.purchase-unit' => static function ( ContainerInterface $container ): PurchaseUnitFactory {
$amount_factory = $container->get( 'api.factory.amount' );
@ -299,6 +314,7 @@ return array(
$payments_factory = $container->get( 'api.factory.payments' );
$prefix = $container->get( 'api.prefix' );
$soft_descriptor = $container->get( 'wcgateway.soft-descriptor' );
$sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new PurchaseUnitFactory(
$amount_factory,
@ -308,7 +324,8 @@ return array(
$shipping_factory,
$payments_factory,
$prefix,
$soft_descriptor
$soft_descriptor,
$sanitizer
);
},
'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory {
@ -351,6 +368,9 @@ return array(
$address_factory = $container->get( 'api.factory.address' );
return new PayerFactory( $address_factory );
},
'api.factory.refund_payer' => static function ( ContainerInterface $container ): RefundPayerFactory {
return new RefundPayerFactory();
},
'api.factory.address' => static function ( ContainerInterface $container ): AddressFactory {
return new AddressFactory();
},
@ -374,7 +394,8 @@ return array(
'api.factory.payments' => static function ( ContainerInterface $container ): PaymentsFactory {
$authorizations_factory = $container->get( 'api.factory.authorization' );
$capture_factory = $container->get( 'api.factory.capture' );
return new PaymentsFactory( $authorizations_factory, $capture_factory );
$refund_factory = $container->get( 'api.factory.refund' );
return new PaymentsFactory( $authorizations_factory, $capture_factory, $refund_factory );
},
'api.factory.authorization' => static function ( ContainerInterface $container ): AuthorizationFactory {
return new AuthorizationFactory();
@ -395,6 +416,12 @@ return array(
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.seller-payable-breakdown' => static function ( ContainerInterface $container ): SellerPayableBreakdownFactory {
return new SellerPayableBreakdownFactory(
$container->get( 'api.factory.money' ),
$container->get( 'api.factory.platform-fee' )
);
},
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
return new FraudProcessorResponseFactory();
},
@ -814,4 +841,19 @@ return array(
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
return new OrderHelper();
},
'api.helper.order-transient' => static function( ContainerInterface $container ): OrderTransient {
$cache = new Cache( 'ppcp-paypal-bearer' );
$purchase_unit_sanitizer = $container->get( 'api.helper.purchase-unit-sanitizer' );
return new OrderTransient( $cache, $purchase_unit_sanitizer );
},
'api.helper.purchase-unit-sanitizer' => SingletonDecorator::make(
static function( ContainerInterface $container ): PurchaseUnitSanitizer {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$behavior = $settings->has( 'subtotal_mismatch_behavior' ) ? $settings->get( 'subtotal_mismatch_behavior' ) : null;
$line_name = $settings->has( 'subtotal_mismatch_line_name' ) ? $settings->get( 'subtotal_mismatch_line_name' ) : null;
return new PurchaseUnitSanitizer( $behavior, $line_name );
}
),
);

View file

@ -9,10 +9,13 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class ApiModule
@ -40,7 +43,6 @@ class ApiModule implements ModuleInterface {
WC()->session->set( 'ppcp_fees', $fees );
}
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
@ -55,6 +57,30 @@ class ApiModule implements ModuleInterface {
return $data;
}
);
add_action(
'woocommerce_paypal_payments_paypal_order_created',
function ( Order $order ) use ( $c ) {
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
if ( $transient instanceof OrderTransient ) {
$transient->on_order_created( $order );
}
},
10,
1
);
add_action(
'woocommerce_paypal_payments_woocommerce_order_created',
function ( WC_Order $wc_order, Order $order ) use ( $c ) {
$transient = $c->has( 'api.helper.order-transient' ) ? $c->get( 'api.helper.order-transient' ) : null;
if ( $transient instanceof OrderTransient ) {
$transient->on_woocommerce_order_created( $wc_order, $order );
}
},
10,
2
);
}
/**

View file

@ -54,8 +54,6 @@ class BillingPlans {
private $plan_factory;
/**
* The logger.
*
* The logger.
*
* @var LoggerInterface

View file

@ -281,6 +281,9 @@ class OrderEndpoint {
throw $error;
}
$order = $this->order_factory->from_paypal_response( $json );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
return $order;
}

View file

@ -112,7 +112,7 @@ class PayUponInvoiceOrderEndpoint {
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array {
return $item->to_array( false );
return $item->to_array( true, false );
},
$items
),
@ -166,8 +166,11 @@ class PayUponInvoiceOrderEndpoint {
throw new PayPalApiException( $json, $status_code );
}
$order = $this->order_factory->from_paypal_response( $json );
return $this->order_factory->from_paypal_response( $json );
do_action( 'woocommerce_paypal_payments_paypal_order_created', $order );
return $order;
}
/**

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WP_Error;
/**
* Class PaymentTokenEndpoint
@ -97,7 +98,7 @@ class PaymentTokenEndpoint {
}
/**
* Returns the payment tokens for a user.
* Returns the payment tokens for the given user id.
*
* @param int $id The user id.
*
@ -118,7 +119,67 @@ class PaymentTokenEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not fetch payment token.', 'woocommerce-paypal-payments' )
__( 'Could not fetch payment token for customer id.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
$error = new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
$tokens = array();
foreach ( $json->payment_tokens as $token_value ) {
$tokens[] = $this->factory->from_paypal_response( $token_value );
}
return $tokens;
}
/**
* Returns the payment tokens for the given guest customer id.
*
* @param string $customer_id The guest customer id.
*
* @return PaymentToken[]
* @throws RuntimeException If the request fails.
*/
public function for_guest( string $customer_id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/vault/payment-tokens/?customer_id=' . $customer_id;
$args = array(
'method' => 'GET',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
$error = new RuntimeException(
__( 'Could not fetch payment token for guest customer id.', 'woocommerce-paypal-payments' )
);
$this->logger->log(
'warning',

View file

@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundCapture;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
@ -196,13 +196,13 @@ class PaymentsEndpoint {
/**
* Refunds a payment.
*
* @param Refund $refund The refund to be processed.
* @param RefundCapture $refund The refund to be processed.
*
* @return string Refund ID.
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function refund( Refund $refund ) : string {
public function refund( RefundCapture $refund ) : string {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/payments/captures/' . $refund->for_capture()->id() . '/refund';
$args = array(

View file

@ -203,7 +203,7 @@ class Item {
*
* @return array
*/
public function to_array() {
public function to_array(): array {
$item = array(
'name' => $this->name(),
'unit_amount' => $this->unit_amount()->to_array(),

View file

@ -28,13 +28,21 @@ class Payments {
*/
private $captures;
/**
* The Refunds.
*
* @var Refund[]
*/
private $refunds;
/**
* Payments constructor.
*
* @param array $authorizations The Authorizations.
* @param array $captures The Captures.
* @param array $refunds The Refunds.
*/
public function __construct( array $authorizations, array $captures ) {
public function __construct( array $authorizations, array $captures, array $refunds = array() ) {
foreach ( $authorizations as $key => $authorization ) {
if ( is_a( $authorization, Authorization::class ) ) {
continue;
@ -47,8 +55,15 @@ class Payments {
}
unset( $captures[ $key ] );
}
foreach ( $refunds as $key => $refund ) {
if ( is_a( $refund, Refund::class ) ) {
continue;
}
unset( $refunds[ $key ] );
}
$this->authorizations = $authorizations;
$this->captures = $captures;
$this->refunds = $refunds;
}
/**
@ -70,6 +85,12 @@ class Payments {
},
$this->captures()
),
'refunds' => array_map(
static function ( Refund $refund ): array {
return $refund->to_array();
},
$this->refunds()
),
);
}
@ -90,4 +111,13 @@ class Payments {
public function captures(): array {
return $this->captures;
}
/**
* Returns the Refunds.
*
* @return Refund[]
**/
public function refunds(): array {
return $this->refunds;
}
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
/**
* Class PurchaseUnit
*/
@ -91,6 +93,13 @@ class PurchaseUnit {
*/
private $contains_physical_goods = false;
/**
* The sanitizer for this purchase unit output.
*
* @var PurchaseUnitSanitizer|null
*/
private $sanitizer;
/**
* PurchaseUnit constructor.
*
@ -220,6 +229,16 @@ class PurchaseUnit {
$this->custom_id = $custom_id;
}
/**
* Sets the sanitizer for this purchase unit output.
*
* @param PurchaseUnitSanitizer|null $sanitizer The sanitizer.
* @return void
*/
public function set_sanitizer( ?PurchaseUnitSanitizer $sanitizer ) {
$this->sanitizer = $sanitizer;
}
/**
* Returns the invoice id.
*
@ -277,11 +296,12 @@ class PurchaseUnit {
/**
* Returns the object as array.
*
* @param bool $ditch_items_when_mismatch Whether ditch items when mismatch or not.
* @param bool $sanitize_output Whether output should be sanitized for PayPal consumption.
* @param bool $allow_ditch_items Whether to allow items to be ditched.
*
* @return array
*/
public function to_array( bool $ditch_items_when_mismatch = true ): array {
public function to_array( bool $sanitize_output = true, bool $allow_ditch_items = true ): array {
$purchase_unit = array(
'reference_id' => $this->reference_id(),
'amount' => $this->amount()->to_array(),
@ -294,17 +314,6 @@ class PurchaseUnit {
),
);
$ditch = $ditch_items_when_mismatch && $this->ditch_items_when_mismatch( $this->amount(), ...$this->items() );
/**
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
*/
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditch, $this );
if ( $ditch ) {
unset( $purchase_unit['items'] );
unset( $purchase_unit['amount']['breakdown'] );
}
if ( $this->payee() ) {
$purchase_unit['payee'] = $this->payee()->to_array();
}
@ -325,101 +334,45 @@ class PurchaseUnit {
if ( $this->soft_descriptor() ) {
$purchase_unit['soft_descriptor'] = $this->soft_descriptor();
}
return $purchase_unit;
$has_ditched_items_breakdown = false;
if ( $sanitize_output && isset( $this->sanitizer ) ) {
$purchase_unit = ( $this->sanitizer->sanitize( $purchase_unit, $allow_ditch_items ) );
$has_ditched_items_breakdown = $this->sanitizer->has_ditched_items_breakdown();
}
return $this->apply_ditch_items_mismatch_filter(
$has_ditched_items_breakdown,
$purchase_unit
);
}
/**
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
* not have this restriction. Therefore the totals of the cart in WooCommerce and the totals
* of the rounded money values of the items, we send to PayPal, can differ. In those cases,
* we can not send the line items.
* Applies the ppcp_ditch_items_breakdown filter.
* If true purchase_unit items and breakdown are ditched from PayPal.
*
* @param Amount $amount The amount.
* @param Item ...$items The items.
* @return bool
* @param bool $ditched_items_breakdown If the breakdown and items were already ditched.
* @param array $purchase_unit The purchase_unit array.
* @return array
*/
private function ditch_items_when_mismatch( Amount $amount, Item ...$items ): bool {
$breakdown = $amount->breakdown();
if ( ! $breakdown ) {
return false;
}
public function apply_ditch_items_mismatch_filter( bool $ditched_items_breakdown, array $purchase_unit ): array {
/**
* The filter can be used to control when the items and totals breakdown are removed from PayPal order info.
*/
$ditch = apply_filters( 'ppcp_ditch_items_breakdown', $ditched_items_breakdown, $this );
$item_total = $breakdown->item_total();
if ( $item_total ) {
$remaining_item_total = array_reduce(
$items,
function ( float $total, Item $item ): float {
return $total - (float) $item->unit_amount()->value_str() * (float) $item->quantity();
},
(float) $item_total->value_str()
);
if ( $ditch ) {
unset( $purchase_unit['items'] );
unset( $purchase_unit['amount']['breakdown'] );
$remaining_item_total = round( $remaining_item_total, 2 );
if ( 0.0 !== $remaining_item_total ) {
return true;
if ( isset( $this->sanitizer ) && ( $ditch !== $ditched_items_breakdown ) ) {
$this->sanitizer->set_last_message(
__( 'Ditch items breakdown filter. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
);
}
}
$tax_total = $breakdown->tax_total();
$items_with_tax = array_filter(
$this->items,
function ( Item $item ): bool {
return null !== $item->tax();
}
);
if ( $tax_total && ! empty( $items_with_tax ) ) {
$remaining_tax_total = array_reduce(
$items,
function ( float $total, Item $item ): float {
$tax = $item->tax();
if ( $tax ) {
$total -= (float) $tax->value_str() * (float) $item->quantity();
}
return $total;
},
(float) $tax_total->value_str()
);
$remaining_tax_total = round( $remaining_tax_total, 2 );
if ( 0.0 !== $remaining_tax_total ) {
return true;
}
}
$shipping = $breakdown->shipping();
$discount = $breakdown->discount();
$shipping_discount = $breakdown->shipping_discount();
$handling = $breakdown->handling();
$insurance = $breakdown->insurance();
$amount_total = 0.0;
if ( $shipping ) {
$amount_total += (float) $shipping->value_str();
}
if ( $item_total ) {
$amount_total += (float) $item_total->value_str();
}
if ( $discount ) {
$amount_total -= (float) $discount->value_str();
}
if ( $tax_total ) {
$amount_total += (float) $tax_total->value_str();
}
if ( $shipping_discount ) {
$amount_total -= (float) $shipping_discount->value_str();
}
if ( $handling ) {
$amount_total += (float) $handling->value_str();
}
if ( $insurance ) {
$amount_total += (float) $insurance->value_str();
}
$amount_str = $amount->value_str();
$amount_total_str = ( new Money( $amount_total, $amount->currency_code() ) )->value_str();
$needs_to_ditch = $amount_str !== $amount_total_str;
return $needs_to_ditch;
return $purchase_unit;
}
}

View file

@ -1,6 +1,8 @@
<?php
/**
* The refund object.
* The refund entity.
*
* @link https://developer.paypal.com/docs/api/orders/v2/#definition-refund
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
@ -15,11 +17,32 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
class Refund {
/**
* The Capture.
* The ID.
*
* @var Capture
* @var string
*/
private $capture;
private $id;
/**
* The status.
*
* @var RefundStatus
*/
private $status;
/**
* The amount.
*
* @var Amount
*/
private $amount;
/**
* The detailed breakdown of the refund activity (fees, ...).
*
* @var SellerPayableBreakdown|null
*/
private $seller_payable_breakdown;
/**
* The invoice id.
@ -29,50 +52,97 @@ class Refund {
private $invoice_id;
/**
* The note to the payer.
* The custom id.
*
* @var string
*/
private $custom_id;
/**
* The acquirer reference number.
*
* @var string
*/
private $acquirer_reference_number;
/**
* The acquirer reference number.
*
* @var string
*/
private $note_to_payer;
/**
* The Amount.
* The payer of the refund.
*
* @var Amount|null
* @var ?RefundPayer
*/
private $amount;
private $payer;
/**
* Refund constructor.
*
* @param Capture $capture The capture where the refund is supposed to be applied at.
* @param string $invoice_id The invoice id.
* @param string $note_to_payer The note to the payer.
* @param Amount|null $amount The Amount.
* @param string $id The ID.
* @param RefundStatus $status The status.
* @param Amount $amount The amount.
* @param string $invoice_id The invoice id.
* @param string $custom_id The custom id.
* @param SellerPayableBreakdown|null $seller_payable_breakdown The detailed breakdown of the refund activity (fees, ...).
* @param string $acquirer_reference_number The acquirer reference number.
* @param string $note_to_payer The note to payer.
* @param RefundPayer|null $payer The payer.
*/
public function __construct(
Capture $capture,
string $id,
RefundStatus $status,
Amount $amount,
string $invoice_id,
string $note_to_payer = '',
Amount $amount = null
string $custom_id,
?SellerPayableBreakdown $seller_payable_breakdown,
string $acquirer_reference_number,
string $note_to_payer,
?RefundPayer $payer
) {
$this->capture = $capture;
$this->invoice_id = $invoice_id;
$this->note_to_payer = $note_to_payer;
$this->amount = $amount;
$this->id = $id;
$this->status = $status;
$this->amount = $amount;
$this->invoice_id = $invoice_id;
$this->custom_id = $custom_id;
$this->seller_payable_breakdown = $seller_payable_breakdown;
$this->acquirer_reference_number = $acquirer_reference_number;
$this->note_to_payer = $note_to_payer;
$this->payer = $payer;
}
/**
* Returns the capture for the refund.
* Returns the ID.
*
* @return Capture
* @return string
*/
public function for_capture() : Capture {
return $this->capture;
public function id() : string {
return $this->id;
}
/**
* Return the invoice id.
* Returns the status.
*
* @return RefundStatus
*/
public function status() : RefundStatus {
return $this->status;
}
/**
* Returns the amount.
*
* @return Amount
*/
public function amount() : Amount {
return $this->amount;
}
/**
* Returns the invoice id.
*
* @return string
*/
@ -81,7 +151,34 @@ class Refund {
}
/**
* Returns the note to the payer.
* Returns the custom id.
*
* @return string
*/
public function custom_id() : string {
return $this->custom_id;
}
/**
* Returns the detailed breakdown of the refund activity (fees, ...).
*
* @return SellerPayableBreakdown|null
*/
public function seller_payable_breakdown() : ?SellerPayableBreakdown {
return $this->seller_payable_breakdown;
}
/**
* The acquirer reference number.
*
* @return string
*/
public function acquirer_reference_number() : string {
return $this->acquirer_reference_number;
}
/**
* The note to payer.
*
* @return string
*/
@ -90,28 +187,38 @@ class Refund {
}
/**
* Returns the Amount.
* Returns the refund payer.
*
* @return Amount|null
* @return RefundPayer|null
*/
public function amount() {
return $this->amount;
public function payer() : ?RefundPayer {
return $this->payer;
}
/**
* Returns the object as array.
* Returns the entity as array.
*
* @return array
*/
public function to_array() : array {
$data = array(
'invoice_id' => $this->invoice_id(),
$data = array(
'id' => $this->id(),
'status' => $this->status()->name(),
'amount' => $this->amount()->to_array(),
'invoice_id' => $this->invoice_id(),
'custom_id' => $this->custom_id(),
'acquirer_reference_number' => $this->acquirer_reference_number(),
'note_to_payer' => (array) $this->note_to_payer(),
);
if ( $this->note_to_payer() ) {
$data['note_to_payer'] = $this->note_to_payer();
$details = $this->status()->details();
if ( $details ) {
$data['status_details'] = array( 'reason' => $details->reason() );
}
if ( $this->amount() ) {
$data['amount'] = $this->amount()->to_array();
if ( $this->seller_payable_breakdown ) {
$data['seller_payable_breakdown'] = $this->seller_payable_breakdown->to_array();
}
if ( $this->payer ) {
$data['payer'] = $this->payer->to_array();
}
return $data;
}

View file

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

View file

@ -0,0 +1,79 @@
<?php
/**
* The refund payer object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundPayer
* The customer who sends the money.
*/
class RefundPayer {
/**
* The email address.
*
* @var string
*/
private $email_address;
/**
* The merchant id.
*
* @var string
*/
private $merchant_id;
/**
* RefundPayer constructor.
*
* @param string $email_address The email.
* @param string $merchant_id The merchant id.
*/
public function __construct(
string $email_address,
string $merchant_id
) {
$this->email_address = $email_address;
$this->merchant_id = $merchant_id;
}
/**
* Returns the email address.
*
* @return string
*/
public function email_address(): string {
return $this->email_address;
}
/**
* Returns the merchant id.
*
* @return string
*/
public function merchant_id(): string {
return $this->merchant_id;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() {
$payer = array(
'email_address' => $this->email_address(),
);
if ( $this->merchant_id ) {
$payer['merchant_id'] = $this->merchant_id();
}
return $payer;
}
}

View file

@ -0,0 +1,77 @@
<?php
/**
* The RefundStatus object.
*
* @see https://developer.paypal.com/docs/api/orders/v2/#definition-refund_status
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundStatus
*/
class RefundStatus {
const COMPLETED = 'COMPLETED';
const CANCELLED = 'CANCELLED';
const FAILED = 'FAILED';
const PENDING = 'PENDING';
/**
* The status.
*
* @var string
*/
private $status;
/**
* The details.
*
* @var RefundStatusDetails|null
*/
private $details;
/**
* RefundStatus constructor.
*
* @param string $status The status.
* @param RefundStatusDetails|null $details The details.
*/
public function __construct( string $status, ?RefundStatusDetails $details = null ) {
$this->status = $status;
$this->details = $details;
}
/**
* Compares the current status with a given one.
*
* @param string $status The status to compare with.
*
* @return bool
*/
public function is( string $status ): bool {
return $this->status === $status;
}
/**
* Returns the status.
*
* @return string
*/
public function name(): string {
return $this->status;
}
/**
* Returns the details.
*
* @return RefundStatusDetails|null
*/
public function details(): ?RefundStatusDetails {
return $this->details;
}
}

View file

@ -0,0 +1,71 @@
<?php
/**
* The RefundStatusDetails object.
*
* @see https://developer.paypal.com/docs/api/payments/v2/#definition-refund_status_details
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class RefundStatusDetails
*/
class RefundStatusDetails {
const ECHECK = 'ECHECK';
/**
* The reason.
*
* @var string
*/
private $reason;
/**
* RefundStatusDetails constructor.
*
* @param string $reason The reason explaining refund status.
*/
public function __construct( string $reason ) {
$this->reason = $reason;
}
/**
* Compares the current reason with a given one.
*
* @param string $reason The reason to compare with.
*
* @return bool
*/
public function is( string $reason ): bool {
return $this->reason === $reason;
}
/**
* Returns the reason explaining refund status.
* One of RefundStatusDetails constants.
*
* @return string
*/
public function reason(): string {
return $this->reason;
}
/**
* Returns the human-readable reason text explaining refund status.
*
* @return string
*/
public function text(): string {
switch ( $this->reason ) {
case self::ECHECK:
return __( 'The payer paid by an eCheck that has not yet cleared.', 'woocommerce-paypal-payments' );
default:
return $this->reason;
}
}
}

View file

@ -0,0 +1,202 @@
<?php
/**
* The info about fees and amount that will be paid by the seller.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class SellerPayableBreakdown
*/
class SellerPayableBreakdown {
/**
* The amount for this refunded payment in the currency of the transaction.
*
* @var Money|null
*/
private $gross_amount;
/**
* The applicable fee for this refunded payment in the currency of the transaction.
*
* @var Money|null
*/
private $paypal_fee;
/**
* The applicable fee for this captured payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @var Money|null
*/
private $paypal_fee_in_receivable_currency;
/**
* The net amount that the payee receives for this refunded payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @var Money|null
*/
private $net_amount;
/**
* The net amount for this refunded payment in the receivable currency.
*
* @var Money|null
*/
private $net_amount_in_receivable_currency;
/**
* The total amount for this refund.
*
* @var Money|null
*/
private $total_refunded_amount;
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*
* @var PlatformFee[]
*/
private $platform_fees;
/**
* SellerPayableBreakdown constructor.
*
* @param Money|null $gross_amount The amount for this refunded payment in the currency of the transaction.
* @param Money|null $paypal_fee The applicable fee for this refunded payment in the currency of the transaction.
* @param Money|null $paypal_fee_in_receivable_currency The applicable fee for this refunded payment in the receivable currency.
* @param Money|null $net_amount The net amount that the payee receives for this refunded payment in their PayPal account.
* @param Money|null $net_amount_in_receivable_currency The net amount for this refunded payment in the receivable currency.
* @param Money|null $total_refunded_amount The total amount for this refund.
* @param PlatformFee[] $platform_fees An array of platform or partner fees, commissions, or brokerage fees that associated with the captured payment.
*/
public function __construct(
?Money $gross_amount,
?Money $paypal_fee,
?Money $paypal_fee_in_receivable_currency,
?Money $net_amount,
?Money $net_amount_in_receivable_currency,
?Money $total_refunded_amount,
array $platform_fees
) {
$this->gross_amount = $gross_amount;
$this->paypal_fee = $paypal_fee;
$this->paypal_fee_in_receivable_currency = $paypal_fee_in_receivable_currency;
$this->net_amount = $net_amount;
$this->net_amount_in_receivable_currency = $net_amount_in_receivable_currency;
$this->total_refunded_amount = $total_refunded_amount;
$this->platform_fees = $platform_fees;
}
/**
* The amount for this refunded payment in the currency of the transaction.
*
* @return Money|null
*/
public function gross_amount(): ?Money {
return $this->gross_amount;
}
/**
* The applicable fee for this refunded payment in the currency of the transaction.
*
* @return Money|null
*/
public function paypal_fee(): ?Money {
return $this->paypal_fee;
}
/**
* The applicable fee for this refunded payment in the receivable currency.
*
* Present only in cases the fee is charged in the receivable currency.
*
* @return Money|null
*/
public function paypal_fee_in_receivable_currency(): ?Money {
return $this->paypal_fee_in_receivable_currency;
}
/**
* The net amount that the payee receives for this refunded payment in their PayPal account.
*
* Computed as gross_amount minus the paypal_fee minus the platform_fees.
*
* @return Money|null
*/
public function net_amount(): ?Money {
return $this->net_amount;
}
/**
* The net amount for this refunded payment in the receivable currency.
*
* @return Money|null
*/
public function net_amount_in_receivable_currency(): ?Money {
return $this->net_amount_in_receivable_currency;
}
/**
* The total amount for this refund.
*
* @return Money|null
*/
public function total_refunded_amount(): ?Money {
return $this->total_refunded_amount;
}
/**
* An array of platform or partner fees, commissions, or brokerage fees that associated with the refunded payment.
*
* @return PlatformFee[]
*/
public function platform_fees(): array {
return $this->platform_fees;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
$data = array();
if ( $this->gross_amount ) {
$data['gross_amount'] = $this->gross_amount->to_array();
}
if ( $this->paypal_fee ) {
$data['paypal_fee'] = $this->paypal_fee->to_array();
}
if ( $this->paypal_fee_in_receivable_currency ) {
$data['paypal_fee_in_receivable_currency'] = $this->paypal_fee_in_receivable_currency->to_array();
}
if ( $this->net_amount ) {
$data['net_amount'] = $this->net_amount->to_array();
}
if ( $this->net_amount_in_receivable_currency ) {
$data['net_amount_in_receivable_currency'] = $this->net_amount_in_receivable_currency->to_array();
}
if ( $this->total_refunded_amount ) {
$data['total_refunded_amount'] = $this->total_refunded_amount->to_array();
}
if ( $this->platform_fees ) {
$data['platform_fees'] = array_map(
function ( PlatformFee $fee ) {
return $fee->to_array();
},
$this->platform_fees
);
}
return $data;
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Authorization;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payments;
/**
@ -32,19 +33,29 @@ class PaymentsFactory {
*/
private $capture_factory;
/**
* The Refund factory.
*
* @var RefundFactory
*/
private $refund_factory;
/**
* PaymentsFactory constructor.
*
* @param AuthorizationFactory $authorization_factory The Authorization factory.
* @param CaptureFactory $capture_factory The Capture factory.
* @param RefundFactory $refund_factory The Refund factory.
*/
public function __construct(
AuthorizationFactory $authorization_factory,
CaptureFactory $capture_factory
CaptureFactory $capture_factory,
RefundFactory $refund_factory
) {
$this->authorization_factory = $authorization_factory;
$this->capture_factory = $capture_factory;
$this->refund_factory = $refund_factory;
}
/**
@ -62,12 +73,18 @@ class PaymentsFactory {
isset( $data->authorizations ) ? $data->authorizations : array()
);
$captures = array_map(
function ( \stdClass $authorization ): Capture {
return $this->capture_factory->from_paypal_response( $authorization );
function ( \stdClass $capture ): Capture {
return $this->capture_factory->from_paypal_response( $capture );
},
isset( $data->captures ) ? $data->captures : array()
);
$payments = new Payments( $authorizations, $captures );
$refunds = array_map(
function ( \stdClass $refund ): Refund {
return $this->refund_factory->from_paypal_response( $refund );
},
isset( $data->refunds ) ? $data->refunds : array()
);
$payments = new Payments( $authorizations, $captures, $refunds );
return $payments;
}
}

View file

@ -13,6 +13,7 @@ use WC_Session_Handler;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
@ -77,17 +78,25 @@ class PurchaseUnitFactory {
*/
private $soft_descriptor;
/**
* The sanitizer for purchase unit output data.
*
* @var PurchaseUnitSanitizer|null
*/
private $sanitizer;
/**
* PurchaseUnitFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param PayeeRepository $payee_repository The Payee repository.
* @param PayeeFactory $payee_factory The Payee factory.
* @param ItemFactory $item_factory The item factory.
* @param ShippingFactory $shipping_factory The shipping factory.
* @param PaymentsFactory $payments_factory The payments factory.
* @param string $prefix The prefix.
* @param string $soft_descriptor The soft descriptor.
* @param AmountFactory $amount_factory The amount factory.
* @param PayeeRepository $payee_repository The Payee repository.
* @param PayeeFactory $payee_factory The Payee factory.
* @param ItemFactory $item_factory The item factory.
* @param ShippingFactory $shipping_factory The shipping factory.
* @param PaymentsFactory $payments_factory The payments factory.
* @param string $prefix The prefix.
* @param string $soft_descriptor The soft descriptor.
* @param ?PurchaseUnitSanitizer $sanitizer The purchase unit to_array sanitizer.
*/
public function __construct(
AmountFactory $amount_factory,
@ -97,7 +106,8 @@ class PurchaseUnitFactory {
ShippingFactory $shipping_factory,
PaymentsFactory $payments_factory,
string $prefix = 'WC-',
string $soft_descriptor = ''
string $soft_descriptor = '',
PurchaseUnitSanitizer $sanitizer = null
) {
$this->amount_factory = $amount_factory;
@ -108,6 +118,7 @@ class PurchaseUnitFactory {
$this->payments_factory = $payments_factory;
$this->prefix = $prefix;
$this->soft_descriptor = $soft_descriptor;
$this->sanitizer = $sanitizer;
}
/**
@ -151,6 +162,9 @@ class PurchaseUnitFactory {
$invoice_id,
$soft_descriptor
);
$this->init_purchase_unit( $purchase_unit );
/**
* Returns PurchaseUnit for the WC order.
*/
@ -221,6 +235,8 @@ class PurchaseUnitFactory {
$soft_descriptor
);
$this->init_purchase_unit( $purchase_unit );
return $purchase_unit;
}
@ -283,6 +299,9 @@ class PurchaseUnitFactory {
$soft_descriptor,
$payments
);
$this->init_purchase_unit( $purchase_unit );
return $purchase_unit;
}
@ -313,4 +332,16 @@ class PurchaseUnitFactory {
$countries = array( 'AE', 'AF', 'AG', 'AI', 'AL', 'AN', 'AO', 'AW', 'BB', 'BF', 'BH', 'BI', 'BJ', 'BM', 'BO', 'BS', 'BT', 'BW', 'BZ', 'CD', 'CF', 'CG', 'CI', 'CK', 'CL', 'CM', 'CO', 'CR', 'CV', 'DJ', 'DM', 'DO', 'EC', 'EG', 'ER', 'ET', 'FJ', 'FK', 'GA', 'GD', 'GH', 'GI', 'GM', 'GN', 'GQ', 'GT', 'GW', 'GY', 'HK', 'HN', 'HT', 'IE', 'IQ', 'IR', 'JM', 'JO', 'KE', 'KH', 'KI', 'KM', 'KN', 'KP', 'KW', 'KY', 'LA', 'LB', 'LC', 'LK', 'LR', 'LS', 'LY', 'ML', 'MM', 'MO', 'MR', 'MS', 'MT', 'MU', 'MW', 'MZ', 'NA', 'NE', 'NG', 'NI', 'NP', 'NR', 'NU', 'OM', 'PA', 'PE', 'PF', 'PY', 'QA', 'RW', 'SA', 'SB', 'SC', 'SD', 'SL', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV', 'SY', 'TC', 'TD', 'TG', 'TL', 'TO', 'TT', 'TV', 'TZ', 'UG', 'UY', 'VC', 'VE', 'VG', 'VN', 'VU', 'WS', 'XA', 'XB', 'XC', 'XE', 'XL', 'XM', 'XN', 'XS', 'YE', 'ZM', 'ZW' );
return in_array( $country_code, $countries, true );
}
/**
* Initializes a purchase unit object.
*
* @param PurchaseUnit $purchase_unit The purchase unit.
* @return void
*/
private function init_purchase_unit( PurchaseUnit $purchase_unit ): void {
if ( $this->sanitizer instanceof PurchaseUnitSanitizer ) {
$purchase_unit->set_sanitizer( $this->sanitizer );
}
}
}

View file

@ -0,0 +1,91 @@
<?php
/**
* The refund factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails;
/**
* Class RefundFactory
*/
class RefundFactory {
/**
* The Amount factory.
*
* @var AmountFactory
*/
private $amount_factory;
/**
* The SellerPayableBreakdownFactory factory.
*
* @var SellerPayableBreakdownFactory
*/
private $seller_payable_breakdown_factory;
/**
* The RefundPayerFactory factory.
*
* @var RefundPayerFactory
*/
private $refund_payer_factory;
/**
* RefundFactory constructor.
*
* @param AmountFactory $amount_factory The amount factory.
* @param SellerPayableBreakdownFactory $seller_payable_breakdown_factory The payable breakdown factory.
* @param RefundPayerFactory $refund_payer_factory The payer breakdown factory.
*/
public function __construct(
AmountFactory $amount_factory,
SellerPayableBreakdownFactory $seller_payable_breakdown_factory,
RefundPayerFactory $refund_payer_factory
) {
$this->amount_factory = $amount_factory;
$this->seller_payable_breakdown_factory = $seller_payable_breakdown_factory;
$this->refund_payer_factory = $refund_payer_factory;
}
/**
* Returns the refund object based off the PayPal response.
*
* @param \stdClass $data The PayPal response.
*
* @return Refund
*/
public function from_paypal_response( \stdClass $data ) : Refund {
$reason = $data->status_details->reason ?? null;
$seller_payable_breakdown = isset( $data->seller_payable_breakdown ) ?
$this->seller_payable_breakdown_factory->from_paypal_response( $data->seller_payable_breakdown )
: null;
$payer = isset( $data->payer ) ?
$this->refund_payer_factory->from_paypal_response( $data->payer )
: null;
return new Refund(
(string) $data->id,
new RefundStatus(
(string) $data->status,
$reason ? new RefundStatusDetails( $reason ) : null
),
$this->amount_factory->from_paypal_response( $data->amount ),
(string) ( $data->invoice_id ?? '' ),
(string) ( $data->custom_id ?? '' ),
$seller_payable_breakdown,
(string) ( $data->acquirer_reference_number ?? '' ),
(string) ( $data->note_to_payer ?? '' ),
$payer
);
}
}

View file

@ -0,0 +1,39 @@
<?php
/**
* The RefundPayerFactory factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerName;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PayerTaxInfo;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Phone;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PhoneWithType;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundPayer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class RefundPayerFactory
*/
class RefundPayerFactory {
/**
* Returns a Refund Payer object based off a PayPal Response.
*
* @param \stdClass $data The JSON object.
*
* @return RefundPayer
*/
public function from_paypal_response( \stdClass $data ): RefundPayer {
return new RefundPayer(
isset( $data->email_address ) ? $data->email_address : '',
isset( $data->merchant_id ) ? $data->merchant_id : ''
);
}
}

View file

@ -0,0 +1,81 @@
<?php
/**
* The SellerPayableBreakdownFactory Factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PlatformFee;
use WooCommerce\PayPalCommerce\ApiClient\Entity\SellerPayableBreakdown;
/**
* Class SellerPayableBreakdownFactory
*/
class SellerPayableBreakdownFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* The PlatformFee factory.
*
* @var PlatformFeeFactory
*/
private $platform_fee_factory;
/**
* SellerPayableBreakdownFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
* @param PlatformFeeFactory $platform_fee_factory The PlatformFee factory.
*/
public function __construct(
MoneyFactory $money_factory,
PlatformFeeFactory $platform_fee_factory
) {
$this->money_factory = $money_factory;
$this->platform_fee_factory = $platform_fee_factory;
}
/**
* Returns a SellerPayableBreakdownFactory object based off a PayPal Response.
*
* @param stdClass $data The JSON object.
*
* @return SellerPayableBreakdown
*/
public function from_paypal_response( stdClass $data ): SellerPayableBreakdown {
$gross_amount = ( isset( $data->gross_amount ) ) ? $this->money_factory->from_paypal_response( $data->gross_amount ) : null;
$paypal_fee = ( isset( $data->paypal_fee ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee ) : null;
$paypal_fee_in_receivable_currency = ( isset( $data->paypal_fee_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->paypal_fee_in_receivable_currency ) : null;
$net_amount = ( isset( $data->net_amount ) ) ? $this->money_factory->from_paypal_response( $data->net_amount ) : null;
$net_amount_in_receivable_currency = ( isset( $data->net_amount_in_receivable_currency ) ) ? $this->money_factory->from_paypal_response( $data->net_amount_in_receivable_currency ) : null;
$total_refunded_amount = ( isset( $data->total_refunded_amount ) ) ? $this->money_factory->from_paypal_response( $data->total_refunded_amount ) : null;
$platform_fees = ( isset( $data->platform_fees ) ) ? array_map(
function ( stdClass $fee_data ): PlatformFee {
return $this->platform_fee_factory->from_paypal_response( $fee_data );
},
$data->platform_fees
) : array();
return new SellerPayableBreakdown(
$gross_amount,
$paypal_fee,
$paypal_fee_in_receivable_currency,
$net_amount,
$net_amount_in_receivable_currency,
$total_refunded_amount,
$platform_fees
);
}
}

View file

@ -33,4 +33,16 @@ class MoneyFormatter {
? (string) round( $value, 0 )
: number_format( $value, 2, '.', '' );
}
/**
* Returns the minimum amount a currency can be incremented or decremented.
*
* @param string $currency The 3-letter currency code.
* @return float
*/
public function minimum_increment( string $currency ): float {
return (float) in_array( $currency, $this->currencies_without_decimals, true )
? 1.00
: 0.01;
}
}

View file

@ -0,0 +1,160 @@
<?php
/**
* PayPal order transient helper.
*
* This class is used to pass transient data between the PayPal order and the WooCommerce order.
* These two orders can be created on different requests and at different times so this transient
* data must be persisted between requests.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class OrderHelper
*/
class OrderTransient {
const CACHE_KEY = 'order_transient';
const CACHE_TIMEOUT = DAY_IN_SECONDS; // If necessary we can increase this.
/**
* The Cache.
*
* @var Cache
*/
private $cache;
/**
* The purchase unit sanitizer.
*
* @var PurchaseUnitSanitizer
*/
private $purchase_unit_sanitizer;
/**
* OrderTransient constructor.
*
* @param Cache $cache The Cache.
* @param PurchaseUnitSanitizer $purchase_unit_sanitizer The purchase unit sanitizer.
*/
public function __construct( Cache $cache, PurchaseUnitSanitizer $purchase_unit_sanitizer ) {
$this->cache = $cache;
$this->purchase_unit_sanitizer = $purchase_unit_sanitizer;
}
/**
* Processes the created PayPal order.
*
* @param Order $order The PayPal order.
* @return void
*/
public function on_order_created( Order $order ): void {
$message = $this->purchase_unit_sanitizer->get_last_message();
$this->add_order_note( $order, $message );
}
/**
* Processes the created WooCommerce order.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param Order $order The PayPal order.
* @return void
*/
public function on_woocommerce_order_created( WC_Order $wc_order, Order $order ): void {
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$this->apply_order_notes( $order, $wc_order );
$this->cache->delete( $cache_key );
}
/**
* Adds an order note associated with a PayPal order.
* It can be added to a WooCommerce order associated with this PayPal order in the future.
*
* @param Order $order The PayPal order.
* @param string $message The message to be added to order notes.
* @return void
*/
private function add_order_note( Order $order, string $message ): void {
if ( ! $message ) {
return;
}
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$transient = $this->cache->get( $cache_key );
if ( ! is_array( $transient ) ) {
$transient = array();
}
if ( ! is_array( $transient['notes'] ) ) {
$transient['notes'] = array();
}
$transient['notes'][] = $message;
$this->cache->set( $cache_key, $transient, self::CACHE_TIMEOUT );
}
/**
* Adds an order note associated with a PayPal order.
* It can be added to a WooCommerce order associated with this PayPal order in the future.
*
* @param Order $order The PayPal order.
* @param WC_Order $wc_order The WooCommerce order.
* @return void
*/
private function apply_order_notes( Order $order, WC_Order $wc_order ): void {
$cache_key = $this->cache_key( $order );
if ( ! $cache_key ) {
return;
}
$transient = $this->cache->get( $cache_key );
if ( ! is_array( $transient ) ) {
return;
}
if ( ! is_array( $transient['notes'] ) ) {
return;
}
foreach ( $transient['notes'] as $note ) {
if ( ! is_string( $note ) ) {
continue;
}
$wc_order->add_order_note( $note );
}
}
/**
* Build cache key.
*
* @param Order $order The PayPal order.
* @return string|null
*/
private function cache_key( Order $order ): ?string {
if ( ! $order->id() ) {
return null;
}
return implode( '_', array( self::CACHE_KEY . $order->id() ) );
}
}

View file

@ -0,0 +1,368 @@
<?php
/**
* Class PurchaseUnitSanitizer.
*
* Sanitizes a purchase_unit array to be consumed by PayPal.
*
* All money values send to PayPal can only have 2 decimal points. WooCommerce internally does
* not have this restriction. Therefore, the totals of the cart in WooCommerce and the totals
* of the rounded money values of the items, we send to PayPal, can differ. In those case we either:
* - Add an extra line with roundings.
* - Don't send the line items.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
/**
* Class PurchaseUnitSanitizer
*/
class PurchaseUnitSanitizer {
const MODE_DITCH = 'ditch';
const MODE_EXTRA_LINE = 'extra_line';
const VALID_MODES = array(
self::MODE_DITCH,
self::MODE_EXTRA_LINE,
);
const EXTRA_LINE_NAME = 'Subtotal mismatch';
/**
* The purchase unit data
*
* @var array
*/
private $purchase_unit = array();
/**
* Whether to allow items to be ditched.
*
* @var bool
*/
private $allow_ditch_items = true;
/**
* The working mode
*
* @var string
*/
private $mode;
/**
* The name for the extra line
*
* @var string
*/
private $extra_line_name;
/**
* The last message. To be added to order notes.
*
* @var string
*/
private $last_message = '';
/**
* If the items and breakdown has been ditched.
*
* @var bool
*/
private $has_ditched_items_breakdown = false;
/**
* PurchaseUnitSanitizer constructor.
*
* @param string|null $mode The mismatch handling mode, ditch or extra_line.
* @param string|null $extra_line_name The name of the extra line.
*/
public function __construct( string $mode = null, string $extra_line_name = null ) {
if ( ! in_array( $mode, self::VALID_MODES, true ) ) {
$mode = self::MODE_DITCH;
}
if ( ! $extra_line_name ) {
$extra_line_name = self::EXTRA_LINE_NAME;
}
$this->mode = $mode;
$this->extra_line_name = $extra_line_name;
}
/**
* The purchase_unit amount.
*
* @return array
*/
private function amount(): array {
return $this->purchase_unit['amount'] ?? array();
}
/**
* The purchase_unit currency code.
*
* @return string
*/
private function currency_code(): string {
return (string) ( $this->amount()['currency_code'] ?? '' );
}
/**
* The purchase_unit breakdown.
*
* @return array
*/
private function breakdown(): array {
return $this->amount()['breakdown'] ?? array();
}
/**
* The purchase_unit breakdown.
*
* @param string $key The breakdown element to get the value from.
* @return float
*/
private function breakdown_value( string $key ): float {
if ( ! isset( $this->breakdown()[ $key ] ) ) {
return 0.0;
}
return (float) ( $this->breakdown()[ $key ]['value'] ?? 0.0 );
}
/**
* The purchase_unit items array.
*
* @return array
*/
private function items(): array {
return $this->purchase_unit['items'] ?? array();
}
/**
* The sanitizes the purchase_unit array.
*
* @param array $purchase_unit The purchase_unit array that should be sanitized.
* @param bool $allow_ditch_items Whether to allow items to be ditched.
* @return array
*/
public function sanitize( array $purchase_unit, bool $allow_ditch_items = true ): array {
$this->purchase_unit = $purchase_unit;
$this->allow_ditch_items = $allow_ditch_items;
$this->has_ditched_items_breakdown = false;
$this->sanitize_item_amount_mismatch();
$this->sanitize_item_tax_mismatch();
$this->sanitize_breakdown_mismatch();
return $this->purchase_unit;
}
/**
* The sanitizes the purchase_unit items amount.
*
* @return void
*/
private function sanitize_item_amount_mismatch(): void {
$item_mismatch = $this->calculate_item_mismatch();
if ( $this->mode === self::MODE_EXTRA_LINE ) {
if ( $item_mismatch < 0 ) {
// Do floors on item amounts so item_mismatch is a positive value.
foreach ( $this->purchase_unit['items'] as $index => $item ) {
// Get a more intelligent adjustment mechanism.
$increment = ( new MoneyFormatter() )->minimum_increment( $item['unit_amount']['currency_code'] );
$this->purchase_unit['items'][ $index ]['unit_amount'] = ( new Money(
( (float) $item['unit_amount']['value'] ) - $increment,
$item['unit_amount']['currency_code']
) )->to_array();
}
}
$item_mismatch = $this->calculate_item_mismatch();
if ( $item_mismatch > 0 ) {
// Add extra line item with roundings.
$line_name = $this->extra_line_name;
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
$this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array();
$this->set_last_message(
__( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' )
);
}
$item_mismatch = $this->calculate_item_mismatch();
}
if ( $item_mismatch !== 0.0 ) {
// Ditch items.
if ( $this->allow_ditch_items && isset( $this->purchase_unit['items'] ) ) {
unset( $this->purchase_unit['items'] );
$this->set_last_message(
__( 'Item amount mismatch. Items ditched.', 'woocommerce-paypal-payments' )
);
}
}
}
/**
* The sanitizes the purchase_unit items tax.
*
* @return void
*/
private function sanitize_item_tax_mismatch(): void {
$tax_mismatch = $this->calculate_tax_mismatch();
if ( $this->allow_ditch_items && $tax_mismatch !== 0.0 ) {
// Unset tax in items.
foreach ( $this->purchase_unit['items'] as $index => $item ) {
if ( isset( $this->purchase_unit['items'][ $index ]['tax'] ) ) {
unset( $this->purchase_unit['items'][ $index ]['tax'] );
}
if ( isset( $this->purchase_unit['items'][ $index ]['tax_rate'] ) ) {
unset( $this->purchase_unit['items'][ $index ]['tax_rate'] );
}
}
}
}
/**
* The sanitizes the purchase_unit breakdown.
*
* @return void
*/
private function sanitize_breakdown_mismatch(): void {
$breakdown_mismatch = $this->calculate_breakdown_mismatch();
if ( $this->allow_ditch_items && $breakdown_mismatch !== 0.0 ) {
// Ditch breakdowns and items.
if ( isset( $this->purchase_unit['items'] ) ) {
unset( $this->purchase_unit['items'] );
}
if ( isset( $this->purchase_unit['amount']['breakdown'] ) ) {
unset( $this->purchase_unit['amount']['breakdown'] );
}
$this->has_ditched_items_breakdown = true;
$this->set_last_message(
__( 'Breakdown mismatch. Items and breakdown ditched.', 'woocommerce-paypal-payments' )
);
}
}
/**
* The calculates amount mismatch of items sums with breakdown.
*
* @return float
*/
private function calculate_item_mismatch(): float {
$item_total = $this->breakdown_value( 'item_total' );
if ( ! $item_total ) {
return 0;
}
$remaining_item_total = array_reduce(
$this->items(),
function ( float $total, array $item ): float {
return $total - (float) $item['unit_amount']['value'] * (float) $item['quantity'];
},
$item_total
);
return round( $remaining_item_total, 2 );
}
/**
* The calculates tax mismatch of items sums with breakdown.
*
* @return float
*/
private function calculate_tax_mismatch(): float {
$tax_total = $this->breakdown_value( 'tax_total' );
$items_with_tax = array_filter(
$this->items(),
function ( array $item ): bool {
return isset( $item['tax'] );
}
);
if ( ! $tax_total || empty( $items_with_tax ) ) {
return 0;
}
$remaining_tax_total = array_reduce(
$this->items(),
function ( float $total, array $item ): float {
$tax = $item['tax'] ?? false;
if ( $tax ) {
$total -= (float) $tax['value'] * (float) $item['quantity'];
}
return $total;
},
$tax_total
);
return round( $remaining_tax_total, 2 );
}
/**
* The calculates mismatch of breakdown sums with total amount.
*
* @return float
*/
private function calculate_breakdown_mismatch(): float {
$breakdown = $this->breakdown();
if ( ! $breakdown ) {
return 0;
}
$amount_total = 0.0;
$amount_total += $this->breakdown_value( 'item_total' );
$amount_total += $this->breakdown_value( 'tax_total' );
$amount_total += $this->breakdown_value( 'shipping' );
$amount_total -= $this->breakdown_value( 'discount' );
$amount_total -= $this->breakdown_value( 'shipping_discount' );
$amount_total += $this->breakdown_value( 'handling' );
$amount_total += $this->breakdown_value( 'insurance' );
$amount_str = $this->amount()['value'] ?? 0;
$amount_total_str = ( new Money( $amount_total, $this->currency_code() ) )->value_str();
return $amount_str - $amount_total_str;
}
/**
* Indicates if the items and breakdown were ditched.
*
* @return bool
*/
public function has_ditched_items_breakdown(): bool {
return $this->has_ditched_items_breakdown;
}
/**
* Returns the last sanitization message.
*
* @return string
*/
public function get_last_message(): string {
return $this->last_message;
}
/**
* Set the last sanitization message.
*
* @param string $message The message.
*/
public function set_last_message( string $message ): void {
$this->last_message = $message;
}
}