Merge branch 'trunk' into PCP-1967-paylater

This commit is contained in:
Alex P 2023-09-08 11:15:50 +03:00
commit ec0b4b8c65
No known key found for this signature in database
GPG key ID: 54487A734A204D71
86 changed files with 5079 additions and 1421 deletions

View file

@ -50,6 +50,27 @@ namespace Vendidero\Germanized\Shipments {
public function add_note( $note, $added_by_user = false ) {
}
/**
* Return an array of items within this shipment.
*
* @return ShipmentItem[]
*/
public function get_items() {
}
}
class ShipmentItem extends WC_Data {
/**
* Get order ID this meta belongs to.
*
* @param string $context What the value is for. Valid values are 'view' and 'edit'.
* @return int
*/
public function get_order_item_id( $context = 'view' ) {
}
}
}

View file

@ -29,6 +29,7 @@
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\": "src",
"WooCommerce\\PayPalCommerce\\Common\\": "lib/common/",
"WooCommerce\\PayPalCommerce\\Vendor\\": "lib/packages/"
},
"files": [

101
composer.lock generated
View file

@ -521,16 +521,16 @@
},
{
"name": "wikimedia/composer-merge-plugin",
"version": "v2.0.1",
"version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/wikimedia/composer-merge-plugin.git",
"reference": "8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912"
"reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912",
"reference": "8ca2ed8ab97c8ebce6b39d9943e9909bb4f18912",
"url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/a03d426c8e9fb2c9c569d9deeb31a083292788bc",
"reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc",
"shasum": ""
},
"require": {
@ -539,9 +539,12 @@
},
"require-dev": {
"composer/composer": "^1.1||^2.0",
"php-parallel-lint/php-parallel-lint": "~1.1.0",
"ext-json": "*",
"mediawiki/mediawiki-phan-config": "0.11.1",
"php-parallel-lint/php-parallel-lint": "~1.3.1",
"phpspec/prophecy": "~1.15.0",
"phpunit/phpunit": "^8.5||^9.0",
"squizlabs/php_codesniffer": "~3.5.4"
"squizlabs/php_codesniffer": "~3.7.1"
},
"type": "composer-plugin",
"extra": {
@ -568,9 +571,9 @@
"description": "Composer plugin to merge multiple composer.json files",
"support": {
"issues": "https://github.com/wikimedia/composer-merge-plugin/issues",
"source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.0.1"
"source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.1.0"
},
"time": "2021-02-24T05:28:06+00:00"
"time": "2023-04-15T19:07:00+00:00"
},
{
"name": "wp-oop/wordpress-interface",
@ -1842,16 +1845,16 @@
},
{
"name": "netresearch/jsonmapper",
"version": "v4.1.0",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/cweiske/jsonmapper.git",
"reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f"
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/cfa81ea1d35294d64adb9c68aa4cb9e92400e53f",
"reference": "cfa81ea1d35294d64adb9c68aa4cb9e92400e53f",
"url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/f60565f8c0566a31acf06884cdaa591867ecc956",
"reference": "f60565f8c0566a31acf06884cdaa591867ecc956",
"shasum": ""
},
"require": {
@ -1887,22 +1890,22 @@
"support": {
"email": "cweiske@cweiske.de",
"issues": "https://github.com/cweiske/jsonmapper/issues",
"source": "https://github.com/cweiske/jsonmapper/tree/v4.1.0"
"source": "https://github.com/cweiske/jsonmapper/tree/v4.2.0"
},
"time": "2022-12-08T20:46:14+00:00"
"time": "2023-04-09T17:37:40+00:00"
},
{
"name": "nikic/php-parser",
"version": "v4.15.4",
"version": "v4.16.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290"
"reference": "19526a33fb561ef417e822e85f08a00db4059c17"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
"reference": "6bb5176bc4af8bcb7d926f88718db9b96a2d4290",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/19526a33fb561ef417e822e85f08a00db4059c17",
"reference": "19526a33fb561ef417e822e85f08a00db4059c17",
"shasum": ""
},
"require": {
@ -1943,9 +1946,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v4.15.4"
"source": "https://github.com/nikic/PHP-Parser/tree/v4.16.0"
},
"time": "2023-03-05T19:49:14+00:00"
"time": "2023-06-25T14:52:30+00:00"
},
{
"name": "openlss/lib-array2xml",
@ -2157,27 +2160,25 @@
},
{
"name": "php-stubs/wordpress-stubs",
"version": "v5.9.5",
"version": "v5.9.6",
"source": {
"type": "git",
"url": "https://github.com/php-stubs/wordpress-stubs.git",
"reference": "13ecf204a7e6d215a7c0d23e2aa27940fe617717"
"reference": "6a18d938d0aef39d091505a4a35b025fb6c10098"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/13ecf204a7e6d215a7c0d23e2aa27940fe617717",
"reference": "13ecf204a7e6d215a7c0d23e2aa27940fe617717",
"url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/6a18d938d0aef39d091505a4a35b025fb6c10098",
"reference": "6a18d938d0aef39d091505a4a35b025fb6c10098",
"shasum": ""
},
"replace": {
"giacocorsiglia/wordpress-stubs": "*"
},
"require-dev": {
"nikic/php-parser": "< 4.12.0",
"php": "~7.3 || ~8.0",
"php-stubs/generator": "^0.8.1",
"php-stubs/generator": "^0.8.3",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpstan": "^1.2"
"phpstan/phpstan": "^1.10.12",
"phpunit/phpunit": "^9.5"
},
"suggest": {
"paragonie/sodium_compat": "Pure PHP implementation of libsodium",
@ -2198,9 +2199,9 @@
],
"support": {
"issues": "https://github.com/php-stubs/wordpress-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.5"
"source": "https://github.com/php-stubs/wordpress-stubs/tree/v5.9.6"
},
"time": "2022-11-09T05:32:14+00:00"
"time": "2023-05-18T04:34:27+00:00"
},
{
"name": "phpcompatibility/php-compatibility",
@ -3136,16 +3137,16 @@
},
{
"name": "sebastian/diff",
"version": "3.0.3",
"version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
"reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6296a0c086dd0117c1b78b059374d7fcbe7545ae",
"reference": "6296a0c086dd0117c1b78b059374d7fcbe7545ae",
"shasum": ""
},
"require": {
@ -3190,7 +3191,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
"source": "https://github.com/sebastianbergmann/diff/tree/3.0.4"
},
"funding": [
{
@ -3198,7 +3199,7 @@
"type": "github"
}
],
"time": "2020-11-30T07:59:04+00:00"
"time": "2023-05-07T05:30:20+00:00"
},
{
"name": "sebastian/environment",
@ -3793,16 +3794,16 @@
},
{
"name": "symfony/console",
"version": "v5.4.21",
"version": "v5.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9"
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c77433ddc6cdc689caf48065d9ea22ca0853fbd9",
"reference": "c77433ddc6cdc689caf48065d9ea22ca0853fbd9",
"url": "https://api.github.com/repos/symfony/console/zipball/b504a3d266ad2bb632f196c0936ef2af5ff6e273",
"reference": "b504a3d266ad2bb632f196c0936ef2af5ff6e273",
"shasum": ""
},
"require": {
@ -3867,12 +3868,12 @@
"homepage": "https://symfony.com",
"keywords": [
"cli",
"command line",
"command-line",
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.4.21"
"source": "https://github.com/symfony/console/tree/v5.4.26"
},
"funding": [
{
@ -3888,7 +3889,7 @@
"type": "tidelift"
}
],
"time": "2023-02-25T16:59:41+00:00"
"time": "2023-07-19T20:11:33+00:00"
},
{
"name": "symfony/deprecation-contracts",
@ -4451,16 +4452,16 @@
},
{
"name": "symfony/string",
"version": "v5.4.21",
"version": "v5.4.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f"
"reference": "1181fe9270e373537475e826873b5867b863883c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/edac10d167b78b1d90f46a80320d632de0bd9f2f",
"reference": "edac10d167b78b1d90f46a80320d632de0bd9f2f",
"url": "https://api.github.com/repos/symfony/string/zipball/1181fe9270e373537475e826873b5867b863883c",
"reference": "1181fe9270e373537475e826873b5867b863883c",
"shasum": ""
},
"require": {
@ -4517,7 +4518,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.4.21"
"source": "https://github.com/symfony/string/tree/v5.4.26"
},
"funding": [
{
@ -4533,7 +4534,7 @@
"type": "tidelift"
}
],
"time": "2023-02-22T08:00:55+00:00"
"time": "2023-06-28T12:46:07+00:00"
},
{
"name": "theseer/tokenizer",

View file

@ -1,5 +1,10 @@
## packages
The packages that are likely to cause conflicts with other plugins (by loading multiple incompatible versions).
Their namespaces are isolated by [Mozart](https://github.com/coenjacobs/mozart).
Currently, the packages are simply added in the repo to avoid making the build process more complex (Mozart has different PHP requirements).
We need to isolate only PSR-11 containers and Dhii modularity packages, which are not supposed to change often.
## common
This folder contains reusable classes or components that do not fit into any specific module.
They are designed to be versatile and can be used by any module within the plugin.

View file

@ -0,0 +1,68 @@
<?php
/**
* The Singleton Trait can be used to wrap an execution block, so it behaves like a Singleton.
* It executes the callable once, on subsequent calls returns the same result.
*/
namespace WooCommerce\PayPalCommerce\Common\Pattern;
/**
* Class SingletonDecorator.
*/
class SingletonDecorator {
/**
* The callable with the executing code
*
* @var callable
*/
private $callable;
/**
* The execution result
*
* @var mixed
*/
private $result;
/**
* Indicates if the callable is resolved
*
* @var bool
*/
private $executed = false;
/**
* SingletonDecorator constructor.
*
* @param callable $callable
*/
public function __construct( callable $callable ) {
$this->callable = $callable;
}
/**
* The make constructor.
*
* @param callable $callable
* @return self
*/
public static function make( callable $callable ): self {
return new static( $callable );
}
/**
* Invokes a callable once and returns the same result on subsequent invokes.
*
* @param mixed ...$args Arguments to be passed to the callable.
* @return mixed
*/
public function __invoke( ...$args ) {
if ( ! $this->executed ) {
$this->result = call_user_func_array( $this->callable, $args );
$this->executed = true;
}
return $this->result;
}
}

View file

@ -0,0 +1,41 @@
<?php
/**
* The Singleton Trait can be used to add singleton behaviour to a class.
*
* @package WooCommerce\PayPalCommerce\Common\Pattern
*/
namespace WooCommerce\PayPalCommerce\Common\Pattern;
/**
* Class SingletonTrait.
*/
trait SingletonTrait {
/**
* The single instance of the class.
*
* @var self
*/
protected static $instance = null;
/**
* Static method to get the instance of the Singleton class
*
* @return self|null
*/
public static function get_instance(): ?self {
return self::$instance;
}
/**
* Static method to get the instance of the Singleton class
*
* @param self $instance
* @return self
*/
protected static function set_instance( self $instance ): self {
self::$instance = $instance;
return self::$instance;
}
}

View file

@ -35,17 +35,26 @@ class Message {
*/
private $dismissable;
/**
* The wrapper selector that will contain the notice.
*
* @var string
*/
private $wrapper;
/**
* Message constructor.
*
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissable Whether the message is dismissable.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $message, string $type, bool $dismissable = true ) {
public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) {
$this->type = $type;
$this->message = $message;
$this->dismissable = $dismissable;
$this->wrapper = $wrapper;
}
/**
@ -74,4 +83,13 @@ class Message {
public function is_dismissable(): bool {
return $this->dismissable;
}
/**
* Returns the wrapper selector that will contain the notice.
*
* @return string
*/
public function wrapper(): string {
return $this->wrapper;
}
}

View file

@ -41,9 +41,10 @@ class Renderer implements RendererInterface {
$messages = $this->repository->current_message();
foreach ( $messages as $message ) {
printf(
'<div class="notice notice-%s %s"><p>%s</p></div>',
'<div class="notice notice-%s %s" %s><p>%s</p></div>',
$message->type(),
( $message->is_dismissable() ) ? 'is-dismissible' : '',
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
wp_kses_post( $message->message() )
);
}

View file

@ -9,6 +9,7 @@ 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;
@ -62,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;
@ -311,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,
@ -320,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 {
@ -836,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,6 +43,44 @@ class ApiModule implements ModuleInterface {
WC()->session->set( 'ppcp_fees', $fees );
}
);
add_filter(
'ppcp_create_order_request_body_data',
function( array $data ) use ( $c ) {
foreach ( $data['purchase_units'] as $purchase_unit_index => $purchase_unit ) {
foreach ( $purchase_unit['items'] as $item_index => $item ) {
$data['purchase_units'][ $purchase_unit_index ]['items'][ $item_index ]['name'] =
apply_filters( 'woocommerce_paypal_payments_cart_line_item_name', $item['name'], $item['cart_item_key'] );
}
}
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

@ -135,7 +135,7 @@ class BillingAgreementsEndpoint {
);
} finally {
$this->is_request_logging_enabled = true;
set_transient( 'ppcp_reference_transaction_enabled', true, 3 * MONTH_IN_SECONDS );
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
}
return true;

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

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use Psr\Log\LoggerInterface;
use WP_Error;
/**
* Class WebhookEndpoint
@ -193,7 +194,7 @@ class WebhookEndpoint {
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
if ( $response instanceof WP_Error ) {
throw new RuntimeException(
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
);
@ -202,6 +203,11 @@ class WebhookEndpoint {
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
$json = null;
/**
* Use in array as consistency check.
*
* @psalm-suppress RedundantConditionGivenDocblockType
*/
if ( is_array( $response ) ) {
$json = json_decode( $response['body'] );
}

View file

@ -66,6 +66,20 @@ class Item {
*/
private $category;
/**
* The product url.
*
* @var string
*/
protected $url;
/**
* The product image url.
*
* @var string
*/
protected $image_url;
/**
* The tax rate.
*
@ -90,6 +104,8 @@ class Item {
* @param Money|null $tax The tax.
* @param string $sku The SKU.
* @param string $category The category.
* @param string $url The product url.
* @param string $image_url The product image url.
* @param float $tax_rate The tax rate.
* @param ?string $cart_item_key The cart key for this item.
*/
@ -101,6 +117,8 @@ class Item {
Money $tax = null,
string $sku = '',
string $category = 'PHYSICAL_GOODS',
string $url = '',
string $image_url = '',
float $tax_rate = 0,
string $cart_item_key = null
) {
@ -111,8 +129,9 @@ class Item {
$this->description = $description;
$this->tax = $tax;
$this->sku = $sku;
$this->category = ( self::DIGITAL_GOODS === $category ) ? self::DIGITAL_GOODS : self::PHYSICAL_GOODS;
$this->category = $category;
$this->url = $url;
$this->image_url = $image_url;
$this->tax_rate = $tax_rate;
$this->cart_item_key = $cart_item_key;
}
@ -180,6 +199,24 @@ class Item {
return $this->category;
}
/**
* Returns the url.
*
* @return string
*/
public function url():string {
return $this->url;
}
/**
* Returns the image url.
*
* @return string
*/
public function image_url():string {
return $this->image_url;
}
/**
* Returns the tax rate.
*
@ -203,7 +240,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(),
@ -211,6 +248,8 @@ class Item {
'description' => $this->description(),
'sku' => $this->sku(),
'category' => $this->category(),
'url' => $this->url(),
'image_url' => $this->image_url(),
);
if ( $this->tax() ) {

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

@ -53,6 +53,7 @@ class ItemFactory {
* @var \WC_Product $product
*/
$quantity = (int) $item['quantity'];
$image = wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' );
$price = (float) $item['line_subtotal'] / (float) $item['quantity'];
return new Item(
@ -63,6 +64,8 @@ class ItemFactory {
null,
$product->get_sku(),
( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product->get_permalink(),
$image[0] ?? '',
0,
$cart_item_key
);
@ -128,6 +131,7 @@ class ItemFactory {
$quantity = (int) $item->get_quantity();
$price_without_tax = (float) $order->get_item_subtotal( $item, false );
$price_without_tax_rounded = round( $price_without_tax, 2 );
$image = $product instanceof WC_Product ? wp_get_attachment_image_src( (int) $product->get_image_id(), 'full' ) : '';
return new Item(
mb_substr( $item->get_name(), 0, 127 ),
@ -136,7 +140,9 @@ class ItemFactory {
$product instanceof WC_Product ? $this->prepare_description( $product->get_description() ) : '',
null,
$product instanceof WC_Product ? $product->get_sku() : '',
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS,
$product instanceof WC_Product ? $product->get_permalink() : '',
$image[0] ?? ''
);
}
@ -190,6 +196,8 @@ class ItemFactory {
: null;
$sku = ( isset( $data->sku ) ) ? $data->sku : '';
$category = ( isset( $data->category ) ) ? $data->category : 'PHYSICAL_GOODS';
$url = ( isset( $data->url ) ) ? $data->url : '';
$image_url = ( isset( $data->image_url ) ) ? $data->image_url : '';
return new Item(
$data->name,
@ -198,7 +206,9 @@ class ItemFactory {
$description,
$tax,
$sku,
$category
$category,
$url,
$image_url
);
}

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

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

View file

@ -2,10 +2,19 @@ class CartHelper {
constructor(cartItemKeys = [])
{
this.endpoint = wc_cart_fragments_params.wc_ajax_url.toString().replace('%%endpoint%%', 'remove_from_cart');
this.cartItemKeys = cartItemKeys;
}
getEndpoint() {
let ajaxUrl = "/?wc-ajax=%%endpoint%%";
if ((typeof wc_cart_fragments_params !== 'undefined') && wc_cart_fragments_params.wc_ajax_url) {
ajaxUrl = wc_cart_fragments_params.wc_ajax_url;
}
return ajaxUrl.toString().replace('%%endpoint%%', 'remove_from_cart');
}
addFromPurchaseUnits(purchaseUnits) {
for (const purchaseUnit of purchaseUnits || []) {
for (const item of purchaseUnit.items || []) {
@ -46,7 +55,7 @@ class CartHelper {
continue;
}
fetch(this.endpoint, {
fetch(this.getEndpoint(), {
method: 'POST',
credentials: 'same-origin',
body: params

View file

@ -155,7 +155,7 @@ return array(
$session_handler = $container->get( 'session.handler' );
$settings = $container->get( 'wcgateway.settings' );
$early_order_handler = $container->get( 'button.helper.early-order-handler' );
$registration_needed = $container->get( 'button.current-user-must-register' );
$registration_needed = $container->get( 'button.current-user-must-register' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new CreateOrderEndpoint(
$request_data,

View file

@ -153,9 +153,10 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
/**
* Handles errors.
*
* @param bool $send_response If this error handling should return the response.
* @return void
*/
private function handle_error(): void {
protected function handle_error( bool $send_response = true ): void {
$message = __(
'Something went wrong. Action aborted',
@ -173,14 +174,16 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
wc_clear_notices();
}
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
if ( $send_response ) {
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
}
}
/**
@ -259,7 +262,9 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
private function add_product( \WC_Product $product, int $quantity ): bool {
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), $quantity );
$this->cart_item_keys[] = $cart_item_key;
if ( $cart_item_key ) {
$this->cart_item_keys[] = $cart_item_key;
}
return false !== $cart_item_key;
}
@ -294,7 +299,9 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
$variations
);
$this->cart_item_keys[] = $cart_item_key;
if ( $cart_item_key ) {
$this->cart_item_keys[] = $cart_item_key;
}
return false !== $cart_item_key;
}
@ -322,7 +329,9 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
$this->cart_item_keys[] = $cart_item_key;
if ( $cart_item_key ) {
$this->cart_item_keys[] = $cart_item_key;
}
return false !== $cart_item_key;
}
@ -333,6 +342,9 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
*/
protected function remove_cart_items(): void {
foreach ( $this->cart_item_keys as $cart_item_key ) {
if ( ! $cart_item_key ) {
continue;
}
$this->cart->remove_cart_item( $cart_item_key );
}
}

View file

@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
@ -330,6 +331,8 @@ class CreateOrderEndpoint implements EndpointInterface {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$wc_order->save_meta_data();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
}
wp_send_json_success( $this->make_response( $order ) );

View file

@ -27,6 +27,13 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
*/
private $smart_button;
/**
* The WooCommerce real active cart.
*
* @var \WC_Cart|null
*/
private $real_cart = null;
/**
* ChangeCartEndpoint constructor.
*
@ -65,24 +72,14 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
return false;
}
// Set WC default cart as the clone.
// Store a reference to the real cart.
$active_cart = WC()->cart;
WC()->cart = $this->cart;
$this->replace_real_cart();
if ( ! $this->add_products( $products ) ) {
return false;
}
$this->add_products( $products );
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items();
// Restore cart and unset cart clone.
WC()->cart = $active_cart;
unset( $this->cart );
$this->restore_real_cart();
// Process filters.
$pay_later_enabled = true;
@ -119,4 +116,44 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
return true;
}
/**
* Handles errors.
*
* @param bool $send_response If this error handling should return the response.
* @return void
*
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
protected function handle_error( bool $send_response = false ): void {
parent::handle_error( $send_response );
}
/**
* Replaces the real cart with the clone.
*
* @return void
*/
private function replace_real_cart() {
// Set WC default cart as the clone.
// Store a reference to the real cart.
$this->real_cart = WC()->cart;
WC()->cart = $this->cart;
}
/**
* Restores the real cart.
*
* @return void
*/
private function restore_real_cart() {
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items();
// Restore cart and unset cart clone.
if ( null !== $this->real_cart ) {
WC()->cart = $this->real_cart;
}
unset( $this->cart );
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -163,6 +164,10 @@ class EarlyOrderHandler {
/**
* Patch Order so we have the \WC_Order id added.
*/
return $this->order_processor->patch_order( $wc_order, $order );
$order = $this->order_processor->patch_order( $wc_order, $order );
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
return $order;
}
}

View file

@ -2,7 +2,7 @@
"name": "ppcp-compat",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"main": "resources/js/compat.js",
"main": "resources/js/tracking-compat.js",
"browserslist": [
"> 0.5%",
"Safari >= 8",

View file

@ -1,32 +0,0 @@
document.addEventListener(
'DOMContentLoaded',
() => {
const orderTrackingContainerId = "ppcp_order-tracking";
const orderTrackingContainerSelector = "#ppcp_order-tracking";
const gzdSaveButton = document.getElementById('order-shipments-save');
const loadLocation = location.href + " " + orderTrackingContainerSelector + ">*";
const setEnabled = function (enabled) {
let childNodes = document.getElementById(orderTrackingContainerId).getElementsByTagName('*');
for (let node of childNodes) {
node.disabled = !enabled;
}
}
const waitForTrackingUpdate = function () {
if (jQuery('#order-shipments-save').css('display') !== 'none') {
setEnabled(false);
setTimeout(waitForTrackingUpdate, 100)
} else {
jQuery(orderTrackingContainerSelector).load(loadLocation,"");
}
}
if (typeof(gzdSaveButton) != 'undefined' && gzdSaveButton != null) {
gzdSaveButton.addEventListener('click', function (event) {
waitForTrackingUpdate();
setEnabled(true);
})
}
},
);

View file

@ -0,0 +1,49 @@
document.addEventListener(
'DOMContentLoaded',
() => {
const config = PayPalCommerceGatewayOrderTrackingCompat;
const orderTrackingContainerId = "ppcp_order-tracking";
const orderTrackingContainerSelector = "#ppcp_order-tracking .ppcp-tracking-column.shipments";
const gzdSaveButton = document.getElementById('order-shipments-save');
const loadLocation = location.href + " " + orderTrackingContainerSelector + ">*";
const gzdSyncEnabled = config.gzd_sync_enabled;
const wcShipmentSyncEnabled = config.wc_shipment_sync_enabled;
const wcShipmentSaveButton = document.querySelector('#woocommerce-shipment-tracking .button-save-form');
const toggleLoaderVisibility = function() {
const loader = document.querySelector('.ppcp-tracking-loader');
if (loader) {
if (loader.style.display === 'none' || loader.style.display === '') {
loader.style.display = 'block';
} else {
loader.style.display = 'none';
}
}
}
const waitForTrackingUpdate = function (elementToCheck) {
if (elementToCheck.css('display') !== 'none') {
setTimeout(() => waitForTrackingUpdate(elementToCheck), 100);
} else {
jQuery(orderTrackingContainerSelector).load(loadLocation, "", function(){
toggleLoaderVisibility();
});
}
}
if (gzdSyncEnabled && typeof(gzdSaveButton) != 'undefined' && gzdSaveButton != null) {
gzdSaveButton.addEventListener('click', function (event) {
toggleLoaderVisibility();
waitForTrackingUpdate(jQuery('#order-shipments-save'));
})
}
if (wcShipmentSyncEnabled && typeof(wcShipmentSaveButton) != 'undefined' && wcShipmentSaveButton != null) {
wcShipmentSaveButton.addEventListener('click', function (event) {
toggleLoaderVisibility();
waitForTrackingUpdate(jQuery('#shipment-tracking-form'));
})
}
},
);

View file

@ -11,11 +11,10 @@ namespace WooCommerce\PayPalCommerce\Compat;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'compat.ppec.mock-gateway' => static function( $container ) {
'compat.ppec.mock-gateway' => static function( $container ) {
$settings = $container->get( 'wcgateway.settings' );
$title = $settings->has( 'title' ) ? $settings->get( 'title' ) : __( 'PayPal', 'woocommerce-paypal-payments' );
$title = sprintf(
@ -27,20 +26,20 @@ return array(
return new PPEC\MockGateway( $title );
},
'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) {
'compat.ppec.subscriptions-handler' => static function ( ContainerInterface $container ) {
$ppcp_renewal_handler = $container->get( 'subscription.renewal-handler' );
$gateway = $container->get( 'compat.ppec.mock-gateway' );
return new PPEC\SubscriptionsHandler( $ppcp_renewal_handler, $gateway );
},
'compat.ppec.settings_importer' => static function( ContainerInterface $container ) : PPEC\SettingsImporter {
'compat.ppec.settings_importer' => static function( ContainerInterface $container ) : PPEC\SettingsImporter {
$settings = $container->get( 'wcgateway.settings' );
return new PPEC\SettingsImporter( $settings );
},
'compat.plugin-script-names' => static function( ContainerInterface $container ) : array {
'compat.plugin-script-names' => static function( ContainerInterface $container ) : array {
return array(
'ppcp-smart-button',
'ppcp-oxxo',
@ -50,16 +49,24 @@ return array(
'ppcp-webhooks-status-page',
'ppcp-tracking',
'ppcp-fraudnet',
'ppcp-gzd-compat',
'ppcp-tracking-compat',
'ppcp-clear-db',
);
},
'compat.gzd.is_supported_plugin_version_active' => function (): bool {
'compat.gzd.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'wc_gzd_get_shipments_by_order' ); // 3.0+
},
'compat.module.url' => static function ( ContainerInterface $container ): string {
'compat.wc_shipment_tracking.is_supported_plugin_version_active' => function (): bool {
return class_exists( 'WC_Shipment_Tracking' );
},
'compat.ywot.is_supported_plugin_version_active' => function (): bool {
return function_exists( 'yith_ywot_init' );
},
'compat.module.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
@ -71,22 +78,13 @@ return array(
);
},
'compat.assets' => function( ContainerInterface $container ) : CompatAssets {
'compat.assets' => function( ContainerInterface $container ) : CompatAssets {
return new CompatAssets(
$container->get( 'compat.module.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'compat.should-initialize-gzd-compat-layer' )
$container->get( 'order-tracking.is-module-enabled' ),
$container->get( 'compat.gzd.is_supported_plugin_version_active' ),
$container->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' )
);
},
'compat.should-initialize-gzd-compat-layer' => function( ContainerInterface $container ) : bool {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$tracking_enabled = $settings->has( 'tracking_enabled' ) && $settings->get( 'tracking_enabled' );
$is_gzd_active = $container->get( 'compat.gzd.is_supported_plugin_version_active' );
return $tracking_enabled && $is_gzd_active;
},
);

View file

@ -28,23 +28,48 @@ class CompatAssets {
private $version;
/**
* Whether Germanized synchronization scripts should be loaded.
* Whether tracking compat scripts should be loaded.
*
* @var bool
*/
protected $should_enqueue_gzd_scripts;
protected $should_enqueue_tracking_scripts;
/**
* Whether Germanized plugin is active.
*
* @var bool
*/
protected $is_gzd_active;
/**
* Whether WC Shipments plugin is active
*
* @var bool
*/
protected $is_wc_shipment_active;
/**
* Compat module assets constructor.
*
* @param string $module_url The URL to the module.
* @param string $version The assets version.
* @param bool $should_enqueue_gzd_scripts Whether Germanized synchronization scripts should be loaded.
* @param bool $should_enqueue_tracking_scripts Whether Germanized synchronization scripts should be loaded.
* @param bool $is_gzd_active Whether Germanized plugin is active.
* @param bool $is_wc_shipment_active Whether WC Shipments plugin is active.
*/
public function __construct( string $module_url, string $version, bool $should_enqueue_gzd_scripts ) {
$this->module_url = $module_url;
$this->version = $version;
$this->should_enqueue_gzd_scripts = $should_enqueue_gzd_scripts;
public function __construct(
string $module_url,
string $version,
bool $should_enqueue_tracking_scripts,
bool $is_gzd_active,
bool $is_wc_shipment_active
) {
$this->module_url = $module_url;
$this->version = $version;
$this->should_enqueue_tracking_scripts = $should_enqueue_tracking_scripts;
$this->is_gzd_active = $is_gzd_active;
$this->is_wc_shipment_active = $is_wc_shipment_active;
}
/**
@ -53,15 +78,23 @@ class CompatAssets {
* @return void
*/
public function register(): void {
$gzd_sync_enabled = apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true );
if ( $this->should_enqueue_gzd_scripts && $gzd_sync_enabled ) {
if ( $this->should_enqueue_tracking_scripts ) {
wp_register_script(
'ppcp-gzd-compat',
untrailingslashit( $this->module_url ) . '/assets/js/gzd-compat.js',
'ppcp-tracking-compat',
untrailingslashit( $this->module_url ) . '/assets/js/tracking-compat.js',
array( 'jquery' ),
$this->version,
true
);
wp_localize_script(
'ppcp-tracking-compat',
'PayPalCommerceGatewayOrderTrackingCompat',
array(
'gzd_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) && $this->is_gzd_active,
'wc_shipment_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) && $this->is_wc_shipment_active,
)
);
}
}
@ -71,9 +104,8 @@ class CompatAssets {
* @return void
*/
public function enqueue(): void {
$gzd_sync_enabled = apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true );
if ( $this->should_enqueue_gzd_scripts && $gzd_sync_enabled ) {
wp_enqueue_script( 'ppcp-gzd-compat' );
if ( $this->should_enqueue_tracking_scripts ) {
wp_enqueue_script( 'ppcp-tracking-compat' );
}
}
}

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use Vendidero\Germanized\Shipments\ShipmentItem;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use Exception;
@ -20,17 +22,15 @@ use WC_Order;
use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WP_Theme;
use WP_REST_Request;
use WP_REST_Response;
/**
* Class CompatModule
*/
class CompatModule implements ModuleInterface {
use AdminContextTrait;
/**
* Setup the compatibility module.
*
@ -49,10 +49,16 @@ class CompatModule implements ModuleInterface {
* @throws NotFoundException
*/
public function run( ContainerInterface $c ): void {
$this->initialize_ppec_compat_layer( $c );
$this->fix_site_ground_optimizer_compatibility( $c );
$this->initialize_tracking_compat_layer( $c );
$this->initialize_gzd_compat_layer( $c );
$asset_loader = $c->get( 'compat.assets' );
assert( $asset_loader instanceof CompatAssets );
add_action( 'init', array( $asset_loader, 'register' ) );
add_action( 'admin_enqueue_scripts', array( $asset_loader, 'enqueue' ) );
$this->migrate_pay_later_settings( $c );
$this->migrate_smart_button_settings( $c );
@ -112,6 +118,30 @@ class CompatModule implements ModuleInterface {
);
}
/**
* Sets up the 3rd party plugins compatibility layer for PayPal tracking.
*
* @param ContainerInterface $c The Container.
* @return void
*/
protected function initialize_tracking_compat_layer( ContainerInterface $c ): void {
$is_gzd_active = $c->get( 'compat.gzd.is_supported_plugin_version_active' );
$is_wc_shipment_tracking_active = $c->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' );
$is_ywot_active = $c->get( 'compat.ywot.is_supported_plugin_version_active' );
if ( $is_gzd_active ) {
$this->initialize_gzd_compat_layer( $c );
}
if ( $is_wc_shipment_tracking_active ) {
$this->initialize_wc_shipment_tracking_compat_layer( $c );
}
if ( $is_ywot_active ) {
$this->initialize_ywot_compat_layer( $c );
}
}
/**
* Sets up the <a href="https://wordpress.org/plugins/woocommerce-germanized/">Germanized for WooCommerce</a>
* plugin compatibility layer.
@ -122,84 +152,204 @@ class CompatModule implements ModuleInterface {
* @return void
*/
protected function initialize_gzd_compat_layer( ContainerInterface $c ): void {
if ( ! $c->get( 'compat.should-initialize-gzd-compat-layer' ) ) {
return;
}
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $hook ) use ( $c ): void {
if ( $hook !== 'post.php' || ! $this->is_paypal_order_edit_page() ) {
'woocommerce_gzd_shipment_status_shipped',
function( int $shipment_id, Shipment $shipment ) use ( $c ) {
if ( ! apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) ) {
return;
}
$asset_loader = $c->get( 'compat.assets' );
assert( $asset_loader instanceof CompatAssets );
$wc_order = $shipment->get_order();
$asset_loader->register();
$asset_loader->enqueue();
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
$order_id = $wc_order->get_id();
$transaction_id = $wc_order->get_transaction_id();
$tracking_number = $shipment->get_tracking_id();
$carrier = $shipment->get_shipping_provider();
$items = array_map(
function ( ShipmentItem $item ): int {
return $item->get_order_item_id();
},
$shipment->get_items()
);
if ( ! $tracking_number || ! $carrier || ! $transaction_id ) {
return;
}
$this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, $items );
},
500,
2
);
}
/**
* Sets up the <a href="https://woocommerce.com/document/shipment-tracking/">Shipment Tracking</a>
* plugin compatibility layer.
*
* @link https://woocommerce.com/document/shipment-tracking/
*
* @param ContainerInterface $c The Container.
* @return void
*/
protected function initialize_wc_shipment_tracking_compat_layer( ContainerInterface $c ): void {
add_action(
'wp_ajax_wc_shipment_tracking_save_form',
function() use ( $c ) {
check_ajax_referer( 'create-tracking-item', 'security', true );
if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) {
return;
}
$order_id = (int) wc_clean( wp_unslash( $_POST['order_id'] ?? '' ) );
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
$transaction_id = $wc_order->get_transaction_id();
$tracking_number = wc_clean( wp_unslash( $_POST['tracking_number'] ?? '' ) );
$carrier = wc_clean( wp_unslash( $_POST['tracking_provider'] ?? '' ) );
$carrier_other = wc_clean( wp_unslash( $_POST['custom_tracking_provider'] ?? '' ) );
$carrier = $carrier ?: $carrier_other ?: '';
if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) {
return;
}
$this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() );
}
);
add_filter(
'woocommerce_rest_prepare_order_shipment_tracking',
function( WP_REST_Response $response, array $tracking_item, WP_REST_Request $request ) use ( $c ): WP_REST_Response {
if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) {
return $response;
}
$callback = $request->get_attributes()['callback']['1'] ?? '';
if ( $callback !== 'create_item' ) {
return $response;
}
$order_id = $tracking_item['order_id'] ?? 0;
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $response;
}
$transaction_id = $wc_order->get_transaction_id();
$tracking_number = $tracking_item['tracking_number'] ?? '';
$carrier = $tracking_item['tracking_provider'] ?? '';
$carrier_other = $tracking_item['custom_tracking_provider'] ?? '';
$carrier = $carrier ?: $carrier_other ?: '';
if ( ! $tracking_number || ! $carrier || ! $transaction_id ) {
return $response;
}
$this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() );
return $response;
},
10,
3
);
}
/**
* Sets up the <a href="https://wordpress.org/plugins/yith-woocommerce-order-tracking/">YITH WooCommerce Order & Shipment Tracking</a>
* plugin compatibility layer.
*
* @link https://wordpress.org/plugins/yith-woocommerce-order-tracking/
*
* @param ContainerInterface $c The Container.
* @return void
*/
protected function initialize_ywot_compat_layer( ContainerInterface $c ): void {
add_action(
'woocommerce_process_shop_order_meta',
function( int $order_id ) use ( $c ) {
if ( ! apply_filters( 'woocommerce_paypal_payments_sync_ywot_tracking', true ) ) {
return;
}
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
$transaction_id = $wc_order->get_transaction_id();
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$tracking_number = wc_clean( wp_unslash( $_POST['ywot_tracking_code'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$carrier = wc_clean( wp_unslash( $_POST['ywot_carrier_name'] ?? '' ) );
if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) {
return;
}
$this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() );
},
500,
1
);
}
/**
* Creates PayPal tracking.
*
* @param ContainerInterface $c The Container.
* @param int $wc_order_id The WC order ID.
* @param string $transaction_id The transaction ID.
* @param string $tracking_number The tracking number.
* @param string $carrier The shipment carrier.
* @param int[] $line_items The list of shipment line item IDs.
* @return void
*/
protected function create_tracking(
ContainerInterface $c,
int $wc_order_id,
string $transaction_id,
string $tracking_number,
string $carrier,
array $line_items
) {
$endpoint = $c->get( 'order-tracking.endpoint.controller' );
assert( $endpoint instanceof OrderTrackingEndpoint );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
add_action(
'woocommerce_gzd_shipment_status_shipped',
static function( int $shipment_id, Shipment $shipment ) use ( $endpoint, $logger ) {
if ( ! apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) ) {
return;
}
$shipment_factory = $c->get( 'order-tracking.shipment.factory' );
assert( $shipment_factory instanceof ShipmentFactoryInterface );
$wc_order = $shipment->get_order();
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
try {
$ppcp_shipment = $shipment_factory->create_shipment(
$wc_order_id,
$transaction_id,
$tracking_number,
'SHIPPED',
'OTHER',
$carrier,
$line_items
);
$transaction_id = $wc_order->get_transaction_id();
if ( empty( $transaction_id ) ) {
return;
}
$tracking_information = $endpoint->get_tracking_information( $wc_order_id, $tracking_number );
$tracking_data = array(
'transaction_id' => $transaction_id,
'status' => 'SHIPPED',
);
$tracking_information
? $endpoint->update_tracking_information( $ppcp_shipment, $wc_order_id )
: $endpoint->add_tracking_information( $ppcp_shipment, $wc_order_id );
$provider = $shipment->get_shipping_provider();
if ( ! empty( $provider ) && $provider !== 'none' ) {
/**
* The filter allowing to change the default Germanized carrier for order tracking,
* such as DHL_DEUTSCHE_POST, DPD_DE, ...
*/
$tracking_data['carrier'] = (string) apply_filters( 'woocommerce_paypal_payments_default_gzd_carrier', 'DHL_DEUTSCHE_POST', $provider );
}
try {
$tracking_information = $endpoint->get_tracking_information( $wc_order->get_id() );
$tracking_data['tracking_number'] = $tracking_information['tracking_number'] ?? '';
if ( $shipment->get_tracking_id() ) {
$tracking_data['tracking_number'] = $shipment->get_tracking_id();
}
! $tracking_information ? $endpoint->add_tracking_information( $tracking_data, $wc_order->get_id() ) : $endpoint->update_tracking_information( $tracking_data, $wc_order->get_id() );
} catch ( Exception $exception ) {
$logger->error( "Couldn't sync tracking information: " . $exception->getMessage() );
}
},
500,
2
);
} catch ( Exception $exception ) {
$logger->error( "Couldn't sync tracking information: " . $exception->getMessage() );
}
}
/**

View file

@ -98,7 +98,7 @@ class PPECHelper {
set_transient(
'ppcp_has_ppec_subscriptions',
! empty( $result ) ? 'true' : 'false',
3 * MONTH_IN_SECONDS
MONTH_IN_SECONDS
);
return ! empty( $result );

View file

@ -6,7 +6,7 @@ module.exports = {
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
'gzd-compat': path.resolve('./resources/js/gzd-compat.js'),
'tracking-compat': path.resolve('./resources/js/tracking-compat.js'),
},
output: {
path: path.resolve(__dirname, 'assets/'),

View file

@ -64,7 +64,7 @@ class OnboardingUrl {
*
* @var int
*/
private $cache_ttl = 3 * MONTH_IN_SECONDS;
private $cache_ttl = MONTH_IN_SECONDS;
/**
* The TTL for the previous token cache.

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,132 @@
font-weight: bold;
}
input,select {
input:not([type="checkbox"]),select {
width: 100%;
}
#items-select-container {
display:none
}
.ppcp-tracking-columns-wrapper {
display: grid;
grid-template-columns: 1fr 2fr;
grid-gap: 60px;
.ppcp-tracking-column + .ppcp-tracking-column {
border-left: 1px solid #c3c4c7;
padding-left: 20px;
}
}
.ppcp-shipment {
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
margin: 20px 0px;
.wc-order-item-sku {
font-size: .92em!important;
color: #888;
}
.ppcp-shipment-header {
padding: 0px 10px;
cursor: pointer;
background: #f0f0f1;
h4 {
display: inline-block;
margin: 10px 0px;
}
button {
float: right;
border: 0;
background: 0 0;
cursor: pointer;
height: 38px;
}
}
.ppcp-shipment-info {
padding: 0px 10px 20px 10px;
}
.ppcp-shipment-info.hidden {
display: none;
}
.select2-container--default .select2-selection__rendered {
font-weight: bold;
}
.active .ppcp-shipment-header {
background-color: #e0e0e0;
border-bottom: 1px solid #ccc;
}
select {
width: auto;
}
}
.ppcp-shipment.closed {
.ppcp-shipment-header .shipment-toggle-indicator .toggle-indicator:before {
content: "\f140";
}
}
.ppcp-tracking-loader {
z-index: 1000;
border: none;
margin: 0px;
padding: 0px;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
background: rgb(255, 255, 255);
opacity: 0.6;
cursor: wait;
position: absolute;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
}
.ppcp-remove-tracking-item {
color: #b32d2e;
font-size: 14px;
cursor: pointer;
}
#side-sortables #ppcp_order-tracking {
.ppcp-tracking-columns-wrapper {
display: block;
}
.ppcp-tracking-columns-wrapper .ppcp-tracking-column+.ppcp-tracking-column {
border-top: 1px solid #c3c4c7;
padding-left: 0px;
margin-top: 20px;
border-left: none;
}
.update_shipment {
margin-top: 20px;
}
.select2-container {
width: 100% !important;
}
}

View file

@ -7,48 +7,161 @@ document.addEventListener(
return;
}
jQuery(document).on('click', '.submit_tracking_info', function () {
const transactionId = document.querySelector('.ppcp-tracking-transaction_id');
const trackingNumber = document.querySelector('.ppcp-tracking-tracking_number');
const status = document.querySelector('.ppcp-tracking-status');
const carrier = document.querySelector('.ppcp-tracking-carrier');
const orderId = document.querySelector('.ppcp-order_id');
const submitButton = document.querySelector('.submit_tracking_info');
const includeAllItemsCheckbox = document.getElementById('include-all-items');
const shipmentsWrapper = '#ppcp_order-tracking .ppcp-tracking-column.shipments';
const transactionId = document.querySelector('.ppcp-tracking-transaction_id');
const orderId = document.querySelector('.ppcp-tracking-order_id');
const carrier = document.querySelector('.ppcp-tracking-carrier');
const carrierNameOther = document.querySelector('.ppcp-tracking-carrier_name_other');
submitButton.setAttribute('disabled', 'disabled');
fetch(config.ajax.tracking_info.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.tracking_info.nonce,
transaction_id: transactionId ? transactionId.value : null,
tracking_number: trackingNumber ? trackingNumber.value : null,
status: status ? status.value : null,
carrier: carrier ? carrier.value : null,
order_id: orderId ? orderId.value : null,
action: submitButton ? submitButton.dataset.action : null,
})
}).then(function (res) {
return res.json();
}).then(function (data) {
if (!data.success) {
jQuery( "<span class='error tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(submitButton);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
submitButton.removeAttribute('disabled');
console.error(data);
throw Error(data.data.message);
}
function toggleLineItemsSelectbox() {
const selectContainer = document.getElementById('items-select-container');
includeAllItemsCheckbox?.addEventListener('change', function(){
selectContainer.style.display = includeAllItemsCheckbox.checked ? 'none' : 'block';
})
}
jQuery( "<span class='success tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(submitButton);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
function toggleShipment() {
jQuery(document).on('click', '.ppcp-shipment-header', function(event) {
const shipmentContainer = event.target.closest('.ppcp-shipment');
const shipmentInfo = shipmentContainer.querySelector('.ppcp-shipment-info');
submitButton.dataset.action = 'update';
submitButton.textContent = 'update';
submitButton.removeAttribute('disabled');
shipmentContainer.classList.toggle('active');
shipmentContainer.classList.toggle('closed');
shipmentInfo.classList.toggle('hidden');
});
})
}
function toggleShipmentUpdateButtonDisabled() {
jQuery(document).on('change', '.ppcp-shipment-status', function(event) {
const shipmentSelectbox = event.target;
const shipment = shipmentSelectbox.closest('.ppcp-shipment');
const updateShipmentButton = shipment.querySelector('.update_shipment');
const selectedValue = shipmentSelectbox.value;
updateShipmentButton.classList.remove('button-disabled');
});
}
function toggleLoaderVisibility() {
const loader = document.querySelector('.ppcp-tracking-loader');
if (loader) {
if (loader.style.display === 'none' || loader.style.display === '') {
loader.style.display = 'block';
} else {
loader.style.display = 'none';
}
}
}
function toggleOtherCarrierName() {
jQuery(carrier).on('change', function() {
const hiddenHtml = carrierNameOther.parentNode;
if (carrier.value === 'OTHER') {
hiddenHtml.classList.remove('hidden');
} else {
if (!hiddenHtml.classList.contains('hidden')) {
hiddenHtml.classList.add('hidden');
}
}
})
}
function handleAddShipment() {
jQuery(document).on('click', '.submit_tracking_info', function () {
const trackingNumber = document.querySelector('.ppcp-tracking-tracking_number');
const status = document.querySelector('.ppcp-tracking-status');
const submitButton = document.querySelector('.submit_tracking_info');
const items = document.querySelector('.ppcp-tracking-items');
let checkedItems = includeAllItemsCheckbox?.checked || !items ? 0 : Array.from(items.selectedOptions).map(option => option.value)
toggleLoaderVisibility()
fetch(config.ajax.tracking_info.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.tracking_info.nonce,
transaction_id: transactionId ? transactionId.value : null,
tracking_number: trackingNumber ? trackingNumber.value : null,
status: status ? status.value : null,
carrier: carrier ? carrier.value : null,
carrier_name_other: carrierNameOther ? carrierNameOther.value : null,
order_id: orderId ? orderId.value : null,
items: checkedItems
})
}).then(function (res) {
return res.json();
}).then(function (data) {
toggleLoaderVisibility()
if (!data.success || ! data.data.shipment) {
jQuery( "<span class='error tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(submitButton);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
submitButton.removeAttribute('disabled');
console.error(data);
throw Error(data.data.message);
}
jQuery( "<span class='success tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(submitButton);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
jQuery(data.data.shipment).appendTo(shipmentsWrapper);
});
})
}
function handleUpdateShipment() {
jQuery(document).on('click', '.update_shipment', function (event) {
const updateShipment = event.target;
const parentElement = updateShipment.parentNode.parentNode;
const shipmentStatus = parentElement.querySelector('.ppcp-shipment-status');
const shipmentTrackingNumber = parentElement.querySelector('.ppcp-shipment-tacking_number');
const shipmentCarrier = parentElement.querySelector('.ppcp-shipment-carrier');
const shipmentCarrierNameOther = parentElement.querySelector('.ppcp-shipment-carrier-other');
toggleLoaderVisibility()
fetch(config.ajax.tracking_info.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.tracking_info.nonce,
transaction_id: transactionId ? transactionId.value : null,
tracking_number: shipmentTrackingNumber ? shipmentTrackingNumber.value : null,
status: shipmentStatus ? shipmentStatus.value : null,
carrier: shipmentCarrier ? shipmentCarrier.value : null,
carrier_name_other: shipmentCarrierNameOther ? shipmentCarrierNameOther.value : null,
order_id: orderId ? orderId.value : null,
action: 'update'
})
}).then(function (res) {
return res.json();
}).then(function (data) {
toggleLoaderVisibility()
if (!data.success) {
jQuery( "<span class='error tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(updateShipment);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
console.error(data);
throw Error(data.data.message);
}
jQuery( "<span class='success tracking-info-message'>" + data.data.message + "</span>" ).insertAfter(updateShipment);
setTimeout(()=> jQuery('.tracking-info-message').remove(),3000);
});
})
}
handleAddShipment();
handleUpdateShipment();
toggleLineItemsSelectbox();
toggleShipment();
toggleShipmentUpdateButtonDisabled();
toggleOtherCarrierName();
},
);

View file

@ -9,9 +9,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactory;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\OrderTracking\Assets\OrderEditPageAssets;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
return array(
'order-tracking.assets' => function( ContainerInterface $container ) : OrderEditPageAssets {
@ -20,12 +25,18 @@ return array(
$container->get( 'ppcp.asset-version' )
);
},
'order-tracking.shipment.factory' => static function ( ContainerInterface $container ) : ShipmentFactoryInterface {
return new ShipmentFactory();
},
'order-tracking.endpoint.controller' => static function ( ContainerInterface $container ) : OrderTrackingEndpoint {
return new OrderTrackingEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.request-data' )
$container->get( 'button.request-data' ),
$container->get( 'order-tracking.shipment.factory' ),
$container->get( 'order-tracking.allowed-shipping-statuses' ),
$container->get( 'order-tracking.is-merchant-country-us' )
);
},
'order-tracking.module.url' => static function ( ContainerInterface $container ): string {
@ -41,17 +52,21 @@ return array(
},
'order-tracking.meta-box.renderer' => static function ( ContainerInterface $container ): MetaBoxRenderer {
return new MetaBoxRenderer(
$container->get( 'order-tracking.endpoint.controller' ),
$container->get( 'order-tracking.allowed-shipping-statuses' ),
$container->get( 'order-tracking.available-carriers' )
$container->get( 'order-tracking.available-carriers' ),
$container->get( 'order-tracking.endpoint.controller' ),
$container->get( 'order-tracking.is-merchant-country-us' )
);
},
'order-tracking.allowed-shipping-statuses' => static function ( ContainerInterface $container ): array {
return array(
'SHIPPED' => 'SHIPPED',
'ON_HOLD' => 'ON_HOLD',
'DELIVERED' => 'DELIVERED',
'CANCELLED' => 'CANCELLED',
return (array) apply_filters(
'woocommerce_paypal_payments_tracking_statuses',
array(
'SHIPPED' => 'Shipped',
'ON_HOLD' => 'On Hold',
'DELIVERED' => 'Delivered',
'CANCELLED' => 'Cancelled',
)
);
},
'order-tracking.allowed-carriers' => static function ( ContainerInterface $container ): array {
@ -73,4 +88,35 @@ return array(
),
);
},
'order-tracking.is-tracking-available' => static function ( ContainerInterface $container ): bool {
try {
$bearer = $container->get( 'api.bearer' );
assert( $bearer instanceof Bearer );
$token = $bearer->bearer();
return $token->is_tracking_available();
} catch ( RuntimeException $exception ) {
return false;
}
},
'order-tracking.is-module-enabled' => static function ( ContainerInterface $container ): bool {
$order_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( empty( $order_id ) ) {
return false;
}
$meta = get_post_meta( $order_id, PayPalGateway::ORDER_ID_META_KEY, true );
if ( empty( $meta ) ) {
return false;
}
$is_tracking_available = $container->get( 'order-tracking.is-tracking-available' );
return $is_tracking_available && apply_filters( 'woocommerce_paypal_payments_shipment_tracking_enabled', true );
},
'order-tracking.is-merchant-country-us' => static function ( ContainerInterface $container ): bool {
return $container->get( 'api.shop.country' ) === 'US';
},
);

View file

@ -78,11 +78,13 @@ class OrderEditPageAssets {
* @return array a map of script data.
*/
public function get_script_data(): array {
return array(
'ajax' => array(
'tracking_info' => array(
'endpoint' => \WC_AJAX::get_endpoint( OrderTrackingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( OrderTrackingEndpoint::nonce() ),
'url' => admin_url( 'admin-ajax.php' ),
),
),
);

View file

@ -11,20 +11,31 @@ namespace WooCommerce\PayPalCommerce\OrderTracking\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\RequestTrait;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\OrderTracking\OrderTrackingModule;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
/**
* The OrderTrackingEndpoint.
*
* @psalm-type SupportedStatuses = 'SHIPPED'|'ON_HOLD'|'DELIVERED'|'CANCELLED'
* @psalm-type TrackingInfo = array{transaction_id: string, status: SupportedStatuses, tracking_number?: string, carrier?: string}
* @psalm-type RequestValues = array{transaction_id: string, status: SupportedStatuses, order_id: int, action: 'create'|'update', tracking_number?: string, carrier?: string}
* @psalm-type TrackingInfo = array{
* transaction_id: string,
* status: SupportedStatuses,
* tracking_number: string,
* carrier: string,
* items?: list<int>,
* carrier_name_other?: string,
* }
* Class OrderTrackingEndpoint
*/
class OrderTrackingEndpoint {
@ -45,40 +56,70 @@ class OrderTrackingEndpoint {
*
* @var string
*/
private $host;
protected $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
protected $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
protected $logger;
/**
* The ShipmentFactory.
*
* @var ShipmentFactoryInterface
*/
protected $shipment_factory;
/**
* Allowed shipping statuses.
*
* @var string[]
*/
protected $allowed_statuses;
/**
* Whether new API should be used.
*
* @var bool
*/
protected $should_use_new_api;
/**
* PartnersEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param RequestData $request_data The Request data.
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
* @param RequestData $request_data The Request data.
* @param ShipmentFactoryInterface $shipment_factory The ShipmentFactory.
* @param string[] $allowed_statuses Allowed shipping statuses.
* @param bool $should_use_new_api Whether new API should be used.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger,
RequestData $request_data
RequestData $request_data,
ShipmentFactoryInterface $shipment_factory,
array $allowed_statuses,
bool $should_use_new_api
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->request_data = $request_data;
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
$this->request_data = $request_data;
$this->shipment_factory = $shipment_factory;
$this->allowed_statuses = $allowed_statuses;
$this->should_use_new_api = $should_use_new_api;
}
/**
@ -91,20 +132,30 @@ class OrderTrackingEndpoint {
}
try {
$data = $this->request_data->read_request( $this->nonce() );
$action = $data['action'];
$request_body = $this->extract_tracking_information( $data );
$order_id = (int) $data['order_id'];
$action === 'create' ? $this->add_tracking_information( $request_body, $order_id ) : $this->update_tracking_information( $request_body, $order_id );
$data = $this->request_data->read_request( $this->nonce() );
$order_id = (int) $data['order_id'];
$action = $data['action'] ?? '';
$action_message = $action === 'create' ? 'created' : 'updated';
$message = sprintf(
// translators: %1$s is the action message (created or updated).
_x( 'successfully %1$s', 'tracking info success message', 'woocommerce-paypal-payments' ),
esc_html( $action_message )
$shipment = $this->create_shipment( $order_id, $data );
$action === 'update'
? $this->update_tracking_information( $shipment, $order_id )
: $this->add_tracking_information( $shipment, $order_id );
$message = $action === 'update'
? _x( 'successfully updated', 'tracking info success message', 'woocommerce-paypal-payments' )
: _x( 'successfully created', 'tracking info success message', 'woocommerce-paypal-payments' );
ob_start();
$shipment->render( $this->allowed_statuses );
$shipment_html = ob_get_clean();
wp_send_json_success(
array(
'message' => $message,
'shipment' => $shipment_html,
)
);
wp_send_json_success( array( 'message' => $message ) );
} catch ( Exception $error ) {
wp_send_json_error( array( 'message' => $error->getMessage() ), 500 );
}
@ -113,99 +164,106 @@ class OrderTrackingEndpoint {
/**
* Creates the tracking information of a given order with the given data.
*
* @param array $data The tracking information to add.
* @psalm-param TrackingInfo $data
* @param int $order_id The order ID.
* @throws RuntimeException If problem creating.
* @param ShipmentInterface $shipment The shipment.
* @param int $order_id The order ID.
*
* @throws RuntimeException If problem adding.
*/
public function add_tracking_information( array $data, int $order_id ) : void {
$url = trailingslashit( $this->host ) . 'v1/shipping/trackers-batch';
public function add_tracking_information( ShipmentInterface $shipment, int $order_id ) : void {
$wc_order = wc_get_order( $order_id );
if ( ! $wc_order instanceof WC_Order ) {
return;
}
$body = array(
'trackers' => array( (array) apply_filters( 'woocommerce_paypal_payments_tracking_data_before_sending', $data, $order_id ) ),
);
$shipment_request_data = $this->generate_request_data( $wc_order, $shipment );
$args = array(
'method' => 'POST',
'headers' => $this->request_headers(),
'body' => wp_json_encode( $body ),
);
$url = $shipment_request_data['url'] ?? '';
$args = $shipment_request_data['args'] ?? array();
do_action( 'woocommerce_paypal_payments_before_tracking_is_added', $order_id, $data );
if ( ! $url || empty( $args ) ) {
$this->throw_runtime_exception( $shipment_request_data, 'create' );
}
do_action( 'woocommerce_paypal_payments_before_tracking_is_added', $order_id, $shipment_request_data );
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
'Could not create order tracking information.'
$args = array(
'args' => $args,
'response' => $response,
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
$this->throw_runtime_exception( $args, 'create' );
}
/**
* Need to ignore Method WP_Error::offsetGet does not exist
*
* @psalm-suppress UndefinedMethod
*/
$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',
sprintf(
'Failed to create order tracking information. PayPal API response: %1$s',
$error->getMessage()
),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
if ( 201 !== $status_code && ! is_wp_error( $response ) ) {
$this->throw_paypal_api_exception( $status_code, $args, $response, 'create' );
}
$wc_order = wc_get_order( $order_id );
if ( is_a( $wc_order, WC_Order::class ) ) {
$wc_order->update_meta_data( '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order->save();
}
$this->save_tracking_metadata( $wc_order, $shipment->tracking_number(), array_keys( $shipment->line_items() ) );
do_action( 'woocommerce_paypal_payments_after_tracking_is_added', $order_id, $response );
}
/**
* Gets the tracking information of a given order.
* Updates the tracking information of a given order with the given shipment.
*
* @param int $wc_order_id The order ID.
* @return array|null The tracking information.
* @psalm-return TrackingInfo|null
* @throws RuntimeException If problem getting.
* @param ShipmentInterface $shipment The shipment.
* @param int $order_id The order ID.
*
* @throws RuntimeException If problem updating.
*/
public function get_tracking_information( int $wc_order_id ) : ?array {
$wc_order = wc_get_order( $wc_order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
throw new RuntimeException( 'wrong order ID' );
public function update_tracking_information( ShipmentInterface $shipment, int $order_id ) : void {
$host = trailingslashit( $this->host );
$tracker_id = $this->find_tracker_id( $shipment->transaction_id(), $shipment->tracking_number() );
$url = "{$host}v1/shipping/trackers/{$tracker_id}";
$shipment_data = $shipment->to_array();
$args = array(
'method' => 'PUT',
'headers' => $this->request_headers(),
'body' => wp_json_encode( (array) apply_filters( 'woocommerce_paypal_payments_tracking_data_before_update', $shipment_data, $order_id ) ),
);
do_action( 'woocommerce_paypal_payments_before_tracking_is_updated', $order_id, $shipment_data );
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$args = array(
'args' => $args,
'response' => $response,
);
$this->throw_runtime_exception( $args, 'update' );
}
if ( ! $wc_order->meta_exists( '_ppcp_paypal_tracking_number' ) ) {
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code && ! is_wp_error( $response ) ) {
$this->throw_paypal_api_exception( $status_code, $args, $response, 'update' );
}
do_action( 'woocommerce_paypal_payments_after_tracking_is_updated', $order_id, $response );
}
/**
* Gets the tracking information of a given order.
*
* @param int $wc_order_id The order ID.
* @param string $tracking_number The tracking number.
*
* @return ShipmentInterface|null The tracking information.
* @throws RuntimeException If problem getting.
*/
public function get_tracking_information( int $wc_order_id, string $tracking_number ) : ?ShipmentInterface {
$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
return null;
}
$transaction_id = $wc_order->get_transaction_id();
$tracking_number = $wc_order->get_meta( '_ppcp_paypal_tracking_number', true );
$url = trailingslashit( $this->host ) . 'v1/shipping/trackers/' . $this->find_tracker_id( $transaction_id, $tracking_number );
$host = trailingslashit( $this->host );
$tracker_id = $this->find_tracker_id( $wc_order->get_transaction_id(), $tracking_number );
$url = "{$host}v1/shipping/trackers/{$tracker_id}";
$args = array(
'method' => 'GET',
@ -215,18 +273,11 @@ class OrderTrackingEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
'Could not fetch the tracking information.'
$args = array(
'args' => $args,
'response' => $response,
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
$this->throw_runtime_exception( $args, 'fetch' );
}
/**
@ -241,46 +292,39 @@ class OrderTrackingEndpoint {
return null;
}
return $this->extract_tracking_information( (array) $data );
return $this->create_shipment( $wc_order_id, (array) $data );
}
/**
* Updates the tracking information of a given order with the given data.
* Gets the list of shipments of a given order.
*
* @param array $data The tracking information to update.
* @psalm-param TrackingInfo $data
* @param int $order_id The order ID.
* @throws RuntimeException If problem updating.
* @param int $wc_order_id The order ID.
* @return ShipmentInterface[] The list of shipments.
* @throws RuntimeException If problem getting.
*/
public function update_tracking_information( array $data, int $order_id ) : void {
$tracking_info = $this->get_tracking_information( $order_id );
$transaction_id = $tracking_info['transaction_id'] ?? '';
$tracking_number = $tracking_info['tracking_number'] ?? '';
$url = trailingslashit( $this->host ) . 'v1/shipping/trackers/' . $this->find_tracker_id( $transaction_id, $tracking_number );
public function list_tracking_information( int $wc_order_id ) : ?array {
$wc_order = wc_get_order( $wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
return array();
}
$host = trailingslashit( $this->host );
$transaction_id = $wc_order->get_transaction_id();
$url = "{$host}v1/shipping/trackers?transaction_id={$transaction_id}";
$args = array(
'method' => 'PUT',
'method' => 'GET',
'headers' => $this->request_headers(),
'body' => wp_json_encode( (array) apply_filters( 'woocommerce_paypal_payments_tracking_data_before_update', $data, $order_id ) ),
);
do_action( 'woocommerce_paypal_payments_before_tracking_is_updated', $order_id, $data );
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
'Could not update order tracking information.'
$args = array(
'args' => $args,
'response' => $response,
);
$this->logger->log(
'warning',
$error->getMessage(),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
$this->throw_runtime_exception( $args, 'fetch' );
}
/**
@ -288,34 +332,20 @@ class OrderTrackingEndpoint {
*
* @psalm-suppress UndefinedMethod
*/
$json = json_decode( $response['body'] );
$data = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
$error = new PayPalApiException(
$json,
$status_code
);
$this->logger->log(
'warning',
sprintf(
'Failed to update the order tracking information. PayPal API response: %1$s',
$error->getMessage()
),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
if ( 200 !== $status_code ) {
return null;
}
$wc_order = wc_get_order( $order_id );
if ( is_a( $wc_order, WC_Order::class ) ) {
$wc_order->update_meta_data( '_ppcp_paypal_tracking_number', $data['tracking_number'] ?? '' );
$wc_order->save();
$shipments = array();
foreach ( $data->trackers as $shipment ) {
$shipments[] = $this->create_shipment( $wc_order_id, (array) $shipment );
}
do_action( 'woocommerce_paypal_payments_after_tracking_is_updated', $order_id, $response );
return $shipments;
}
/**
@ -328,33 +358,72 @@ class OrderTrackingEndpoint {
}
/**
* Extracts the needed tracking information from given data.
* Creates the shipment based on requested data.
*
* @param int $wc_order_id The WC order ID.
* @param array $data The request data map.
* @psalm-param RequestValues $data
* @return array A map of tracking information keys to values.
* @psalm-return TrackingInfo
* @throws RuntimeException If problem extracting.
* @psalm-param TrackingInfo $data
*
* @return ShipmentInterface The shipment.
* @throws RuntimeException If problem creating.
*/
protected function extract_tracking_information( array $data ): array {
if ( empty( $data['transaction_id'] ) || empty( $data['status'] ) ) {
$this->logger->log( 'warning', 'Missing transaction_id or status.' );
throw new RuntimeException( 'Missing transaction_id or status.' );
}
protected function create_shipment( int $wc_order_id, array $data ): ShipmentInterface {
$carrier = $data['carrier'] ?? '';
$tracking_info = array(
'transaction_id' => $data['transaction_id'],
'status' => $data['status'],
'transaction_id' => $data['transaction_id'] ?? '',
'status' => $data['status'] ?? '',
'tracking_number' => $data['tracking_number'] ?? '',
'carrier' => $carrier,
);
if ( ! empty( $data['tracking_number'] ) ) {
$tracking_info['tracking_number'] = $data['tracking_number'];
if ( ! empty( $data['items'] ) ) {
$tracking_info['items'] = array_map( 'intval', $data['items'] );
}
if ( ! empty( $data['carrier'] ) ) {
$tracking_info['carrier'] = $data['carrier'];
if ( $carrier === 'OTHER' ) {
$tracking_info['carrier_name_other'] = $data['carrier_name_other'] ?? '';
}
return $tracking_info;
$this->validate_tracking_info( $tracking_info );
return $this->shipment_factory->create_shipment(
$wc_order_id,
$tracking_info['transaction_id'],
$tracking_info['tracking_number'],
$tracking_info['status'],
$tracking_info['carrier'],
$tracking_info['carrier_name_other'] ?? '',
$tracking_info['items'] ?? array()
);
}
/**
* Validates the requested tracking info.
*
* @param array<string, mixed> $tracking_info A map of tracking information keys to values.
* @return void
* @throws RuntimeException If validation failed.
*/
protected function validate_tracking_info( array $tracking_info ): void {
$error_message = __( 'Missing required information:', 'woocommerce-paypal-payments' );
$empty_keys = array();
foreach ( $tracking_info as $key => $value ) {
if ( ! empty( $value ) ) {
continue;
}
$empty_keys[] = $key;
}
if ( empty( $empty_keys ) ) {
return;
}
$error_message .= implode( ' ,', $empty_keys );
throw new RuntimeException( $error_message );
}
/**
@ -379,4 +448,111 @@ class OrderTrackingEndpoint {
protected function find_tracker_id( string $transaction_id, string $tracking_number ): string {
return ! empty( $tracking_number ) ? "{$transaction_id}-{$tracking_number}" : "{$transaction_id}-NOTRACKER";
}
/**
* Saves the tracking metadata for given line items.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param string $tracking_number The tracking number.
* @param int[] $line_items The list of shipment line items.
* @return void
*/
protected function save_tracking_metadata( WC_Order $wc_order, string $tracking_number, array $line_items ): void {
$tracking_meta = $wc_order->get_meta( OrderTrackingModule::PPCP_TRACKING_INFO_META_NAME );
if ( ! is_array( $tracking_meta ) ) {
$tracking_meta = array();
}
foreach ( $line_items as $item ) {
$tracking_meta[ $tracking_number ][] = $item;
}
$wc_order->update_meta_data( OrderTrackingModule::PPCP_TRACKING_INFO_META_NAME, $tracking_meta );
$wc_order->save();
}
/**
* Generates the request data.
*
* @param WC_Order $wc_order The WC order.
* @param ShipmentInterface $shipment The shipment.
* @return array
*/
protected function generate_request_data( WC_Order $wc_order, ShipmentInterface $shipment ): array {
$paypal_order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
$host = trailingslashit( $this->host );
$shipment_data = $shipment->to_array();
$old_api_data = $shipment_data;
unset( $old_api_data['items'] );
$request_shipment_data = array( 'trackers' => array( $old_api_data ) );
if ( $this->should_use_new_api ) {
unset( $shipment_data['transaction_id'] );
$shipment_data['capture_id'] = $shipment->transaction_id();
$request_shipment_data = $shipment_data;
}
$url = $this->should_use_new_api ? "{$host}v2/checkout/orders/{$paypal_order_id}/track" : "{$host}v1/shipping/trackers";
$args = array(
'method' => 'POST',
'headers' => $this->request_headers(),
'body' => wp_json_encode( (array) apply_filters( 'woocommerce_paypal_payments_tracking_data_before_sending', $request_shipment_data, $wc_order->get_id() ) ),
);
return array(
'url' => $url,
'args' => $args,
);
}
/**
* Throws PayPal APi exception and logs the error message with given arguments.
*
* @param int $status_code The response status code.
* @param array<string, mixed> $args The arguments.
* @param array $response The request response.
* @param string $message_part The part of the message.
* @return void
*
* @throws PayPalApiException PayPal APi exception.
*/
protected function throw_paypal_api_exception( int $status_code, array $args, array $response, string $message_part ): void {
$error = new PayPalApiException(
json_decode( $response['body'] ),
$status_code
);
$this->logger->log(
'warning',
sprintf(
"Failed to {$message_part} order tracking information. PayPal API response: %s",
$error->getMessage()
),
array(
'args' => $args,
'response' => $response,
)
);
throw $error;
}
/**
* Throws the exception && logs the error message with given arguments.
*
* @param array $args The arguments.
* @param string $message_part The part of the message.
* @return void
*
* @throws RuntimeException The exception.
*/
protected function throw_runtime_exception( array $args, string $message_part ): void {
$error = new RuntimeException( "Could not {$message_part} the order tracking information." );
$this->logger->log(
'warning',
$error->getMessage(),
$args
);
throw $error;
}
}

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\OrderTracking;
use WC_Order;
use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentInterface;
use WP_Post;
/**
@ -24,15 +25,6 @@ use WP_Post;
*/
class MetaBoxRenderer {
public const NAME_PREFIX = 'ppcp-tracking';
/**
* The OrderTrackingEndpoint.
*
* @var OrderTrackingEndpoint
*/
protected $order_tracking_endpoint;
/**
* Allowed shipping statuses.
*
@ -48,85 +40,136 @@ class MetaBoxRenderer {
*/
protected $carriers;
/**
* The order tracking endpoint.
*
* @var OrderTrackingEndpoint
*/
protected $order_tracking_endpoint;
/**
* Whether new API should be used.
*
* @var bool
*/
protected $should_use_new_api;
/**
* MetaBoxRenderer constructor.
*
* @param OrderTrackingEndpoint $order_tracking_endpoint The OrderTrackingEndpoint.
* @param string[] $allowed_statuses Allowed shipping statuses.
* @param array $carriers Available shipping carriers.
* @psalm-param Carriers $carriers
* @param OrderTrackingEndpoint $order_tracking_endpoint The order tracking endpoint.
* @param bool $should_use_new_api Whether new API should be used.
*/
public function __construct(
OrderTrackingEndpoint $order_tracking_endpoint,
array $allowed_statuses,
array $carriers
array $carriers,
OrderTrackingEndpoint $order_tracking_endpoint,
bool $should_use_new_api
) {
$this->order_tracking_endpoint = $order_tracking_endpoint;
$this->allowed_statuses = $allowed_statuses;
$this->carriers = $carriers;
$this->order_tracking_endpoint = $order_tracking_endpoint;
$this->should_use_new_api = $should_use_new_api;
}
/**
* Renders the order tracking MetaBox.
*
* @param mixed $post_or_order_object Either WP_Post or WC_Order when COT is data source.
*
* @return void
*/
public function render( $post_or_order_object ): void {
$wc_order = ( $post_or_order_object instanceof WP_Post ) ? wc_get_order( $post_or_order_object->ID ) : $post_or_order_object;
if ( ! is_a( $wc_order, WC_Order::class ) ) {
if ( ! $wc_order instanceof WC_Order ) {
return;
}
$tracking_info = $this->order_tracking_endpoint->get_tracking_information( $wc_order->get_id() );
$transaction_id = $wc_order->get_transaction_id() ?: '';
$order_items = $wc_order->get_items();
$order_item_count = ! empty( $order_items ) ? count( $order_items ) : 0;
$transaction_id = $tracking_info['transaction_id'] ?? $wc_order->get_transaction_id() ?: '';
$tracking_number = $tracking_info['tracking_number'] ?? '';
$status_value = $tracking_info['status'] ?? 'SHIPPED';
$carrier_value = $tracking_info['carrier'] ?? '';
$carriers = (array) apply_filters( 'woocommerce_paypal_payments_tracking_carriers', $this->carriers, $wc_order->get_id() );
$statuses = (array) apply_filters( 'woocommerce_paypal_payments_tracking_statuses', $this->allowed_statuses, $wc_order->get_id() );
$tracking_number = (string) apply_filters( 'woocommerce_paypal_payments_tracking_number', $tracking_number, $wc_order->get_id() );
$action = ! $tracking_info ? 'create' : 'update';
/**
* The shipments
*
* @var ShipmentInterface[] $shipments
*/
$shipments = $this->order_tracking_endpoint->list_tracking_information( $wc_order->get_id() ) ?? array();
?>
<p>
<label for="<?php echo esc_attr( self::NAME_PREFIX ); ?>-transaction_id"><?php echo esc_html__( 'Transaction ID', 'woocommerce-paypal-payments' ); ?></label>
<input type="text" disabled class="<?php echo esc_attr( self::NAME_PREFIX ); ?>-transaction_id" id="<?php echo esc_attr( self::NAME_PREFIX ); ?>-transaction_id" name="<?php echo esc_attr( self::NAME_PREFIX ); ?>[transaction_id]" value="<?php echo esc_html( $transaction_id ); ?>"/></p>
<p>
<label for="<?php echo esc_attr( self::NAME_PREFIX ); ?>-tracking_number"><?php echo esc_html__( 'Tracking Number', 'woocommerce-paypal-payments' ); ?></label>
<input type="text" class="<?php echo esc_attr( self::NAME_PREFIX ); ?>-tracking_number" id="<?php echo esc_attr( self::NAME_PREFIX ); ?>-tracking_number" name="<?php echo esc_attr( self::NAME_PREFIX ); ?>[tracking_number]" value="<?php echo esc_html( $tracking_number ); ?>"/></p>
<p>
<label for="<?php echo esc_attr( self::NAME_PREFIX ); ?>-status"><?php echo esc_html__( 'Status', 'woocommerce-paypal-payments' ); ?></label>
<select class="<?php echo esc_attr( self::NAME_PREFIX ); ?>-status" id="<?php echo esc_attr( self::NAME_PREFIX ); ?>-status" name="<?php echo esc_attr( self::NAME_PREFIX ); ?>[status]">
<?php foreach ( $statuses as $key => $status ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $status_value, $key ); ?>><?php echo esc_html( $status ); ?></option>
<?php endforeach; ?>
</select>
</p>
<p>
<label for="ppcp-tracking-carrier"><?php echo esc_html__( 'Carrier', 'woocommerce-paypal-payments' ); ?></label>
<select class="ppcp-tracking-carrier" id="ppcp-tracking-carrier" name="ppcp-tracking[carrier]">
<option value=""><?php echo esc_html__( 'Select Carrier', 'woocommerce-paypal-payments' ); ?></option>
<?php
foreach ( $carriers as $carrier ) :
$country = $carrier['name'] ?? '';
$carrier_items = $carrier['items'] ?? array();
?>
<optgroup label="<?php echo esc_attr( $country ); ?>">
<?php foreach ( $carrier_items as $carrier_code => $carrier_name ) : ?>
<option value="<?php echo esc_attr( $carrier_code ); ?>" <?php selected( $carrier_value, $carrier_code ); ?>><?php echo esc_html( $carrier_name ); ?></option>
<div class="ppcp-tracking-columns-wrapper">
<div class="ppcp-tracking-column">
<h3><?php echo esc_html__( 'Add New Shipment Tracking to PayPal order', 'woocommerce-paypal-payments' ); ?></h3>
<p>
<label for="ppcp-tracking-transaction_id"><?php echo esc_html__( 'Transaction ID', 'woocommerce-paypal-payments' ); ?></label>
<input type="text" disabled class="ppcp-tracking-transaction_id disabled" id="ppcp-tracking-transaction_id" name="ppcp-tracking[transaction_id]" value="<?php echo esc_attr( $transaction_id ); ?>" />
</p>
<?php if ( $order_item_count > 1 && $this->should_use_new_api ) : ?>
<p>
<label for="include-all-items"><?php echo esc_html__( 'Include All Products', 'woocommerce-paypal-payments' ); ?></label>
<input type="checkbox" id="include-all-items" checked>
<div id="items-select-container">
<label for="ppcp-tracking-items"><?php echo esc_html__( 'Select items for this shipment', 'woocommerce-paypal-payments' ); ?></label>
<select multiple class="wc-enhanced-select ppcp-tracking-items" id="ppcp-tracking-items" name="ppcp-tracking[items]">
<?php foreach ( $order_items as $item ) : ?>
<option value="<?php echo intval( $item->get_id() ); ?>"><?php echo esc_html( $item->get_name() ); ?></option>
<?php endforeach; ?>
</select>
</div>
</p>
<?php endif; ?>
<p>
<label for="ppcp-tracking-tracking_number"><?php echo esc_html__( 'Tracking Number*', 'woocommerce-paypal-payments' ); ?></label>
<input type="text" class="ppcp-tracking-tracking_number" id="ppcp-tracking-tracking_number" name="ppcp-tracking[tracking_number]" />
</p>
<p>
<label for="ppcp-tracking-status"><?php echo esc_html__( 'Status', 'woocommerce-paypal-payments' ); ?></label>
<select class="wc-enhanced-select ppcp-tracking-status" id="ppcp-tracking-status" name="ppcp-tracking[status]">
<?php foreach ( $this->allowed_statuses as $status_key => $status ) : ?>
<option value="<?php echo esc_attr( $status_key ); ?>"><?php echo esc_html( $status ); ?></option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</p>
<input type="hidden" class="ppcp-order_id" name="<?php echo esc_attr( self::NAME_PREFIX ); ?>[order_id]" value="<?php echo (int) $wc_order->get_id(); ?>"/>
<p>
<button type="button" class="button submit_tracking_info" data-action="<?php echo esc_attr( $action ); ?>"><?php echo esc_html( ucfirst( $action ) ); ?></button></p>
</select>
</p>
<p>
<label for="ppcp-tracking-carrier"><?php echo esc_html__( 'Carrier', 'woocommerce-paypal-payments' ); ?></label>
<select class="wc-enhanced-select ppcp-tracking-carrier" id="ppcp-tracking-carrier" name="ppcp-tracking[carrier]">
<?php
foreach ( $this->carriers as $carrier ) :
if ( empty( $carrier ) ) {
continue;
}
$country = $carrier['name'] ?? '';
$carrier_items = $carrier['items'] ?? array();
?>
<optgroup label="<?php echo esc_attr( $country ); ?>">
<?php foreach ( $carrier_items as $carrier_code => $carrier_name ) : ?>
<option value="<?php echo esc_attr( $carrier_code ); ?>"><?php echo esc_html( $carrier_name ); ?></option>
<?php endforeach; ?>
</optgroup>
<?php endforeach; ?>
</select>
</p>
<p class="hidden">
<label for="ppcp-tracking-carrier_name_other"><?php echo esc_html__( 'Carrier Name*', 'woocommerce-paypal-payments' ); ?></label>
<input type="text" class="ppcp-tracking-carrier_name_other" id="ppcp-tracking-carrier_name_other" name="ppcp-tracking[carrier_name_other]" />
</p>
<input type="hidden" class="ppcp-tracking-order_id" name="ppcp-tracking[order_id]" value="<?php echo (int) $wc_order->get_id(); ?>"/>
<p><button type="button" class="button submit_tracking_info"><?php echo esc_html__( 'Add Shipment', 'woocommerce-paypal-payments' ); ?></button></p>
</div>
<div class="ppcp-tracking-column shipments">
<h3><?php echo esc_html__( 'Shipments', 'woocommerce-paypal-payments' ); ?></h3>
<?php
foreach ( $shipments as $shipment ) {
$shipment->render( $this->allowed_statuses );
}
?>
<?php if ( empty( $shipments ) ) : ?>
<p><?php echo esc_html__( 'No PayPal Shipment Tracking added to this order yet. Add new Shipment Tracking or reload the page to refresh', 'woocommerce-paypal-payments' ); ?></p>
<?php endif; ?>
</div>
<div class="blockUI blockOverlay ppcp-tracking-loader"></div>
</div>
<?php
}
}

View file

@ -10,7 +10,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking;
use Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController;
use WooCommerce\PayPalCommerce\Compat\AdminContextTrait;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use Exception;
@ -23,13 +22,14 @@ use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceHelper;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
/**
* Class OrderTrackingModule
*/
class OrderTrackingModule implements ModuleInterface {
use AdminContextTrait;
public const PPCP_TRACKING_INFO_META_NAME = '_ppcp_paypal_tracking_info_meta_name';
/**
* {@inheritDoc}
@ -48,61 +48,30 @@ class OrderTrackingModule implements ModuleInterface {
* @throws NotFoundException
*/
public function run( ContainerInterface $c ): void {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$pui_helper = $c->get( 'wcgateway.pay-upon-invoice-helper' );
assert( $pui_helper instanceof PayUponInvoiceHelper );
if ( $pui_helper->is_pui_gateway_enabled() ) {
$settings->set( 'tracking_enabled', true );
$settings->persist();
}
$tracking_enabled = $settings->has( 'tracking_enabled' ) && $settings->get( 'tracking_enabled' );
if ( ! $tracking_enabled ) {
return;
}
$tracking_enabled = $c->get( 'order-tracking.is-module-enabled' );
$endpoint = $c->get( 'order-tracking.endpoint.controller' );
assert( $endpoint instanceof OrderTrackingEndpoint );
add_action( 'wc_ajax_' . OrderTrackingEndpoint::ENDPOINT, array( $endpoint, 'handle_request' ) );
if ( ! $tracking_enabled ) {
return;
}
$asset_loader = $c->get( 'order-tracking.assets' );
assert( $asset_loader instanceof OrderEditPageAssets );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
add_action(
'admin_enqueue_scripts',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function ( $hook ) use ( $c ): void {
if ( $hook !== 'post.php' || ! $this->is_paypal_order_edit_page() ) {
return;
}
$asset_loader = $c->get( 'order-tracking.assets' );
assert( $asset_loader instanceof OrderEditPageAssets );
$asset_loader->register();
$asset_loader->enqueue();
}
);
add_action(
'wc_ajax_' . OrderTrackingEndpoint::ENDPOINT,
array( $endpoint, 'handle_request' )
);
add_action( 'init', array( $asset_loader, 'register' ) );
add_action( 'admin_enqueue_scripts', array( $asset_loader, 'enqueue' ) );
$meta_box_renderer = $c->get( 'order-tracking.meta-box.renderer' );
add_action(
'add_meta_boxes',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $post_type ) use ( $c ) {
static function() use ( $meta_box_renderer ) {
/**
* Class and function exist in WooCommerce.
*
@ -112,54 +81,17 @@ class OrderTrackingModule implements ModuleInterface {
$screen = class_exists( CustomOrdersTableController::class ) && wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
? wc_get_page_screen_id( 'shop-order' )
: 'shop_order';
if ( $post_type !== $screen || ! $this->is_paypal_order_edit_page() ) {
return;
}
$meta_box_renderer = $c->get( 'order-tracking.meta-box.renderer' );
add_meta_box(
'ppcp_order-tracking',
__( 'Tracking Information', 'woocommerce-paypal-payments' ),
__( 'PayPal Shipment Tracking', 'woocommerce-paypal-payments' ),
array( $meta_box_renderer, 'render' ),
$screen,
'side'
'normal'
);
},
10,
1
);
add_action(
'woocommerce_order_status_completed',
static function( int $order_id ) use ( $endpoint, $logger ) {
$tracking_information = $endpoint->get_tracking_information( $order_id );
if ( $tracking_information ) {
return;
}
$wc_order = wc_get_order( $order_id );
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return;
}
$transaction_id = $wc_order->get_transaction_id();
if ( empty( $transaction_id ) ) {
return;
}
$tracking_data = array(
'transaction_id' => $transaction_id,
'status' => 'SHIPPED',
);
try {
$endpoint->add_tracking_information( $tracking_data, $order_id );
} catch ( Exception $exception ) {
$logger->error( "Couldn't create tracking information: " . $exception->getMessage() );
throw $exception;
}
}
2
);
}
}

View file

@ -0,0 +1,285 @@
<?php
/**
* The Shipment.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Shipment
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking\Shipment;
use WC_Order;
use WC_Order_Item_Product;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\OrderTracking\OrderTrackingModule;
/**
* Class Shipment
*/
class Shipment implements ShipmentInterface {
/**
* The WC order ID.
*
* @var int
*/
private $wc_order_id;
/**
* The transaction ID.
*
* @var string
*/
protected $transaction_id;
/**
* The tracking number.
*
* @var string
*/
protected $tracking_number;
/**
* The shipment status.
*
* @var string
*/
protected $status;
/**
* The shipment carrier.
*
* @var string
*/
protected $carrier;
/**
* The shipment carrier name for "OTHER".
*
* @var string
*/
protected $carrier_name_other;
/**
* The list of shipment line item IDs.
*
* @var int[]
*/
protected $line_items;
/**
* Shipment constructor.
*
* @param int $wc_order_id The WC order ID.
* @param string $transaction_id The transaction ID.
* @param string $tracking_number The tracking number.
* @param string $status The shipment status.
* @param string $carrier The shipment carrier.
* @param string $carrier_name_other The shipment carrier name for "OTHER".
* @param int[] $line_items The list of shipment line item IDs.
*/
public function __construct(
int $wc_order_id,
string $transaction_id,
string $tracking_number,
string $status,
string $carrier,
string $carrier_name_other,
array $line_items
) {
$this->tracking_number = $tracking_number;
$this->status = $status;
$this->carrier = $carrier;
$this->carrier_name_other = $carrier_name_other;
$this->line_items = $line_items;
$this->transaction_id = $transaction_id;
$this->wc_order_id = $wc_order_id;
}
/**
* {@inheritDoc}
*/
public function transaction_id(): string {
return $this->transaction_id;
}
/**
* {@inheritDoc}
*/
public function tracking_number(): string {
return $this->tracking_number;
}
/**
* {@inheritDoc}
*/
public function status(): string {
return $this->status;
}
/**
* {@inheritDoc}
*/
public function carrier(): string {
return $this->carrier;
}
/**
* {@inheritDoc}
*/
public function carrier_name_other(): string {
return $this->carrier_name_other;
}
/**
* {@inheritDoc}
*/
public function line_items(): array {
$wc_order = wc_get_order( $this->wc_order_id );
if ( ! $wc_order instanceof WC_Order ) {
return array();
}
$wc_order_items = $wc_order->get_items();
$tracking_meta = $wc_order->get_meta( OrderTrackingModule::PPCP_TRACKING_INFO_META_NAME );
$saved_line_items = $tracking_meta[ $this->tracking_number() ] ?? array();
$line_items = $this->line_items ?: $saved_line_items;
$tracking_items = array();
foreach ( $wc_order_items as $item ) {
assert( $item instanceof WC_Order_Item_Product );
if ( ! empty( $line_items ) && ! in_array( $item->get_id(), $line_items, true ) ) {
continue;
}
$product = $item->get_product();
$currency = $wc_order->get_currency();
$quantity = (int) $item->get_quantity();
$price_without_tax = (float) $wc_order->get_item_subtotal( $item, false );
$price_without_tax_rounded = round( $price_without_tax, 2 );
$ppcp_order_item = new Item(
mb_substr( $item->get_name(), 0, 127 ),
new Money( $price_without_tax_rounded, $currency ),
$quantity,
$product instanceof WC_Product ? $this->prepare_description( $product->get_description() ) : '',
null,
$product instanceof WC_Product ? $product->get_sku() : '',
( $product instanceof WC_Product && $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS
);
$tracking_items[ $item->get_id() ] = $ppcp_order_item->to_array();
}
return $tracking_items;
}
/**
* {@inheritDoc}
*/
public function render( array $allowed_statuses ): void {
$carrier = $this->carrier();
$tracking_number = $this->tracking_number();
$carrier_name_other = $this->carrier_name_other();
?>
<div class="ppcp-shipment closed">
<div class="ppcp-shipment-header">
<h4><?php echo esc_html__( 'Shipment: ', 'woocommerce-paypal-payments' ); ?><?php echo esc_html( $tracking_number ); ?></h4>
<button type="button" class="shipment-toggle-indicator" aria-expanded="false">
<span class="toggle-indicator" aria-hidden="true"></span>
</button>
</div>
<div class="ppcp-shipment-info hidden">
<p><strong><?php echo esc_html__( 'Tracking Number:', 'woocommerce-paypal-payments' ); ?></strong> <span><?php echo esc_html( $tracking_number ); ?></span></p>
<p><strong><?php echo esc_html__( 'Carrier:', 'woocommerce-paypal-payments' ); ?></strong> <span><?php echo esc_html( $carrier ); ?></span></p>
<?php if ( $carrier === 'OTHER' ) : ?>
<p><strong><?php echo esc_html__( 'Carrier Name:', 'woocommerce-paypal-payments' ); ?></strong> <span><?php echo esc_html( $carrier_name_other ); ?></span></p>
<?php endif; ?>
<?php $this->render_shipment_line_item_info(); ?>
<label for="ppcp-shipment-status"><?php echo esc_html__( 'Status', 'woocommerce-paypal-payments' ); ?></label>
<select class="wc-enhanced-select ppcp-shipment-status" id="ppcp-shipment-status" name="ppcp-shipment-status">
<?php foreach ( $allowed_statuses as $status_key => $status ) : ?>
<option value="<?php echo esc_attr( $status_key ); ?>" <?php selected( $status_key, $this->status() ); ?>><?php echo esc_html( $status ); ?></option>
<?php endforeach; ?>
</select>
<input type="hidden" class="ppcp-shipment-tacking_number" name="ppcp-shipment-tacking_number" value="<?php echo esc_html( $tracking_number ); ?>"/>
<input type="hidden" class="ppcp-shipment-carrier" name="ppcp-shipment-carrier" value="<?php echo esc_html( $carrier ); ?>"/>
<input type="hidden" class="ppcp-shipment-carrier-other" name="ppcp-shipment-carrier-other" value="<?php echo esc_html( $carrier_name_other ); ?>"/>
<button type="button" class="button button-disabled update_shipment"><?php echo esc_html__( 'Update Status', 'woocommerce-paypal-payments' ); ?></button>
</div>
</div>
<?php
}
/**
* {@inheritDoc}
*/
public function to_array(): array {
$shipment = array(
'transaction_id' => $this->transaction_id(),
'tracking_number' => $this->tracking_number(),
'status' => $this->status(),
'carrier' => $this->carrier(),
'items' => array_values( $this->line_items() ),
);
if ( ! empty( $this->carrier_name_other() ) ) {
$shipment['carrier_name_other'] = $this->carrier_name_other();
}
return $shipment;
}
/**
* Cleanups the description and prepares it for sending to PayPal.
*
* @param string $description Item description.
* @return string
*/
protected function prepare_description( string $description ): string {
$description = strip_shortcodes( wp_strip_all_tags( $description ) );
return substr( $description, 0, 127 ) ?: '';
}
/**
* Renders the shipment line items info.
*
* @return void
*/
protected function render_shipment_line_item_info(): void {
$line_items = $this->line_items();
if ( empty( $line_items ) ) {
return;
}
$format = '<p><strong>%1$s</strong> <span>%2$s</span></p>';
$order_items_info = array();
foreach ( $this->line_items() as $shipment_line_item ) {
$sku = $shipment_line_item['sku'] ?? '';
$name = $shipment_line_item['name'] ?? '';
$sku_markup = sprintf(
'#<span class="wc-order-item-sku">%1$s</span>',
esc_html( $sku )
);
$order_items_info_markup = sprintf(
'<strong>%1$s</strong>%2$s',
esc_html( $name ),
$sku ? $sku_markup : ''
);
$order_items_info[] = $order_items_info_markup;
}
printf(
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$format,
esc_html__( 'Shipped Products:', 'woocommerce-paypal-payments' ),
wp_kses_post( implode( ', ', $order_items_info ) )
);
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
* The ShipmentFactory.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Shipment
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking\Shipment;
/**
* Class ShipmentFactory
*/
class ShipmentFactory implements ShipmentFactoryInterface {
/**
* {@inheritDoc}
*/
public function create_shipment(
int $wc_order_id,
string $transaction_id,
string $tracking_number,
string $status,
string $carrier,
string $carrier_name_other,
array $line_items
): ShipmentInterface {
return new Shipment( $wc_order_id, $transaction_id, $tracking_number, $status, $carrier, $carrier_name_other, $line_items );
}
}

View file

@ -0,0 +1,42 @@
<?php
/**
* The ShipmentFactory interface.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Shipment
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking\Shipment;
use RuntimeException;
/**
* Can create order tracking shipment
*/
interface ShipmentFactoryInterface {
/**
* Returns the new shipment instance.
*
* @param int $wc_order_id The WC order ID.
* @param string $transaction_id The transaction ID.
* @param string $tracking_number The tracking number.
* @param string $status The shipment status.
* @param string $carrier The shipment carrier.
* @param string $carrier_name_other The shipment carrier name for "OTHER".
* @param int[] $line_items The list of shipment line item IDs.
*
* @return ShipmentInterface
* @throws RuntimeException If problem creating.
*/
public function create_shipment(
int $wc_order_id,
string $transaction_id,
string $tracking_number,
string $status,
string $carrier,
string $carrier_name_other,
array $line_items
): ShipmentInterface;
}

View file

@ -0,0 +1,96 @@
<?php
/**
* The Shipment interface.
*
* @package WooCommerce\PayPalCommerce\OrderTracking\Shipment
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\OrderTracking\Shipment;
/**
* Represents order tracking shipment
*
* @psalm-type LineItemId = int
* @psalm-type LineItemMap = array{
* name: string,
* unit_amount: array{currency_code: string, value: string},
* quantity: int,
* description: string,
* sku: string,
* category: string,
* tax?: array{currency_code: string, value: string},
* tax_rate?: string
* }
* @psalm-type shipmentMap = array{
* transaction_id: string,
* tracking_number: string,
* status: string,
* carrier: string,
* items: array<LineItemMap>,
* carrier_name_other?: string,
* }
*/
interface ShipmentInterface {
/**
* The transaction ID.
*
* @return string
*/
public function transaction_id(): string;
/**
* The tracking number.
*
* @return string
*/
public function tracking_number(): string;
/**
* The shipment status.
*
* @return string
*/
public function status(): string;
/**
* The shipment carrier.
*
* @return string
*/
public function carrier(): string;
/**
* The shipment carrier name for "OTHER".
*
* @return string
*/
public function carrier_name_other(): string;
/**
* The list of shipment line items.
*
* @return array<int, array<string, scalar>> The map of shipment line item ID to line item map.
* @psalm-return array<LineItemId, LineItemMap>
*/
public function line_items(): array;
/**
* Renders the shipment.
*
* @param string[] $allowed_statuses Allowed shipping statuses.
*
* @return void
*/
public function render( array $allowed_statuses ): void;
/**
* Returns the object as array.
*
* @return array<string, scalar> The map of shipment object.
* @psalm-return shipmentMap
*/
public function to_array(): array;
}

View file

@ -78,8 +78,6 @@ class StatusReportModule implements ModuleInterface {
$had_ppec_plugin = PPECHelper::is_plugin_configured();
$is_tracking_available = $c->get( 'order-tracking.is-tracking-available' );
$items = array(
array(
'label' => esc_html__( 'Onboarded', 'woocommerce-paypal-payments' ),
@ -165,12 +163,6 @@ class StatusReportModule implements ModuleInterface {
$had_ppec_plugin
),
),
array(
'label' => esc_html__( 'Tracking enabled', 'woocommerce-paypal-payments' ),
'exported_label' => 'Tracking enabled',
'description' => esc_html__( 'Whether tracking is enabled on PayPal account or not.', 'woocommerce-paypal-payments' ),
'value' => $this->bool_to_html( $is_tracking_available ),
),
);
// For now only show this status if PPCP_FLAG_SUBSCRIPTIONS_API is true.

View file

@ -241,7 +241,7 @@ class RenewalHandler {
* @param \WC_Customer $customer The customer.
* @param \WC_Order $wc_order The current WooCommerce order we want to process.
*
* @return PaymentToken|null
* @return PaymentToken|null|false
*/
private function get_token_for_customer( \WC_Customer $customer, \WC_Order $wc_order ) {
/**

View file

@ -45,6 +45,12 @@ return array(
);
},
'uninstall.ppcp-all-action-names' => function( ContainerInterface $container ) : array {
return array(
'woocommerce_paypal_payments_uninstall',
);
},
'uninstall.clear-db-endpoint' => function( ContainerInterface $container ) : string {
return 'ppcp-clear-db';
},

View file

@ -31,4 +31,13 @@ class ClearDatabase implements ClearDatabaseInterface {
as_unschedule_action( $action_name );
}
}
/**
* {@inheritDoc}
*/
public function clear_actions( array $action_names ): void {
foreach ( $action_names as $action_name ) {
do_action( $action_name );
}
}
}

View file

@ -29,4 +29,12 @@ interface ClearDatabaseInterface {
*/
public function clear_scheduled_actions( array $action_names ): void;
/**
* Clears the given actions.
*
* @param string[] $action_names The list of action names.
* @throws RuntimeException If problem clearing.
*/
public function clear_actions( array $action_names ): void;
}

View file

@ -47,8 +47,9 @@ class UninstallModule implements ModuleInterface {
$clear_db_endpoint = $container->get( 'uninstall.clear-db-endpoint' );
$option_names = $container->get( 'uninstall.ppcp-all-option-names' );
$scheduled_action_names = $container->get( 'uninstall.ppcp-all-scheduled-action-names' );
$action_names = $container->get( 'uninstall.ppcp-all-action-names' );
$this->handleClearDbAjaxRequest( $request_data, $clear_db, $clear_db_endpoint, $option_names, $scheduled_action_names );
$this->handleClearDbAjaxRequest( $request_data, $clear_db, $clear_db_endpoint, $option_names, $scheduled_action_names, $action_names );
}
/**
@ -69,17 +70,19 @@ class UninstallModule implements ModuleInterface {
* @param string $nonce The nonce.
* @param string[] $option_names The list of option names.
* @param string[] $scheduled_action_names The list of scheduled action names.
* @param string[] $action_names The list of action names.
*/
protected function handleClearDbAjaxRequest(
RequestData $request_data,
ClearDatabaseInterface $clear_db,
string $nonce,
array $option_names,
array $scheduled_action_names
array $scheduled_action_names,
array $action_names
): void {
add_action(
"wc_ajax_{$nonce}",
static function () use ( $request_data, $clear_db, $nonce, $option_names, $scheduled_action_names ) {
static function () use ( $request_data, $clear_db, $nonce, $option_names, $scheduled_action_names, $action_names ) {
try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
@ -91,6 +94,7 @@ class UninstallModule implements ModuleInterface {
$clear_db->delete_options( $option_names );
$clear_db->clear_scheduled_actions( $scheduled_action_names );
$clear_db->clear_actions( $action_names );
wp_send_json_success();
return true;

View file

@ -56,10 +56,14 @@ return array(
'vaulting.payment-token-factory' => function( ContainerInterface $container ): PaymentTokenFactory {
return new PaymentTokenFactory();
},
'vaulting.payment-token-helper' => function( ContainerInterface $container ): PaymentTokenHelper {
return new PaymentTokenHelper();
},
'vaulting.payment-tokens-migration' => function( ContainerInterface $container ): PaymentTokensMigration {
return new PaymentTokensMigration(
$container->get( 'vaulting.payment-token-factory' ),
$container->get( 'vaulting.repository.payment-token' ),
$container->get( 'vaulting.payment-token-helper' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},

View file

@ -0,0 +1,35 @@
<?php
/**
* Payment Tokens helper methods.
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WC_Payment_Token;
/**
* Class PaymentTokenHelper
*/
class PaymentTokenHelper {
/**
* Checks if given token exist as WC Payment Token.
*
* @param WC_Payment_Token[] $wc_tokens WC Payment Tokens.
* @param string $token_id Payment Token ID.
* @return bool
*/
public function token_exist( array $wc_tokens, string $token_id ): bool {
foreach ( $wc_tokens as $wc_token ) {
if ( $wc_token->get_token() === $token_id ) {
return true;
}
}
return false;
}
}

View file

@ -35,6 +35,13 @@ class PaymentTokensMigration {
*/
private $payment_token_repository;
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* The logger.
*
@ -47,16 +54,19 @@ class PaymentTokensMigration {
*
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
PaymentTokenFactory $payment_token_factory,
PaymentTokenRepository $payment_token_repository,
PaymentTokenHelper $payment_token_helper,
LoggerInterface $logger
) {
$this->payment_token_factory = $payment_token_factory;
$this->payment_token_repository = $payment_token_repository;
$this->logger = $logger;
$this->payment_token_helper = $payment_token_helper;
}
/**
@ -72,7 +82,7 @@ class PaymentTokensMigration {
foreach ( $tokens as $token ) {
if ( isset( $token->source()->card ) ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, CreditCardGateway::ID );
if ( $this->token_exist( $wc_tokens, $token ) ) {
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token->id() ) ) {
$this->logger->info( 'Token already exist for user ' . (string) $id );
continue;
}
@ -97,7 +107,7 @@ class PaymentTokensMigration {
}
} elseif ( $token->source()->paypal ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, PayPalGateway::ID );
if ( $this->token_exist( $wc_tokens, $token ) ) {
if ( $this->payment_token_helper->token_exist( $wc_tokens, $token->id() ) ) {
$this->logger->info( 'Token already exist for user ' . (string) $id );
continue;
}
@ -126,21 +136,4 @@ class PaymentTokensMigration {
}
}
}
/**
* Checks if given PayPal token exist as WC Payment Token.
*
* @param array $wc_tokens WC Payment Tokens.
* @param PaymentToken $token PayPal Token ID.
* @return bool
*/
private function token_exist( array $wc_tokens, PaymentToken $token ): bool {
foreach ( $wc_tokens as $wc_token ) {
if ( $wc_token->get_token() === $token->id() ) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,51 @@
class SubElementsHandler {
constructor(element, options) {
const fieldSelector = 'input, select, textarea';
this.element = element;
this.values = options.values;
this.elements = options.elements;
this.elementsSelector = this.elements.join(',');
this.input = jQuery(this.element).is(fieldSelector)
? this.element
: jQuery(this.element).find(fieldSelector).get(0);
this.updateElementsVisibility();
jQuery(this.input).change(() => {
this.updateElementsVisibility();
});
}
updateElementsVisibility() {
const $elements = jQuery(this.elementsSelector);
let value = this.getValue(this.input);
value = (value !== null ? value.toString() : value);
if (this.values.indexOf(value) !== -1) {
$elements.show();
} else {
$elements.hide();
}
}
getValue(element) {
const $el = jQuery(element);
if ($el.is(':checkbox') || $el.is(':radio')) {
if ($el.is(':checked')) {
return $el.val();
} else {
return null;
}
} else {
return $el.val();
}
}
}
export default SubElementsHandler;

View file

@ -0,0 +1,10 @@
import moveWrappedElements from "./common/wrapped-elements";
document.addEventListener(
'DOMContentLoaded',
() => {
// Wait for current execution context to end.
setTimeout(function () {
moveWrappedElements();
}, 0);
}
);

View file

@ -0,0 +1,14 @@
// This function is needed because WordPress moves our custom notices to the global placeholder.
function moveWrappedElements() {
(($) => {
$('*[data-ppcp-wrapper]').each(function() {
let $wrapper = $('.' + $(this).data('ppcpWrapper'));
if ($wrapper.length) {
$wrapper.append(this);
}
});
})(jQuery)
}
export default moveWrappedElements;

View file

@ -4,6 +4,7 @@ import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Rendere
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
import SubElementsHandler from "./SettingsHandler/SubElementsHandler";
document.addEventListener(
'DOMContentLoaded',
@ -307,5 +308,16 @@ document.addEventListener(
createButtonPreview(() => getButtonDefaultSettings('#ppcpPayLaterButtonPreview'));
});
}
// Generic behaviours, can be moved to common.js once it's on trunk branch.
jQuery( '*[data-ppcp-handlers]' ).each( (index, el) => {
const handlers = jQuery(el).data('ppcpHandlers');
for (const handlerConfig of handlers) {
new {
SubElementsHandler: SubElementsHandler
}[handlerConfig.handler](el, handlerConfig.options)
}
});
}
);

View file

@ -212,10 +212,18 @@ return array(
return new ConnectAdminNotice( $state, $settings );
},
'wcgateway.notice.currency-unsupported' => static function ( ContainerInterface $container ): UnsupportedCurrencyAdminNotice {
$state = $container->get( 'onboarding.state' );
$shop_currency = $container->get( 'api.shop.currency' );
$supported_currencies = $container->get( 'api.supported-currencies' );
return new UnsupportedCurrencyAdminNotice( $state, $shop_currency, $supported_currencies );
$state = $container->get( 'onboarding.state' );
$shop_currency = $container->get( 'api.shop.currency' );
$supported_currencies = $container->get( 'api.supported-currencies' );
$is_wc_gateways_list_page = $container->get( 'wcgateway.is-wc-gateways-list-page' );
$is_ppcp_settings_page = $container->get( 'wcgateway.is-ppcp-settings-page' );
return new UnsupportedCurrencyAdminNotice(
$state,
$shop_currency,
$supported_currencies,
$is_wc_gateways_list_page,
$is_ppcp_settings_page
);
},
'wcgateway.notice.dcc-without-paypal' => static function ( ContainerInterface $container ): GatewayWithoutPayPalAdminNotice {
return new GatewayWithoutPayPalAdminNotice(
@ -1200,24 +1208,11 @@ return array(
'wcgateway.settings.has_enabled_separate_button_gateways' => static function ( ContainerInterface $container ): bool {
return (bool) $container->get( 'wcgateway.settings.allow_card_button_gateway' );
},
'order-tracking.is-tracking-available' => static function ( ContainerInterface $container ): bool {
try {
$bearer = $container->get( 'api.bearer' );
assert( $bearer instanceof Bearer );
$token = $bearer->bearer();
return $token->is_tracking_available();
} catch ( RuntimeException $exception ) {
return false;
}
},
'wcgateway.settings.should-disable-tracking-checkbox' => static function ( ContainerInterface $container ): bool {
$pui_helper = $container->get( 'wcgateway.pay-upon-invoice-helper' );
assert( $pui_helper instanceof PayUponInvoiceHelper );
$is_tracking_available = $container->get( 'order-tracking.is-tracking-available' );
$is_tracking_available = $container->get( 'order-tracking.is-module-enabled' );
if ( ! $is_tracking_available ) {
return true;
@ -1258,44 +1253,6 @@ return array(
return $label;
},
'wcgateway.settings.tracking-label' => static function ( ContainerInterface $container ): string {
$tracking_label = sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Enable %1$sshipment tracking information%2$s to be sent to PayPal for seller protection features.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#shipment-tracking" target="_blank">',
'</a>'
);
if ( 'DE' === $container->get( 'api.shop.country' ) ) {
$tracking_label .= '<br/>' . sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Required when %1$sPay upon Invoice%2$s is used.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#pay-upon-invoice-PUI" target="_blank">',
'</a>'
);
}
$is_tracking_available = $container->get( 'order-tracking.is-tracking-available' );
if ( $is_tracking_available ) {
return $tracking_label;
}
$tracking_label .= '<br/>' . sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__(
' To use tracking features, you must %1$senable tracking on your account%2$s.',
'woocommerce-paypal-payments'
),
'<a
href="https://docs.woocommerce.com/document/woocommerce-paypal-payments/#enable-tracking-on-your-live-account"
target="_blank"
>',
'</a>'
);
return $tracking_label;
},
'wcgateway.enable-dcc-url-sandbox' => static function ( ContainerInterface $container ): string {
return 'https://www.sandbox.paypal.com/bizsignup/entry/product/ppcp';
},

View file

@ -87,6 +87,13 @@ class SettingsPageAssets {
*/
protected $all_funding_sources;
/**
* Whether it's a settings page of this plugin.
*
* @var bool
*/
private $is_settings_page;
/**
* Assets constructor.
*
@ -100,6 +107,7 @@ class SettingsPageAssets {
* @param bool $is_pay_later_button_enabled Whether Pay Later button is enabled either for checkout, cart or product page.
* @param array $disabled_sources The list of disabled funding sources.
* @param array $all_funding_sources The list of all existing funding sources.
* @param bool $is_settings_page Whether it's a settings page of this plugin.
*/
public function __construct(
string $module_url,
@ -111,7 +119,8 @@ class SettingsPageAssets {
Environment $environment,
bool $is_pay_later_button_enabled,
array $disabled_sources,
array $all_funding_sources
array $all_funding_sources,
bool $is_settings_page
) {
$this->module_url = $module_url;
$this->version = $version;
@ -123,6 +132,7 @@ class SettingsPageAssets {
$this->is_pay_later_button_enabled = $is_pay_later_button_enabled;
$this->disabled_sources = $disabled_sources;
$this->all_funding_sources = $all_funding_sources;
$this->is_settings_page = $is_settings_page;
}
/**
@ -138,11 +148,13 @@ class SettingsPageAssets {
return;
}
if ( ! $this->is_paypal_payment_method_page() ) {
return;
if ( $this->is_settings_page ) {
$this->register_admin_assets();
}
$this->register_admin_assets();
if ( $this->is_paypal_payment_method_page() ) {
$this->register_paypal_admin_assets();
}
}
);
@ -173,9 +185,9 @@ class SettingsPageAssets {
}
/**
* Register assets for admin pages.
* Register assets for PayPal admin pages.
*/
private function register_admin_assets(): void {
private function register_paypal_admin_assets(): void {
wp_enqueue_style(
'ppcp-gateway-settings',
trailingslashit( $this->module_url ) . 'assets/css/gateway-settings.css',
@ -212,4 +224,18 @@ class SettingsPageAssets {
)
);
}
/**
* Register assets for PayPal admin pages.
*/
private function register_admin_assets(): void {
wp_enqueue_script(
'ppcp-admin-common',
trailingslashit( $this->module_url ) . 'assets/js/common.js',
array(),
$this->version,
true
);
}
}

View file

@ -44,4 +44,14 @@ class GatewayRepository {
}
);
}
/**
* Indicates if a given gateway ID is registered.
*
* @param string $gateway_id The gateway ID.
* @return bool
*/
public function exists( string $gateway_id ): bool {
return in_array( $gateway_id, $this->ppcp_gateway_ids, true );
}
}

View file

@ -290,9 +290,11 @@ class PayPalGateway extends \WC_Payment_Gateway {
// in the constructor, so must do it here.
global $theorder;
if ( $theorder instanceof WC_Order ) {
$payment_method_title = $theorder->get_payment_method_title();
if ( $payment_method_title ) {
$this->title = $payment_method_title;
if ( $theorder->get_payment_method() === self::ID ) {
$payment_method_title = $theorder->get_payment_method_title();
if ( $payment_method_title ) {
$this->title = $payment_method_title;
}
}
}
}

View file

@ -135,12 +135,12 @@ class DCCProductStatus {
$this->settings->set( 'products_dcc_enabled', true );
$this->settings->persist();
$this->current_status_cache = true;
$this->cache->set( self::DCC_STATUS_CACHE_KEY, 'true', 3 * MONTH_IN_SECONDS );
$this->cache->set( self::DCC_STATUS_CACHE_KEY, 'true', MONTH_IN_SECONDS );
return true;
}
}
$expiration = 3 * MONTH_IN_SECONDS;
$expiration = MONTH_IN_SECONDS;
if ( $this->dcc_applies->for_country_currency() ) {
$expiration = 3 * HOUR_IN_SECONDS;
}

View file

@ -127,11 +127,11 @@ class PayUponInvoiceProductStatus {
$this->settings->set( 'products_pui_enabled', true );
$this->settings->persist();
$this->current_status_cache = true;
$this->cache->set( self::PUI_STATUS_CACHE_KEY, 'true', 3 * MONTH_IN_SECONDS );
$this->cache->set( self::PUI_STATUS_CACHE_KEY, 'true', MONTH_IN_SECONDS );
return true;
}
}
$this->cache->set( self::PUI_STATUS_CACHE_KEY, 'false', 3 * MONTH_IN_SECONDS );
$this->cache->set( self::PUI_STATUS_CACHE_KEY, 'false', MONTH_IN_SECONDS );
$this->current_status_cache = false;
return false;

View file

@ -40,17 +40,42 @@ class UnsupportedCurrencyAdminNotice {
*/
private $shop_currency;
/**
* Indicates if we're on the WooCommerce gateways list page.
*
* @var bool
*/
private $is_wc_gateways_list_page;
/**
* Indicates if we're on a PPCP Settings page.
*
* @var bool
*/
private $is_ppcp_settings_page;
/**
* UnsupportedCurrencyAdminNotice constructor.
*
* @param State $state The state.
* @param string $shop_currency The shop currency.
* @param array $supported_currencies The supported currencies.
* @param bool $is_wc_gateways_list_page Indicates if we're on the WooCommerce gateways list page.
* @param bool $is_ppcp_settings_page Indicates if we're on a PPCP Settings page.
*/
public function __construct( State $state, string $shop_currency, array $supported_currencies ) {
$this->state = $state;
$this->shop_currency = $shop_currency;
$this->supported_currencies = $supported_currencies;
public function __construct(
State $state,
string $shop_currency,
array $supported_currencies,
bool $is_wc_gateways_list_page,
bool $is_ppcp_settings_page
) {
$this->state = $state;
$this->shop_currency = $shop_currency;
$this->supported_currencies = $supported_currencies;
$this->is_wc_gateways_list_page = $is_wc_gateways_list_page;
$this->is_ppcp_settings_page = $is_ppcp_settings_page;
}
/**
@ -63,16 +88,19 @@ class UnsupportedCurrencyAdminNotice {
return null;
}
$paypal_currency_support_url = 'https://developer.paypal.com/api/rest/reference/currency-codes/';
$message = sprintf(
/* translators: %1$s the shop currency, 2$s the gateway name. */
/* translators: %1$s the shop currency, %2$s the PayPal currency support page link opening HTML tag, %3$s the link ending HTML tag. */
__(
'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the <a href="%2$s">PayPal currency support page</a> for more information on supported currencies.',
'Attention: Your current WooCommerce store currency (%1$s) is not supported by PayPal. Please update your store currency to one that is supported by PayPal to ensure smooth transactions. Visit the %2$sPayPal currency support page%3$s for more information on supported currencies.',
'woocommerce-paypal-payments'
),
$this->shop_currency,
'https://developer.paypal.com/api/rest/reference/currency-codes/'
'<a href="' . esc_url( $paypal_currency_support_url ) . '">',
'</a>'
);
return new Message( $message, 'warning' );
return new Message( $message, 'warning', true, 'ppcp-notice-wrapper' );
}
/**
@ -81,7 +109,9 @@ class UnsupportedCurrencyAdminNotice {
* @return bool
*/
protected function should_display(): bool {
return $this->state->current_state() === State::STATE_ONBOARDED && ! $this->currency_supported();
return $this->state->current_state() === State::STATE_ONBOARDED
&& ! $this->currency_supported()
&& ( $this->is_wc_gateways_list_page || $this->is_ppcp_settings_page );
}
/**

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Processor;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -22,14 +23,16 @@ trait OrderMetaTrait {
/**
* Adds common metadata to the order.
*
* @param WC_Order $wc_order The WC order to which metadata will be added.
* @param Order $order The PayPal order.
* @param Environment $environment The environment.
* @param WC_Order $wc_order The WC order to which metadata will be added.
* @param Order $order The PayPal order.
* @param Environment $environment The environment.
* @param OrderTransient|null $order_transient The order transient helper.
*/
protected function add_paypal_meta(
WC_Order $wc_order,
Order $order,
Environment $environment
Environment $environment,
OrderTransient $order_transient = null
): void {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
@ -43,6 +46,8 @@ trait OrderMetaTrait {
}
$wc_order->save();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
}
/**

View file

@ -116,6 +116,13 @@ class OrderProcessor {
*/
private $order_helper;
/**
* Array to store temporary order data changes to restore after processing.
*
* @var array
*/
private $restore_order_data = array();
/**
* OrderProcessor constructor.
*
@ -292,8 +299,12 @@ class OrderProcessor {
* @return Order
*/
public function patch_order( \WC_Order $wc_order, Order $order ): Order {
$this->apply_outbound_order_filters( $wc_order );
$updated_order = $this->order_factory->from_wc_order( $wc_order, $order );
$order = $this->order_endpoint->patch_order_with( $order, $updated_order );
$this->restore_order_from_filters( $wc_order );
$order = $this->order_endpoint->patch_order_with( $order, $updated_order );
return $order;
}
@ -323,4 +334,48 @@ class OrderProcessor {
true
);
}
/**
* Applies filters to the WC_Order, so they are reflected only on PayPal Order.
*
* @param WC_Order $wc_order The WoocOmmerce Order.
* @return void
*/
private function apply_outbound_order_filters( WC_Order $wc_order ): void {
$items = $wc_order->get_items();
$this->restore_order_data['names'] = array();
foreach ( $items as $item ) {
if ( ! $item instanceof \WC_Order_Item ) {
continue;
}
$original_name = $item->get_name();
$new_name = apply_filters( 'woocommerce_paypal_payments_order_line_item_name', $original_name, $item->get_id(), $wc_order->get_id() );
if ( $new_name !== $original_name ) {
$this->restore_order_data['names'][ $item->get_id() ] = $original_name;
$item->set_name( $new_name );
}
}
}
/**
* Restores the WC_Order to it's state before filters.
*
* @param WC_Order $wc_order The WooCommerce Order.
* @return void
*/
private function restore_order_from_filters( WC_Order $wc_order ): void {
if ( is_array( $this->restore_order_data['names'] ?? null ) ) {
foreach ( $this->restore_order_data['names'] as $wc_item_id => $original_name ) {
$wc_item = $wc_order->get_item( $wc_item_id, false );
if ( $wc_item ) {
$wc_item->set_name( $original_name );
}
}
}
}
}

View file

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
@ -401,20 +402,6 @@ return function ( ContainerInterface $container, array $fields ): array {
'requirements' => array( 'pui_ready' ),
'gateway' => Settings::CONNECTION_TAB_ID,
),
'tracking_enabled' => array(
'title' => __( 'Shipment Tracking', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => $container->get( 'wcgateway.settings.tracking-label' ),
'description' => __( 'Allows to send shipment tracking numbers to PayPal for PayPal transactions.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
'input_class' => $container->get( 'wcgateway.settings.should-disable-tracking-checkbox' ) ? array( 'ppcp-disabled-checkbox' ) : array(),
),
'fraudnet_enabled' => array(
'title' => __( 'FraudNet', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
@ -496,6 +483,55 @@ return function ( ContainerInterface $container, array $fields ): array {
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
),
'subtotal_mismatch_behavior' => array(
'title' => __( 'Subtotal mismatch behavior', 'woocommerce-paypal-payments' ),
'type' => 'select',
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'vertical',
'desc_tip' => true,
'description' => __(
'Differences between WooCommerce and PayPal roundings may cause mismatch in order items subtotal calculations. If not handled, these mismatches will cause the PayPal transaction to fail.',
'woocommerce-paypal-payments'
),
'options' => array(
PurchaseUnitSanitizer::MODE_DITCH => __( 'Do not send line items to PayPal', 'woocommerce-paypal-payments' ),
PurchaseUnitSanitizer::MODE_EXTRA_LINE => __( 'Add another line item', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => Settings::CONNECTION_TAB_ID,
'custom_attributes' => array(
'data-ppcp-handlers' => wp_json_encode(
array(
array(
'handler' => 'SubElementsHandler',
'options' => array(
'values' => array( PurchaseUnitSanitizer::MODE_EXTRA_LINE ),
'elements' => array( '#field-subtotal_mismatch_line_name' ),
),
),
)
),
),
),
'subtotal_mismatch_line_name' => array(
'title' => __( 'Subtotal mismatch line name', 'woocommerce-paypal-payments' ),
'type' => 'text',
'desc_tip' => true,
'description' => __( 'The name of the extra line that will be sent to PayPal to correct the subtotal mismatch.', 'woocommerce-paypal-payments' ),
'maxlength' => 22,
'default' => '',
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'placeholder' => PurchaseUnitSanitizer::EXTRA_LINE_NAME,
'gateway' => Settings::CONNECTION_TAB_ID,
),
);
return array_merge( $fields, $connection_fields );

View file

@ -77,6 +77,8 @@ class HeaderRenderer {
'</a>
</span>
</div>
<div class="ppcp-notice-wrapper"></div>
';
}
}

View file

@ -401,9 +401,7 @@ class SettingsListener {
$this->webhook_registrar->unregister();
foreach ( $this->signup_link_ids as $key ) {
if ( $this->signup_link_cache->has( $key ) ) {
$this->signup_link_cache->delete( $key );
}
( new OnboardingUrl( $this->signup_link_cache, $key, get_current_user_id() ) )->delete();
}
}
}
@ -638,4 +636,15 @@ class SettingsListener {
throw $exception;
}
}
/**
* Handles onboarding URLs deletion
*/
public function listen_for_uninstall(): void {
// Clear onboarding links from cache.
foreach ( $this->signup_link_ids as $key ) {
( new OnboardingUrl( $this->signup_link_cache, $key, get_current_user_id() ) )->delete();
}
}
}

View file

@ -33,6 +33,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCProductStatus;
use WooCommerce\PayPalCommerce\WcGateway\Helper\PayUponInvoiceProductStatus;
@ -183,7 +184,8 @@ class WCGatewayModule implements ModuleInterface {
$c->get( 'onboarding.environment' ),
$settings_status->is_pay_later_button_enabled(),
$settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(),
$c->get( 'wcgateway.settings.funding-sources' )
$c->get( 'wcgateway.settings.funding-sources' ),
$c->get( 'wcgateway.is-ppcp-settings-page' )
);
$assets->register_assets();
}
@ -355,6 +357,14 @@ class WCGatewayModule implements ModuleInterface {
return;
}
$gateway_repository = $c->get( 'wcgateway.gateway-repository' );
assert( $gateway_repository instanceof GatewayRepository );
// Only allow to proceed if the payment method is one of our Gateways.
if ( ! $gateway_repository->exists( $wc_order->get_payment_method() ) ) {
return;
}
$intent = strtoupper( (string) $wc_order->get_meta( PayPalGateway::INTENT_META_KEY ) );
$captured = wc_string_to_bool( $wc_order->get_meta( AuthorizedPaymentsProcessor::CAPTURED_META_KEY ) );
if ( $intent !== 'AUTHORIZE' || $captured ) {
@ -391,6 +401,16 @@ class WCGatewayModule implements ModuleInterface {
3
);
add_action(
'woocommerce_paypal_payments_uninstall',
static function () use ( $c ) {
$listener = $c->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener );
$listener->listen_for_uninstall();
}
);
if ( defined( 'WP_CLI' ) && WP_CLI ) {
\WP_CLI::add_command(
'pcp settings',
@ -496,7 +516,6 @@ class WCGatewayModule implements ModuleInterface {
try {
$listener->listen_for_vaulting_enabled();
$listener->listen_for_tracking_enabled();
} catch ( RuntimeException $exception ) {
add_action(
'admin_notices',

View file

@ -6,6 +6,7 @@ module.exports = {
mode: isProduction ? 'production' : 'development',
target: 'web',
entry: {
'common': path.resolve('./resources/js/common.js'),
'gateway-settings': path.resolve('./resources/js/gateway-settings.js'),
'fraudnet': path.resolve('./resources/js/fraudnet.js'),
'oxxo': path.resolve('./resources/js/oxxo.js'),

View file

@ -80,6 +80,7 @@ return array(
$order_endpoint = $container->get( 'api.endpoint.order' );
$authorized_payments_processor = $container->get( 'wcgateway.processor.authorized-payments' );
$payment_token_factory = $container->get( 'vaulting.payment-token-factory' );
$payment_token_helper = $container->get( 'vaulting.payment-token-helper' );
$refund_fees_updater = $container->get( 'wcgateway.helper.refund-fees-updater' );
return array(
@ -95,7 +96,7 @@ return array(
new PaymentCaptureRefunded( $logger, $refund_fees_updater ),
new PaymentCaptureReversed( $logger ),
new PaymentCaptureCompleted( $logger, $order_endpoint ),
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ),
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory, $payment_token_helper ),
new VaultPaymentTokenDeleted( $logger ),
new PaymentCapturePending( $logger ),
new PaymentSaleCompleted( $logger ),

View file

@ -112,8 +112,7 @@ class PaymentCaptureRefunded implements RequestHandler {
'amount' => $request['resource']['amount']['value'],
)
);
if ( is_wp_error( $refund ) ) {
assert( $refund instanceof WP_Error );
if ( $refund instanceof WP_Error ) {
$message = sprintf(
'Order %1$s could not be refunded. %2$s',
(string) $wc_order->get_id(),

View file

@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use WC_Payment_Token_CC;
use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenHelper;
use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenPayPal;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -54,6 +55,13 @@ class VaultPaymentTokenCreated implements RequestHandler {
*/
protected $payment_token_factory;
/**
* The payment token helper.
*
* @var PaymentTokenHelper
*/
private $payment_token_helper;
/**
* VaultPaymentTokenCreated constructor.
*
@ -61,17 +69,20 @@ class VaultPaymentTokenCreated implements RequestHandler {
* @param string $prefix The prefix.
* @param AuthorizedPaymentsProcessor $authorized_payments_processor The authorized payment processor.
* @param PaymentTokenFactory $payment_token_factory The payment token factory.
* @param PaymentTokenHelper $payment_token_helper The payment token helper.
*/
public function __construct(
LoggerInterface $logger,
string $prefix,
AuthorizedPaymentsProcessor $authorized_payments_processor,
PaymentTokenFactory $payment_token_factory
PaymentTokenFactory $payment_token_factory,
PaymentTokenHelper $payment_token_helper
) {
$this->logger = $logger;
$this->prefix = $prefix;
$this->authorized_payments_processor = $authorized_payments_processor;
$this->payment_token_factory = $payment_token_factory;
$this->payment_token_helper = $payment_token_helper;
}
/**
@ -123,33 +134,39 @@ class VaultPaymentTokenCreated implements RequestHandler {
if ( ! is_null( $request['resource'] ) && isset( $request['resource']['id'] ) ) {
if ( ! is_null( $request['resource']['source'] ) && isset( $request['resource']['source']['card'] ) ) {
$token = new WC_Payment_Token_CC();
$token->set_token( $request['resource']['id'] );
$token->set_user_id( $wc_customer_id );
$token->set_gateway_id( CreditCardGateway::ID );
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_customer_id, CreditCardGateway::ID );
if ( ! $this->payment_token_helper->token_exist( $wc_tokens, $request['resource']['id'] ) ) {
$token = new WC_Payment_Token_CC();
$token->set_token( $request['resource']['id'] );
$token->set_user_id( $wc_customer_id );
$token->set_gateway_id( CreditCardGateway::ID );
$token->set_last4( $request['resource']['source']['card']['last_digits'] ?? '' );
$expiry = explode( '-', $request['resource']['source']['card']['expiry'] ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $request['resource']['source']['card']['brand'] ?? '' );
$token->save();
WC_Payment_Tokens::set_users_default( $wc_customer_id, $token->get_id() );
} elseif ( isset( $request['resource']['source']['paypal'] ) ) {
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->set_token( $request['resource']['id'] );
$payment_token_paypal->set_user_id( $wc_customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
$email = $request['resource']['source']['paypal']['payer']['email_address'] ?? '';
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
$token->set_last4( $request['resource']['source']['card']['last_digits'] ?? '' );
$expiry = explode( '-', $request['resource']['source']['card']['expiry'] ?? '' );
$token->set_expiry_year( $expiry[0] ?? '' );
$token->set_expiry_month( $expiry[1] ?? '' );
$token->set_card_type( $request['resource']['source']['card']['brand'] ?? '' );
$token->save();
WC_Payment_Tokens::set_users_default( $wc_customer_id, $token->get_id() );
}
} elseif ( isset( $request['resource']['source']['paypal'] ) ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $wc_customer_id, PayPalGateway::ID );
if ( ! $this->payment_token_helper->token_exist( $wc_tokens, $request['resource']['id'] ) ) {
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal );
$payment_token_paypal->save();
WC_Payment_Tokens::set_users_default( $wc_customer_id, $payment_token_paypal->get_id() );
$payment_token_paypal->set_token( $request['resource']['id'] );
$payment_token_paypal->set_user_id( $wc_customer_id );
$payment_token_paypal->set_gateway_id( PayPalGateway::ID );
$email = $request['resource']['source']['paypal']['payer']['email_address'] ?? '';
if ( $email && is_email( $email ) ) {
$payment_token_paypal->set_email( $email );
}
$payment_token_paypal->save();
WC_Payment_Tokens::set_users_default( $wc_customer_id, $payment_token_paypal->get_id() );
}
}
}

View file

@ -259,6 +259,11 @@
<code>DAY_IN_SECONDS</code>
</UndefinedConstant>
</file>
<file src="modules/ppcp-api-client/src/Helper/OrderTransient.php">
<UndefinedConstant occurrences="1">
<code>DAY_IN_SECONDS</code>
</UndefinedConstant>
</file>
<file src="modules/ppcp-button/services.php">
<PossiblyFalseArgument occurrences="1">
<code>realpath( __FILE__ )</code>

View file

@ -66,7 +66,9 @@ class ItemTest extends TestCase
'description',
$tax,
'sku',
'PHYSICAL_GOODS'
'PHYSICAL_GOODS',
'url',
'image_url'
);
$expected = [
@ -76,6 +78,8 @@ class ItemTest extends TestCase
'description' => 'description',
'sku' => 'sku',
'category' => 'PHYSICAL_GOODS',
'url' => 'url',
'image_url' => 'image_url',
'tax' => [2],
];

View file

@ -3,6 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\TestCase;
use Mockery;
@ -75,24 +76,43 @@ class PurchaseUnitTest extends TestCase
$this->assertEquals($expected, $testee->to_array());
}
/**
* @dataProvider dataForDitchTests
* @param array $items
* @param Amount $amount
* @param bool $doDitch
*/
public function testDitchMethod(array $items, Amount $amount, bool $doDitch, string $message)
/**
* @dataProvider dataForDitchTests
* @param array $items
* @param Amount $amount
* @param bool|array $doDitch
* @param string $message
*/
public function testDitchMethod(array $items, Amount $amount, $doDitch, string $message)
{
if (is_array($doDitch)) {
$doDitchItems = $doDitch['items'];
$doDitchBreakdown = $doDitch['breakdown'];
$doDitchTax = $doDitch['tax'];
} else {
$doDitchItems = $doDitch;
$doDitchBreakdown = $doDitch;
$doDitchTax = $doDitch;
}
$testee = new PurchaseUnit(
$amount,
$items
);
$testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_DITCH));
$array = $testee->to_array();
$resultItems = $doDitch === ! array_key_exists('items', $array);
$resultBreakdown = $doDitch === ! array_key_exists('breakdown', $array['amount']);
$resultItems = $doDitchItems === ! array_key_exists('items', $array);
$resultBreakdown = $doDitchBreakdown === ! array_key_exists('breakdown', $array['amount']);
$this->assertTrue($resultItems, $message);
$this->assertTrue($resultBreakdown, $message);
foreach ($array['items'] ?? [] as $item) {
$resultTax = $doDitchTax === ! array_key_exists('tax', $item);
$this->assertTrue($resultTax, $message);
}
}
public function dataForDitchTests() : array
@ -406,6 +426,58 @@ class PurchaseUnitTest extends TestCase
'insurance' => null,
],
],
'ditch_items_total_but_not_breakdown' => [
'message' => 'Items should be ditched because the item total does not add up. But not breakdown because it adds up.',
'ditch' => [
'items' => true,
'breakdown' => false,
'tax' => true,
],
'items' => [
[
'value' => 11,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 26,
'breakdown' => [
'item_total' => 20,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
'ditch_items_tax_with_incorrect_tax_total' => [
'message' => 'Ditch tax from items. Items should not be ditched because the mismatch is on the tax.',
'ditch' => [
'items' => false,
'breakdown' => false,
'tax' => true,
],
'items' => [
[
'value' => 10,
'quantity' => 2,
'tax' => 4,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 26,
'breakdown' => [
'item_total' => 20,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
];
$values = [];
@ -421,10 +493,16 @@ class PurchaseUnitTest extends TestCase
'tax' => $tax,
'quantity'=> $item['quantity'],
'category' => $item['category'],
'to_array' => [],
'to_array' => [
'unit_amount' => $unitAmount->to_array(),
'tax' => $tax->to_array(),
'quantity'=> $item['quantity'],
'category' => $item['category'],
],
]
);
}
$breakdown = null;
if ($test['breakdown']) {
$breakdown = Mockery::mock(AmountBreakdown::class);
@ -438,10 +516,29 @@ class PurchaseUnitTest extends TestCase
return $money;
});
}
$breakdown
->shouldReceive('to_array')
->andReturn(
array_map(
function ($value) {
return $value ? (new Money($value, 'EUR'))->to_array() : null;
},
$test['breakdown']
)
);
}
$amountMoney = new Money($test['amount'], 'EUR');
$amount = Mockery::mock(Amount::class);
$amount->shouldReceive('to_array')->andReturn(['value' => number_format( $test['amount'], 2, '.', '' ), 'breakdown' => []]);
$amount->shouldReceive('value_str')->andReturn(number_format( $test['amount'], 2, '.', '' ));
$amount
->shouldReceive('to_array')
->andReturn([
'value' => $amountMoney->value_str(),
'currency_code' => $amountMoney->currency_code(),
'breakdown' => $breakdown ? $breakdown->to_array() : [],
]);
$amount->shouldReceive('value_str')->andReturn($amountMoney->value_str());
$amount->shouldReceive('currency_code')->andReturn('EUR');
$amount->shouldReceive('breakdown')->andReturn($breakdown);
@ -456,6 +553,262 @@ class PurchaseUnitTest extends TestCase
return $values;
}
/**
* @dataProvider dataForExtraLineTests
* @param array $items
* @param Amount $amount
* @param array $expected
* @param string $message
*/
public function testExtraLineMethod(array $items, Amount $amount, array $expected, string $message)
{
$testee = new PurchaseUnit(
$amount,
$items
);
$testee->set_sanitizer(new PurchaseUnitSanitizer(PurchaseUnitSanitizer::MODE_EXTRA_LINE, $expected['extra_line_name'] ?? null));
$countItemsBefore = count($items);
$array = $testee->to_array();
$countItemsAfter = count($array['items']);
$extraItem = array_pop($array['items']);
$this->assertEquals($countItemsBefore + 1, $countItemsAfter, $message);
$this->assertEquals($expected['extra_line_value'], $extraItem['unit_amount']['value'], $message);
$this->assertEquals($expected['extra_line_name'] ?? PurchaseUnitSanitizer::EXTRA_LINE_NAME, $extraItem['name'], $message);
foreach ($array['items'] as $i => $item) {
$this->assertEquals($expected['item_value'][$i], $item['unit_amount']['value'], $message);
}
}
public function dataForExtraLineTests() : array
{
$data = [
'default' => [
'message' => 'Extra line should be added with price 0.01 and line amount 10.',
'expected' => [
'item_value' => [10],
'extra_line_value' => 0.01,
],
'items' => [
[
'value' => 10,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 26.01,
'breakdown' => [
'item_total' => 20.01,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
'with_custom_name' => [
'message' => 'Extra line should be added with price 0.01 and line amount 10.',
'expected' => [
'item_value' => [10],
'extra_line_value' => 0.01,
'extra_line_name' => 'My custom line name',
],
'items' => [
[
'value' => 10,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 26.01,
'breakdown' => [
'item_total' => 20.01,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
'with_rounding_down' => [
'message' => 'Extra line should be added with price 0.01 and line amount 10.00.',
'expected' => [
'item_value' => [10.00],
'extra_line_value' => 0.01
],
'items' => [
[
'value' => 10.005,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 26.01,
'breakdown' => [
'item_total' => 20.01,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
'with_two_rounding_down' => [
'message' => 'Extra line should be added with price 0.03 and lines amount 10.00 and 4.99.',
'expected' => [
'item_value' => [10.00, 4.99],
'extra_line_value' => 0.03
],
'items' => [
[
'value' => 10.005,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
[
'value' => 5,
'quantity' => 2,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 36.01,
'breakdown' => [
'item_total' => 30.01,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
],
'with_many_roundings_down' => [
'message' => 'Extra line should be added with price 0.01 and lines amount 10.00, 5.00 and 6.66.',
'expected' => [
'item_value' => [10.00, 4.99, 6.66],
'extra_line_value' => 0.02
],
'items' => [
[
'value' => 10.005,
'quantity' => 1,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
[
'value' => 5.001,
'quantity' => 1,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
[
'value' => 6.666,
'quantity' => 1,
'tax' => 3,
'category' => Item::PHYSICAL_GOODS,
],
],
'amount' => 27.67,
'breakdown' => [
'item_total' => 21.67,
'tax_total' => 6,
'shipping' => null,
'discount' => null,
'shipping_discount' => null,
'handling' => null,
'insurance' => null,
],
]
];
$values = [];
foreach ($data as $testKey => $test) {
$items = [];
foreach ($test['items'] as $key => $item) {
$unitAmount = new Money($item['value'], 'EUR');
$tax = new Money($item['tax'], 'EUR');
$items[$key] = Mockery::mock(
Item::class,
[
'unit_amount' => $unitAmount,
'tax' => $tax,
'quantity'=> $item['quantity'],
'category' => $item['category'],
]
);
$items[$key]->shouldReceive('to_array')->andReturnUsing(function (bool $roundToFloor = false) use ($unitAmount, $tax, $item) {
return [
'unit_amount' => $unitAmount->to_array($roundToFloor),
'tax' => $tax->to_array(),
'quantity'=> $item['quantity'],
'category' => $item['category'],
];
});
}
$breakdown = null;
if ($test['breakdown']) {
$breakdown = Mockery::mock(AmountBreakdown::class);
foreach ($test['breakdown'] as $method => $value) {
$breakdown->shouldReceive($method)->andReturnUsing(function () use ($value) {
if (! is_numeric($value)) {
return null;
}
$money = new Money($value, 'EUR');
return $money;
});
}
$breakdown
->shouldReceive('to_array')
->andReturn(
array_map(
function ($value) {
return $value ? (new Money($value, 'EUR'))->to_array() : null;
},
$test['breakdown']
)
);
}
$amountMoney = new Money($test['amount'], 'EUR');
$amount = Mockery::mock(Amount::class);
$amount
->shouldReceive('to_array')
->andReturn([
'value' => $amountMoney->value_str(),
'currency_code' => $amountMoney->currency_code(),
'breakdown' => $breakdown ? $breakdown->to_array() : [],
]);
$amount->shouldReceive('value_str')->andReturn($amountMoney->value_str());
$amount->shouldReceive('currency_code')->andReturn('EUR');
$amount->shouldReceive('breakdown')->andReturn($breakdown);
$values[$testKey] = [
$items,
$amount,
$test['expected'],
$test['message'],
];
}
return $values;
}
public function testPayee()
{
$amount = Mockery::mock(Amount::class);

View file

@ -52,6 +52,14 @@ class ItemFactoryTest extends TestCase
$woocommerce->session = $session;
$session->shouldReceive('get')->andReturn([]);
when('wp_get_attachment_image_src')->justReturn('image_url');
$product
->expects('get_image_id')
->andReturn(1);
$product
->expects('get_permalink')
->andReturn('url');
$result = $testee->from_wc_cart($cart);
$this->assertCount(1, $result);
@ -107,6 +115,13 @@ class ItemFactoryTest extends TestCase
$woocommerce->session = $session;
$session->shouldReceive('get')->andReturn([]);
when('wp_get_attachment_image_src')->justReturn('image_url');
$product
->expects('get_image_id')
->andReturn(1);
$product
->expects('get_permalink')
->andReturn('url');
$result = $testee->from_wc_cart($cart);
@ -132,6 +147,14 @@ class ItemFactoryTest extends TestCase
expect('wp_strip_all_tags')->andReturnFirstArg();
expect('strip_shortcodes')->andReturnFirstArg();
when('wp_get_attachment_image_src')->justReturn('image_url');
$product
->expects('get_image_id')
->andReturn(1);
$product
->expects('get_permalink')
->andReturn('url');
$item = Mockery::mock(\WC_Order_Item_Product::class);
$item
->expects('get_product')
@ -217,6 +240,14 @@ class ItemFactoryTest extends TestCase
->expects('get_fees')
->andReturn([]);
when('wp_get_attachment_image_src')->justReturn('image_url');
$product
->expects('get_image_id')
->andReturn(1);
$product
->expects('get_permalink')
->andReturn('url');
$result = $testee->from_wc_order($order);
$item = current($result);
/**
@ -271,6 +302,14 @@ class ItemFactoryTest extends TestCase
->expects('get_fees')
->andReturn([]);
when('wp_get_attachment_image_src')->justReturn('image_url');
$product
->expects('get_image_id')
->andReturn(1);
$product
->expects('get_permalink')
->andReturn('url');
$result = $testee->from_wc_order($order);
$item = current($result);
/**

View file

@ -3,7 +3,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Helper;
use PHPUnit\Framework\TestCase;
use WooCommerce\PayPalCommerce\TestCase;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use RuntimeException;
use function Brain\Monkey\Functions\when;
@ -15,7 +15,7 @@ class OnboardingUrlTest extends TestCase
private $user_id = 123;
private $onboardingUrl;
protected function setUp(): void
public function setUp(): void
{
parent::setUp();

View file

@ -27,8 +27,9 @@ class SettingsPagesAssetsTest extends TestCase
Mockery::mock(Environment::class),
true,
array(),
array()
);
array(),
true
);
when('is_admin')
->justReturn(true);

View file

@ -57,6 +57,7 @@ class OrderProcessorTest extends TestCase
->andReturn($payments);
$wcOrder = Mockery::mock(\WC_Order::class);
$wcOrder->expects('get_items')->andReturn([]);
$wcOrder->expects('update_meta_data')
->with(PayPalGateway::ORDER_PAYMENT_MODE_META_KEY, 'live');
$wcOrder->shouldReceive('get_id')->andReturn(1);
@ -193,7 +194,8 @@ class OrderProcessorTest extends TestCase
->andReturn($payments);
$wcOrder = Mockery::mock(\WC_Order::class);
$orderStatus = Mockery::mock(OrderStatus::class);
$wcOrder->expects('get_items')->andReturn([]);
$orderStatus = Mockery::mock(OrderStatus::class);
$orderStatus
->shouldReceive('is')
->with(OrderStatus::APPROVED)

View file

@ -364,6 +364,11 @@ class PurchaseUnitTest extends TestCase
],
self::adaptAmountFormat([
'value' => 10.69,
'breakdown' => [
'item_total' => 10.69,
'tax_total' => 0,
'shipping' => 0,
],
]),
];
}
@ -432,6 +437,11 @@ class PurchaseUnitTest extends TestCase
],
self::adaptAmountFormat([
'value' => 10.69,
'breakdown' => [
'item_total' => 10.69,
'tax_total' => 0,
'shipping' => 0,
],
], get_woocommerce_currency()),
];
}