PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / includes / rest-api / Controllers / Version3 / class-wc-rest-paypal-standard-controller.php
woocommerce / includes / rest-api / Controllers / Version3 Last commit date
class-wc-rest-controller.php 1 year ago class-wc-rest-coupons-controller.php 4 years ago class-wc-rest-crud-controller.php 7 months ago class-wc-rest-customer-downloads-controller.php 5 years ago class-wc-rest-customers-controller.php 3 years ago class-wc-rest-data-continents-controller.php 4 months ago class-wc-rest-data-controller.php 5 years ago class-wc-rest-data-countries-controller.php 4 months ago class-wc-rest-data-currencies-controller.php 4 months ago class-wc-rest-layout-templates-controller.php 2 years ago class-wc-rest-network-orders-controller.php 5 years ago class-wc-rest-order-notes-controller.php 5 years ago class-wc-rest-order-refunds-controller.php 2 months ago class-wc-rest-orders-controller.php 2 months ago class-wc-rest-payment-gateways-controller.php 1 year ago class-wc-rest-paypal-buttons-controller.php 5 months ago class-wc-rest-paypal-standard-controller.php 3 months ago class-wc-rest-paypal-webhooks-controller.php 3 months ago class-wc-rest-posts-controller.php 2 years ago class-wc-rest-product-attribute-terms-controller.php 5 years ago class-wc-rest-product-attributes-controller.php 2 years ago class-wc-rest-product-brands-controller.php 1 year ago class-wc-rest-product-categories-controller.php 3 months ago class-wc-rest-product-custom-fields-controller.php 1 year ago class-wc-rest-product-reviews-controller.php 1 year ago class-wc-rest-product-shipping-classes-controller.php 1 year ago class-wc-rest-product-tags-controller.php 5 years ago class-wc-rest-product-variations-controller.php 1 month ago class-wc-rest-products-catalog-controller.php 7 months ago class-wc-rest-products-controller.php 2 months ago class-wc-rest-refunds-controller.php 2 years ago class-wc-rest-report-coupons-totals-controller.php 5 years ago class-wc-rest-report-customers-totals-controller.php 5 years ago class-wc-rest-report-orders-totals-controller.php 2 years ago class-wc-rest-report-products-totals-controller.php 5 years ago class-wc-rest-report-reviews-totals-controller.php 5 years ago class-wc-rest-report-sales-controller.php 5 years ago class-wc-rest-report-top-sellers-controller.php 5 years ago class-wc-rest-reports-controller.php 5 years ago class-wc-rest-setting-options-controller.php 4 months ago class-wc-rest-settings-controller.php 5 years ago class-wc-rest-shipping-methods-controller.php 5 years ago class-wc-rest-shipping-zone-locations-controller.php 5 years ago class-wc-rest-shipping-zone-methods-controller.php 5 years ago class-wc-rest-shipping-zones-controller-base.php 5 years ago class-wc-rest-shipping-zones-controller.php 5 years ago class-wc-rest-system-status-controller.php 5 years ago class-wc-rest-system-status-tools-controller.php 5 years ago class-wc-rest-tax-classes-controller.php 5 years ago class-wc-rest-taxes-controller.php 5 years ago class-wc-rest-terms-controller.php 1 month ago class-wc-rest-variations-controller.php 9 months ago class-wc-rest-webhooks-controller.php 5 years ago
class-wc-rest-paypal-standard-controller.php
401 lines
1 <?php
2 /**
3 *
4 * REST API PayPal Standard controller
5 *
6 * Handles requests to the /paypal-standard endpoint.
7 *
8 * @package WooCommerce\RestApi
9 * @since 10.3.0
10 */
11
12 declare(strict_types=1);
13
14 defined( 'ABSPATH' ) || exit;
15
16 use Automattic\WooCommerce\Gateways\PayPal\Helper as PayPalHelper;
17 use Automattic\WooCommerce\Gateways\PayPal\Request as PayPalRequest;
18 use Automattic\WooCommerce\Gateways\PayPal\Constants as PayPalConstants;
19 use Automattic\WooCommerce\Enums\OrderStatus;
20
21 if ( ! class_exists( 'WC_Gateway_Paypal' ) ) {
22 require_once WC_ABSPATH . 'includes/gateways/paypal/class-wc-gateway-paypal.php';
23 }
24
25 // Require the deprecated classes for backward compatibility.
26 // This will be removed in 11.0.0.
27 if ( ! class_exists( 'WC_Gateway_Paypal_Request' ) ) {
28 require_once WC_ABSPATH . 'includes/gateways/paypal/includes/class-wc-gateway-paypal-request.php';
29 }
30
31 /**
32 * REST API PayPal Standard controller class.
33 *
34 * @package WooCommerce\RestApi
35 * @extends WC_REST_Controller
36 */
37 class WC_REST_Paypal_Standard_Controller extends WC_REST_Controller {
38
39 /**
40 * Endpoint namespace.
41 *
42 * @var string
43 */
44 protected $namespace = 'wc/v3';
45
46 /**
47 * Route base.
48 *
49 * @var string
50 */
51 protected $rest_base = 'paypal-standard';
52
53 /**
54 * Register the routes for PayPal Standard REST API requests.
55 *
56 * @return void
57 */
58 public function register_routes() {
59 register_rest_route(
60 $this->namespace,
61 '/' . $this->rest_base . '/update-shipping',
62 array(
63 'methods' => WP_REST_Server::CREATABLE,
64 'callback' => array( $this, 'process_shipping_callback' ),
65 'permission_callback' => array( $this, 'validate_shipping_callback_request' ),
66 )
67 );
68 }
69
70 /**
71 * Validate the shipping callback request.
72 *
73 * @since 10.6.0
74 * @param WP_REST_Request<array<string, mixed>> $request The request object.
75 * @return bool True if the request is valid, false otherwise.
76 */
77 public function validate_shipping_callback_request( WP_REST_Request $request ) { // phpcs:ignore Squiz.Commenting.FunctionComment.IncorrectTypeHint
78 $purchase_units = $request->get_param( 'purchase_units' );
79 if ( empty( $purchase_units ) || empty( $purchase_units[0]['custom_id'] ) ) {
80 return false;
81 }
82
83 $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $purchase_units[0]['custom_id'] );
84 if ( ! $order ) {
85 return false;
86 }
87
88 // If shipping callback token is not stored in order meta, return true for this order as the token is not generated for the original order.
89 // We will not validate the token if the order did not generate a token in the create order request.
90 // This is done to prevent orders created before the shipping callback token feature was introduced from being blocked from updating their shipping details.
91 if ( ! $order->meta_exists( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN ) ) {
92 return true;
93 }
94
95 // Validate the token.
96 $token = $request->get_param( 'token' );
97 if ( empty( $token ) ) {
98 return false;
99 }
100
101 $shipping_callback_token = $order->get_meta( PayPalConstants::PAYPAL_ORDER_META_SHIPPING_CALLBACK_TOKEN, true );
102
103 if ( empty( $shipping_callback_token ) || ! hash_equals( $token, $shipping_callback_token ) ) {
104 return false;
105 }
106
107 return true;
108 }
109
110 /**
111 * Callback for when the customer updates their shipping details in PayPal.
112 * https://developer.paypal.com/docs/checkout/standard/customize/shipping-module/#server-side-shipping-callbacks
113 *
114 * @param WP_REST_Request $request The request object.
115 * @return WP_REST_Response The response object.
116 */
117 public function process_shipping_callback( WP_REST_Request $request ) {
118 $paypal_order_id = $request->get_param( 'id' );
119 $shipping_address = $request->get_param( 'shipping_address' );
120 $shipping_option = $request->get_param( 'shipping_option' );
121 $purchase_units = $request->get_param( 'purchase_units' );
122
123 // Note: shipping_option may or may not be present.
124 if ( empty( $paypal_order_id ) || empty( $shipping_address ) || empty( $purchase_units ) ) {
125 $response = $this->get_update_shipping_error_response();
126 return new WP_REST_Response( $response, 422 );
127 }
128
129 // Get the WC order.
130 $order = PayPalHelper::get_wc_order_from_paypal_custom_id( $purchase_units[0]['custom_id'] ?? '{}' );
131 if ( ! $order ) {
132 $custom_id = isset( $purchase_units[0]['custom_id'] ) ? $purchase_units[0]['custom_id'] : '{}';
133 WC_Gateway_Paypal::log( 'Unable to determine WooCommerce order from PayPal custom ID: ' . $custom_id );
134 $response = $this->get_update_shipping_error_response();
135 return new WP_REST_Response( $response, 422 );
136 }
137
138 // Compare PayPal order IDs.
139 $paypal_order_id_from_order_meta = $order->get_meta( '_paypal_order_id', true );
140 if ( empty( $paypal_order_id_from_order_meta ) || $paypal_order_id !== $paypal_order_id_from_order_meta ) {
141 WC_Gateway_Paypal::log(
142 'PayPal order ID mismatch. Order ID: ' . $order->get_id() .
143 '. PayPal order ID (request): ' . $paypal_order_id .
144 '. PayPal order ID (order meta): ' . $paypal_order_id_from_order_meta
145 );
146 $response = $this->get_update_shipping_error_response();
147 return new WP_REST_Response( $response, 422 );
148 }
149
150 // Validate that the order is in a valid state for shipping updates.
151 // Only draft or pending orders should accept shipping updates.
152 if ( ! in_array( $order->get_status(), array( OrderStatus::CHECKOUT_DRAFT, OrderStatus::PENDING ), true ) ) {
153 WC_Gateway_Paypal::log(
154 'Order is not in a valid state for shipping updates. Order ID: ' . $order->get_id() .
155 '. Order status: ' . $order->get_status()
156 );
157 $response = $this->get_update_shipping_error_response();
158 return new WP_REST_Response( $response, 422 );
159 }
160
161 // If the order has a PayPal transaction ID, a charge has already occurred, so we shouldn't change the shipping address.
162 $transaction_id = $order->get_transaction_id();
163 if ( ! empty( $transaction_id ) ) {
164 WC_Gateway_Paypal::log(
165 'Order already has a transaction ID, cannot update shipping. Order ID: ' . $order->get_id() .
166 '. Transaction ID: ' . $transaction_id
167 );
168 $response = $this->get_update_shipping_error_response();
169 return new WP_REST_Response( $response, 422 );
170 }
171
172 if ( ! WC()->session ) {
173 WC()->session = new WC_Session_Handler();
174 }
175 WC()->session->init();
176
177 // Update the shipping address before we do anything else.
178 $this->update_order_shipping_address( $order, $shipping_address );
179
180 // We need to rebuild the cart from the order, as we do not have session cart data
181 // for REST API requests.
182 $this->rebuild_cart_from_order( $order );
183
184 // Get the new shipping options, which depend on the new shipping address.
185 $updated_shipping_options = $this->get_updated_shipping_options( $order, $shipping_option );
186 if ( empty( $updated_shipping_options ) ) {
187 WC_Gateway_Paypal::log(
188 'No shipping options found for address. Order ID: ' . $order->get_id() .
189 '. Address: ' . wp_json_encode( $shipping_address )
190 );
191 $response = $this->get_update_shipping_error_response();
192 return new WP_REST_Response( $response, 422 );
193 }
194
195 // Set the chosen shipping method in the session.
196 if ( ! empty( $shipping_option ) ) {
197 WC()->session->set( 'chosen_shipping_methods', array( $shipping_option['id'] ) );
198 }
199
200 // Recompute fees after everything has been updated.
201 $this->recompute_fees( $order );
202
203 $paypal_request = new PayPalRequest( WC_Gateway_Paypal::get_instance() );
204 $updated_amount = $paypal_request->get_paypal_order_purchase_unit_amount( $order );
205
206 $response = array(
207 'id' => $paypal_order_id,
208 'purchase_units' => array(
209 array(
210 'reference_id' => isset( $purchase_units[0]['reference_id'] ) ? $purchase_units[0]['reference_id'] : '', // No change.
211 'amount' => $updated_amount,
212 'shipping_options' => $updated_shipping_options,
213 ),
214 ),
215 );
216
217 return new WP_REST_Response( $response, 200 );
218 }
219
220 /**
221 * Rebuild the session cart.
222 *
223 * @param WC_Order $order The order object.
224 * @return void
225 */
226 private function rebuild_cart_from_order( $order ) {
227 wc_load_cart();
228 WC()->cart->empty_cart();
229 foreach ( $order->get_items() as $item ) {
230 $product_id = $item->get_product_id();
231 $product = $item->get_product();
232
233 if ( ! $product ) {
234 continue;
235 }
236
237 if ( $product->is_type( 'variation' ) ) {
238 $variation_id = $item->get_variation_id();
239 WC()->cart->add_to_cart( $product_id, $item->get_quantity(), $variation_id );
240 continue;
241 }
242
243 WC()->cart->add_to_cart( $product_id, $item->get_quantity() );
244 }
245
246 // Re-apply coupons present on the order so discounts/totals are accurate.
247 if ( method_exists( $order, 'get_coupon_codes' ) ) {
248 foreach ( (array) $order->get_coupon_codes() as $code ) {
249 if ( $code ) {
250 WC()->cart->apply_coupon( $code );
251 }
252 }
253 }
254
255 // Re-apply shipping methods present on the order so totals are accurate.
256 $order_shipping_rate_id = $this->get_order_shipping_rate_id( $order );
257 if ( ! empty( $order_shipping_rate_id ) ) {
258 WC()->session->set( 'chosen_shipping_methods', array( $order_shipping_rate_id ) );
259 }
260 }
261
262 /**
263 * Recompute the fees for the order.
264 *
265 * @param WC_Order $order The order object.
266 * @return void
267 */
268 private function recompute_fees( $order ) {
269 WC()->cart->calculate_fees();
270 WC()->cart->calculate_shipping();
271 WC()->cart->calculate_totals();
272
273 $order->remove_order_items();
274 WC()->checkout->set_data_from_cart( $order );
275 $order->save();
276 }
277
278 /**
279 * Update the WooCommerce order with the new shipping address.
280 *
281 * @param WC_Order $order The order object.
282 * @param array $shipping_address The shipping address.
283 * @return void
284 */
285 private function update_order_shipping_address( $order, $shipping_address ) {
286 $country = $shipping_address['country_code'] ?? '';
287 $postcode = $shipping_address['postal_code'] ?? '';
288 $state = $shipping_address['admin_area_1'] ?? '';
289 $city = $shipping_address['admin_area_2'] ?? '';
290
291 $order->set_shipping_country( $country );
292 $order->set_shipping_postcode( $postcode );
293 $order->set_shipping_state( $state );
294 $order->set_shipping_city( $city );
295
296 // We do not have the address line 1 and 2 -- we are clearing them here to avoid
297 // showing stale data. The final address will be updated when the
298 // customer approves the order, via 'woocommerce_thankyou_paypal' hook.
299 $order->set_shipping_address_1( '' );
300 $order->set_shipping_address_2( '' );
301 $order->save();
302
303 // Get customer from order and update their shipping location.
304 $customer = new WC_Customer();
305 $customer->set_location( $country, $state, $postcode, $city );
306 $customer->set_shipping_location( $country, $state, $postcode, $city );
307 $customer->set_calculated_shipping( true );
308 WC()->customer = $customer;
309 }
310
311 /**
312 * Get the shipping options for the order.
313 *
314 * @param WC_Order $order The order object.
315 * @param array $selected_shipping_option The selected shipping option.
316 * @return array The shipping options.
317 */
318 private function get_updated_shipping_options( $order, $selected_shipping_option ) {
319 WC()->cart->calculate_shipping();
320 $packages = WC()->shipping()->get_packages();
321 $order_shipping_rate_id = $this->get_order_shipping_rate_id( $order );
322
323 $has_selected_shipping_option = false;
324 $options = array();
325 foreach ( $packages as $package ) {
326 $rates = $package['rates'] ?? array();
327 foreach ( $rates as $rate ) {
328 if ( ! $rate instanceof \WC_Shipping_Rate ) {
329 continue;
330 }
331
332 $shipping_option_id = $rate->get_id();
333 // If a selected shipping option is sent in the request, check if it matches the shipping option id.
334 // Otherwise, if the order has a shipping method, check if the rate id matches the shipping option id.
335 if ( isset( $selected_shipping_option['id'] ) ) {
336 $is_selected = $shipping_option_id === $selected_shipping_option['id'];
337 } else {
338 $is_selected = $shipping_option_id === $order_shipping_rate_id;
339 }
340
341 if ( $is_selected ) {
342 $has_selected_shipping_option = true;
343 }
344 $options[] = array(
345 'id' => $shipping_option_id,
346 'type' => 'SHIPPING',
347 'amount' => array(
348 'currency_code' => $order->get_currency(),
349 'value' => wc_format_decimal( (float) $rate->get_cost(), wc_get_price_decimals() ),
350 ),
351 'label' => $rate->get_label(),
352 'selected' => $is_selected,
353 );
354 }
355 }
356
357 // Set first option as selected if no option is selected.
358 if ( ! empty( $options ) && ! $has_selected_shipping_option ) {
359 $options[0]['selected'] = true;
360 }
361
362 return $options;
363 }
364
365 /**
366 * Get the shipping rate id from the order.
367 *
368 * @param WC_Order $order The order object.
369 * @return string The shipping rate id.
370 */
371 private function get_order_shipping_rate_id( $order ) {
372 $order_shipping_item = current( $order->get_items( 'shipping' ) ) ?? null;
373
374 if ( $order_shipping_item ) {
375 $method_id = $order_shipping_item->get_method_id();
376 $instance_id = $order_shipping_item->get_instance_id();
377 $rate_id = ( '' === $instance_id || null === $instance_id ) ? $method_id : "{$method_id}:{$instance_id}";
378
379 return $rate_id;
380 }
381
382 return '';
383 }
384
385 /**
386 * Get the error response for the update shipping request.
387 *
388 * @param string $issue The issue with the shipping address.
389 * @return array The error response.
390 */
391 private function get_update_shipping_error_response( $issue = 'ADDRESS_ERROR' ) {
392 // See https://developer.paypal.com/docs/checkout/standard/customize/shipping-module/#merchant-decline-response.
393 return array(
394 'name' => 'UNPROCESSABLE_ENTITY',
395 'details' => array(
396 array( 'issue' => $issue ),
397 ),
398 );
399 }
400 }
401