PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 8.6.0-beta.1
WooCommerce v8.6.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 / src / StoreApi / Utilities / OrderController.php
woocommerce / src / StoreApi / Utilities Last commit date
ArrayUtils.php 2 years ago CartController.php 2 years ago CheckoutTrait.php 2 years ago DraftOrderTrait.php 2 years ago JsonWebToken.php 2 years ago LocalPickupUtils.php 2 years ago NoticeHandler.php 2 years ago OrderAuthorizationTrait.php 2 years ago OrderController.php 2 years ago Pagination.php 2 years ago ProductItemTrait.php 2 years ago ProductQuery.php 2 years ago ProductQueryFilters.php 2 years ago QuantityLimits.php 2 years ago RateLimits.php 2 years ago ValidationUtils.php 2 years ago
OrderController.php
745 lines
1 <?php
2 namespace Automattic\WooCommerce\StoreApi\Utilities;
3
4 use \Exception;
5 use Automattic\WooCommerce\StoreApi\Exceptions\RouteException;
6 use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
7 use Automattic\WooCommerce\Blocks\Package;
8
9 /**
10 * OrderController class.
11 * Helper class which creates and syncs orders with the cart.
12 */
13 class OrderController {
14
15 /**
16 * Checkout fields controller.
17 *
18 * @var CheckoutFields
19 */
20 private CheckoutFields $additional_fields_controller;
21
22 /**
23 * Constructor.
24 */
25 public function __construct() {
26 $this->additional_fields_controller = Package::container()->get( CheckoutFields::class );
27 }
28
29 /**
30 * Create order and set props based on global settings.
31 *
32 * @throws RouteException Exception if invalid data is detected.
33 *
34 * @return \WC_Order A new order object.
35 */
36 public function create_order_from_cart() {
37 if ( wc()->cart->is_empty() ) {
38 throw new RouteException(
39 'woocommerce_rest_cart_empty',
40 __( 'Cannot create order from empty cart.', 'woocommerce' ),
41 400
42 );
43 }
44
45 add_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
46
47 $order = new \WC_Order();
48 $order->set_status( 'checkout-draft' );
49 $order->set_created_via( 'store-api' );
50 $this->update_order_from_cart( $order );
51
52 remove_filter( 'woocommerce_default_order_status', array( $this, 'default_order_status' ) );
53
54 return $order;
55 }
56
57 /**
58 * Update an order using data from the current cart.
59 *
60 * @param \WC_Order $order The order object to update.
61 * @param boolean $update_totals Whether to update totals or not.
62 */
63 public function update_order_from_cart( \WC_Order $order, $update_totals = true ) {
64 /**
65 * This filter ensures that local pickup locations are still used for order taxes by forcing the address used to
66 * calculate tax for an order to match the current address of the customer.
67 *
68 * - The method `$customer->get_taxable_address()` runs the filter `woocommerce_customer_taxable_address`.
69 * - While we have a session, our `ShippingController::filter_taxable_address` function uses this hook to set
70 * the customer address to the pickup location address if local pickup is the chosen method.
71 *
72 * Without this code in place, `$customer->get_taxable_address()` is not used when order taxes are calculated,
73 * resulting in the wrong taxes being applied with local pickup.
74 *
75 * The alternative would be to instead use `woocommerce_order_get_tax_location` to return the pickup location
76 * address directly, however since we have the customer filter in place we don't need to duplicate effort.
77 *
78 * @see \WC_Abstract_Order::get_tax_location()
79 */
80 add_filter(
81 'woocommerce_order_get_tax_location',
82 function( $location ) {
83
84 if ( ! is_null( wc()->customer ) ) {
85
86 $taxable_address = wc()->customer->get_taxable_address();
87
88 $location = array(
89 'country' => $taxable_address[0],
90 'state' => $taxable_address[1],
91 'postcode' => $taxable_address[2],
92 'city' => $taxable_address[3],
93 );
94 }
95
96 return $location;
97 }
98 );
99
100 // Ensure cart is current.
101 if ( $update_totals ) {
102 wc()->cart->calculate_shipping();
103 wc()->cart->calculate_totals();
104 }
105
106 // Update the current order to match the current cart.
107 $this->update_line_items_from_cart( $order );
108 $this->update_addresses_from_cart( $order );
109 $order->set_currency( get_woocommerce_currency() );
110 $order->set_prices_include_tax( 'yes' === get_option( 'woocommerce_prices_include_tax' ) );
111 $order->set_customer_id( get_current_user_id() );
112 $order->set_customer_ip_address( \WC_Geolocation::get_ip_address() );
113 $order->set_customer_user_agent( wc_get_user_agent() );
114 $order->update_meta_data( 'is_vat_exempt', wc()->cart->get_customer()->get_is_vat_exempt() ? 'yes' : 'no' );
115 $order->calculate_totals();
116 }
117
118 /**
119 * Copies order data to customer object (not the session), so values persist for future checkouts.
120 *
121 * @param \WC_Order $order Order object.
122 */
123 public function sync_customer_data_with_order( \WC_Order $order ) {
124 if ( $order->get_customer_id() ) {
125 $customer = new \WC_Customer( $order->get_customer_id() );
126 $customer->set_props(
127 array(
128 'billing_first_name' => $order->get_billing_first_name(),
129 'billing_last_name' => $order->get_billing_last_name(),
130 'billing_company' => $order->get_billing_company(),
131 'billing_address_1' => $order->get_billing_address_1(),
132 'billing_address_2' => $order->get_billing_address_2(),
133 'billing_city' => $order->get_billing_city(),
134 'billing_state' => $order->get_billing_state(),
135 'billing_postcode' => $order->get_billing_postcode(),
136 'billing_country' => $order->get_billing_country(),
137 'billing_email' => $order->get_billing_email(),
138 'billing_phone' => $order->get_billing_phone(),
139 'shipping_first_name' => $order->get_shipping_first_name(),
140 'shipping_last_name' => $order->get_shipping_last_name(),
141 'shipping_company' => $order->get_shipping_company(),
142 'shipping_address_1' => $order->get_shipping_address_1(),
143 'shipping_address_2' => $order->get_shipping_address_2(),
144 'shipping_city' => $order->get_shipping_city(),
145 'shipping_state' => $order->get_shipping_state(),
146 'shipping_postcode' => $order->get_shipping_postcode(),
147 'shipping_country' => $order->get_shipping_country(),
148 'shipping_phone' => $order->get_shipping_phone(),
149 )
150 );
151 $order_fields = $this->additional_fields_controller->get_all_fields_from_order( $order );
152
153 $customer_fields = $this->additional_fields_controller->filter_fields_for_customer( $order_fields );
154 foreach ( $customer_fields as $key => $value ) {
155 $this->additional_fields_controller->persist_field_for_customer( $key, $value, $customer );
156 }
157 $customer->save();
158 };
159 }
160
161 /**
162 * Final validation ran before payment is taken.
163 *
164 * By this point we have an order populated with customer data and items.
165 *
166 * @throws RouteException Exception if invalid data is detected.
167 * @param \WC_Order $order Order object.
168 */
169 public function validate_order_before_payment( \WC_Order $order ) {
170 $needs_shipping = wc()->cart->needs_shipping();
171 $chosen_shipping_methods = wc()->session->get( 'chosen_shipping_methods' );
172
173 $this->validate_coupons( $order );
174 $this->validate_email( $order );
175 $this->validate_selected_shipping_methods( $needs_shipping, $chosen_shipping_methods );
176 $this->validate_addresses( $order );
177 }
178
179 /**
180 * Convert a coupon code to a coupon object.
181 *
182 * @param string $coupon_code Coupon code.
183 * @return \WC_Coupon Coupon object.
184 */
185 protected function get_coupon( $coupon_code ) {
186 return new \WC_Coupon( $coupon_code );
187 }
188
189 /**
190 * Validate coupons applied to the order and remove those that are not valid.
191 *
192 * @throws RouteException Exception if invalid data is detected.
193 * @param \WC_Order $order Order object.
194 */
195 protected function validate_coupons( \WC_Order $order ) {
196 $coupon_codes = $order->get_coupon_codes();
197 $coupons = array_filter( array_map( array( $this, 'get_coupon' ), $coupon_codes ) );
198 $validators = array( 'validate_coupon_email_restriction', 'validate_coupon_usage_limit' );
199 $coupon_errors = array();
200
201 foreach ( $coupons as $coupon ) {
202 try {
203 array_walk(
204 $validators,
205 function( $validator, $index, $params ) {
206 call_user_func_array( array( $this, $validator ), $params );
207 },
208 array( $coupon, $order )
209 );
210 } catch ( Exception $error ) {
211 $coupon_errors[ $coupon->get_code() ] = $error->getMessage();
212 }
213 }
214
215 if ( $coupon_errors ) {
216 // Remove all coupons that were not valid.
217 foreach ( $coupon_errors as $coupon_code => $message ) {
218 wc()->cart->remove_coupon( $coupon_code );
219 }
220
221 // Recalculate totals.
222 wc()->cart->calculate_totals();
223
224 // Re-sync order with cart.
225 $this->update_order_from_cart( $order );
226
227 // Return exception so customer can review before payment.
228 if ( 1 === count( $coupon_errors ) ) {
229 throw new RouteException(
230 'woocommerce_rest_cart_coupon_errors',
231 sprintf(
232 /* translators: %1$s Coupon codes, %2$s Reason */
233 __( '"%1$s" was removed from the cart. %2$s', 'woocommerce' ),
234 array_keys( $coupon_errors )[0],
235 array_values( $coupon_errors )[0],
236 ),
237 409,
238 array(
239 'removed_coupons' => $coupon_errors,
240 )
241 );
242 } else {
243 throw new RouteException(
244 'woocommerce_rest_cart_coupon_errors',
245 sprintf(
246 /* translators: %s Coupon codes. */
247 __( 'Invalid coupons were removed from the cart: "%s"', 'woocommerce' ),
248 implode( '", "', array_keys( $coupon_errors ) )
249 ),
250 409,
251 array(
252 'removed_coupons' => $coupon_errors,
253 )
254 );
255 }
256 }
257 }
258
259 /**
260 * Validates the customer email. This is a required field.
261 *
262 * @throws RouteException Exception if invalid data is detected.
263 * @param \WC_Order $order Order object.
264 */
265 protected function validate_email( \WC_Order $order ) {
266 $email = $order->get_billing_email();
267
268 if ( empty( $email ) ) {
269 throw new RouteException(
270 'woocommerce_rest_missing_email_address',
271 __( 'A valid email address is required', 'woocommerce' ),
272 400
273 );
274 }
275
276 if ( ! is_email( $email ) ) {
277 throw new RouteException(
278 'woocommerce_rest_invalid_email_address',
279 sprintf(
280 /* translators: %s provided email. */
281 __( 'The provided email address (%s) is not valid—please provide a valid email address', 'woocommerce' ),
282 esc_html( $email )
283 ),
284 400
285 );
286 }
287 }
288
289 /**
290 * Validates customer address data based on the locale to ensure required fields are set.
291 *
292 * @throws RouteException Exception if invalid data is detected.
293 * @param \WC_Order $order Order object.
294 */
295 protected function validate_addresses( \WC_Order $order ) {
296 $errors = new \WP_Error();
297 $needs_shipping = wc()->cart->needs_shipping();
298 $billing_country = $order->get_billing_country();
299 $shipping_country = $order->get_shipping_country();
300
301 if ( $needs_shipping && ! $this->validate_allowed_country( $shipping_country, (array) wc()->countries->get_shipping_countries() ) ) {
302 throw new RouteException(
303 'woocommerce_rest_invalid_address_country',
304 sprintf(
305 /* translators: %s country code. */
306 __( 'Sorry, we do not ship orders to the provided country (%s)', 'woocommerce' ),
307 $shipping_country
308 ),
309 400,
310 array(
311 'allowed_countries' => array_keys( wc()->countries->get_shipping_countries() ),
312 )
313 );
314 }
315
316 if ( ! $this->validate_allowed_country( $billing_country, (array) wc()->countries->get_allowed_countries() ) ) {
317 throw new RouteException(
318 'woocommerce_rest_invalid_address_country',
319 sprintf(
320 /* translators: %s country code. */
321 __( 'Sorry, we do not allow orders from the provided country (%s)', 'woocommerce' ),
322 $billing_country
323 ),
324 400,
325 array(
326 'allowed_countries' => array_keys( wc()->countries->get_allowed_countries() ),
327 )
328 );
329 }
330
331 if ( $needs_shipping ) {
332 $this->validate_address_fields( $order, 'shipping', $errors );
333 }
334 $this->validate_address_fields( $order, 'billing', $errors );
335
336 if ( ! $errors->has_errors() ) {
337 return;
338 }
339
340 $errors_by_code = array();
341 $error_codes = $errors->get_error_codes();
342 foreach ( $error_codes as $code ) {
343 $errors_by_code[ $code ] = $errors->get_error_messages( $code );
344 }
345
346 // Surface errors from first code.
347 foreach ( $errors_by_code as $code => $error_messages ) {
348 throw new RouteException(
349 'woocommerce_rest_invalid_address',
350 sprintf(
351 /* translators: %s Address type. */
352 __( 'There was a problem with the provided %s:', 'woocommerce' ) . ' ' . implode( ', ', $error_messages ),
353 'shipping' === $code ? __( 'shipping address', 'woocommerce' ) : __( 'billing address', 'woocommerce' )
354 ),
355 400,
356 array(
357 'errors' => $errors_by_code,
358 )
359 );
360 }
361 }
362
363 /**
364 * Check all required address fields are set and return errors if not.
365 *
366 * @param string $country Country code.
367 * @param array $allowed_countries List of valid country codes.
368 * @return boolean True if valid.
369 */
370 protected function validate_allowed_country( $country, array $allowed_countries ) {
371 return array_key_exists( $country, $allowed_countries );
372 }
373
374 /**
375 * Check all required address fields are set and return errors if not.
376 *
377 * @param \WC_Order $order Order object.
378 * @param string $address_type billing or shipping address, used in error messages.
379 * @param \WP_Error $errors Error object.
380 */
381 protected function validate_address_fields( \WC_Order $order, $address_type, \WP_Error $errors ) {
382 $all_locales = wc()->countries->get_country_locale();
383 $address = $order->get_address( $address_type );
384 $current_locale = isset( $all_locales[ $address['country'] ] ) ? $all_locales[ $address['country'] ] : array();
385
386 $additional_fields = $this->additional_fields_controller->get_all_fields_from_order( $order );
387
388 foreach ( $additional_fields as $field_id => $field_value ) {
389 $prefix = '/' . $address_type . '/';
390 if ( strpos( $field_id, $prefix ) === 0 ) {
391 $address[ str_replace( $prefix, '', $field_id ) ] = $field_value;
392 }
393 }
394
395 $fields = $this->additional_fields_controller->get_additional_fields();
396 $address_fields_keys = $this->additional_fields_controller->get_address_fields_keys();
397 $address_fields = array_filter(
398 $fields,
399 function( $key ) use ( $address_fields_keys ) {
400 return in_array( $key, $address_fields_keys, true );
401 },
402 ARRAY_FILTER_USE_KEY
403 );
404
405 if ( $current_locale ) {
406 foreach ( $current_locale as $key => $field ) {
407 if ( isset( $address_fields[ $key ] ) ) {
408 $address_fields[ $key ]['label'] = isset( $field['label'] ) ? $field['label'] : $address_fields[ $key ]['label'];
409 $address_fields[ $key ]['required'] = isset( $field['required'] ) ? $field['required'] : $address_fields[ $key ]['required'];
410 }
411 }
412 }
413
414 foreach ( $address_fields as $address_field_key => $address_field ) {
415 if ( empty( $address[ $address_field_key ] ) && $address_field['required'] ) {
416 /* translators: %s Field label. */
417 $errors->add( $address_type, sprintf( __( '%s is required', 'woocommerce' ), $address_field['label'] ), $address_field_key );
418 }
419 }
420 }
421
422 /**
423 * Check email restrictions of a coupon against the order.
424 *
425 * @throws Exception Exception if invalid data is detected.
426 * @param \WC_Coupon $coupon Coupon object applied to the cart.
427 * @param \WC_Order $order Order object.
428 */
429 protected function validate_coupon_email_restriction( \WC_Coupon $coupon, \WC_Order $order ) {
430 $restrictions = $coupon->get_email_restrictions();
431
432 if ( ! empty( $restrictions ) && $order->get_billing_email() && ! wc()->cart->is_coupon_emails_allowed( array( $order->get_billing_email() ), $restrictions ) ) {
433 throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_NOT_YOURS_REMOVED ) );
434 }
435 }
436
437 /**
438 * Check usage restrictions of a coupon against the order.
439 *
440 * @throws Exception Exception if invalid data is detected.
441 * @param \WC_Coupon $coupon Coupon object applied to the cart.
442 * @param \WC_Order $order Order object.
443 */
444 protected function validate_coupon_usage_limit( \WC_Coupon $coupon, \WC_Order $order ) {
445 $coupon_usage_limit = $coupon->get_usage_limit_per_user();
446
447 if ( 0 === $coupon_usage_limit ) {
448 return;
449 }
450
451 // First, we check a logged in customer usage count, which happens against their user id, billing email, and account email.
452 if ( $order->get_customer_id() ) {
453 // We get usage per user id and associated emails.
454 $usage_count = $this->get_usage_per_aliases(
455 $coupon,
456 array(
457 $order->get_billing_email(),
458 $order->get_customer_id(),
459 $this->get_email_from_user_id( $order->get_customer_id() ),
460 )
461 );
462 } else {
463 // Otherwise we check if the email doesn't belong to an existing user.
464 $customer_data_store = \WC_Data_Store::load( 'customer' );
465
466 // This will get us any user ids for the given billing email.
467 $user_ids = $customer_data_store->get_user_ids_for_billing_email( array( $order->get_billing_email() ) );
468
469 // Convert all found user ids to a list of email addresses.
470 $user_emails = array_map( array( $this, 'get_email_from_user_id' ), $user_ids );
471
472 // This matches a user against the given billing email and gets their ID/email/billing email.
473 $found_user = get_user_by( 'email', $order->get_billing_email() );
474 if ( $found_user ) {
475 $user_ids[] = $found_user->ID;
476 $user_emails[] = $found_user->user_email;
477 $user_emails[] = get_user_meta( $found_user->ID, 'billing_email', true );
478 }
479
480 // Finally, grab usage count for all found IDs and emails.
481 $usage_count = $this->get_usage_per_aliases(
482 $coupon,
483 array_merge(
484 $user_emails,
485 $user_ids,
486 array( $order->get_billing_email() )
487 )
488 );
489 }
490
491 if ( $usage_count >= $coupon_usage_limit ) {
492 throw new Exception( $coupon->get_coupon_error( \WC_Coupon::E_WC_COUPON_USAGE_LIMIT_REACHED ) );
493 }
494 }
495
496 /**
497 * Get user email from user id.
498 *
499 * @param integer $user_id User ID.
500 * @return string Email or empty string.
501 */
502 private function get_email_from_user_id( $user_id ) {
503 $user_data = get_userdata( $user_id );
504 return $user_data ? $user_data->user_email : '';
505 }
506
507 /**
508 * Get the usage count for a coupon based on a list of aliases (ids, emails).
509 *
510 * @param \WC_Coupon $coupon Coupon object applied to the cart.
511 * @param array $aliases List of aliases to check.
512 *
513 * @return integer
514 */
515 private function get_usage_per_aliases( $coupon, $aliases ) {
516 global $wpdb;
517 $aliases = array_unique( array_filter( $aliases ) );
518 $aliases_string = "('" . implode( "','", array_map( 'esc_sql', $aliases ) ) . "')";
519 $usage_count = $wpdb->get_var(
520 $wpdb->prepare(
521 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
522 "SELECT COUNT( meta_id ) FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = '_used_by' AND meta_value IN {$aliases_string};",
523 $coupon->get_id(),
524 )
525 );
526
527 $data_store = $coupon->get_data_store();
528 // Coupons can be held for an x amount of time before being applied to an order, so we need to check if it's already being held in (maybe via another flow).
529 $tentative_usage_count = $data_store->get_tentative_usages_for_user( $coupon->get_id(), $aliases );
530 return $tentative_usage_count + $usage_count;
531 }
532
533 /**
534 * Check there is a shipping method if it requires shipping.
535 *
536 * @throws RouteException Exception if invalid data is detected.
537 * @param boolean $needs_shipping Current order needs shipping.
538 * @param array $chosen_shipping_methods Array of shipping methods.
539 */
540 public function validate_selected_shipping_methods( $needs_shipping, $chosen_shipping_methods = array() ) {
541 if ( ! $needs_shipping || ! is_array( $chosen_shipping_methods ) ) {
542 return;
543 }
544
545 foreach ( $chosen_shipping_methods as $chosen_shipping_method ) {
546 if ( false === $chosen_shipping_method ) {
547 throw new RouteException(
548 'woocommerce_rest_invalid_shipping_option',
549 __( 'Sorry, this order requires a shipping option.', 'woocommerce' ),
550 400,
551 array()
552 );
553 }
554 }
555 }
556
557 /**
558 * Validate a given order key against an existing order.
559 *
560 * @throws RouteException Exception if invalid data is detected.
561 * @param integer $order_id Order ID.
562 * @param string $order_key Order key.
563 */
564 public function validate_order_key( $order_id, $order_key ) {
565 $order = wc_get_order( $order_id );
566
567 if ( ! $order || ! $order_key || $order->get_id() !== $order_id || ! hash_equals( $order->get_order_key(), $order_key ) ) {
568 throw new RouteException( 'woocommerce_rest_invalid_order', __( 'Invalid order ID or key provided.', 'woocommerce' ), 401 );
569 }
570 }
571
572 /**
573 * Get errors for order stock on failed orders.
574 *
575 * @throws RouteException Exception if invalid data is detected.
576 * @param integer $order_id Order ID.
577 */
578 public function get_failed_order_stock_error( $order_id ) {
579 $order = wc_get_order( $order_id );
580
581 // Ensure order items are still stocked if paying for a failed order. Pending orders do not need this check because stock is held.
582 if ( ! $order->has_status( wc_get_is_pending_statuses() ) ) {
583 $quantities = array();
584
585 foreach ( $order->get_items() as $item_key => $item ) {
586 if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
587 $product = $item->get_product();
588
589 if ( ! $product ) {
590 continue;
591 }
592
593 $quantities[ $product->get_stock_managed_by_id() ] = isset( $quantities[ $product->get_stock_managed_by_id() ] ) ? $quantities[ $product->get_stock_managed_by_id() ] + $item->get_quantity() : $item->get_quantity();
594 }
595 }
596
597 // Stock levels may already have been adjusted for this order (in which case we don't need to worry about checking for low stock).
598 if ( ! $order->get_data_store()->get_stock_reduced( $order->get_id() ) ) {
599 foreach ( $order->get_items() as $item_key => $item ) {
600 if ( $item && is_callable( array( $item, 'get_product' ) ) ) {
601 $product = $item->get_product();
602
603 if ( ! $product ) {
604 continue;
605 }
606
607 /**
608 * Filters whether or not the product is in stock for this pay for order.
609 *
610 * @param boolean True if in stock.
611 * @param \WC_Product $product Product.
612 * @param \WC_Order $order Order.
613 *
614 * @since 9.8.0-dev
615 */
616 if ( ! apply_filters( 'woocommerce_pay_order_product_in_stock', $product->is_in_stock(), $product, $order ) ) {
617 return array(
618 'code' => 'woocommerce_rest_out_of_stock',
619 /* translators: %s: product name */
620 'message' => sprintf( __( 'Sorry, "%s" is no longer in stock so this order cannot be paid for. We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name() ),
621 );
622 }
623
624 // We only need to check products managing stock, with a limited stock qty.
625 if ( ! $product->managing_stock() || $product->backorders_allowed() ) {
626 continue;
627 }
628
629 // Check stock based on all items in the cart and consider any held stock within pending orders.
630 $held_stock = wc_get_held_stock_quantity( $product, $order->get_id() );
631 $required_stock = $quantities[ $product->get_stock_managed_by_id() ];
632
633 /**
634 * Filters whether or not the product has enough stock.
635 *
636 * @param boolean True if has enough stock.
637 * @param \WC_Product $product Product.
638 * @param \WC_Order $order Order.
639 *
640 * @since 9.8.0-dev
641 */
642 if ( ! apply_filters( 'woocommerce_pay_order_product_has_enough_stock', ( $product->get_stock_quantity() >= ( $held_stock + $required_stock ) ), $product, $order ) ) {
643 /* translators: 1: product name 2: quantity in stock */
644 return array(
645 'code' => 'woocommerce_rest_out_of_stock',
646 /* translators: %s: product name */
647 'message' => sprintf( __( 'Sorry, we do not have enough "%1$s" in stock to fulfill your order (%2$s available). We apologize for any inconvenience caused.', 'woocommerce' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity() - $held_stock, $product ) ),
648 );
649 }
650 }
651 }
652 }
653 }
654
655 return null;
656 }
657
658 /**
659 * Changes default order status to draft for orders created via this API.
660 *
661 * @return string
662 */
663 public function default_order_status() {
664 return 'checkout-draft';
665 }
666
667 /**
668 * Create order line items.
669 *
670 * @param \WC_Order $order The order object to update.
671 */
672 protected function update_line_items_from_cart( \WC_Order $order ) {
673 $cart_controller = new CartController();
674 $cart = $cart_controller->get_cart_instance();
675 $cart_hashes = $cart_controller->get_cart_hashes();
676
677 if ( $order->get_cart_hash() !== $cart_hashes['line_items'] ) {
678 $order->set_cart_hash( $cart_hashes['line_items'] );
679 $order->remove_order_items( 'line_item' );
680 wc()->checkout->create_order_line_items( $order, $cart );
681 }
682
683 if ( $order->get_meta_data( '_shipping_hash' ) !== $cart_hashes['shipping'] ) {
684 $order->update_meta_data( '_shipping_hash', $cart_hashes['shipping'] );
685 $order->remove_order_items( 'shipping' );
686 wc()->checkout->create_order_shipping_lines( $order, wc()->session->get( 'chosen_shipping_methods' ), wc()->shipping()->get_packages() );
687 }
688
689 if ( $order->get_meta_data( '_coupons_hash' ) !== $cart_hashes['coupons'] ) {
690 $order->remove_order_items( 'coupon' );
691 $order->update_meta_data( '_coupons_hash', $cart_hashes['coupons'] );
692 wc()->checkout->create_order_coupon_lines( $order, $cart );
693 }
694
695 if ( $order->get_meta_data( '_fees_hash' ) !== $cart_hashes['fees'] ) {
696 $order->update_meta_data( '_fees_hash', $cart_hashes['fees'] );
697 $order->remove_order_items( 'fee' );
698 wc()->checkout->create_order_fee_lines( $order, $cart );
699 }
700
701 if ( $order->get_meta_data( '_taxes_hash' ) !== $cart_hashes['taxes'] ) {
702 $order->update_meta_data( '_taxes_hash', $cart_hashes['taxes'] );
703 $order->remove_order_items( 'tax' );
704 wc()->checkout->create_order_tax_lines( $order, $cart );
705 }
706 }
707
708 /**
709 * Update address data from cart and/or customer session data.
710 *
711 * @param \WC_Order $order The order object to update.
712 */
713 protected function update_addresses_from_cart( \WC_Order $order ) {
714 $order->set_props(
715 array(
716 'billing_first_name' => wc()->customer->get_billing_first_name(),
717 'billing_last_name' => wc()->customer->get_billing_last_name(),
718 'billing_company' => wc()->customer->get_billing_company(),
719 'billing_address_1' => wc()->customer->get_billing_address_1(),
720 'billing_address_2' => wc()->customer->get_billing_address_2(),
721 'billing_city' => wc()->customer->get_billing_city(),
722 'billing_state' => wc()->customer->get_billing_state(),
723 'billing_postcode' => wc()->customer->get_billing_postcode(),
724 'billing_country' => wc()->customer->get_billing_country(),
725 'billing_email' => wc()->customer->get_billing_email(),
726 'billing_phone' => wc()->customer->get_billing_phone(),
727 'shipping_first_name' => wc()->customer->get_shipping_first_name(),
728 'shipping_last_name' => wc()->customer->get_shipping_last_name(),
729 'shipping_company' => wc()->customer->get_shipping_company(),
730 'shipping_address_1' => wc()->customer->get_shipping_address_1(),
731 'shipping_address_2' => wc()->customer->get_shipping_address_2(),
732 'shipping_city' => wc()->customer->get_shipping_city(),
733 'shipping_state' => wc()->customer->get_shipping_state(),
734 'shipping_postcode' => wc()->customer->get_shipping_postcode(),
735 'shipping_country' => wc()->customer->get_shipping_country(),
736 'shipping_phone' => wc()->customer->get_shipping_phone(),
737 )
738 );
739 $customer_fields = $this->additional_fields_controller->get_all_fields_from_customer( wc()->customer );
740 foreach ( $customer_fields as $key => $value ) {
741 $this->additional_fields_controller->persist_field_for_order( $key, $value, $order, false );
742 }
743 }
744 }
745