Merge branch 'trunk' into PCP-1968-subscriptions-api-renewals

This commit is contained in:
Emili Castells Guasch 2023-09-07 12:06:27 +02:00
commit 70dc55dcc1
35 changed files with 3369 additions and 1230 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' ) {
}
}
}

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

@ -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' )
);
@ -201,10 +202,7 @@ class WebhookEndpoint {
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
$json = null;
if ( is_array( $response ) ) {
$json = json_decode( $response['body'] );
}
$json = json_decode( $response['body'] ) ?? null;
throw new PayPalApiException(
$json,
$status_code

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

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

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

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

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

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

@ -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/'),

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

@ -1208,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;
@ -1266,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

@ -402,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',

View file

@ -613,29 +613,4 @@ class SettingsListener {
}
return true;
}
/**
* Prevent enabling tracking if it is not enabled for merchant account.
*
* @throws RuntimeException When API request fails.
*/
public function listen_for_tracking_enabled(): void {
if ( State::STATE_ONBOARDED !== $this->state->current_state() ) {
return;
}
try {
$token = $this->bearer->bearer();
if ( ! $token->is_tracking_available() ) {
$this->settings->set( 'tracking_enabled', false );
$this->settings->persist();
return;
}
} catch ( RuntimeException $exception ) {
$this->settings->set( 'tracking_enabled', false );
$this->settings->persist();
throw $exception;
}
}
}

View file

@ -497,7 +497,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

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

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

@ -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);
/**