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 / api / orders.php

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

1,171 lines 37.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\API;
4
5 use Exception;
6 use StoreEngine\API\Schema\OrderSchema;
7 use StoreEngine\Classes\AbstractEntity;
8 use StoreEngine\Classes\Countries;
9 use StoreEngine\Classes\Exceptions\StoreEngineException;
10 use StoreEngine\Classes\Exceptions\StoreEngineInvalidArgumentException;
11 use StoreEngine\Classes\Order as StoreEngineOrder;
12 use StoreEngine\Classes\OrderCollection;
13 use StoreEngine\Classes\OrderStatus\OrderStatus;
14 use StoreEngine\Classes\Price;
15 use StoreEngine\Utils\ArrayUtil;
16 use StoreEngine\Utils\Formatting;
17 use StoreEngine\Utils\Helper;
18 use StoreEngine\Utils\StringUtil;
19 use WP_Error;
20 use WP_HTTP_Response;
21 use WP_REST_Request;
22 use WP_REST_Response;
23 use WP_REST_Server;
24
25 if ( ! defined( 'ABSPATH' ) ) {
26 exit;
27 }
28
29 class Orders extends AbstractRestApiController {
30 use OrderSchema;
31
32 protected $rest_base = 'order';
33
34 public static function init() {
35 $self = new self();
36 add_action( 'rest_api_init', array( $self, 'register_routes' ) );
37 }
38
39 /**
40 * Register the routes for the objects of the controller.
41 */
42 public function register_routes() {
43 register_rest_route( $this->namespace, '/' . $this->rest_base, [
44 [
45 'methods' => WP_REST_Server::READABLE,
46 'callback' => [ $this, 'get_items' ],
47 'permission_callback' => [ $this, 'permissions_check' ],
48 'args' => $this->get_collection_params(),
49 ],
50 [
51 'methods' => WP_REST_Server::CREATABLE,
52 'callback' => [ $this, 'create_item' ],
53 'permission_callback' => [ $this, 'permissions_check' ],
54 'args' => $this->get_post_item_schema(),
55 ],
56 'schema' => [ $this, 'get_public_item_schema' ],
57 ] );
58 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', [
59 'args' => [
60 'id' => [
61 'description' => __( 'Unique identifier for the object.', 'storeengine' ),
62 'type' => 'integer',
63 ],
64 ],
65 [
66 'methods' => WP_REST_Server::READABLE,
67 'callback' => [ $this, 'get_item' ],
68 'permission_callback' => [ $this, 'permissions_check' ],
69 'args' => [
70 'context' => $this->get_context_param( [ 'default' => 'view' ] ),
71 ],
72 ],
73 [
74 'methods' => WP_REST_Server::EDITABLE,
75 'callback' => [ $this, 'update_item' ],
76 'permission_callback' => [ $this, 'permissions_check' ],
77 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
78 ],
79 [
80 'methods' => WP_REST_Server::DELETABLE,
81 'callback' => [ $this, 'delete_item' ],
82 'permission_callback' => [ $this, 'permissions_check' ],
83 ],
84 'schema' => [ $this, 'get_item_schema' ],
85 ] );
86 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/order_item', [
87 'args' => [
88 'id' => [
89 'description' => __( 'Unique identifier for the object.', 'storeengine' ),
90 'type' => 'integer',
91 ],
92 ],
93 [
94 'methods' => WP_REST_Server::CREATABLE,
95 'callback' => [ $this, 'add_order_item' ],
96 'permission_callback' => [ $this, 'permissions_check' ],
97 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
98 ],
99 [
100 'methods' => WP_REST_Server::DELETABLE,
101 'callback' => [ $this, 'delete_order_item' ],
102 'permission_callback' => [ $this, 'permissions_check' ],
103 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
104 ],
105 ] );
106 register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/order_item/(?P<item_id>[\d]+)', [
107 'args' => [
108 'id' => [
109 'description' => __( 'Unique identifier for the order.', 'storeengine' ),
110 'type' => 'integer',
111 ],
112 'item_id' => [
113 'description' => __( 'Unique identifier for the order item.', 'storeengine' ),
114 'type' => 'integer',
115 ],
116 ],
117 [
118 'methods' => WP_REST_Server::EDITABLE,
119 'callback' => [ $this, 'update_order_item' ],
120 'permission_callback' => [ $this, 'permissions_check' ],
121 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
122 ],
123 ] );
124
125 // @TODO move to common api.
126 register_rest_route( $this->namespace, '/' . $this->rest_base . '/get_states/(?P<cc>[\S]{2})', [
127 'args' => [
128 'cc' => [
129 'description' => __( 'ISO 3166-1 alpha-2 (two-letter) country code.', 'storeengine' ),
130 'type' => 'string',
131 'required' => true,
132 ],
133 ],
134 [
135 'methods' => WP_REST_Server::READABLE,
136 'callback' => [ $this, 'get_states' ],
137 'permission_callback' => [ $this, 'permissions_check' ],
138 'args' => [
139 'context' => $this->get_context_param( [ 'default' => 'view' ] ),
140 ],
141 ],
142 'schema' => [ $this, 'get_item_schema' ],
143 ] );
144 }
145
146 public function add_order_item( $request ) {
147 $order_id = $request->get_param( 'id' );
148 $order = Helper::get_order( $order_id );
149
150 if ( is_wp_error( $order ) ) {
151 return $order;
152 }
153
154 if ( ! $order ) {
155 return new WP_Error( 'no-order', __( 'Order not found.', 'storeengine' ), [ 'status' => 404 ] );
156 }
157
158 if ( ! $order->is_editable() ) {
159 return new WP_Error( 'order-not-editable', __( 'Order is no longer editable.', 'storeengine' ), [ 'status' => 400 ] );
160 }
161
162 $body = $request->get_json_params();
163 $type = $body['type'] ?? '';
164
165 switch ( $type ) {
166 case 'product':
167 if ( ! empty( $body['price_id'] ) ) {
168 $price = new \StoreEngine\Classes\Price( absint( $body['price_id'] ) );
169 if ( ! $price->get_id() ) {
170 return new WP_Error( 'product-not-found', __( 'Product not found.', 'storeengine' ) );
171 }
172
173 $order_item = $order->add_product(
174 $price,
175 max( 1, absint( $body['quantity'] ?? 1 ) ),
176 [
177 'order' => $order,
178 'variation_id' => $body['variation'] ?? 0,
179 ]
180 );
181
182 if ( ! is_wp_error( $order_item ) ) {
183 $order->maybe_set_digital_auto_complete();
184 }
185 } else {
186 $order_item = new WP_Error( 'invalid-request', __( 'Product Price ID is required.', 'storeengine' ), [ 'status' => 400 ] );
187 }
188 break;
189 case 'coupon':
190 if ( ! empty( $body['code'] ) && ! StringUtil::is_null_or_whitespace( $body['code'] ) ) {
191 $order_item = $order->apply_coupon( strtolower( $body['code'] ) );
192 } else {
193 $order_item = new WP_Error( 'invalid-request', __( 'Coupon code is required.', 'storeengine' ), [ 'status' => 400 ] );
194 }
195 break;
196 case 'fee':
197 if ( ! empty( $body['name'] ) && ! empty( $body['amount'] ) && (float) $body['amount'] > 0 ) {
198 $order_item = $order->add_fee( $body['name'], (float) $body['amount'] );
199 } else {
200 $order_item = new WP_Error( 'invalid-request', __( 'Fee name & amount is required.', 'storeengine' ), [ 'status' => 400 ] );
201 }
202 break;
203 default:
204 $order_item = new WP_Error( 'invalid-request', __( 'Invalid request.', 'storeengine' ), [ 'status' => 400 ] );
205 break;
206 }
207
208 if ( is_wp_error( $order_item ) ) {
209 return $order_item;
210 }
211
212 $order->recalculate_coupons();
213 $order->calculate();
214 $order->save();
215
216 do_action( 'storeengine/api/order/add_order_item', $order, $order_item, $type );
217
218 return rest_ensure_response( $this->prepare_item_for_response( $order, $request ) );
219 }
220
221 public function update_order_item( $request ) {
222 $order_id = $request->get_param( 'id' );
223 $item_id = $request->get_param( 'item_id' );
224
225 if ( ! $order_id || ! $item_id ) {
226 return new WP_Error( 'invalid-request', __( 'Invalid request.', 'storeengine' ), [ 'status' => 400 ] );
227 }
228
229 $order = Helper::get_order( $order_id );
230
231 if ( is_wp_error( $order ) ) {
232 return $order;
233 }
234
235 if ( ! $order ) {
236 return new WP_Error( 'no-order', __( 'Order not found.', 'storeengine' ), [ 'status' => 404 ] );
237 }
238
239 if ( ! $order->is_editable() ) {
240 return new WP_Error( 'order-not-editable', __( 'Order is no longer editable.', 'storeengine' ), [ 'status' => 400 ] );
241 }
242
243 $item = $order->get_item( absint( $item_id ) );
244
245 if ( ! $item ) {
246 return new WP_Error( 'no-order-item', __( 'Order item not found.', 'storeengine' ), [ 'status' => 404 ] );
247 }
248
249 $body = $request->get_json_params();
250 $quantity = absint( $body['quantity'] ?? 0 );
251
252 if ( ! $quantity ) {
253 return new WP_Error( 'no-order-item', __( 'Quantity is required.', 'storeengine' ), [ 'status' => 404 ] );
254 }
255
256 $total = Formatting::get_price_excluding_tax(
257 $item->get_price( 'edit' ),
258 $item->get_price_id( 'edit' ),
259 $item->get_product_id( 'edit' ),
260 [
261 'qty' => $quantity,
262 'order' => $order,
263 ]
264 );
265
266 $item->set_quantity( $quantity );
267 $item->set_subtotal( $total );
268 $item->set_total( $total );
269 $item->save();
270
271 $order->recalculate_coupons();
272 $order->calculate();
273 $order->save();
274
275 do_action( 'storeengine/api/order/update_order_item', $order, $item );
276
277 return rest_ensure_response( $this->prepare_item_for_response( $order, $request ) );
278 }
279
280 public function delete_order_item( $request ) {
281 $order_id = $request->get_param( 'id' );
282 $order = Helper::get_order( $order_id );
283
284 if ( ! $order ) {
285 return new WP_Error( 'no-order', __( 'Order not found.', 'storeengine' ), [ 'status' => 404 ] );
286 }
287
288 if ( is_wp_error( $order ) ) {
289 return $order;
290 }
291
292 if ( ! $order->is_editable() ) {
293 return new WP_Error( 'order-not-editable', __( 'Order is no longer editable.', 'storeengine' ), [ 'status' => 400 ] );
294 }
295
296 $body = $request->get_json_params();
297
298 if ( empty( $body['item_id'] ) ) {
299 return new WP_Error( 'no-item', __( 'Order item not found.', 'storeengine' ), [ 'status' => 404 ] );
300 }
301
302 $item_id = absint( $body['item_id'] );
303 $item = $order->get_item( $item_id, false );
304
305 $removed = $order->remove_item( $item_id );
306
307 if ( ! $removed ) {
308 return new WP_Error( 'failed-to-remove', __( 'Failed to remove order item. Maybe order item already removed.', 'storeengine' ), [ 'status' => 404 ] );
309 }
310
311 if ( $removed->is_type( 'line_item' ) ) {
312 $order->maybe_set_digital_auto_complete();
313 }
314
315
316 $order->recalculate_coupons();
317 $order->calculate();
318 $order->save();
319
320 do_action( 'storeengine/api/order/delete_order_item', $order, $item, $item_id );
321
322 return rest_ensure_response( $this->prepare_item_for_response( $order, $request ) );
323 }
324
325 /**
326 * @param WP_REST_Request $request Full details about the request.
327 *
328 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
329 * @throws StoreEngineInvalidArgumentException
330 */
331 public function get_items( $request ) {
332 $page = max( 1, absint( $request->get_param( 'page' ) ) );
333 $status = $request->get_param( 'status' );
334 $search = $request->get_param( 'search' );
335 $customer = absint( $request->get_param( 'customer' ) );
336 $where = [
337 'relation' => 'AND',
338 [
339 'key' => 'type',
340 'value' => 'order',
341 ],
342 ];
343
344 if ( $status && ! in_array( $status, [ 'all', 'any', 'draft' ], true ) ) {
345 $where[] = [
346 'key' => 'status',
347 'value' => $status,
348 ];
349 } else {
350 $where[] = [
351 'key' => 'status',
352 'value' => 'draft',
353 'compare' => '!=',
354 ];
355 }
356
357 if ( $customer ) {
358 $where[] = [
359 'key' => 'customer_id',
360 'value' => $customer,
361 'compare' => '=',
362 'type' => 'NUMERIC',
363 ];
364 }
365
366 if ( $search ) {
367 if ( is_numeric( $search ) ) {
368 $where[] = [
369 'key' => 'id',
370 'value' => $search,
371 ];
372 } elseif ( is_email( $search ) ) {
373 $_email_where = [
374 'relation' => 'OR',
375 [
376 'key' => 'billing_email',
377 'value' => $search,
378 ],
379 ];
380
381 if ( email_exists( $search ) ) {
382 $_email_where[] = [
383 'key' => 'customer_id',
384 'value' => email_exists( $search ),
385 ];
386 }
387
388 $where[] = $_email_where;
389 } else {
390 $where[] = [
391 'relation' => 'OR',
392 [
393 'key' => 'billing_email',
394 'value' => "%$search%",
395 'compare' => 'LIKE',
396 ],
397 [
398 'key' => 'payment_method',
399 'value' => $search,
400 ],
401 [
402 'key' => 'payment_method_title',
403 'value' => "%$search%",
404 'compare' => 'LIKE',
405 ],
406 ];
407 }
408 }
409
410 $query = new OrderCollection( [
411 'per_page' => absint( $request->get_param( 'per_page' ) ),
412 'page' => $page,
413 'where' => $where,
414 ] );
415 $data = [];
416
417 foreach ( $query->get_results() as $order ) {
418 $response = $this->prepare_item_for_response( $order, $request );
419 $data[] = $this->prepare_response_for_collection( $response );
420 }
421
422 return $this->prepare_query_response( $data, $query, $request );
423 }
424
425 /**
426 * Prepare links for the request.
427 *
428 * @param StoreEngineOrder $item
429 * @param WP_REST_Request $request Request object.
430 *
431 * @return array Links for the given order.
432 */
433 protected function prepare_links( $item, WP_REST_Request $request ): array {
434 $links = parent::prepare_links( $item, $request );
435
436 if ( $item->get_user_id() ) {
437 $links['customer'] = [
438 'href' => rest_url( sprintf( '/%s/customers/%d', $this->namespace, $item->get_user_id() ) ),
439 ];
440 }
441
442 if ( $item->get_parent_id() ) {
443 $links['up'] = [
444 'href' => rest_url( sprintf( '/%s/orders/%d', $this->namespace, $item->get_parent_id() ) ),
445 ];
446 }
447
448 return $links;
449 }
450
451 /**
452 * @param StoreEngineOrder $item Order object.
453 * @param WP_REST_Request $request Requests.
454 *
455 * @return WP_Error|WP_HTTP_Response|WP_REST_Response
456 */
457 public function prepare_item_for_response( $item, $request ) {
458 $schema = $this->get_public_item_schema();
459 $data = [
460 'id' => $item->get_id(),
461 'is_editable' => $item->is_editable(),
462 'currency' => $item->get_currency(),
463 ];
464
465 if ( isset( $schema['properties']['status'] ) ) {
466 $data['status'] = $item->get_status();
467 }
468
469 $data['paid_status'] = $item->get_paid_status();
470
471 if ( $item->get_date_paid_gmt() ) {
472 $data['date_paid_gmt'] = $item->get_date_paid_gmt()->format( 'Y-m-d H:i:s' );
473 } else {
474 $data['date_paid_gmt'] = null;
475 }
476
477 if ( isset( $schema['properties']['type'] ) ) {
478 $data['type'] = $item->get_type();
479 }
480
481 if ( isset( $schema['properties']['tax_amount'] ) ) {
482 $data['tax_amount'] = $item->get_tax_amount();
483 }
484
485 if ( isset( $schema['properties']['refunds_total'] ) ) {
486 $data['refunds_total'] = $item->get_total_refunded();
487 }
488
489 // Refund capability fields consumed by the shared RefundModal (same shape as
490 // the Payments page payment data). Computed via the same logic as get_payment_data.
491 if ( isset( $schema['properties']['can_refund'] ) ) {
492 $data['can_refund'] = false;
493 $data['gateway_can_refund_order'] = false;
494 $data['gateway_name'] = __( 'Payment gateway', 'storeengine' );
495
496 if ( $item->is_paid() && 'refunded' !== $item->get_status() ) {
497 $data['can_refund'] = (bool) apply_filters(
498 'storeengine/refund/can_admin_refund_order',
499 ( 0 < $item->get_total() - $item->get_total_refunded() ),
500 $item->get_id(),
501 $item
502 );
503
504 $gateway = Helper::get_payment_gateway_by_order( $item );
505
506 if ( false !== $gateway ) {
507 $data['gateway_name'] = ! empty( $gateway->method_title ) ? $gateway->method_title : $gateway->get_title();
508 $data['gateway_can_refund_order'] = (bool) $gateway->can_refund_order( $item );
509 }
510 }
511 }
512
513 if ( isset( $schema['properties']['subtotal_amount'] ) ) {
514 $data['subtotal_amount'] = $item->get_subtotal();
515 }
516
517 if ( isset( $schema['properties']['date_created_gmt'] ) ) {
518 if ( $item->get_date_created_gmt() ) {
519 $data['date_created_gmt'] = $item->get_date_created_gmt()->format( 'Y-m-d H:i:s' );
520 } else {
521 $data['date_created_gmt'] = null;
522 }
523 }
524
525 if ( isset( $schema['properties']['order_placed_date_gmt'] ) ) {
526 if ( $item->get_order_placed_date_gmt() ) {
527 $data['order_placed_date_gmt'] = $item->get_order_placed_date_gmt()->format( 'Y-m-d H:i:s' );
528 } else {
529 $data['order_placed_date_gmt'] = null;
530 }
531 }
532
533 if ( isset( $schema['properties']['order_placed_date'] ) ) {
534 if ( $item->get_order_placed_date() ) {
535 $data['order_placed_date'] = $item->get_order_placed_date()->format( 'Y-m-d H:i:s' );
536 } else {
537 $data['order_placed_date'] = null;
538 }
539 }
540
541 if ( isset( $schema['properties']['coupons'] ) ) {
542 $data['coupons'] = array_values( array_map( fn( $coupon ) => [
543 'id' => $coupon->get_id(),
544 'code' => $coupon->get_name(),
545 ], $item->get_coupons() ) );
546 $data['total_discount'] = $item->get_total_discount();
547 }
548
549 $data['shipping_methods'] = array_values( array_map( fn( $shipping ) => [
550 'name' => $shipping->get_name(),
551 'taxable' => $shipping->get_tax_status(),
552 ], $item->get_shipping_methods() ) );
553
554 $data['shipping_total'] = $item->get_shipping_total();
555
556 $data['fees'] = array_values( array_map( fn( $fee ) => [
557 'id' => $fee->get_id(),
558 'name' => $fee->get_name( 'edit' ),
559 'tax_class' => $fee->get_tax_class(),
560 'tax_status' => $fee->get_tax_status(),
561 'amount' => Formatting::round_tax_total( $fee->get_amount( 'edit' ) ),
562 'total' => $fee->get_total(),
563 'total_tax' => $fee->get_total_tax(),
564 'taxes' => $fee->get_taxes(),
565 ], $item->get_fees() ) );
566
567 $data['taxes'] = array_values( array_map( fn( $tax ) => [
568 'code' => $tax->code,
569 'label' => $tax->label,
570 'amount' => Formatting::round_tax_total( $tax->amount ),
571 ], $item->get_tax_totals() ) );
572
573 if ( isset( $schema['properties']['total_amount'] ) ) {
574 $data['total_amount'] = $item->get_total_amount();
575 $data['subtotal_amount'] = $item->get_subtotal();
576 }
577
578 if ( isset( $schema['properties']['customer_id'] ) ) {
579 $data['customer_id'] = $item->get_customer_id();
580 }
581
582 // customer email and customer name
583 if ( isset( $schema['properties']['customer_email'] ) ) {
584 $data['customer_email'] = $item->get_billing_email();
585 $data['email_hash'] = md5( $item->get_billing_email() );
586 }
587
588 if ( isset( $schema['properties']['customer_name'] ) ) {
589 $data['customer_name'] = trim( $item->get_billing_first_name() . ' ' . $item->get_billing_last_name() );
590 }
591
592 if ( isset( $schema['properties']['billing_email'] ) ) {
593 $data['billing_email'] = $item->get_order_email();
594 }
595
596 if ( isset( $schema['properties']['payment_method'] ) ) {
597 $data['payment_method'] = $item->get_payment_method();
598 }
599
600 if ( isset( $schema['properties']['payment_method_title'] ) ) {
601 $data['payment_method_title'] = $item->get_payment_method_title();
602 }
603
604 if ( isset( $schema['properties']['transaction_id'] ) ) {
605 $data['transaction_id'] = $item->get_transaction_id();
606 }
607
608 if ( isset( $schema['properties']['customer_note'] ) ) {
609 $data['customer_note'] = $item->get_customer_note();
610 }
611
612 $data['is_auto_complete_digital_order'] = $item->get_auto_complete_digital_order();
613
614 if ( isset( $schema['properties']['meta'] ) ) {
615 $data['meta'] = $item->get_meta_data();
616 }
617
618 // purchase_items
619 if ( isset( $schema['properties']['purchase_items'] ) ) {
620 global $wpdb;
621 $order_id_val = $item->get_id();
622 $data['purchase_items'] = array_map( function ( $order_item ) use ( $item, $order_id_val, $wpdb ) {
623 $oitem_id = (int) $order_item->get_id();
624
625 // Per-line shipping status (lookup) + saved courier/tracking (order
626 // meta) so the admin order screen can prefill the fulfilment UI.
627 // phpcs:disable WordPress.DB.DirectDatabaseQuery
628 $shipping_status = (string) $wpdb->get_var( $wpdb->prepare(
629 "SELECT shipping_status FROM {$wpdb->prefix}storeengine_order_product_lookup WHERE order_item_id = %d AND order_item_id > 0",
630 $oitem_id
631 ) );
632 // phpcs:enable
633 $shipment = $item->get_meta( '_storeengine_shipment_' . $oitem_id );
634
635 return array_merge(
636 [
637 'id' => $oitem_id,
638 'edit_url' => get_post( $order_item->get_product_id() ) ? admin_url( 'admin.php?page=storeengine-products&id=' . $order_item->get_product_id() . '&action=edit' ) : '',
639 ],
640 $order_item->get_data(),
641 [
642 'formatted_metadata' => array_values( $order_item->get_all_formatted_metadata() ),
643 'order_id' => $order_id_val,
644 'order_item_id' => $oitem_id,
645 'shipping_type' => get_post_meta( $order_item->get_product_id(), '_storeengine_product_shipping_type', true ),
646 'shipping_status' => $shipping_status,
647 'shipment' => is_array( $shipment ) ? $shipment : null,
648 ]
649 );
650 }, $item->get_items() );
651 $data['purchase_items'] = array_values( $data['purchase_items'] );
652 }
653
654 if ( isset( $schema['properties']['refunds'] ) ) {
655 try {
656 $data['refunds'] = array_map( fn( $refund ) => [
657 'id' => $refund->get_id(),
658 'amount' => $refund->get_total(),
659 'reason' => $refund->get_reason(),
660 'refund_by' => [
661 'user_id' => $refund->get_refunded_by() ? $refund->get_refunded_by_user()->get_id() : null,
662 'name' => $refund->get_refunded_by() ? trim( $refund->get_refunded_by_user()->get_first_name() . ' ' . $refund->get_refunded_by_user()->get_last_name() ) : null,
663 'display_name' => $refund->get_refunded_by() ? $refund->get_refunded_by_user()->get_display_name() : null,
664 ],
665 'created_at' => $refund->get_date_created_gmt() ? $refund->get_date_created_gmt()->format( 'Y-m-d H:i:s' ) : null,
666 ], $item->get_refunds() );
667 } catch ( StoreEngineException $e ) {
668 $data['refunds'] = [];
669 }
670 }
671
672 if ( isset( $schema['properties']['billing_address'] ) ) {
673 $data['billing_address'] = $item->get_address();
674 if ( isset( $data['billing_address']['address_type'] ) ) {
675 unset( $data['billing_address']['address_type'] );
676 }
677 }
678 if ( isset( $schema['properties']['shipping_address'] ) ) {
679 $data['shipping_address'] = $item->get_address( 'shipping' );
680 if ( isset( $data['shipping_address']['address_type'] ) ) {
681 unset( $data['shipping_address']['address_type'] );
682 }
683 }
684
685 $data['notes'] = $item->get_order_notes();
686
687 // Wrap the data in a response object.
688 $response = rest_ensure_response( $data );
689
690 $response->add_links( $this->prepare_links( $item, $request ) );
691
692 return $response;
693 }
694
695 /**
696 * @param WP_REST_Request $request
697 *
698 * @return WP_REST_Response|WP_Error
699 */
700 public function create_item( $request ) {
701 do_action( 'storeengine/api/before_create_order', $request );
702
703 try {
704 $order = new StoreEngineOrder();
705 $order->set_status( OrderStatus::AUTO_DRAFT );
706 $order->set_created_via( 'admin-api' );
707 $order->save();
708 $order->read( true );
709
710 $response = $this->prepare_item_for_response( $order, $request );
711
712 return rest_ensure_response( $response );
713 } catch ( StoreEngineException $exception ) {
714 return rest_ensure_response( $exception->toWpError() );
715 } catch ( Exception $exception ) {
716 return rest_ensure_response( new WP_Error( 'something-went-wrong', $exception->getMessage(), [ 'status' => 500 ] ) );
717 }
718
719 // @TODO create api endpoint with the ability to add order item
720 // directly so we can trigger 'storeengine/api/after_create_order' hook for the lookup.
721 // phpcs:disable Squiz.PHP.CommentedOutCode.Found
722
723 // if ( ! isset( $request['purchase_items'] ) || empty( $request['purchase_items'] ) ) {
724 // return new WP_Error( 'purchase_items_required', __( 'Please select at least one product!', 'storeengine' ) );
725 // }
726 //
727 // $order = new StoreEngineOrder();
728 // $customer_id = $request->get_param( 'customer_id' );
729 // $order->set_customer_id( $customer_id );
730 //
731 // $order_email = $request['billing_email'] ?? '';
732 // if ( ! $order_email && ! $order->get_order_email( 'edit' ) && $order->get_customer_id( 'edit' ) ) {
733 // $order_email = $order->get_customer()->get_email( 'edit' );
734 // }
735 //
736 // if ( $order_email && $order_email !== $order->get_order_email( 'edit' ) ) {
737 // $order->set_order_email( $order_email );
738 // }
739 //
740 // $payment_gateway = null;
741 // if ( isset( $request['payment_method'] ) ) {
742 // $payment_gateway = Helper::get_payment_gateway( $request['payment_method'] );
743 //
744 // if ( ! $payment_gateway ) {
745 // return rest_ensure_response( new WP_Error( 'payment_gateway_error', __( 'Payment method not found.', 'storeengine' ), [ 'status' => 400 ] ) );
746 // }
747 // }
748 //
749 // $order->set_status( $request['status'] ?? Constants::ORDER_STATUS_PENDING_PAYMENT );
750 // $order->set_order_email( $order_email );
751 // $order->set_payment_method( $payment_gateway );
752 // $order->set_customer_note( $request['order_note'] ?? '' );
753 // $order->set_transaction_id( $request['transaction_id'] ?? '' );
754 //
755 // if ( ! empty( $request['billing_address'] ) && is_array( $request['billing_address'] ) ) {
756 // $order->set_billing_address( $request['billing_address'] );
757 // }
758 //
759 // if ( ! empty( $request['shipping_address'] ) && is_array( $request['shipping_address'] ) ) {
760 // $order->set_shipping_address( $request['shipping_address'] );
761 // }
762 //
763 // foreach ( $request['purchase_items'] as $purchase_item ) {
764 // $order->add_product( $purchase_item['price_id'], $purchase_item['quantity'], array_merge( $purchase_item, [ 'order' => $order ] ) );
765 // }
766 //
767 // if ( ! empty( $request['coupons'] ) && is_array( $request['coupons'] ) ) {
768 // foreach ( $request['coupons'] as $coupon ) {
769 // if ( StringUtil::is_null_or_whitespace( $coupon ) ) {
770 // continue;
771 // }
772 // $coupon = new Coupon( $coupon );
773 // $is_valid = $coupon->validate_coupon();
774 // if ( $is_valid ) {
775 // $order->apply_coupon( $coupon );
776 // }
777 // }
778 // }
779 //
780 // $order->save();
781 // $order->recalculate_coupons();
782 // $order->calculate();
783 // $order->save();
784 //
785 // do_action( 'storeengine/api/after_create_order', $order );
786 //
787 // return rest_ensure_response( $this->prepare_item_for_response( $order, $request ) );
788 }
789
790 public function get_item( $request ) {
791 try {
792 $order = Helper::get_order( $request['id'] );
793
794 if ( is_wp_error( $order ) ) {
795 return $order;
796 }
797
798 $response = $this->prepare_item_for_response( $order, $request );
799
800 return rest_ensure_response( $response );
801 } catch ( StoreEngineException $exception ) {
802 return rest_ensure_response( $exception->toWpError() );
803 } catch ( Exception $exception ) {
804 return rest_ensure_response( new WP_Error( 'something-went-wrong', $exception->getMessage(), [ 'status' => 500 ] ) );
805 }
806 }
807
808 /**
809 * @param WP_REST_Request $request
810 *
811 * @return WP_REST_Response|WP_Error
812 * @throws StoreEngineException
813 */
814 public function update_item( $request ) {
815 do_action( 'storeengine/api/before_update_order', $request );
816 $order_id = $request->get_param( 'id' );
817 $order = Helper::get_order( $order_id );
818
819 if ( is_wp_error( $order ) ) {
820 return $order;
821 }
822
823 if ( ! $order ) {
824 return new WP_Error( 'invalid_order_id', __( 'Invalid order id!', 'storeengine' ) );
825 }
826
827 // Status can be updated even if order is no longer editable.
828
829 $previous_status = $order->get_status( 'edit' );
830 $body = $request->get_json_params();
831 $order = $this->set_core_order( $order, $body );
832
833 $order_email = $request['billing_email'] ?? ( $body['billing_email'] ?? '' );
834 if ( ! $order_email && ! $order->get_order_email( 'edit' ) && $order->get_customer_id( 'edit' ) ) {
835 $order_email = $order->get_customer()->get_email( 'edit' );
836 }
837
838 if ( $order_email && $order_email !== $order->get_order_email( 'edit' ) ) {
839 $order->set_order_email( $order_email );
840 }
841
842 if ( array_key_exists( 'billing_address', $body ) ) {
843 $order = $this->set_order_address( $order, $body['billing_address'] );
844 }
845
846 if ( array_key_exists( 'shipping_address', $body ) ) {
847 $order = $this->set_order_address( $order, $body['shipping_address'], 'shipping' );
848 }
849
850 $order->save();
851 $order->recalculate_coupons();
852 $order->calculate();
853 $order->save();
854
855 $response = $this->prepare_item_for_response( $order, $request );
856
857 if ( 'auto-draft' === $previous_status && $previous_status !== $order->get_status( 'edit' ) ) {
858 do_action( 'storeengine/api/after_create_order', $order );
859 } else {
860 do_action( 'storeengine/api/after_update_order', $order );
861 }
862
863 return new WP_REST_Response( $response, 200 );
864 }
865
866 /**
867 * @param StoreEngineOrder $order Order Object.
868 * @param array $data Data.
869 *
870 * @return StoreEngineOrder
871 * @throws StoreEngineException
872 */
873 protected function set_core_order( StoreEngineOrder $order, array $data ): StoreEngineOrder {
874 if ( array_key_exists( 'billing_email', $data ) ) {
875 $order->set_order_email( $data['billing_email'] );
876 }
877
878 if ( array_key_exists( 'currency', $data ) ) {
879 $order->set_currency( $data['currency'] );
880 }
881
882 if ( array_key_exists( 'customer_id', $data ) ) {
883 $order->set_customer_id( $data['customer_id'] );
884 }
885
886 if ( array_key_exists( 'payment_method', $data ) ) {
887 $order->set_payment_method( $data['payment_method'] );
888 }
889
890 if ( array_key_exists( 'payment_method_title', $data ) ) {
891 $order->set_payment_method_title( $data['payment_method_title'] );
892 }
893
894 if ( array_key_exists( 'status', $data ) ) {
895 $order->set_status( $data['status'] );
896 }
897
898 if ( array_key_exists( 'transaction_id', $data ) ) {
899 $order->set_transaction_id( (string) $data['transaction_id'] );
900 }
901
902 return $order;
903 }
904
905 /**
906 * @param StoreEngineOrder $order Order Object.
907 * @param array $address Address Data.
908 * @param string $type Address Type.
909 *
910 * @return StoreEngineOrder
911 */
912 protected function set_order_address( StoreEngineOrder $order, array $address, string $type = 'billing' ): StoreEngineOrder {
913 $fields = array(
914 'first_name',
915 'last_name',
916 'email',
917 'phone',
918 'address_1',
919 'address_2',
920 'state',
921 'city',
922 'country',
923 'postcode',
924 );
925 foreach ( $fields as $field ) {
926 if ( array_key_exists( $field, $address ) ) {
927 $order->{"set_{$type}_{$field}"}( $address[ $field ] ?? '' );
928 }
929 }
930
931 return $order;
932 }
933
934 public function delete_item( $request ) {
935 $force = (bool) $request['force'];
936 $order_id = (int) $request['id'];
937 $object = Helper::get_order( $order_id );
938
939 if ( is_wp_error( $object ) ) {
940 return $object;
941 }
942
943 if ( ! $object || ! $object->get_id() ) {
944 return new WP_Error( 'invalid_item_id', __( 'Invalid ID!', 'storeengine' ), [ 'status' => 404 ] );
945 }
946
947 $result = false;
948 $supports_trash = EMPTY_TRASH_DAYS > 0 && is_callable( [ $object, 'get_status' ] ) && $object->is_trashable();
949
950 /**
951 * Filter whether an object is trashable.
952 *
953 * Return false to disable trash support for the object.
954 *
955 * @param boolean $supports_trash Whether the object type support trashing.
956 * @param AbstractEntity $object The object being considered for trashing support.
957 */
958 $supports_trash = apply_filters( "storeengine/api/{$object->get_object_type()}/object_trashable", $supports_trash, $object );
959
960 // Maybe check additional permissions.
961
962 do_action( 'storeengine/api/before_delete_order', $request );
963
964 $request->set_param( 'context', 'edit' );
965 $response = $this->prepare_item_for_response( $object, $request );
966
967 if ( $force ) {
968 $object->delete( true );
969 $result = 0 === $object->get_id();
970 } else {
971 // If we don't support trashing for this type, error out.
972 if ( ! $supports_trash ) {
973 /* translators: %s: post type */
974 return new WP_Error( 'storeengine_rest_trash_not_supported', sprintf( __( 'The %s does not support trashing.', 'storeengine' ), $object->get_object_type() ), [ 'status' => 501 ] );
975 }
976
977 // Otherwise, only trash if we haven't already.
978 if ( is_callable( [ $object, 'get_status' ] ) ) {
979 if ( 'trash' === $object->get_status() ) {
980 /* translators: %s: post type */
981 return new WP_Error( 'storeengine_rest_already_trashed', sprintf( __( 'The %s has already been trashed.', 'storeengine' ), $object->get_object_type() ), [ 'status' => 410 ] );
982 }
983
984 $object->trash();
985 $result = 'trash' === $object->get_status();
986 }
987 }
988
989 if ( ! $result ) {
990 /* translators: %s: post type */
991 return new WP_Error( 'storeengine_rest_cannot_delete', sprintf( __( 'Failed to delete %s.', 'storeengine' ), $object->get_object_type() ), [ 'status' => 500 ] );
992 }
993
994 /**
995 * Fires after a single object is deleted or trashed via the REST API.
996 *
997 * @param AbstractEntity $object The deleted or trashed object.
998 * @param WP_REST_Response $response The response data.
999 * @param WP_REST_Request $request The request sent to the API.
1000 */
1001 do_action( "storeengine/api/{$object->get_object_type()}delete__object", $object, $response, $request );
1002
1003 do_action( 'storeengine/api/after_delete_order', $object, $order_id, $force );
1004
1005 return rest_ensure_response( [
1006 'previous' => $response,
1007 'id' => $order_id,
1008 'force' => $force,
1009 ] );
1010 }
1011
1012 public function get_states( WP_REST_Request $request ): WP_REST_Response {
1013 $cc = strtoupper( $request->get_param( 'cc' ) );
1014 $states = Countries::init()->get_states( $cc );
1015
1016 return new WP_REST_Response( [
1017 'cc' => $cc,
1018 'states' => $states ? $states : false,
1019 ], 200 );
1020 }
1021
1022 public function permissions_check() {
1023 return Helper::check_rest_user_cap( 'manage_options' );
1024 }
1025
1026 /**
1027 * @param $cart
1028 *
1029 * @return array
1030 *
1031 * @deprecated
1032 */
1033 protected function prepare_purchase_items_data( $cart ): array {
1034 $purchase_items = [];
1035 foreach ( $cart->get_cart_items() as $product ) {
1036 $purchase_items[] = [
1037 'product_id' => $product['id'],
1038 'variation_id' => 0,
1039 'price_id' => $product['price_id'],
1040 'product_qty' => $product['quantity'],
1041 'price' => $product['price'],
1042 'coupon_amount' => 0,
1043 'tax_amount' => 0,
1044 'shipping_amount' => 0,
1045 'shipping_tax_amount' => 0,
1046 ];
1047 }
1048
1049 return $purchase_items;
1050 }
1051
1052 /**
1053 * @param $request
1054 *
1055 * @return array
1056 *
1057 * @deprecated
1058 */
1059 protected function prepare_item_for_database( $request ): array {
1060 // Insert purchase items into cart with for each loop.
1061 $request['customer_id'] = empty( $request['customer_id'] ) ? $request['id'] : $request['customer_id'];
1062 $cart_model = new \StoreEngine\Models\Cart( $request['customer_id'] );
1063 $purchase_items = $this->prepare_purchase_items_data( $cart_model );
1064
1065 return [
1066 'status' => OrderStatus::PAYMENT_PENDING,
1067 'currency' => Formatting::get_currency(),
1068 'type' => 'onetime',
1069 'customer_id' => $request['customer_id'],
1070 'billing_email' => $request['billing_email'],
1071 'payment_method' => $request['payment_method'] ?? '',
1072 'payment_method_title' => $request['payment_method'] ?? '',
1073 'customer_note' => $request['order_note'] ?? '',
1074 'transaction_id' => $request['transaction_id'] ?? '',
1075 'meta' => [],
1076 'purchase_items' => $purchase_items,
1077 'billing_address' => [
1078 'first_name' => $request['billing_address']['first_name'] ?? '',
1079 'last_name' => $request['billing_address']['last_name'] ?? '',
1080 'company' => $request['billing_address']['company'] ?? '',
1081 'address_1' => $request['billing_address']['address_1'] ?? '',
1082 'address_2' => $request['billing_address']['address_2'] ?? '',
1083 'city' => $request['billing_address']['city'] ?? '',
1084 'state' => $request['billing_address']['state'] ?? '',
1085 'postcode' => $request['billing_address']['postcode'] ?? '',
1086 'country' => $request['billing_address']['country'] ?? '',
1087 'email' => $request['billing_address']['email'] ?? '',
1088 'phone' => $request['billing_address']['phone'] ?? '',
1089 ],
1090 'shipping_address' => [
1091 'first_name' => $request['shipping_address']['first_name'] ?? '',
1092 'last_name' => $request['shipping_address']['last_name'] ?? '',
1093 'company' => $request['shipping_address']['company'] ?? '',
1094 'address_1' => $request['shipping_address']['address_1'] ?? '',
1095 'address_2' => $request['shipping_address']['address_2'] ?? '',
1096 'city' => $request['shipping_address']['city'] ?? '',
1097 'state' => $request['shipping_address']['state'] ?? '',
1098 'postcode' => $request['shipping_address']['postcode'] ?? '',
1099 'country' => $request['shipping_address']['country'] ?? '',
1100 'email' => $request['shipping_address']['email'] ?? '',
1101 'phone' => $request['shipping_address']['phone'] ?? '',
1102 ],
1103 ];
1104 }
1105
1106 /**
1107 * @param array $schema
1108 * @param $order_purchase_items
1109 *
1110 * @return array
1111 * @deprecated
1112 */
1113 protected function _prepare_order_purchase_items_for_database( array $schema, $order_purchase_items ): array {
1114 $order_details = [];
1115 $keys = [
1116 'product_id' => 'int',
1117 'variation_id' => 'int',
1118 'product_qty' => 'int',
1119 'coupon_amount' => 'float',
1120 'tax_amount' => 'float',
1121 'shipping_amount' => 'float',
1122 'shipping_tax_amount' => 'float',
1123 ];
1124
1125 foreach ( $order_purchase_items as $key => $order_item ) {
1126 foreach ( $keys as $field => $type ) {
1127 if ( ! empty( $schema['purchase_items']['properties'][ $field ] ) && isset( $order_item[ $field ] ) ) {
1128 settype( $order_item[ $field ], $type );
1129 $order_details[ $key ][ $field ] = $order_item[ $field ];
1130 }
1131 }
1132 }
1133
1134 return $order_details;
1135 }
1136
1137 /**
1138 * @param array $schema
1139 * @param $request_billing_address
1140 *
1141 * @return array
1142 * @deprecated
1143 */
1144 protected function _prepare_order_billing_address_for_database( array $schema, $request_billing_address ): array {
1145 $billing_address = [];
1146 $keys = [
1147 'first_name' => 'string',
1148 'last_name' => 'string',
1149 'company' => 'string',
1150 'address_1' => 'string',
1151 'address_2' => 'string',
1152 'city' => 'string',
1153 'state' => 'string',
1154 'postcode' => 'string',
1155 'country' => 'string',
1156 'email' => 'string',
1157 'phone' => 'string',
1158 ];
1159
1160 foreach ( $keys as $key => $type ) {
1161 if ( ! empty( $schema['billing_address']['properties'][ $key ] ) && isset( $request_billing_address[ $key ] ) ) {
1162 $value = $request_billing_address[ $key ];
1163 settype( $value, $type );
1164 $billing_address[ $key ] = $value;
1165 }
1166 }
1167
1168 return $billing_address;
1169 }
1170 }
1171