PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / ajax / checkout.php

checkout.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/ajax/checkout.php

647 lines 23.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Ajax;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use StoreEngine;
10 use StoreEngine\Classes\AbstractAjaxHandler;
11 use StoreEngine\Classes\AbstractRequestHandler;
12 use StoreEngine\Classes\Cache\OrderCache;
13 use StoreEngine\Classes\CheckoutService;
14 use StoreEngine\Classes\Countries;
15 use StoreEngine\Classes\Coupon;
16 use StoreEngine\Classes\Exceptions\StoreEngineException;
17 use StoreEngine\Classes\Order;
18 use StoreEngine\Classes\OrderContext;
19 use StoreEngine\models\Order as OrderModel;
20 use StoreEngine\Utils\Formatting;
21 use StoreEngine\Utils\Geolocation;
22 use StoreEngine\Utils\Helper;
23 use StoreEngine\Utils\TaxUtil;
24
25 class Checkout extends AbstractAjaxHandler {
26
27 public function __construct() {
28 $this->actions = [
29 'get_states' => [
30 'callback' => [ $this, 'get_states' ],
31 'allow_visitor_action' => true,
32 'fields' => [
33 'cc' => 'string',
34 ],
35 ],
36 'refund' => [
37 'callback' => [ $this, 'refund' ],
38 'allow_visitor_action' => false,
39 'capability' => 'manage_options',
40 'fields' => [
41 'order_id' => 'int',
42 'refund_type' => 'string',
43 'refund_amount' => 'float',
44 'refund_reason' => 'string',
45 'api_refund' => 'boolean',
46 'refunded_amount' => 'float',
47 'line_item_qtys' => 'string',
48 'line_item_totals' => 'string',
49 'line_item_tax_totals' => 'string',
50 'restock_refunded_items' => 'string',
51 ],
52 ],
53 'get_order' => [
54 'callback' => [ $this, 'get_order' ],
55 'fields' => [ 'order_id' => 'integer' ]
56 ]
57 ];
58 }
59
60 protected function get_states( $payload ) {
61 if ( empty( $payload['cc'] ) ) {
62 wp_send_json_error( esc_html__( 'Country code is required.', 'storeengine' ) );
63 }
64
65 $cc = strtoupper( $payload['cc'] );
66 $states = Countries::init()->get_states( $cc );
67
68 wp_send_json_success( [
69 'cc' => $cc,
70 'states' => $states ? $states : false,
71 ] );
72 }
73
74 /**
75 * Set checkout data.
76 *
77 * @param Order $order
78 * @param array $data
79 *
80 * @return void
81 * @throws StoreEngineException
82 */
83 protected function set_checkout_data( Order $order, array $data ): void {
84 OrderCache::delete_draft_order();
85 $same_as_shipping = $data['same_as_shipping'] ?? false;
86
87 $need_shipping = StoreEngine::init()->get_cart()->needs_shipping();
88 if ( $need_shipping && $same_as_shipping ) {
89 $data['billing_first_name'] = $data['shipping_first_name'] ?? '';
90 $data['billing_last_name'] = $data['shipping_last_name'] ?? '';
91 $data['billing_address_1'] = $data['shipping_address_1'] ?? '';
92 $data['billing_address_2'] = $data['shipping_address_2'] ?? '';
93 $data['billing_city'] = $data['shipping_city'] ?? '';
94 $data['billing_state'] = $data['shipping_state'] ?? '';
95 $data['billing_postcode'] = $data['shipping_postal_code'] ?? '';
96 $data['billing_country'] = $data['shipping_country'] ?? '';
97 $data['billing_email'] = $data['user_email'] ?? '';
98 $data['billing_phone'] = $data['shipping_phone'] ?? '';
99 }
100
101 // Set order email.
102 $order->set_order_email( $data['user_email'] ?? '' );
103
104 // Set current currency code, in case store currency changed.
105 $order->set_currency( Formatting::get_currency() );
106
107
108 // Billing data.
109 $billing_address = [
110 'billing_first_name' => $data['billing_first_name'] ?? '',
111 'billing_last_name' => $data['billing_last_name'] ?? '',
112 'billing_address_1' => $data['billing_address_1'] ?? '',
113 'billing_address_2' => $data['billing_address_2'] ?? '',
114 'billing_country' => $data['billing_country'] ?? '',
115 'billing_state' => $data['billing_state'] ?? '',
116 'billing_city' => $data['billing_city'] ?? '',
117 'billing_postcode' => $data['billing_postcode'] ?? '',
118 'billing_email' => $data['billing_email'] ?? '',
119 'billing_phone' => $data['billing_phone'] ?? '',
120 ];
121 $order->set_props( $billing_address );
122
123 if ( ! empty( $data['shipping_method'] ) ) {
124 Helper::cart()->set_meta( 'chosen_shipping_methods', [ $data['shipping_method'] ] );
125 } elseif ( $need_shipping ) {
126 Helper::cart()->set_meta( 'chosen_shipping_methods', [] );
127 }
128
129 if ( ! $need_shipping ) {
130 $shipping_address = [
131 'shipping_first_name' => $data['billing_first_name'] ?? '',
132 'shipping_last_name' => $data['billing_last_name'] ?? '',
133 'shipping_address_1' => $data['billing_address_1'] ?? '',
134 'shipping_address_2' => $data['billing_address_2'] ?? '',
135 'shipping_country' => $data['billing_country'] ?? '',
136 'shipping_state' => $data['billing_state'] ?? '',
137 'shipping_city' => $data['billing_city'] ?? '',
138 'shipping_postcode' => $data['billing_postcode'] ?? '',
139 'shipping_email' => $data['billing_email'] ?? '',
140 'shipping_phone' => $data['billing_phone'] ?? '',
141 ];
142 } else {
143 $shipping_address = [
144 'shipping_first_name' => $data['shipping_first_name'] ?? '',
145 'shipping_last_name' => $data['shipping_last_name'] ?? '',
146 'shipping_address_1' => $data['shipping_address_1'] ?? '',
147 'shipping_address_2' => $data['shipping_address_2'] ?? '',
148 'shipping_country' => $data['shipping_country'] ?? '',
149 'shipping_state' => $data['shipping_state'] ?? '',
150 'shipping_city' => $data['shipping_city'] ?? '',
151 'shipping_postcode' => $data['shipping_postal_code'] ?? '',
152 'shipping_email' => $data['shipping_email'] ?? '',
153 'shipping_phone' => $data['shipping_phone'] ?? '',
154 ];
155 }
156
157 $order->set_props( $shipping_address );
158
159 if ( isset( $data['payment_method'] ) ) {
160 $order->set_payment_method( Helper::get_payment_gateway( $data['payment_method'] ) );
161 }
162
163 // set totals.
164 $order->set_shipping_total( Helper::cart()->get_shipping_total() );
165 $order->set_shipping_tax( Helper::cart()->get_shipping_tax() );
166 $order->set_discount_total( Helper::cart()->get_discount_total() );
167 $order->set_discount_tax( Helper::cart()->get_discount_tax() );
168 $order->set_cart_tax( Helper::cart()->get_cart_contents_tax() + Helper::cart()->get_fee_tax() );
169 $order->set_total( Helper::cart()->get_total( 'edit' ) );
170
171 $order->save();
172 }
173
174 /**
175 * @return array
176 * @deprecated
177 */
178 protected function prepare_purchase_items_data(): array {
179 $cart = Helper::cart();
180 do_action( 'storeengine/cart/check_items' );
181
182 $purchase_items = [];
183 foreach ( Helper::cart()->get_cart_items() as $cart_item ) {
184 $purchase_items[] = [
185 'product_id' => $cart_item['product_id'],
186 'variation_id' => 0,
187 'price_id' => $cart_item['price_id'],
188 'price' => $cart_item['price'],
189 'product_qty' => $cart_item['quantity'],
190 'coupon_amount' => $cart->get_total_discount(),
191 'tax_amount' => $cart->get_taxes_total( true, false ),
192 'shipping_amount' => 0,
193 'shipping_tax_amount' => 0,
194 ];
195 }
196
197 return [ $cart, $purchase_items ];
198 }
199
200 /**
201 * Prepare order data.
202 *
203 * @param array $data Data.
204 * @param array $purchase_items Purchase Items.
205 *
206 * @return array
207 * @deprecated 1.5.8
208 */
209 protected function prepare_order_data( array $data, array $purchase_items ): array {
210 return [
211 'status' => 'draft',
212 'currency' => Helper::get_settings( 'store_currency', 'USD' ),
213 'type' => Helper::cart()->has_subscription_product() ? 'subscription' : 'onetime',
214 'tax_amount' => Helper::cart()->get_taxes_total( true, false ),
215 'total_amount' => Helper::cart()->get_total( 'draft_order' ),
216 'customer_id' => get_current_user_id(),
217 'billing_email' => $data['billing_email'],
218 'payment_method' => $data['payment_method'] ?? '',
219 'payment_method_title' => $data['payment_method'] ?? '',
220 'customer_note' => $data['order_note'] ?? '',
221 'transaction_id' => null,
222 'purchase_items' => $purchase_items,
223 'order_billing_address' => [
224 'first_name' => $data['billing_first_name'] ?? '',
225 'last_name' => $data['billing_last_name'] ?? '',
226 'company' => $data['billing_company'] ?? '',
227 'address_1' => $data['billing_address_1'] ?? '',
228 'address_2' => $data['billing_address_2'] ?? '',
229 'city' => $data['billing_city'] ?? '',
230 'state' => $data['billing_state'] ?? '',
231 'postcode' => $data['billing_postcode'] ?? '',
232 'country' => $data['billing_country'] ?? '',
233 'email' => $data['billing_email'] ?? '',
234 'phone' => $data['billing_phone'] ?? '',
235 ],
236 'order_shipping_address' => [
237 'first_name' => $data['billing_first_name'] ?? '',
238 'last_name' => $data['billing_last_name'] ?? '',
239 'company' => $data['billing_company'] ?? '',
240 'address_1' => $data['billing_address_1'] ?? '',
241 'address_2' => $data['billing_address_2'] ?? '',
242 'city' => $data['billing_city'] ?? '',
243 'state' => $data['billing_state'] ?? '',
244 'postcode' => $data['billing_postcode'] ?? '',
245 'country' => $data['billing_country'] ?? '',
246 'email' => $data['billing_email'] ?? '',
247 'phone' => $data['billing_phone'] ?? '',
248 ],
249 ];
250 }
251
252 public function get_order( array $payload ) {
253 if ( empty( $payload['order_id'] ) ) {
254 wp_send_json_error( esc_html__( 'Order ID is required.', 'storeengine' ) );
255 }
256
257 $order = Helper::get_order( $payload['order_id'] );
258
259 if ( ! current_user_can( 'manage_orders' ) && get_current_user_id() !== $order->get_customer_id() ) {
260 wp_send_json_error( esc_html__( "You doesn't have enough permission to view this order.", 'storeengine' ) );
261 }
262
263 $result = CheckoutService::prepare_checkout_response( $order, [] );
264
265 wp_send_json_success( $result['order'] );
266 }
267
268 protected function create_or_update_customer( Order $order ) {
269 $customer_id = $order->get_customer_id();
270 if ( ! $customer_id ) {
271 $customer_id = $this->create_customer( $order->get_billing_first_name(), $order->get_billing_last_name(), $order->get_order_email() );
272
273 if ( is_wp_error( $customer_id ) ) {
274 return $customer_id;
275 } else {
276 $order->set_customer_id( $customer_id );
277 $order->save();
278 }
279 }
280
281 $customer = Helper::get_customer( $customer_id );
282
283 // Save Billing details.
284 $customer->set_billing_first_name( $order->get_billing_first_name() );
285 $customer->set_billing_last_name( $order->get_billing_last_name() );
286 $customer->set_billing_address_1( $order->get_billing_address_1() );
287 $customer->set_billing_address_2( $order->get_billing_address_2() );
288 $customer->set_billing_city( $order->get_billing_city() );
289 $customer->set_billing_state( $order->get_billing_state() );
290 $customer->set_billing_postcode( $order->get_billing_postcode() );
291 $customer->set_billing_country( $order->get_billing_country() );
292 $customer->set_billing_phone( $order->get_billing_phone() );
293 $customer->set_billing_email( $order->get_billing_email() );
294
295 if ( $order->needs_shipping_address() ) {
296 // Save Shipping details.
297 $customer->set_shipping_first_name( $order->get_shipping_first_name() );
298 $customer->set_shipping_last_name( $order->get_shipping_last_name() );
299 $customer->set_shipping_address_1( $order->get_shipping_address_1() );
300 $customer->set_shipping_address_2( $order->get_shipping_address_2() );
301 $customer->set_shipping_city( $order->get_shipping_city() );
302 $customer->set_shipping_state( $order->get_shipping_state() );
303 $customer->set_shipping_postcode( $order->get_shipping_postcode() );
304 $customer->set_shipping_country( $order->get_shipping_country() );
305 $customer->set_shipping_phone( $order->get_shipping_phone() );
306 $customer->set_shipping_email( $order->get_shipping_email() );
307 }
308
309 $customer->save();
310
311 return $customer;
312 }
313
314 protected function create_customer( $first_name, $last_name, $email ) {
315 $email_exists = email_exists( $email );
316 if ( $email_exists ) {
317 // An account already uses this email. A guest checkout must NOT silently
318 // bind to (and then overwrite) another user's customer record — otherwise
319 // anyone who knows a victim's email could attach an order to their account
320 // and clobber their stored billing/shipping data. Require the shopper to
321 // log in (mirrors the conventional storefront behaviour). A logged-in request that already matches
322 // its own account falls through to the normal update path.
323 if ( get_current_user_id() === (int) $email_exists ) {
324 return $email_exists;
325 }
326
327 return new \WP_Error(
328 'storeengine_email_belongs_to_existing_account',
329 __( 'An account is already registered with this email address. Please log in to continue.', 'storeengine' ),
330 [
331 // Surface an actionable login URL (returns to checkout, email
332 // pre-filled) so the UI can render a real "Log in to continue"
333 // button next to the message instead of a dead-end error.
334 'status' => 409,
335 'login_url' => storeengine_checkout_login_url( $email ),
336 ]
337 );
338 }
339
340 $base_username = strstr( $email, '@', true );
341 $username = $base_username;
342 $counter = 1;
343
344 while ( username_exists( $username ) ) {
345 $username = $base_username . $counter;
346 $counter ++;
347 }
348
349 $userdata = apply_filters( 'storeengine/checkout/create_customer_data', [
350 'user_login' => $username,
351 'user_email' => $email,
352 'role' => 'storeengine_customer',
353 'display_name' => $first_name . ' ' . $last_name,
354 'first_name' => $first_name,
355 'last_name' => $last_name,
356 ] );
357
358 // Don't pass password through any filter.
359 $userdata['user_pass'] = wp_generate_password();
360
361 $user_id = wp_insert_user( $userdata );
362
363 if ( ! is_wp_error( $user_id ) ) {
364 wp_signon( array(
365 'user_login' => $username,
366 'user_password' => $userdata['user_pass'],
367 'remember' => true,
368 ), is_ssl() );
369
370 do_action( 'storeengine/checkout/customer_created', $user_id, $userdata );
371 }
372
373 return $user_id;
374 }
375
376 /**
377 * @param Coupon[] $coupons
378 *
379 * @return void
380 */
381 protected function update_coupon_usage( array $coupons, Order $order ) {
382 foreach ( $coupons as $coupon ) {
383 add_post_meta( $coupon->get_id(), '_storeengine_coupon_used_by', $order->get_customer_id() );
384 $usage_count = (int) get_post_meta( $coupon->get_id(), '_storeengine_coupon_usage_count', true );
385 update_post_meta( $coupon->get_id(), '_storeengine_coupon_usage_count', $usage_count + 1 );
386 }
387 }
388
389 private function validate_checkout_data( array $data ) {
390 $required_fields = [
391 'billing_first_name',
392 'billing_last_name',
393 'billing_address_1',
394 'billing_city',
395 'billing_postcode',
396 'billing_country',
397 'billing_email',
398 'billing_phone',
399 ];
400 $missing_fields = [];
401
402 $cart = Helper::cart();
403 if ( $cart->needs_payment() ) {
404 $required_fields[] = 'payment_method';
405 }
406
407 // loop through all the fields and check if they are empty if empty return error for that field
408 foreach ( $required_fields as $field ) {
409 if ( empty( $data[ $field ] ) ) {
410 $missing_fields[] = $field;
411 }
412 }
413
414 if ( ! empty( $missing_fields ) ) {
415 // Name the actual missing fields so the shopper knows exactly what to
416 // fix, matching CheckoutService::validate().
417 $labels_by_key = [];
418 foreach ( \StoreEngine\Utils\CheckoutFields::all() as $row ) {
419 $labels_by_key[ $row['payload_key'] ] = $row['label'];
420 }
421 $labels_by_key['payment_method'] = __( 'Payment method', 'storeengine' );
422
423 $missing_labels = array_values( array_unique( array_map(
424 static function ( $key ) use ( $labels_by_key ) {
425 return $labels_by_key[ $key ] ?? $key;
426 },
427 $missing_fields
428 ) ) );
429
430 wp_send_json_error( [
431 'message' => sprintf(
432 /* translators: %s: comma-separated list of the missing required field labels. */
433 esc_html__( 'Please fill in the required field(s): %s', 'storeengine' ),
434 implode( ', ', $missing_labels )
435 ),
436 'fields' => $missing_fields,
437 ] );
438 }
439
440
441 $coupons = $cart->get_coupons();
442
443 if ( empty( $coupons ) || is_user_logged_in() || ! $cart->has_items() ) {
444 return;
445 }
446
447 foreach ( $coupons as $coupon ) {
448 $is_valid = $coupon->validate_coupon( false );
449
450 if ( is_wp_error( $is_valid ) ) {
451 wp_send_json_error( $is_valid->get_error_message() );
452
453 continue;
454 }
455
456 if ( $coupon->get_usage_limit_per_user() > 0 ) {
457 $user = get_user_by( 'email', $data['user_email'] );
458 if ( ! $user ) {
459 continue;
460 }
461
462 if ( $coupon->get_usage_by_user_id( $user->ID ) >= $coupon->get_usage_limit_per_user() ) {
463 wp_send_json_error( esc_html__( 'Sorry, Coupon has reached its limit', 'storeengine' ) );
464 }
465 }
466 }
467 }
468
469 /**
470 * @param $data
471 *
472 * @return void
473 * @deprecated 1.5.8
474 */
475 public function change_subscription( $data ) {
476 if ( empty( $data['order_id'] ) ) {
477 wp_send_json_error( __( 'Order ID is required.', 'storeengine' ) );
478 }
479
480 if ( empty( $data['upgrade_price_id'] ) ) {
481 wp_send_json_error( __( 'Price ID is required.', 'storeengine' ) );
482 }
483
484 $order_id = $data['order_id'];
485 $upgrade_price_id = $data['upgrade_price_id'];
486
487 $order = new OrderModel();
488 $order_data = $order->get_by_primary_key( $order_id );
489 $cancel_result = $order->cancel_subscription( $order_data );
490 if ( is_wp_error( $cancel_result ) ) {
491 wp_send_json_error( $cancel_result->get_error_message() );
492 }
493
494 Helper::cart()->clear_cart();
495 Helper::cart()->add_product_to_cart( $upgrade_price_id );
496
497 $purchase_items = $this->prepare_purchase_items_data();
498
499 $order_data = $this->prepare_order_data( $order_data, $purchase_items );
500 $order->save( $order_data );
501
502 do_action( 'storeengine/frontend/subscription/after_change_subscription', $data, $order_id );
503
504 wp_send_json_success( 'Subscription status updated successfully.' );
505 }
506
507 public function refund( $data ) {
508 $order = Helper::get_order( absint( $data['order_id'] ?? 0 ) );
509
510 if ( is_wp_error( $order ) ) {
511 wp_send_json_error( $order->get_error_message() );
512 }
513
514 if ( ! $order->get_id() ) {
515 wp_send_json_error( esc_html__( 'Order not found', 'storeengine' ), 404 );
516 }
517
518 if ( empty( $data['refund_type'] ) ) {
519 wp_send_json_error( esc_html__( 'Select if it is a full or partial refund.', 'storeengine' ) );
520 }
521
522 if ( 'full' === $data['refund_type'] ) {
523 $refund_amount = Formatting::format_decimal( $order->get_total( 'refund' ) - $order->get_total_refunded( 'refund' ), Formatting::get_price_decimals() );
524 } else {
525 $refund_amount = Formatting::format_decimal( sanitize_text_field( wp_unslash( $data['refund_amount'] ?? 0 ) ), Formatting::get_price_decimals() );
526 }
527 // Required.
528 $refund_reason = isset( $data['refund_reason'] ) ? sanitize_text_field( wp_unslash( $data['refund_reason'] ) ) : '';
529 $api_refund = isset( $data['api_refund'] ) && Formatting::string_to_bool( $data['api_refund'] );
530 $refunded_amount = Formatting::format_decimal( sanitize_text_field( wp_unslash( $data['refunded_amount'] ?? 0 ) ), Formatting::get_price_decimals() );
531
532 // Optional.
533 $line_item_qtys = isset( $data['line_item_qtys'] ) ? json_decode( sanitize_text_field( wp_unslash( $data['line_item_qtys'] ) ), true ) : [];
534 $line_item_totals = isset( $data['line_item_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $data['line_item_totals'] ) ), true ) : [];
535 $line_item_tax_totals = isset( $data['line_item_tax_totals'] ) ? json_decode( sanitize_text_field( wp_unslash( $data['line_item_tax_totals'] ) ), true ) : [];
536 $restock_refunded_items = isset( $data['restock_refunded_items'] ) && Formatting::string_to_bool( $data['restock_refunded_items'] );
537
538 try {
539 $max_refund = Formatting::format_decimal( $order->get_total() - $order->get_total_refunded(), Formatting::get_price_decimals() );
540 if ( ( ! $refund_amount && ( Formatting::format_decimal( 0, Formatting::get_price_decimals() ) !== $refund_amount ) ) || $max_refund < $refund_amount || 0 > $refund_amount ) {
541 throw new StoreEngineException( esc_html__( 'Invalid refund amount', 'storeengine' ), 'invalid_refund_amount' );
542 }
543
544 if ( Formatting::format_decimal( $order->get_total_refunded(), Formatting::get_price_decimals() ) !== $refunded_amount ) {
545 throw new StoreEngineException( esc_html__( 'Error processing refund. Please try again.', 'storeengine' ), 'invalid_refunded_amount' );
546 }
547
548 // Prepare line items which we are refunding.
549 $line_items = [];
550 $item_ids = array_unique( array_merge( array_keys( $line_item_qtys ), array_keys( $line_item_totals ) ) );
551
552 foreach ( $item_ids as $item_id ) {
553 $line_items[ $item_id ] = [
554 'qty' => 0,
555 'refund_total' => 0,
556 'refund_tax' => [],
557 ];
558 }
559 foreach ( $line_item_qtys as $item_id => $qty ) {
560 $line_items[ $item_id ]['qty'] = max( $qty, 0 );
561 }
562 foreach ( $line_item_totals as $item_id => $total ) {
563 $line_items[ $item_id ]['refund_total'] = Formatting::format_decimal( $total );
564 }
565 foreach ( $line_item_tax_totals as $item_id => $tax_totals ) {
566 $line_items[ $item_id ]['refund_tax'] = array_filter( Formatting::format_decimal_array( $tax_totals ) );
567 }
568
569 // Optional opt-in (used by the order editor refund modal) to deactivate
570 // licenses generated by this order. Travels in create_refund() $args to
571 // the storeengine/order/refund_created action (handled by license-management).
572 $deactivate_licenses = isset( $data['deactivate_licenses'] ) && Formatting::string_to_bool( $data['deactivate_licenses'] );
573
574 // Create the refund object.
575 // Mirrors the standard line-item refund handler.
576 $refund = Helper::create_refund( [
577 'amount' => $refund_amount,
578 'reason' => $refund_reason,
579 'order_id' => $order->get_id(),
580 'line_items' => $line_items,
581 'refund_payment' => $api_refund,
582 'restock_items' => $restock_refunded_items,
583 'deactivate_licenses' => $deactivate_licenses,
584 ] );
585
586
587 if ( is_wp_error( $refund ) ) {
588 throw StoreEngineException::from_wp_error( $refund );
589 }
590
591 // create_refund() flips order status/paid_status in the DB but does not
592 // invalidate the cached order object; drop it so the next order fetch
593 // (the editor refetches after refund) reflects the new status/totals.
594 wp_cache_delete( 'orders_' . $order->get_id(), 'storeengine_orders' );
595
596 $status = '';
597 if ( did_action( 'storeengine/order/partially_refunded' ) ) {
598 $status = 'partially_refunded';
599 }
600
601 if ( did_action( 'storeengine/order/fully_refunded' ) ) {
602 $status = 'fully_refunded';
603 }
604
605 // Reload order data from db.
606 $order->read( true );
607
608 wp_send_json_success( array_merge(
609 Helper::get_payment_data( $order ),
610 [
611 'refunds_total' => $order->get_total_refunded( true ),
612 'refunded_amount' => $order->get_total_refunded( true ),
613 'refund_status' => $status,
614 ]
615 ) );
616
617 } catch ( StoreEngineException $e ) {
618 $order_id = absint( $data['order_id'] ?? 0 );
619 $order_obj = Helper::get_order( $order_id );
620 $customer_email = ( ! is_wp_error( $order_obj ) && $order_obj ) ? $order_obj->get_billing_email() : 'Unknown Email';
621
622 \StoreEngine\Classes\Logger::log(
623 sprintf( 'Refund Failed - Order #%d', $order_id ),
624 [
625 'customer_email' => $customer_email,
626 'message' => $e->getMessage(),
627 'code' => $e->get_wp_error_code(),
628 'request_data' => $data
629 ],
630 \StoreEngine\Classes\Logger::ERROR,
631 'refund'
632 );
633
634 wp_send_json_error( $e->get_wp_error() );
635 }
636 }
637
638 protected function subscribe_to_email( array $data ) {
639 if ( isset( $data['subscribe_to_email'] ) ) {
640 $customer = Helper::get_customer();
641 $customer->set_subscribe_to_email( true );
642 $customer->save();
643 }
644 }
645
646 }
647