PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.1.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.1.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 / classes / order.php

order.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.1.0, at includes/classes/order.php

3,006 lines 90.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes;
4
5 use Exception;
6 use stdClass;
7 use StoreEngine\Classes\Exceptions\StoreEngineException;
8 use StoreEngine\Classes\Exceptions\StoreEngineInvalidArgumentException;
9 use StoreEngine\Classes\Order\OrderItemCoupon;
10 use StoreEngine\Classes\OrderStatus\OrderStatus;
11 use StoreEngine\Hooks;
12 use StoreEngine\Payment\Gateways\PaymentGateway;
13 use StoreEngine\Utils\ArrayUtil;
14 use StoreEngine\Utils\Caching;
15 use StoreEngine\Utils\Formatting;
16 use StoreEngine\Utils\Helper;
17 use StoreEngine\Utils\PaymentUtil;
18 use StoreEngine\Utils\TaxUtil;
19 use WP_Comment;
20
21 if ( ! defined( 'ABSPATH' ) ) {
22 exit;
23 }
24
25 /**
26 * @see \WC_Order
27 * @see \WC_Order_Data_Store_CPT
28 * @see \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore
29 */
30 class Order extends AbstractOrder {
31
32 protected array $extra_data = [
33 'order_placed_date_gmt' => null,
34 'order_placed_date' => null,
35 'paid_status' => null,
36 // Addresses
37 'billing' => [
38 'first_name' => '',
39 'last_name' => '',
40 'company' => '',
41 'address_1' => '',
42 'address_2' => '',
43 'city' => '',
44 'state' => '',
45 'postcode' => '',
46 'country' => '',
47 'email' => '',
48 'phone' => '',
49 'address_type' => 'billing',
50 ],
51 'shipping' => [
52 'first_name' => '',
53 'last_name' => '',
54 'company' => '',
55 'address_1' => '',
56 'address_2' => '',
57 'city' => '',
58 'state' => '',
59 'postcode' => '',
60 'country' => '',
61 'email' => '',
62 'phone' => '',
63 'address_type' => 'shipping',
64 ],
65 'download_permissions_granted' => false,
66 'auto_complete_digital_order' => false,
67 ];
68
69 /**
70 * Stores data about status changes so relevant hooks can be fired.
71 *
72 * @var bool|array
73 */
74 protected $status_transition = false;
75
76 public function __construct( $read = 0 ) {
77 $this->internal_meta_keys[] = '_order_placed_date_gmt';
78 $this->internal_meta_keys[] = '_order_placed_date';
79 $this->internal_meta_keys[] = '_download_permissions_granted';
80 $this->internal_meta_keys[] = '_auto_complete_digital_order';
81 $this->meta_key_to_props['_order_placed_date_gmt'] = 'order_placed_date_gmt';
82 $this->meta_key_to_props['_order_placed_date'] = 'order_placed_date';
83 $this->meta_key_to_props['_paid_status'] = 'paid_status';
84 $this->meta_key_to_props['_download_permissions_granted'] = 'download_permissions_granted';
85 $this->meta_key_to_props['_auto_complete_digital_order'] = 'auto_complete_digital_order';
86 parent::__construct( $read );
87 }
88
89 protected function read_db_data( $value, string $field = 'id' ): array {
90 return array_merge(
91 parent::read_db_data( $value, $field ),
92 [
93 'order_placed_date_gmt' => $this->get_metadata( '_order_placed_date_gmt' ),
94 'order_placed_date' => $this->get_metadata( '_order_placed_date' ),
95 'paid_status' => $this->get_metadata( '_paid_status' ),
96 'download_permissions_granted' => $this->get_metadata( '_download_permissions_granted' ),
97 'auto_complete_digital_order' => $this->get_metadata( '_auto_complete_digital_order' ),
98 ]
99 );
100 }
101
102 public function save() {
103 $this->maybe_set_user_billing_email();
104
105 $saved = parent::save();
106
107 if ( is_wp_error( $saved ) ) {
108 return $saved;
109 }
110
111 $this->status_transition();
112
113 return $this->get_id();
114 }
115
116 public function create() {
117 parent::create();
118
119 if ( ! $this->is_type( 'refund' ) && ( array_key_exists( 'billing', $this->data ) || array_key_exists( 'shipping', $this->data ) ) ) {
120 foreach ( [ 'billing', 'shipping' ] as $type ) {
121 $address = $this->get_address( $type );
122
123 if ( ! $this->{'has_' . $type . '_address'}( 'edit' ) ) {
124 continue;
125 }
126
127 $address['order_id'] = $this->get_id();
128 $formats = array_fill( 0, count( $address ), '%s' );
129
130 $this->wpdb->insert( "{$this->wpdb->prefix}storeengine_order_addresses", $address, $formats );
131
132 if ( $this->wpdb->last_error ) {
133 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' );
134 }
135 }
136 }
137 }
138
139 public function update() {
140 parent::update();
141
142 foreach ( [ 'billing', 'shipping' ] as $type ) {
143 $address = $this->get_address( $type );
144 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared.
145 $this->wpdb->query(
146 $this->wpdb->prepare(
147 "
148 INSERT INTO `{$this->wpdb->prefix}storeengine_order_addresses`
149 (`order_id`, `address_type`, `first_name`, `last_name`, `company`, `address_1`, `address_2`, `city`, `state`, `postcode`, `country`, `email`, `phone`)
150 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
151 ON DUPLICATE KEY UPDATE
152 `order_id` = VALUES(`order_id`),
153 `first_name` = VALUES(`first_name`),
154 `last_name` = VALUES(`last_name`),
155 `company` = VALUES(`company`),
156 `address_1` = VALUES(`address_1`),
157 `address_2` = VALUES(`address_2`),
158 `city` = VALUES(`city`),
159 `state` = VALUES(`state`),
160 `postcode` = VALUES(`postcode`),
161 `country` = VALUES(`country`),
162 `email` = VALUES(`email`),
163 `phone` = VALUES(`phone`);
164 ",
165 $this->get_id(),
166 $type,
167 $address['first_name'],
168 $address['last_name'],
169 $address['company'],
170 $address['address_1'],
171 $address['address_2'],
172 $address['city'],
173 $address['state'],
174 $address['postcode'],
175 $address['country'],
176 $address['email'],
177 $address['phone']
178 )
179 );
180 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared.
181
182 if ( $this->wpdb->last_error ) {
183 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' );
184 }
185 }
186 }
187
188 public function delete( bool $force_delete = false ): bool {
189 if ( ! $force_delete && $this->is_trashable() ) {
190 $this->set_status( 'trash' );
191 $this->save();
192
193 return true;
194 }
195
196 $refunds = $this->get_refunds();
197
198 if ( ! empty( $refunds ) ) {
199 foreach ( $refunds as $refund ) {
200 $refund->delete( true );
201 }
202 }
203
204 if ( $this->has_address( 'edit' ) ) {
205 $this->wpdb->delete( "{$this->wpdb->prefix}storeengine_order_addresses", [ 'order_id' => $this->get_id() ], [ '%d' ] );
206
207 if ( $this->wpdb->last_error ) {
208 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-delete-order_addresses' );
209 }
210 }
211
212 return parent::delete( true );
213 }
214
215 /**
216 * Log an error about this order is exception is encountered.
217 *
218 * @param StoreEngineException $e Exception object.
219 * @param string $message Message regarding exception thrown.
220 */
221 protected function handle_exception( StoreEngineException $e, string $message = 'Error' ) {
222 $this->add_order_note( $message . ' ' . $e->getMessage() );
223 }
224
225 /**
226 * When a payment is complete this function is called.
227 *
228 * Most of the time this should mark an order as 'processing' so that admin can process/post the items.
229 * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action.
230 * Stock levels are reduced at this point.
231 * Sales are also recorded for products.
232 * Finally, record the date of payment.
233 *
234 * @param string $transaction_id Optional transaction id to store in post meta.
235 *
236 * @return bool success
237 */
238 public function payment_complete( string $transaction_id = '' ): bool {
239 if ( ! $this->get_id() ) { // Order must exist.
240 return false;
241 }
242
243 try {
244 $order_id = $this->get_id();
245 /**
246 * Fires before payment complete process of an order.
247 *
248 * @param int $order_id Order id.
249 * @param string $transaction_id Transaction id.
250 */
251 do_action( 'storeengine/pre_payment_complete', $order_id, $transaction_id );
252
253 /**
254 * Filters the valid order statuses for payment complete.
255 *
256 * @param array $valid_completed_statuses Array of valid order statuses for payment complete.
257 * @param Order $this Order object.
258 */
259 $valid_completed_statuses = apply_filters( 'storeengine/valid_order_statuses_for_payment_complete', [
260 OrderStatus::ON_HOLD,
261 OrderStatus::PAYMENT_PENDING,
262 OrderStatus::PAYMENT_FAILED,
263 OrderStatus::CANCELLED,
264 ], $this );
265
266 if ( $this->has_status( $valid_completed_statuses ) ) {
267 if ( ! empty( $transaction_id ) ) {
268 $this->set_transaction_id( $transaction_id );
269 }
270
271 if ( ! $this->get_date_paid_gmt( 'edit' ) ) {
272 $this->set_date_paid_gmt( current_time( 'mysql', 1 ) );
273 }
274
275 /**
276 * Filters the order status to set after payment complete.
277 *
278 * @param string $status Order status.
279 * @param int $order_id Order ID.
280 * @param Order $this Order object.
281 */
282 $this->set_status( apply_filters( 'storeengine/payment_complete_order_status', $this->needs_processing() ? OrderStatus::PROCESSING : OrderStatus::COMPLETED, $this->get_id(), $this ) );
283 $this->save();
284
285 /**
286 * Fires after payment complete process of an order.
287 *
288 * @param int $order_id Order id.
289 * @param string $transaction_id Transaction id.
290 */
291 do_action( 'storeengine/payment_complete', $order_id, $transaction_id );
292 } else {
293 $status = $this->get_status();
294 /**
295 * If order status isn't valid for mark order as processing/completed, then fire this hook.
296 *
297 * @param int $order_id Order ID.
298 * @param array $transaction_id Transaction ID.
299 */
300 do_action( "storeengine/payment_complete_order_status_{$status}", $order_id, $transaction_id );
301 }
302 } catch ( Exception $e ) {
303 Helper::log_error( $e );
304
305 $this->add_order_note( __( 'Payment complete event failed.', 'storeengine' ) . ' ' . $e->getMessage() );
306
307 return false;
308 }
309
310 return true;
311 }
312
313 /**
314 * Forcibly mark an order as paid and advance it to a paid state, running the
315 * full payment-complete flow (status transition, digital auto-complete, paid
316 * status + date, downstream hooks/emails).
317 *
318 * Unlike payment_complete() this handles EVERY starting status — including
319 * `draft`/`auto-draft` (a Paddle order stranded by the client-side flow) — by
320 * first "placing" the order, then advancing it. Idempotent: a no-op that
321 * returns true if the order is already paid.
322 *
323 * Shared seam used by both the Paddle webhook reconciliation
324 * (GatewayPaddle::complete_order_from_transaction) and the admin
325 * "Mark as paid" action (Ajax\Order::mark_order_as_paid).
326 *
327 * @param string $note Order note attached to the status transition.
328 *
329 * @return bool True if the order ended in a paid state.
330 */
331 public function mark_as_paid_force( string $note = '' ): bool {
332 if ( ! $this->get_id() ) {
333 return false;
334 }
335
336 if ( $this->is_paid() ) {
337 return true;
338 }
339
340 try {
341 $status = $this->get_status();
342
343 // Draft orders must be "placed" first (draft -> pending_payment) before
344 // they can be processed. order_placed resets paid_status to unpaid, so
345 // we (re)assert paid below for the pending_payment branch.
346 if ( in_array( $status, [ OrderStatus::DRAFT, OrderStatus::AUTO_DRAFT ], true ) ) {
347 ( new OrderContext( $status ) )->proceed_to_next_status( 'order_placed', $this, [ 'note' => $note ] );
348 $status = $this->get_status();
349 }
350
351 if ( OrderStatus::PAYMENT_PENDING === $status ) {
352 // Mark paid BEFORE advancing so OrderContext's digital auto-complete
353 // branch (which checks paid_status === 'paid') fires correctly.
354 $this->set_paid_status( 'paid' );
355 ( new OrderContext( $status ) )->proceed_to_next_status( 'process_order', $this, [ 'note' => $note ] );
356 } elseif ( in_array( $status, [ OrderStatus::ON_HOLD, OrderStatus::PAYMENT_FAILED, OrderStatus::CANCELLED ], true ) ) {
357 // payment_complete() accepts exactly these statuses and moves the
358 // order to processing/completed based on needs_processing().
359 $this->payment_complete( $this->get_transaction_id() );
360
361 // set_status() only auto-marks paid for COMPLETED/PAYMENT_CONFIRMED,
362 // so assert it for the processing case too.
363 if ( 'paid' !== $this->get_paid_status( 'edit' ) ) {
364 $this->set_paid_status( 'paid' );
365 }
366 } else {
367 // Already in a post-payment status (processing/payment_confirmed/
368 // completed) but flagged unpaid — just assert the paid flag.
369 $this->set_paid_status( 'paid' );
370 }
371
372 $this->save();
373 } catch ( \Throwable $e ) {
374 Helper::log_error( $e );
375 $this->add_order_note( __( 'Force mark as paid failed.', 'storeengine' ) . ' ' . $e->getMessage() );
376
377 return false;
378 }
379
380 return $this->is_paid();
381 }
382
383 /**
384 * Gets order total - formatted for display.
385 *
386 * @param string $tax_display Type of tax display.
387 * @param bool $display_refunded If should include refunded value.
388 *
389 * @return string
390 */
391 public function get_formatted_order_total( string $tax_display = '', bool $display_refunded = true ): string {
392 $formatted_total = Formatting::price( $this->get_total(), [ 'currency' => $this->get_currency() ] );
393 $order_total = $this->get_total();
394 $total_refunded = $this->get_total_refunded();
395 $tax_string = '';
396
397 // Tax for inclusive prices.
398 if ( TaxUtil::is_tax_enabled() && 'incl' === $tax_display ) {
399 $tax_string_array = [];
400 $tax_totals = $this->get_tax_totals();
401
402 if ( 'itemized' === Helper::get_settings( 'tax_total_display' ) ) {
403 foreach ( $tax_totals as $code => $tax ) {
404 $tax_amount = ( $total_refunded && $display_refunded ) ? Formatting::price( Tax::round( $tax->amount - $this->get_total_tax_refunded_by_rate_id( $tax->rate_id ) ), [ 'currency' => $this->get_currency() ] ) : $tax->formatted_amount;
405 $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label );
406 }
407 } elseif ( ! empty( $tax_totals ) ) {
408 $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax();
409 $tax_string_array[] = sprintf( '%s %s', Formatting::price( $tax_amount, [ 'currency' => $this->get_currency() ] ), Countries::init()->tax_or_vat() );
410 }
411
412 if ( ! empty( $tax_string_array ) ) {
413 /* translators: %s: tax amounts */
414 $tax_string = ' <small class="includes_tax">' . sprintf( __( '(includes %s)', 'storeengine' ), implode( ', ', $tax_string_array ) ) . '</small>';
415 }
416 }
417
418 if ( $total_refunded && $display_refunded ) {
419 $current_total = Formatting::price( $order_total - $total_refunded, [ 'currency' => $this->get_currency() ] );
420 // Strikethrough pricing.
421 $formatted_total = '<del aria-hidden="true">' . $formatted_total . '</del> ';
422
423 // For accessibility (a11y) we'll also display that information to screen readers.
424 $formatted_total .= '<span class="screen-reader-text"> ';
425 // translators: %s is total order amount without refund.
426 $formatted_total .= esc_html( sprintf( __( 'Original amount was: %s.', 'storeengine' ), wp_strip_all_tags( $formatted_total ) ) );
427 $formatted_total .= '</span>';
428
429 // Add the sale price.
430 $formatted_total .= ' <ins aria-hidden="true">' . $current_total . $tax_string . '</ins> ';
431
432 // For accessibility (a11y) we'll also display that information to screen readers.
433 $formatted_total .= '<span class="screen-reader-text"> ';
434 // translators: %s is total order amount after refund.
435 $formatted_total .= esc_html( sprintf( __( 'Current amount is: %s.', 'storeengine' ), wp_strip_all_tags( $current_total ) ) );
436 $formatted_total .= '</span>';
437 } else {
438 $formatted_total .= $tax_string;
439 }
440
441 /**
442 * Filter StoreEngine formatted order total.
443 *
444 * @param string $formatted_total Total to display.
445 * @param Order $order Order data.
446 * @param string $tax_display Type of tax display.
447 * @param bool $display_refunded If should include refunded value.
448 */
449 return apply_filters( 'storeengine/get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded );
450 }
451
452 /**
453 * Set order status.
454 *
455 * @param string $new_status Status to change the order to. No internal wc- prefix is required.
456 * @param string $note
457 * @param bool $manual_update
458 *
459 * @return array
460 */
461 public function set_status( string $new_status, string $note = '', bool $manual_update = false ): array {
462 $result = parent::set_status( $new_status );
463
464 if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) {
465 $this->status_transition = [
466 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'],
467 'to' => $result['to'],
468 'note' => $note,
469 'manual' => $manual_update,
470 ];
471
472 if ( $manual_update ) {
473 $order_id = $this->get_id();
474 $new_order_status = $result['to'];
475 /**
476 * Fires during set new status when manual update is set to true.
477 *
478 * @param int $order_id Order ID.
479 * @param string $new_order_status New Order Status.
480 */
481 do_action( 'storeengine/order_edit_status', $order_id, $new_order_status );
482 }
483
484 if ( $this->is_type( 'order' ) ) {
485 // Maybe set order-placed-date
486 if ( ! $this->get_order_placed_date_gmt( 'edit' ) && $this->has_status( [
487 OrderStatus::PROCESSING,
488 OrderStatus::PAYMENT_CONFIRMED,
489 OrderStatus::COMPLETED
490 ] ) ) {
491 $this->set_order_placed_date_gmt();
492 $this->set_order_placed_date();
493 }
494
495 // Maybe set paid status.
496 if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::COMPLETED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) {
497 $this->set_paid_status( 'paid' );
498 $this->add_order_note( __( 'Order marked as paid automatically as order is completed.', 'storeengine' ) );
499 }
500
501 if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_CONFIRMED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) {
502 $this->set_paid_status( 'paid' );
503 $this->add_order_note( __( 'Order marked as paid as order status set to payment confirmed.', 'storeengine' ) );
504 }
505
506 if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_PENDING ) ) {
507 $this->set_paid_status( 'unpaid' );
508 $this->set_date_paid_gmt( 0 );
509 $this->add_order_note( __( 'Payment status marked as unpaid as order status set to payment pending.', 'storeengine' ) );
510 }
511
512 if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::ON_HOLD ) ) {
513 $this->set_paid_status( 'on_hold' );
514 $this->set_date_paid_gmt( 0 );
515 $this->add_order_note( __( 'Payment status marked as oh-hold as order status set to payment on-hold.', 'storeengine' ) );
516 }
517
518 if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( 'refunded' ) ) {
519 $this->set_paid_status( 'refunded' );
520 $this->set_date_paid_gmt( 0 );
521 $this->add_order_note( __( 'Payment status marked as refunded as order status set to payment refunded.', 'storeengine' ) );
522 }
523
524 $this->maybe_set_date_completed();
525 }
526 }
527
528 return $result;
529 }
530
531 /**
532 * Maybe set date paid.
533 *
534 * Sets the date paid variable when transitioning to the payment complete
535 * order status. This is either processing or completed. This is not filtered
536 * to avoid infinite loops e.g. if loading an order via the filter.
537 *
538 * Date paid is set once in this manner - only when it is not already set.
539 * This ensures the data exists even if a gateway does not use the
540 * `payment_complete` method.
541 *
542 * @deprecated use paid_status
543 */
544 public function maybe_set_date_paid() {
545 // This logic only runs if the date_paid prop has not been set yet.
546 if ( ! $this->get_date_paid_gmt( 'edit' ) ) {
547 $paid_statuses = [ OrderStatus::PAYMENT_CONFIRMED, OrderStatus::PROCESSING, OrderStatus::COMPLETED ];
548 if ( $this->has_status( $paid_statuses ) ) {
549 // If payment complete status is reached, set paid now.
550 $this->set_date_paid_gmt( current_time( 'mysql', 1 ) );
551 $this->set_prop( 'paid_status', 'paid' );
552 } else {
553 $this->set_date_paid_gmt( 0 );
554 $this->set_prop( 'paid_status', 'unpaid' );
555 }
556 }
557
558 $unpaid_statuses = [ OrderStatus::PAYMENT_PENDING, OrderStatus::AUTO_DRAFT, OrderStatus::DRAFT ];
559 if ( $this->get_date_paid_gmt( 'edit' ) && $this->has_status( $unpaid_statuses ) ) {
560 $this->set_date_paid_gmt( 0 );
561 $this->set_prop( 'paid_status', 'unpaid' );
562 }
563 }
564
565 /**
566 * @param string $status
567 * @param string|null $transaction_id
568 *
569 * @return void
570 * @throws StoreEngineException
571 */
572 public function set_paid_status( string $status, ?string $transaction_id = null ) {
573 $paid_stati = [ 'paid', 'partially_paid', 'unpaid', 'failed', 'on_hold', 'refunded' ];
574 if ( ! in_array( $status, $paid_stati, true ) ) {
575 throw StoreEngineInvalidArgumentException::create( 1, 'status', $paid_stati, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
576 }
577
578 if ( $transaction_id ) {
579 $this->set_transaction_id( $transaction_id );
580 }
581
582 $old_status = $this->get_paid_status();
583
584 if ( 'paid' === $status ) {
585 if ( ! $this->get_date_paid_gmt( 'edit' ) || 'partially_paid' === $old_status ) {
586 $this->set_date_paid_gmt( current_time( 'mysql', 1 ) );
587 }
588 } elseif ( 'partially_paid' === $status ) {
589 $this->set_date_paid_gmt( current_time( 'mysql', 1 ) );
590 } else {
591 $this->set_date_paid_gmt( 0 );
592 }
593
594 $this->set_prop( 'paid_status', $status );
595
596 if ( true === $this->object_read && ! empty( $old_status ) && $old_status !== $status ) {
597 do_action_ref_array( 'storeengine/order/payment_status_changed', [ &$this, $status, $old_status ] );
598 }
599 }
600
601 public function get_paid_status( string $context = 'view' ): ?string {
602 $status = $this->get_prop( 'paid_status', $context );
603
604 if ( ! $status && 'view' === $context ) {
605 $status = 'unpaid';
606 }
607
608 return $status;
609 }
610
611 public function is_paid( $context = 'view' ): bool {
612 return (bool) apply_filters( 'storeengine/order/is_paid', 'paid' === $this->get_paid_status( $context ) );
613 }
614
615 // /**
616 // * Returns if an order has been paid for based on the order status.
617 // *
618 // * @return bool
619 // */
620 // public function is_paid(): bool {
621 // return apply_filters( 'storeengine/order_is_paid', $this->has_status( OrderStatus::get_is_paid_statuses() ), $this );
622 // }
623
624 /**
625 * Maybe set date completed.
626 *
627 * Sets the date completed variable when transitioning to completed status.
628 */
629 protected function maybe_set_date_completed() {
630 if ( $this->has_status( OrderStatus::COMPLETED ) ) {
631 $this->set_date_completed_gmt( time() );
632 }
633 }
634
635 /**
636 * Updates status of order immediately.
637 *
638 * @param string $new_status Status to change the order to. No internal wc- prefix is required.
639 * @param string $note Optional note to add.
640 * @param bool $manual Is this a manual order status change?.
641 *
642 * @return bool
643 * @uses self::set_status()
644 */
645 public function update_status( string $new_status, string $note = '', bool $manual = false ): bool {
646 if ( ! $this->get_id() ) { // Order must exist.
647 return false;
648 }
649
650 try {
651 $this->set_status( $new_status, $note, $manual );
652 $this->save();
653 } catch ( Exception $e ) {
654 Helper::log_error( $e );
655 $this->add_order_note( __( 'Update status event failed.', 'storeengine' ) . ' ' . $e->getMessage() );
656
657 return false;
658 }
659
660 return true;
661 }
662
663 /**
664 * Handle the status transition.
665 */
666 protected function status_transition() {
667 $status_transition = $this->status_transition;
668
669 // Reset status transition variable.
670 $this->status_transition = false;
671
672 if ( $status_transition ) {
673 try {
674 $new_status = $status_transition['to'];
675 $order_id = $this->get_id();
676 /**
677 * Fires when order status is changed.
678 *
679 * @param int $order_id Order ID.
680 * @param Order $this Order object.
681 * @param array $status_transition Status transition data.
682 */
683 do_action( "storeengine/order_status_{$new_status}", $order_id, $this, $status_transition );
684
685 if ( ! empty( $status_transition['from'] ) ) {
686 /* translators: 1: old order status 2: new order status */
687 $transition_note = sprintf( __( 'Order status changed from %1$s to %2$s.', 'storeengine' ), OrderStatus::get_order_status_name( $status_transition['from'] ), OrderStatus::get_order_status_name( $status_transition['to'] ) );
688
689 // Note the transition occurred.
690 $this->add_status_transition_note( $transition_note, $status_transition );
691
692 $old_status = $status_transition['from'];
693
694 /**
695 * Fires when order status is changed.
696 *
697 * @param int $order_id Order ID.
698 * @param Order $this Order object.
699 */
700 do_action( "storeengine/order_status_{$old_status}_to_{$new_status}", $order_id, $this );
701
702 /**
703 * Fires when order status is changed.
704 *
705 * @param int $order_id Order ID.
706 * @param string $old_status Old Status.
707 * @param string $new_status New Status.
708 * @param Order $this Order object.
709 */
710 do_action( 'storeengine/order/status_changed', $order_id, $old_status, $new_status, $this );
711
712 /**
713 * Fires when order status is changed.
714 *
715 * @param int $order_id Order ID.
716 * @param string $old_status Old Status.
717 * @param Order $this Order object.
718 */
719 do_action( "storeengine/order/status_{$new_status}", $order_id, $old_status, $this );
720
721 // Work out if this was for a payment, and trigger a payment_status hook instead.
722 /**
723 * Filter the valid order statuses for payment.
724 *
725 * @param array $valid_order_statuses Array of valid order statuses for payment.
726 * @param Order $order Order object.
727 */
728 $check_transition_from = in_array( $status_transition['from'], apply_filters( 'storeengine/valid_order_statuses_for_payment', [
729 OrderStatus::PAYMENT_PENDING,
730 OrderStatus::PAYMENT_FAILED,
731 ], $this ), true );
732 $check_transition_to = in_array( $status_transition['to'], OrderStatus::get_is_paid_statuses(), true );
733 if ( $check_transition_from && $check_transition_to ) {
734 /**
735 * Fires when the order progresses from a pending payment status to a paid one.
736 *
737 * @param int $order_id Order ID.
738 * @param Order $this Order object.
739 */
740 do_action( 'storeengine/order_payment_status_changed', $order_id, $this );
741 }
742 } else {
743 /* translators: %s: new order status */
744 $transition_note = sprintf( __( 'Order status set to %s.', 'storeengine' ), OrderStatus::get_order_status_name( $status_transition['to'] ) );
745
746 // Note the transition occurred.
747 $this->add_status_transition_note( $transition_note, $status_transition );
748 }
749 } catch ( Exception $e ) {
750 Helper::log_error( $e );
751 $this->add_order_note( __( 'Error during status transition.', 'storeengine' ) . ' ' . $e->getMessage() );
752 }
753 }
754 }
755
756 /*
757 |--------------------------------------------------------------------------
758 | Getters
759 |--------------------------------------------------------------------------
760 |
761 | Methods for getting data from the order object.
762 |
763 */
764
765 /**
766 * Get basic order data in array format.
767 *
768 * @return array
769 */
770 public function get_base_data(): array {
771 return array_merge(
772 [ 'id' => $this->get_id() ],
773 $this->data,
774 [ 'number' => $this->get_order_number() ]
775 );
776 }
777
778 /**
779 * Get all class data in array format.
780 *
781 * @return array
782 */
783 public function get_data(): array {
784 return array_merge(
785 $this->get_base_data(),
786 [
787 'meta_data' => $this->get_meta_data(),
788 'line_items' => $this->get_items( 'line_item' ),
789 'tax_lines' => $this->get_items( 'tax' ),
790 'shipping_lines' => $this->get_items( 'shipping' ),
791 'fee_lines' => $this->get_items( 'fee' ),
792 'coupon_lines' => $this->get_items( 'coupon' ),
793 ]
794 );
795 }
796
797 /**
798 * Expands the shipping and billing information in the changes array.
799 */
800 public function get_changes(): array {
801 $changed_props = parent::get_changes();
802 $subs = [ 'shipping', 'billing' ];
803 foreach ( $subs as $sub ) {
804 if ( ! empty( $changed_props[ $sub ] ) ) {
805 foreach ( $changed_props[ $sub ] as $sub_prop => $value ) {
806 $changed_props[ $sub . '_' . $sub_prop ] = $value;
807 }
808 }
809 }
810 if ( isset( $changed_props['customer_note'] ) ) {
811 $changed_props['post_excerpt'] = $changed_props['customer_note'];
812 }
813
814 return $changed_props;
815 }
816
817 /**
818 * Gets the order number for display (by default, order ID).
819 *
820 * @return string
821 */
822 public function get_order_number(): string {
823 return (string) apply_filters( 'storeengine/order_number', $this->get_id(), $this );
824 }
825
826 /**
827 * Gets a prop for a getter method.
828 *
829 * @param string $prop Name of prop to get.
830 * @param string $address_type Type of address; 'billing' or 'shipping'.
831 * @param string $context What the value is for. Valid values are view and edit.
832 *
833 * @return ?string
834 */
835 protected function get_address_prop( string $prop, string $address_type = 'billing', string $context = 'view' ): ?string {
836 $value = null;
837
838 if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) {
839 $value = $this->changes[ $address_type ][ $prop ] ?? $this->data[ $address_type ][ $prop ];
840
841 if ( 'view' === $context ) {
842 /**
843 * Filter: 'storeengine/order_get_[billing|shipping]_[prop]'
844 *
845 * Allow developers to change the returned value for any order address property.
846 *
847 * @param string $value The address property value.
848 * @param Order $order The order object being read.
849 *
850 * @ignore Ignore from HookParser.
851 */
852 $value = apply_filters( $this->get_hook_prefix( $address_type . '_' . $prop ), $value, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
853 }
854 }
855
856 return $value;
857 }
858
859 /**
860 * Get billing first name.
861 *
862 * @param string $context What the value is for. Valid values are view and edit.
863 *
864 * @return ?string
865 */
866 public function get_billing_first_name( string $context = 'view' ): ?string {
867 return $this->get_address_prop( 'first_name', 'billing', $context );
868 }
869
870 /**
871 * Get billing last name.
872 *
873 * @param ?string $context What the value is for. Valid values are view and edit.
874 *
875 * @return string
876 */
877 public function get_billing_last_name( string $context = 'view' ): ?string {
878 return $this->get_address_prop( 'last_name', 'billing', $context );
879 }
880
881 /**
882 * Get billing company.
883 *
884 * @param string $context What the value is for. Valid values are view and edit.
885 *
886 * @return ?string
887 */
888 public function get_billing_company( string $context = 'view' ): ?string {
889 return $this->get_address_prop( 'company', 'billing', $context );
890 }
891
892 /**
893 * Get billing address line 1.
894 *
895 * @param string $context What the value is for. Valid values are view and edit.
896 *
897 * @return ?string
898 */
899 public function get_billing_address_1( string $context = 'view' ): ?string {
900 return $this->get_address_prop( 'address_1', 'billing', $context );
901 }
902
903 /**
904 * Get billing address line 2.
905 *
906 * @param string $context What the value is for. Valid values are view and edit.
907 *
908 * @return ?string
909 */
910 public function get_billing_address_2( string $context = 'view' ): ?string {
911 return $this->get_address_prop( 'address_2', 'billing', $context );
912 }
913
914 /**
915 * Get billing city.
916 *
917 * @param string $context What the value is for. Valid values are view and edit.
918 *
919 * @return ?string
920 */
921 public function get_billing_city( string $context = 'view' ): ?string {
922 return $this->get_address_prop( 'city', 'billing', $context );
923 }
924
925 /**
926 * Get billing state.
927 *
928 * @param string $context What the value is for. Valid values are view and edit.
929 *
930 * @return ?string
931 */
932 public function get_billing_state( string $context = 'view' ): ?string {
933 return $this->get_address_prop( 'state', 'billing', $context );
934 }
935
936 /**
937 * Get billing postcode.
938 *
939 * @param string $context What the value is for. Valid values are view and edit.
940 *
941 * @return ?string
942 */
943 public function get_billing_postcode( string $context = 'view' ): ?string {
944 return $this->get_address_prop( 'postcode', 'billing', $context );
945 }
946
947 /**
948 * Get billing country.
949 *
950 * @param string $context What the value is for. Valid values are view and edit.
951 *
952 * @return ?string
953 */
954 public function get_billing_country( string $context = 'view' ): ?string {
955 return $this->get_address_prop( 'country', 'billing', $context );
956 }
957
958 /**
959 * Get billing email.
960 *
961 * @param string $context What the value is for. Valid values are view and edit.
962 *
963 * @return ?string
964 */
965 public function get_billing_email( string $context = 'view' ): ?string {
966 return $this->get_address_prop( 'email', 'billing', $context );
967 }
968
969 /**
970 * Get billing phone.
971 *
972 * @param string $context What the value is for. Valid values are view and edit.
973 *
974 * @return ?string
975 */
976 public function get_billing_phone( string $context = 'view' ): ?string {
977 return $this->get_address_prop( 'phone', 'billing', $context );
978 }
979
980 /**
981 * Get shipping first name.
982 *
983 * @param string $context What the value is for. Valid values are view and edit.
984 *
985 * @return ?string
986 */
987 public function get_shipping_first_name( string $context = 'view' ): ?string {
988 return $this->get_address_prop( 'first_name', 'shipping', $context );
989 }
990
991 /**
992 * Get shipping_last_name.
993 *
994 * @param string $context What the value is for. Valid values are view and edit.
995 *
996 * @return ?string
997 */
998 public function get_shipping_last_name( string $context = 'view' ): ?string {
999 return $this->get_address_prop( 'last_name', 'shipping', $context );
1000 }
1001
1002 /**
1003 * Get shipping company.
1004 *
1005 * @param string $context What the value is for. Valid values are view and edit.
1006 *
1007 * @return ?string
1008 */
1009 public function get_shipping_company( string $context = 'view' ): ?string {
1010 return $this->get_address_prop( 'company', 'shipping', $context );
1011 }
1012
1013 /**
1014 * Get shipping address line 1.
1015 *
1016 * @param string $context What the value is for. Valid values are view and edit.
1017 *
1018 * @return? string
1019 */
1020 public function get_shipping_address_1( string $context = 'view' ): ?string {
1021 return $this->get_address_prop( 'address_1', 'shipping', $context );
1022 }
1023
1024 /**
1025 * Get shipping address line 2.
1026 *
1027 * @param string $context What the value is for. Valid values are view and edit.
1028 *
1029 * @return ?string
1030 */
1031 public function get_shipping_address_2( string $context = 'view' ): ?string {
1032 return $this->get_address_prop( 'address_2', 'shipping', $context );
1033 }
1034
1035 /**
1036 * Get shipping city.
1037 *
1038 * @param string $context What the value is for. Valid values are view and edit.
1039 *
1040 * @return string
1041 */
1042 public function get_shipping_city( string $context = 'view' ): ?string {
1043 return $this->get_address_prop( 'city', 'shipping', $context );
1044 }
1045
1046 /**
1047 * Get shipping state.
1048 *
1049 * @param string $context What the value is for. Valid values are view and edit.
1050 *
1051 * @return ?string
1052 */
1053 public function get_shipping_state( string $context = 'view' ): ?string {
1054 return $this->get_address_prop( 'state', 'shipping', $context );
1055 }
1056
1057 /**
1058 * Get shipping postcode.
1059 *
1060 * @param string $context What the value is for. Valid values are view and edit.
1061 *
1062 * @return ?string
1063 */
1064 public function get_shipping_postcode( string $context = 'view' ): ?string {
1065 return $this->get_address_prop( 'postcode', 'shipping', $context );
1066 }
1067
1068 /**
1069 * Get shipping country.
1070 *
1071 * @param string $context What the value is for. Valid values are view and edit.
1072 *
1073 * @return ?string
1074 */
1075 public function get_shipping_country( string $context = 'view' ): ?string {
1076 return $this->get_address_prop( 'country', 'shipping', $context );
1077 }
1078
1079 /**
1080 * Get shipping country.
1081 *
1082 * @param string $context What the value is for. Valid values are view and edit.
1083 *
1084 * @return ?string
1085 */
1086 public function get_shipping_email( string $context = 'view' ): ?string {
1087 return $this->get_address_prop( 'email', 'shipping', $context );
1088 }
1089
1090 /**
1091 * Get shipping phone.
1092 *
1093 * @param string $context What the value is for. Valid values are view and edit.
1094 *
1095 * @return ?string
1096 */
1097 public function get_shipping_phone( string $context = 'view' ): ?string {
1098 return $this->get_address_prop( 'phone', 'shipping', $context );
1099 }
1100
1101 /**
1102 * Get the payment method.
1103 *
1104 * @param string $context What the value is for. Valid values are view and edit.
1105 *
1106 * @return string
1107 */
1108 public function get_payment_method( string $context = 'view' ) {
1109 return $this->get_prop( 'payment_method', $context );
1110 }
1111
1112 /**
1113 * Get payment method title.
1114 *
1115 * @param string $context What the value is for. Valid values are view and edit.
1116 *
1117 * @return string
1118 */
1119 public function get_payment_method_title( string $context = 'view' ) {
1120 return $this->get_prop( 'payment_method_title', $context );
1121 }
1122
1123 /**
1124 * Get transaction id.
1125 *
1126 * @param string $context What the value is for. Valid values are view and edit.
1127 *
1128 * @return ?string
1129 */
1130 public function get_transaction_id( string $context = 'view' ) {
1131 return $this->get_prop( 'transaction_id', $context );
1132 }
1133
1134 /**
1135 * Get customer ip address.
1136 *
1137 * @param string $context What the value is for. Valid values are view and edit.
1138 *
1139 * @return ?string
1140 */
1141 public function get_ip_address( string $context = 'view' ) {
1142 return $this->get_prop( 'ip_address', $context );
1143 }
1144
1145 /**
1146 * Get customer user agent.
1147 *
1148 * @param string $context What the value is for. Valid values are view and edit.
1149 *
1150 * @return ?string
1151 */
1152 public function get_user_agent( string $context = 'view' ) {
1153 return $this->get_prop( 'user_agent', $context );
1154 }
1155
1156 /**
1157 * Get created via.
1158 *
1159 * @param string $context What the value is for. Valid values are view and edit.
1160 *
1161 * @return ?string
1162 */
1163 public function get_created_via( string $context = 'view' ) {
1164 return $this->get_prop( 'created_via', $context );
1165 }
1166
1167 /**
1168 * Get customer note.
1169 *
1170 * @param string $context What the value is for. Valid values are view and edit.
1171 *
1172 * @return string
1173 */
1174 public function get_customer_note( string $context = 'view' ) {
1175 return $this->get_prop( 'customer_note', $context );
1176 }
1177
1178 /**
1179 * Get cart hash.
1180 *
1181 * @param string $context What the value is for. Valid values are view and edit.
1182 *
1183 * @return string
1184 */
1185 public function get_cart_hash( string $context = 'view' ) {
1186 return $this->get_hash( $context );
1187 }
1188
1189 /**
1190 * Get cart hash.
1191 *
1192 * @param string $context What the value is for. Valid values are view and edit.
1193 *
1194 * @return string
1195 */
1196 public function get_hash( string $context = 'view' ) {
1197 return $this->get_prop( 'hash', $context );
1198 }
1199
1200 /**
1201 * Returns the requested address in raw, non-formatted way.
1202 * Note: Merges raw data with get_prop data so changes are returned too.
1203 *
1204 * @param string $address_type Type of address; 'billing' or 'shipping'.
1205 *
1206 * @return array The stored address after filter.
1207 */
1208 public function get_address( $address_type = 'billing' ) {
1209 /**
1210 * Filter: 'storeengine/get_order_address'
1211 *
1212 * Allow developers to change the returned value for an order's billing or shipping address.
1213 *
1214 * @param array $address_data The raw address data merged with the data from get_prop.
1215 * @param string $address_type Type of address; 'billing' or 'shipping'.
1216 */
1217 return apply_filters( 'storeengine/get_order_address', array_merge( $this->data[ $address_type ], $this->get_prop( $address_type, 'view' ) ), $address_type, $this );
1218 }
1219
1220 /**
1221 * Get a formatted shipping address for the order.
1222 *
1223 * @return string
1224 */
1225 public function get_shipping_address_map_url() {
1226 $address = $this->get_address( 'shipping' );
1227
1228 // Remove name and company before generate the Google Maps URL.
1229 unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] );
1230
1231 $address = apply_filters( 'storeengine/shipping_address_map_url_parts', $address, $this );
1232
1233 return apply_filters( 'storeengine/shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this );
1234 }
1235
1236 /**
1237 * Get a formatted billing full name.
1238 *
1239 * @return string
1240 */
1241 public function get_formatted_billing_full_name() {
1242 /* translators: 1: first name 2: last name */
1243 return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_billing_first_name(), $this->get_billing_last_name() );
1244 }
1245
1246 /**
1247 * Get a formatted shipping full name.
1248 *
1249 * @return string
1250 */
1251 public function get_formatted_shipping_full_name() {
1252 /* translators: 1: first name 2: last name */
1253 return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() );
1254 }
1255
1256 /**
1257 * Get a formatted billing address for the order.
1258 *
1259 * @param string $empty_content Content to show if no address is present.
1260 *
1261 * @return string
1262 */
1263 public function get_formatted_billing_address( $empty_content = '' ) {
1264 $raw_address = apply_filters( 'storeengine/order_formatted_billing_address', $this->get_address( 'billing' ), $this );
1265 $address = Countries::init()->get_formatted_address( $raw_address );
1266
1267 /**
1268 * Filter orders formatted billing address.
1269 *
1270 * @param string $address Formatted billing address string.
1271 * @param array $raw_address Raw billing address.
1272 * @param Order $order Order data.
1273 */
1274 return apply_filters( 'storeengine/order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this );
1275 }
1276
1277 /**
1278 * Get a formatted shipping address for the order.
1279 *
1280 * @param string $empty_content Content to show if no address is present.
1281 *
1282 * @return string
1283 */
1284 public function get_formatted_shipping_address( $empty_content = '' ) {
1285 $address = '';
1286 $raw_address = $this->get_address( 'shipping' );
1287
1288 if ( $this->has_shipping_address() ) {
1289 $raw_address = apply_filters( 'storeengine/order_formatted_shipping_address', $raw_address, $this );
1290 $address = Countries::init()->get_formatted_address( $raw_address );
1291 }
1292
1293 /**
1294 * Filter orders formatted shipping address.
1295 *
1296 * @param string $address Formatted shipping address string.
1297 * @param array $raw_address Raw shipping address.
1298 * @param Order $order Order data.
1299 */
1300 return apply_filters( 'storeengine/order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this );
1301 }
1302
1303 /**
1304 * Returns true if the order has a billing address.
1305 *
1306 * @param string $context
1307 *
1308 * @return boolean
1309 */
1310 public function has_billing_address( string $context = 'view' ): bool {
1311 return $this->get_billing_address_1( $context ) || $this->get_billing_address_2( $context );
1312 }
1313
1314 /**
1315 * Returns true if the order has a shipping address.
1316 *
1317 * @param string $context
1318 *
1319 * @return boolean
1320 */
1321 public function has_shipping_address( string $context = 'view' ): bool {
1322 return $this->get_shipping_address_1( $context ) || $this->get_shipping_address_2( $context );
1323 }
1324
1325 /**
1326 * Gets information about whether stock was reduced.
1327 *
1328 * @param string $context What the value is for. Valid values are view and edit.
1329 *
1330 * @return bool
1331 */
1332 public function get_order_stock_reduced( string $context = 'view' ): bool {
1333 return Formatting::string_to_bool( $this->get_prop( 'order_stock_reduced', $context ) );
1334 }
1335
1336 /**
1337 * Gets information about whether permissions were generated yet.
1338 *
1339 * @param string $context What the value is for. Valid values are view and edit.
1340 *
1341 * @return bool True if permissions were generated, false otherwise.
1342 */
1343 public function get_download_permissions_granted( string $context = 'view' ): bool {
1344 return Formatting::string_to_bool( $this->get_prop( 'download_permissions_granted', $context ) );
1345 }
1346
1347 public function get_auto_complete_digital_order( string $context = 'view' ): bool {
1348 return Formatting::string_to_bool( $this->get_prop( 'auto_complete_digital_order', $context ) );
1349 }
1350
1351 /**
1352 * Whether email have been sent for this order.
1353 *
1354 * @param string $context What the value is for. Valid values are view and edit.
1355 *
1356 * @return bool
1357 */
1358 public function get_new_order_email_sent( string $context = 'view' ): bool {
1359 return Formatting::string_to_bool( $this->get_prop( 'new_order_email_sent', $context ) );
1360 }
1361
1362 /**
1363 * Gets information about whether sales were recorded.
1364 *
1365 * @param string $context What the value is for. Valid values are view and edit.
1366 *
1367 * @return bool True if sales were recorded, false otherwise.
1368 */
1369 public function get_recorded_sales( string $context = 'view' ): bool {
1370 return Formatting::string_to_bool( $this->get_prop( 'recorded_sales', $context ) );
1371 }
1372
1373 /**
1374 * @param string $context
1375 *
1376 * @return null|StoreengineDatetime
1377 */
1378 public function get_order_placed_date_gmt( string $context = 'view' ): ?StoreengineDatetime {
1379 return $this->get_prop( 'order_placed_date_gmt', $context );
1380 }
1381
1382 /**
1383 * @param string $context
1384 *
1385 * @return null|StoreengineDatetime
1386 */
1387 public function get_order_placed_date( string $context = 'view' ): ?StoreengineDatetime {
1388 return $this->get_prop( 'order_placed_date', $context );
1389 }
1390
1391 /*
1392 |--------------------------------------------------------------------------
1393 | Setters
1394 |--------------------------------------------------------------------------
1395 |
1396 | Functions for setting order data. These should not update anything in the
1397 | database itself and should only change what is stored in the class
1398 | object. However, for backwards compatibility pre 3.0.0 some of these
1399 | setters may handle both.
1400 |
1401 */
1402
1403 /**
1404 * Sets a prop for a setter method.
1405 *
1406 * @param string $prop Name of prop to set.
1407 * @param string $address_type Type of address; 'billing' or 'shipping'.
1408 * @param ?string $value Value of the prop.
1409 */
1410 protected function set_address_prop( $prop, string $address_type, ?string $value ) {
1411 if ( isset( $this->data[ $address_type ] ) && array_key_exists( $prop, $this->data[ $address_type ] ) ) {
1412 if ( true === $this->object_read ) {
1413 if ( $value !== $this->data[ $address_type ][ $prop ] || ( isset( $this->changes[ $address_type ] ) && array_key_exists( $prop, $this->changes[ $address_type ] ) ) ) {
1414 $this->changes[ $address_type ][ $prop ] = $value;
1415 }
1416 } else {
1417 $this->data[ $address_type ][ $prop ] = $value;
1418 }
1419 }
1420 }
1421
1422 /**
1423 * Setter for billing address, expects the $address parameter to be key value pairs for individual address props.
1424 *
1425 * @param array $address Address to set.
1426 *
1427 * @return void
1428 */
1429 public function set_billing_address( array $address ) {
1430 foreach ( $address as $key => $value ) {
1431 $this->set_address_prop( $key, 'billing', $value );
1432 }
1433 }
1434
1435 /**
1436 * Shortcut for calling set_billing_address.
1437 *
1438 * This is useful in scenarios where set_$prop_name is invoked, and since we store the billing address as 'billing' prop in data, it can be called directly.
1439 *
1440 * @param array $address Address to set.
1441 *
1442 * @return void
1443 */
1444 public function set_billing( array $address ) {
1445 $this->set_billing_address( $address );
1446 }
1447
1448 /**
1449 * Setter for shipping address, expects the $address parameter to be key value pairs for individual address props.
1450 *
1451 * @param array $address Address to set.
1452 *
1453 * @return void
1454 */
1455 public function set_shipping_address( array $address ) {
1456 foreach ( $address as $key => $value ) {
1457 $this->set_address_prop( $key, 'shipping', $value );
1458 }
1459 }
1460
1461 /**
1462 * Shortcut for calling set_shipping_address. This is useful in scenarios where set_$prop_name is invoked, and since we store the shipping address as 'shipping' prop in data, it can be called directly.
1463 *
1464 * @param array $address Address to set.
1465 *
1466 * @return void
1467 */
1468 public function set_shipping( array $address ) {
1469 $this->set_shipping_address( $address );
1470 }
1471
1472 /**
1473 * Set billing first name.
1474 *
1475 * @param ?string $value Billing first name.
1476 */
1477 public function set_billing_first_name( ?string $value ) {
1478 $this->set_address_prop( 'first_name', 'billing', $value );
1479 }
1480
1481 /**
1482 * Set billing last name.
1483 *
1484 * @param ?string $value Billing last name.
1485 */
1486 public function set_billing_last_name( ?string $value ) {
1487 $this->set_address_prop( 'last_name', 'billing', $value );
1488 }
1489
1490 /**
1491 * Set billing company.
1492 *
1493 * @param ?string $value Billing company.
1494 */
1495 public function set_billing_company( ?string $value ) {
1496 $this->set_address_prop( 'company', 'billing', $value );
1497 }
1498
1499 /**
1500 * Set billing address line 1.
1501 *
1502 * @param ?string $value Billing address line 1.
1503 */
1504 public function set_billing_address_1( ?string $value ) {
1505 $this->set_address_prop( 'address_1', 'billing', $value );
1506 }
1507
1508 /**
1509 * Set billing address line 2.
1510 *
1511 * @param ?string $value Billing address line 2.
1512 */
1513 public function set_billing_address_2( ?string $value ) {
1514 $this->set_address_prop( 'address_2', 'billing', $value );
1515 }
1516
1517 /**
1518 * Set billing city.
1519 *
1520 * @param ?string $value Billing city.
1521 */
1522 public function set_billing_city( ?string $value ) {
1523 $this->set_address_prop( 'city', 'billing', $value );
1524 }
1525
1526 /**
1527 * Set billing state.
1528 *
1529 * @param ?string $value Billing state.
1530 */
1531 public function set_billing_state( ?string $value ) {
1532 $this->set_address_prop( 'state', 'billing', $value );
1533 }
1534
1535 /**
1536 * Set billing postcode.
1537 *
1538 * @param ?string $value Billing postcode.
1539 */
1540 public function set_billing_postcode( ?string $value ) {
1541 $this->set_address_prop( 'postcode', 'billing', $value );
1542 }
1543
1544 /**
1545 * Set billing country.
1546 *
1547 * @param ?string $value Billing country.
1548 */
1549 public function set_billing_country( ?string $value ) {
1550 $this->set_address_prop( 'country', 'billing', $value );
1551 }
1552
1553 /**
1554 * Maybe set empty billing email to that of the user who owns the order.
1555 */
1556 protected function maybe_set_user_billing_email() {
1557 $user = $this->get_user();
1558 if ( ! $this->get_billing_email() && $user ) {
1559 try {
1560 $this->set_billing_email( $user->user_email );
1561 } catch ( Exception $e ) {
1562 unset( $e );
1563 }
1564 }
1565 }
1566
1567 /**
1568 * Set billing email.
1569 *
1570 * @param ?string $value Billing email.
1571 *
1572 * @throws StoreEngineException
1573 */
1574 public function set_billing_email( ?string $value = '' ) {
1575 $value = $value ?? '';
1576 if ( $value && ! is_email( $value ) ) {
1577 $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'storeengine' ) );
1578 }
1579
1580 $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) );
1581 }
1582
1583 /**
1584 * Set billing phone.
1585 *
1586 * @param ?string $value Billing phone.
1587 */
1588 public function set_billing_phone( ?string $value ) {
1589 $this->set_address_prop( 'phone', 'billing', $value );
1590 }
1591
1592 /**
1593 * Set shipping first name.
1594 *
1595 * @param ?string $value Shipping first name.
1596 */
1597 public function set_shipping_first_name( ?string $value ) {
1598 $this->set_address_prop( 'first_name', 'shipping', $value );
1599 }
1600
1601 /**
1602 * Set shipping last name.
1603 *
1604 * @param ?string $value Shipping last name.
1605 */
1606 public function set_shipping_last_name( ?string $value ) {
1607 $this->set_address_prop( 'last_name', 'shipping', $value );
1608 }
1609
1610 /**
1611 * Set shipping company.
1612 *
1613 * @param ?string $value Shipping company.
1614 */
1615 public function set_shipping_company( ?string $value ) {
1616 $this->set_address_prop( 'company', 'shipping', $value );
1617 }
1618
1619 /**
1620 * Set shipping address line 1.
1621 *
1622 * @param ?string $value Shipping address line 1.
1623 */
1624 public function set_shipping_address_1( ?string $value ) {
1625 $this->set_address_prop( 'address_1', 'shipping', $value );
1626 }
1627
1628 /**
1629 * Set shipping address line 2.
1630 *
1631 * @param ?string $value Shipping address line 2.
1632 */
1633 public function set_shipping_address_2( ?string $value ) {
1634 $this->set_address_prop( 'address_2', 'shipping', $value );
1635 }
1636
1637 /**
1638 * Set shipping city.
1639 *
1640 * @param ?string $value Shipping city.
1641 */
1642 public function set_shipping_city( ?string $value ) {
1643 $this->set_address_prop( 'city', 'shipping', $value );
1644 }
1645
1646 /**
1647 * Set shipping state.
1648 *
1649 * @param ?string $value Shipping state.
1650 */
1651 public function set_shipping_state( ?string $value ) {
1652 $this->set_address_prop( 'state', 'shipping', $value );
1653 }
1654
1655 /**
1656 * Set shipping postcode.
1657 *
1658 * @param ?string $value Shipping postcode.
1659 */
1660 public function set_shipping_postcode( ?string $value ) {
1661 $this->set_address_prop( 'postcode', 'shipping', $value );
1662 }
1663
1664 /**
1665 * Set shipping country.
1666 *
1667 * @param ?string $value Shipping country.
1668 */
1669 public function set_shipping_country( ?string $value ) {
1670 $this->set_address_prop( 'country', 'shipping', $value );
1671 }
1672
1673 /**
1674 * Set shipping phone.
1675 *
1676 * @param ?string $value Shipping phone.
1677 */
1678 public function set_shipping_phone( ?string $value ) {
1679 $this->set_address_prop( 'phone', 'shipping', $value );
1680 }
1681
1682 /**
1683 * Set shipping phone.
1684 *
1685 * @param ?string $value Shipping phone.
1686 *
1687 * @throws StoreEngineException
1688 */
1689 public function set_shipping_email( ?string $value ) {
1690 $value = $value ?? '';
1691 if ( $value && ! is_email( $value ) ) {
1692 $this->error( 'order_invalid_shipping_email', __( 'Invalid shipping email address', 'storeengine' ) );
1693 }
1694
1695 $this->set_address_prop( 'email', 'shipping', sanitize_email( $value ) );
1696 }
1697
1698 /**
1699 * Set the payment method.
1700 *
1701 * @param string|PaymentGateway $payment_method Supports WC_Payment_Gateway for bw compatibility with < 3.0.
1702 */
1703 public function set_payment_method( $payment_method = '' ) {
1704 if ( is_object( $payment_method ) ) {
1705 $this->set_payment_method( $payment_method->id );
1706 $this->set_payment_method_title( $payment_method->get_title() );
1707 } elseif ( '' === $payment_method ) {
1708 $this->set_prop( 'payment_method', '' );
1709 $this->set_prop( 'payment_method_title', '' );
1710 } else {
1711 $this->set_prop( 'payment_method', $payment_method );
1712 }
1713 }
1714
1715 /**
1716 * Set payment method title.
1717 *
1718 * @param ?string $value Payment method title.
1719 */
1720 public function set_payment_method_title( ?string $value ) {
1721 $this->set_prop( 'payment_method_title', $value );
1722 }
1723
1724 /**
1725 * Check if the subscription has a payment gateway.
1726 *
1727 * @return bool
1728 */
1729 public function has_payment_gateway(): bool {
1730 return (bool) Helper::get_payment_gateway_by_order( $this );
1731 }
1732
1733 /**
1734 * Set transaction id.
1735 *
1736 * @param ?string $value Transaction id.
1737 */
1738 public function set_transaction_id( ?string $value ) {
1739 $this->set_prop( 'transaction_id', $value );
1740 }
1741
1742 /**
1743 * Set customer ip address.
1744 *
1745 * @param ?string $value Customer ip address.
1746 */
1747 public function set_ip_address( ?string $value ) {
1748 $this->set_prop( 'ip_address', $value );
1749 }
1750
1751 /**
1752 * Set customer user agent.
1753 *
1754 * @param ?string $value Customer user agent.
1755 */
1756 public function set_user_agent( ?string $value ) {
1757 $this->set_prop( 'user_agent', $value );
1758 }
1759
1760 /**
1761 * Set created via.
1762 *
1763 * @param ?string $value Created via.
1764 */
1765 public function set_created_via( ?string $value ) {
1766 $this->set_prop( 'created_via', $value );
1767 }
1768
1769 /**
1770 * Set customer note.
1771 *
1772 * @param ?string $value Customer note.
1773 */
1774 public function set_customer_note( ?string $value ) {
1775 $this->set_prop( 'customer_note', $value );
1776 }
1777
1778 /**
1779 * Set cart hash.
1780 *
1781 * @param string $value Cart hash.
1782 */
1783 public function set_cart_hash( $value ) {
1784 $this->set_hash( $value );
1785 }
1786
1787 /**
1788 * Set cart hash.
1789 *
1790 * @param string $value Cart hash.
1791 */
1792 public function set_hash( $value ) {
1793 $this->set_prop( 'hash', $value );
1794 }
1795
1796 /**
1797 * Stores information about whether stock was reduced.
1798 *
1799 * @param bool|string $value True if stock was reduced, false if not.
1800 *
1801 * @return void
1802 */
1803 public function set_order_stock_reduced( $value ) {
1804 $this->set_prop( 'order_stock_reduced', Formatting::string_to_bool( $value ) );
1805 }
1806
1807 /**
1808 * Stores information about whether permissions were generated yet.
1809 *
1810 * @param bool|string $value True if permissions were generated, false if not.
1811 *
1812 * @return void
1813 */
1814 public function set_download_permissions_granted( $value ) {
1815 $this->set_prop( 'download_permissions_granted', Formatting::string_to_bool( $value ) );
1816 }
1817
1818 public function set_auto_complete_digital_order( $value ) {
1819 $this->set_prop( 'auto_complete_digital_order', Formatting::string_to_bool( $value ) );
1820 }
1821
1822 /**
1823 * Stores information about whether email was sent.
1824 *
1825 * @param bool|string $value True if email was sent, false if not.
1826 *
1827 * @return void
1828 */
1829 public function set_new_order_email_sent( $value ) {
1830 $this->set_prop( 'new_order_email_sent', Formatting::string_to_bool( $value ) );
1831 }
1832
1833 /**
1834 * Stores information about whether sales were recorded.
1835 *
1836 * @param bool|string $value True if sales were recorded, false if not.
1837 *
1838 * @return void
1839 */
1840 public function set_recorded_sales( $value ) {
1841 $this->set_prop( 'recorded_sales', Formatting::string_to_bool( $value ) );
1842 }
1843
1844 /*
1845 |--------------------------------------------------------------------------
1846 | Conditionals
1847 |--------------------------------------------------------------------------
1848 |
1849 | Checks if a condition is true or false.
1850 |
1851 */
1852
1853 /**
1854 * Check if an order key is valid.
1855 *
1856 * @param string $key Order key.
1857 *
1858 * @return bool
1859 */
1860 public function key_is_valid( $key ) {
1861 return hash_equals( $this->get_order_key(), $key );
1862 }
1863
1864 /**
1865 * See if order matches cart_hash.
1866 *
1867 * @param string $cart_hash Cart hash.
1868 *
1869 * @return bool
1870 */
1871 public function has_cart_hash( $cart_hash = '' ) {
1872 return hash_equals( $this->get_cart_hash(), $cart_hash );
1873 }
1874
1875 /**
1876 * Checks if an order can be edited, specifically for use on the Edit Order screen.
1877 *
1878 * @return bool
1879 */
1880 public function is_editable(): bool {
1881 $editable_statuses = [
1882 OrderStatus::PAYMENT_PENDING,
1883 OrderStatus::ON_HOLD,
1884 OrderStatus::AUTO_DRAFT,
1885 ];
1886
1887 /**
1888 * Filter to check if an order is editable.
1889 *
1890 * @param bool $is_editable Is the order editable.
1891 * @param Order $order Order object.
1892 *
1893 * @see WC_Stripe_Subscriptions_Trait::disable_subscription_edit_for_india
1894 */
1895 return apply_filters(
1896 'storeengine/order/is_editable',
1897 in_array( $this->get_status(), $editable_statuses, true ),
1898 $this
1899 );
1900 }
1901
1902 /**
1903 * Checks if product download is permitted.
1904 *
1905 * @return bool
1906 */
1907 public function is_download_permitted(): bool {
1908 /**
1909 * Filter to check if an order is downloadable.
1910 *
1911 * @param bool $is_download_permitted Is the order downloadable.
1912 * @param Order $this Order object.
1913 */
1914 return apply_filters( 'storeengine/order_is_download_permitted', $this->has_status( OrderStatus::COMPLETED ) || ( 'yes' === get_option( 'storeengine/downloads_grant_access_after_payment' ) && $this->has_status( OrderStatus::PROCESSING ) ), $this );
1915 }
1916
1917 /**
1918 * Checks if an order needs display the shipping address, based on shipping method.
1919 *
1920 * @return bool
1921 */
1922 public function needs_shipping_address(): bool {
1923 if ( 'no' === get_option( 'storeengine/calc_shipping' ) ) {
1924 return false;
1925 }
1926
1927 $hide = apply_filters( 'storeengine/order_hide_shipping_address', [ 'local_pickup' ], $this );
1928 $needs_address = false;
1929
1930 foreach ( $this->get_shipping_methods() as $shipping_method ) {
1931 $shipping_method_id = $shipping_method->get_method_id();
1932
1933 if ( ! in_array( $shipping_method_id, $hide, true ) ) {
1934 $needs_address = true;
1935 break;
1936 }
1937 }
1938
1939 return apply_filters( 'storeengine/order_needs_shipping_address', $needs_address, $hide, $this );
1940 }
1941
1942 /**
1943 * Returns true if the order contains a downloadable product.
1944 *
1945 * @return bool
1946 */
1947 public function has_downloadable_item() {
1948 foreach ( $this->get_items() as $item ) {
1949 if ( $item->is_type( 'line_item' ) ) {
1950 $product = $item->get_product();
1951
1952 if ( $product && $product->has_file() ) {
1953 return true;
1954 }
1955 }
1956 }
1957
1958 return false;
1959 }
1960
1961 /**
1962 * Get downloads from all line items for this order.
1963 *
1964 * @return array
1965 */
1966 public function get_downloadable_items(): array {
1967 $downloads = [];
1968
1969 foreach ( $this->get_items() as $item ) {
1970 if ( ! is_object( $item ) ) {
1971 continue;
1972 }
1973
1974 // Check item refunds.
1975 $refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) );
1976 if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) {
1977 continue;
1978 }
1979
1980 if ( $item->is_type( 'line_item' ) ) {
1981 $item_downloads = $item->get_item_downloads();
1982 $product = $item->get_product();
1983 if ( $product && $item_downloads ) {
1984 foreach ( $item_downloads as $file ) {
1985 $downloads[] = [
1986 'download_url' => $file['download_url'],
1987 'download_id' => $file['id'],
1988 'product_id' => $product->get_id(),
1989 'product_name' => $product->get_name(),
1990 'product_url' => $product->is_visible() ? $product->get_permalink() : '',
1991 'download_name' => $file['name'],
1992 'order_id' => $this->get_id(),
1993 'order_key' => $this->get_order_key(),
1994 'downloads_remaining' => $file['downloads_remaining'],
1995 'access_expires' => $file['access_expires'],
1996 'file' => [
1997 'name' => $file['name'],
1998 'file' => $file['file'],
1999 ],
2000 ];
2001 }
2002 }
2003 }
2004 }
2005
2006 return apply_filters( 'storeengine/order_get_downloadable_items', $downloads, $this );
2007 }
2008
2009 /**
2010 * Checks if an order needs payment, based on status and order total.
2011 *
2012 * @return bool
2013 */
2014 public function needs_payment(): bool {
2015 /**
2016 * Filter the valid order statuses for payment.
2017 *
2018 * @param array $valid_order_statuses Array of valid order statuses for payment.
2019 * @param Order $order Order object.
2020 */
2021 $paid_status = $this->get_paid_status();
2022 $valid_unpaid_statuses = [ 'unpaid', 'failed' ];
2023 $valid_order_statuses = apply_filters( 'storeengine/order/valid_unpaid_statuses', $valid_unpaid_statuses, $this );
2024 $valid_statuses_for_payment = [ OrderStatus::PAYMENT_PENDING, OrderStatus::PAYMENT_FAILED ];
2025 $valid_statuses_for_payment = apply_filters( 'storeengine/order/valid_statuses_for_payment', $valid_statuses_for_payment, $this );
2026 $need_payment = (
2027 in_array( $paid_status, $valid_order_statuses, true ) &&
2028 in_array( $this->get_status(), $valid_statuses_for_payment, true ) &&
2029 $this->get_total() > 0
2030 );
2031
2032 return apply_filters( 'storeengine/order_needs_payment', $need_payment, $this, $valid_order_statuses );
2033 }
2034
2035 /**
2036 * See if the order needs processing before it can be completed.
2037 *
2038 * Orders which only contain virtual, downloadable items do not need admin
2039 * intervention.
2040 *
2041 * Uses a transient so these calls are not repeated multiple times, and because
2042 * once the order is processed this code/transient does not need to persist.
2043 *
2044 * @return bool
2045 */
2046 public function needs_processing(): bool {
2047 $transient_name = 'storeengine/order_' . $this->get_id() . '_needs_processing';
2048 $needs_processing = get_transient( $transient_name );
2049
2050 if ( false === $needs_processing ) {
2051 $needs_processing = 0;
2052
2053 if ( count( $this->get_items() ) > 0 ) {
2054 foreach ( $this->get_items() as $item ) {
2055 if ( $item->is_type( 'line_item' ) ) {
2056 /** @var $product AbstractProduct */
2057 $product = $item->get_product();
2058
2059 if ( ! $product ) {
2060 continue;
2061 }
2062
2063 $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual();
2064
2065 if ( apply_filters( 'storeengine/order/item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) {
2066 $needs_processing = 1;
2067 break;
2068 }
2069 }
2070 }
2071 }
2072
2073 set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS );
2074 }
2075
2076 return 1 === absint( $needs_processing );
2077 }
2078
2079 /*
2080 |--------------------------------------------------------------------------
2081 | URLs and Endpoints
2082 |--------------------------------------------------------------------------
2083 */
2084
2085 /**
2086 * Generates a URL so that a customer can pay for their (unpaid - pending) order. Pass 'true' for the checkout version which doesn't offer gateway choices.
2087 *
2088 * @param bool $on_checkout If on checkout.
2089 *
2090 * @return string
2091 */
2092 public function get_checkout_payment_url( bool $on_checkout = false ): string {
2093 $pay_url = Helper::get_endpoint_url( 'order-pay', $this->get_id(), Helper::get_checkout_url() );
2094
2095 if ( $on_checkout ) {
2096 $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url );
2097 } else {
2098 $pay_url = add_query_arg( [
2099 'pay_for_order' => 'true',
2100 'key' => $this->get_order_key(),
2101 ], $pay_url );
2102 }
2103
2104 return apply_filters( 'storeengine/get_checkout_payment_url', $pay_url, $this );
2105 }
2106
2107 /**
2108 * Generates a URL for the thanks page (order received).
2109 *
2110 * @return string
2111 */
2112 public function get_checkout_order_received_url(): string {
2113 $order_received_url = add_query_arg( 'order_hash', $this->get_order_key(), Helper::get_thankyou_page_url() );
2114
2115 return apply_filters( 'storeengine/order/get_checkout_order_received_url', $order_received_url, $this );
2116 }
2117
2118 /**
2119 * Generates a URL so that a customer can cancel their (unpaid - pending) order.
2120 *
2121 * @param string $redirect Redirect URL.
2122 *
2123 * @return string
2124 * @see \WC_Form_Handler::cancel_order
2125 */
2126 public function get_cancel_order_url( string $redirect = '' ): string {
2127 /**
2128 * Filter the URL to cancel the order in the frontend.
2129 *
2130 * @param string $url
2131 * @param Order $order Order data.
2132 * @param string $redirect Redirect URL.
2133 */
2134 return apply_filters(
2135 'storeengine/order/get_cancel_order_url',
2136 wp_nonce_url(
2137 add_query_arg(
2138 [
2139 'cancel_order' => 'true',
2140 'order' => $this->get_order_key(),
2141 'order_id' => $this->get_id(),
2142 'redirect' => $redirect,
2143 ],
2144 $this->get_cancel_endpoint()
2145 ),
2146 'storeengine-cancel_order'
2147 ),
2148 $this,
2149 $redirect
2150 );
2151 }
2152
2153 /**
2154 * Generates a raw (unescaped) cancel-order URL for use by payment gateways.
2155 *
2156 * @param string $redirect Redirect URL.
2157 *
2158 * @return string The unescaped cancel-order URL.
2159 *
2160 * @see \WC_Form_Handler::cancel_order
2161 */
2162 public function get_cancel_order_url_raw( string $redirect = '' ): string {
2163 /**
2164 * Filter the raw URL to cancel the order in the frontend.
2165 *
2166 * @param string $url
2167 * @param Order $order Order data.
2168 * @param string $redirect Redirect URL.
2169 */
2170 return apply_filters(
2171 'storeengine/order/get_cancel_order_url_raw',
2172 add_query_arg(
2173 [
2174 'cancel_order' => 'true',
2175 'order' => $this->get_order_key(),
2176 'order_id' => $this->get_id(),
2177 'redirect' => $redirect,
2178 '_wpnonce' => wp_create_nonce( 'storeengine-cancel_order' ),
2179 ],
2180 $this->get_cancel_endpoint()
2181 ),
2182 $this,
2183 $redirect
2184 );
2185 }
2186
2187 /**
2188 * Helper method to return the cancel endpoint.
2189 *
2190 * @return string the cancel endpoint; either the cart page or the home page.
2191 */
2192 public function get_cancel_endpoint(): string {
2193 $cancel_endpoint = Helper::get_cart_url();
2194 if ( ! $cancel_endpoint ) {
2195 $cancel_endpoint = home_url();
2196 }
2197
2198 if ( false === strpos( $cancel_endpoint, '?' ) ) {
2199 $cancel_endpoint = trailingslashit( $cancel_endpoint );
2200 }
2201
2202 return $cancel_endpoint;
2203 }
2204
2205 /**
2206 * Generates a URL to view an order from the myaccount page.
2207 *
2208 * @return string
2209 */
2210 public function get_view_order_url(): string {
2211 return apply_filters( 'storeengine/order/get_view_url', Helper::get_account_endpoint_url( 'orders', $this->get_id() ), $this );
2212 }
2213
2214 /**
2215 * Get the URL to edit the order in the backend.
2216 *
2217 * @return string
2218 */
2219 public function get_edit_order_url(): string {
2220 $edit_url = admin_url( 'admin.php?page=storeengine-orders&id=' . $this->get_id() . '&action=edit' );
2221
2222 /**
2223 * Filter the URL to edit the order in the backend.
2224 */
2225 return apply_filters( 'storeengine/order/get_edit_url', $edit_url, $this );
2226 }
2227
2228 /*
2229 |--------------------------------------------------------------------------
2230 | Order notes.
2231 |--------------------------------------------------------------------------
2232 */
2233
2234 /**
2235 * Adds a note (comment) to the order. Order must exist.
2236 *
2237 * @param string $note Note to add.
2238 * @param int|string $is_customer_note Is this a note for the customer?.
2239 * @param bool $added_by_user Was the note added by a user?.
2240 *
2241 * @return int|false Comment ID.
2242 */
2243 public function add_order_note( string $note, $is_customer_note = 0, bool $added_by_user = false ) {
2244 if ( ! $this->get_id() ) {
2245 return 0;
2246 }
2247
2248 $is_customer_note = absint( $is_customer_note );
2249
2250 // @TODO edit_shop_orders cap doesn't exists in storeengine.
2251
2252 if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) {
2253 $user = get_user_by( 'id', get_current_user_id() );
2254 $comment_author = $user->display_name;
2255 $comment_author_email = $user->user_email;
2256 } else {
2257 $comment_author = _x( 'StoreEngine', 'System Comment Author', 'storeengine' );
2258 $comment_author_email = strtolower( $comment_author ) . '@' . wp_parse_url( get_site_url(), PHP_URL_HOST );
2259 $comment_author_email = sanitize_email( $comment_author_email );
2260 }
2261
2262 $commentdata = apply_filters(
2263 'storeengine/new_order_note_data',
2264 [
2265 'comment_post_ID' => $this->get_id(),
2266 'comment_author' => $comment_author,
2267 'comment_author_email' => $comment_author_email,
2268 'comment_author_url' => '',
2269 'comment_content' => $note,
2270 'comment_agent' => 'StoreEngine',
2271 'comment_type' => 'order_note',
2272 'comment_parent' => 0,
2273 'comment_approved' => 1,
2274 ],
2275 [
2276 'order_id' => $this->get_id(),
2277 'is_customer_note' => $is_customer_note,
2278 ]
2279 );
2280
2281 $comment_id = wp_insert_comment( $commentdata );
2282
2283 if ( ! $comment_id ) {
2284 return false;
2285 }
2286
2287 if ( $is_customer_note ) {
2288 add_comment_meta( $comment_id, 'is_customer_note', 1 );
2289
2290 /**
2291 * Action hook fired after an order note is added for the customer.
2292 *
2293 * @param string $note Comment data.
2294 * @param Order $this Comment data.
2295 */
2296 do_action( 'storeengine/order/new_customer_note', $note, $this );
2297 }
2298
2299 /**
2300 * Action hook fired after an order note is added.
2301 *
2302 * @param int $comment_id Order note ID.
2303 * @param Order $this Order object.
2304 */
2305 do_action( 'storeengine/order/note_added', $comment_id, $this );
2306
2307 return $comment_id;
2308 }
2309
2310 /**
2311 * Add an order note for status transition
2312 *
2313 * @param string $note Note to be added giving status transition from and to details.
2314 * @param bool $transition Details of the status transition.
2315 *
2316 * @return int Comment ID.
2317 * @uses self::add_order_note()
2318 */
2319 protected function add_status_transition_note( $note, $transition ) {
2320 return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] );
2321 }
2322
2323 /**
2324 * List order notes (public) for the customer.
2325 *
2326 * @return WP_Comment[]
2327 */
2328 public function get_customer_order_notes(): array {
2329 return $this->get_order_notes( 'customer' );
2330 }
2331
2332 /**
2333 * List order notes.
2334 *
2335 * @param string $customer_notes switch for customer (public) notes or internal (admin) notes. Default all notes.
2336 * @param bool $ids
2337 *
2338 * @return WP_Comment[]
2339 * @see wc_get_order_note
2340 */
2341 public function get_order_notes( string $customer_notes = '', bool $ids = false ): array {
2342 $notes = [];
2343
2344 if ( ! $this->get_id() ) {
2345 return $notes;
2346 }
2347
2348 $args = [
2349 'post_id' => $this->get_id(),
2350 'orderby' => 'comment_ID',
2351 'order' => 'DESC',
2352 'approve' => 'approve',
2353 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
2354 'relation' => 'AND',
2355 ],
2356 ];
2357
2358 // type->order_note & author->StoreEngine conditions are added via filter below.
2359
2360 if ( $ids ) {
2361 $args['fields'] = 'ids';
2362 }
2363
2364 if ( 'customer' === $customer_notes ) {
2365 $args['meta_query'][] = [
2366 'key' => 'is_customer_note',
2367 'value' => 1,
2368 'compare' => '=',
2369 'type' => 'UNSIGNED',
2370 ];
2371 } elseif ( 'internal' === $customer_notes ) {
2372 $args['meta_query'][] = [
2373 'key' => 'is_customer_note',
2374 'compare' => 'NOT EXISTS',
2375 ];
2376 }
2377
2378 remove_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] );
2379 add_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] );
2380
2381 $comments = get_comments( $args );
2382
2383 foreach ( $comments as $comment ) {
2384 $comment->comment_content = make_clickable( $comment->comment_content );
2385 $notes[] = $comment;
2386 }
2387
2388 remove_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] );
2389 add_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] );
2390
2391 return array_filter( array_map( [ __CLASS__, 'get_order_note' ], $notes ) );
2392 }
2393
2394 /**
2395 * Get an order note.
2396 *
2397 * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only).
2398 *
2399 * @return stdClass|null Object with order note details or null when does not exists.
2400 * @throws StoreEngineException
2401 */
2402 public static function get_order_note( $data ) {
2403 if ( is_numeric( $data ) ) {
2404 $data = get_comment( $data );
2405 }
2406
2407 if ( ! is_a( $data, 'WP_Comment' ) ) {
2408 return null;
2409 }
2410
2411 // @TODO use OrderNote object.
2412 return (object) apply_filters( 'storeengine/order/get_order_note', [
2413 'id' => (int) $data->comment_ID,
2414 'date_created' => $data->comment_date_gmt,
2415 //'date_created' => Formatting::string_to_datetime( $data->comment_date ),
2416 'content' => $data->comment_content,
2417 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ),
2418 'added_by' => __( 'StoreEngine', 'storeengine' ) === $data->comment_author ? 'system' : $data->comment_author,
2419 'order_id' => absint( $data->comment_post_ID ),
2420 ], $data );
2421 }
2422
2423 /**
2424 * Delete an order note.
2425 *
2426 * @param int $note_id Order note.
2427 *
2428 * @return bool True on success, false on failure.
2429 * @throws StoreEngineException
2430 */
2431 public static function delete_order_note( int $note_id ): bool {
2432 $note = self::get_order_note( $note_id );
2433 if ( $note && wp_delete_comment( $note_id, true ) ) {
2434 /**
2435 * Action hook fired after an order note is deleted.
2436 *
2437 * @param int $note_id Order note ID.
2438 * @param stdClass $note Object with the deleted order note details.
2439 */
2440 do_action( 'storeengine/order/note_deleted', $note_id, $note );
2441
2442 return true;
2443 }
2444
2445 return false;
2446 }
2447
2448 /*
2449 |--------------------------------------------------------------------------
2450 | Refunds
2451 |--------------------------------------------------------------------------
2452 */
2453
2454 /**
2455 * Get order refunds.
2456 *
2457 * @return Refund[] of Order_Refund objects
2458 * @throws StoreEngineException
2459 */
2460 public function get_refunds(): array {
2461 $cache_key = Caching::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id();
2462 $ids = wp_cache_get( $cache_key, $this->cache_group );
2463 $refunds = [];
2464
2465 if ( false === $ids || ! is_array( $ids ) ) {
2466 $query = ( new Refund() )->query();
2467 // @TODO cache properly to prevent 2x query while creating refund object.
2468 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class.
2469 $results = $this->wpdb->get_results( $this->wpdb->prepare( "$query WHERE o.parent_order_id = %d AND o.type = %s GROUP BY o_id;", $this->get_id(), 'refund_order' ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- common query prepared
2470 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class.
2471
2472 if ( ! $results ) {
2473 return $refunds;
2474 }
2475
2476 $ids = array_unique( array_filter( array_map( 'absint', array_column( $results, 'o_id' ) ) ) );
2477 wp_cache_set( $cache_key, $ids, $this->cache_group );
2478 }
2479
2480 foreach ( $ids as $id ) {
2481 $refunds[] = new Refund( $id );
2482 }
2483
2484 return $refunds;
2485 }
2486
2487 /**
2488 * Get amount already refunded.
2489 *
2490 * @param bool $refresh
2491 *
2492 * @return float|int
2493 * @see \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_total_refunded()
2494 */
2495 public function get_total_refunded( bool $refresh = false ) {
2496 $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id();
2497 $cached_data = wp_cache_get( $cache_key, $this->cache_group );
2498
2499 if ( false !== $cached_data && ! $refresh ) {
2500 return (float) $cached_data;
2501 }
2502
2503 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded.
2504 $total_refunded = $this->wpdb->get_var(
2505 $this->wpdb->prepare(
2506 "SELECT SUM( total_amount ) FROM $this->table WHERE type = %s AND parent_order_id = %d;",
2507 'refund_order',
2508 $this->get_id()
2509 )
2510 ) ?? 0;
2511 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded.
2512
2513 $total_refunded = - 1 * floatval( $total_refunded );
2514
2515 wp_cache_set( $cache_key, $total_refunded, $this->cache_group );
2516
2517 return $total_refunded;
2518 }
2519
2520 /**
2521 * Get the total tax refunded.
2522 *
2523 * @return float
2524 */
2525 public function get_total_tax_refunded(): float {
2526 $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id();
2527 $cached_data = wp_cache_get( $cache_key, $this->cache_group );
2528
2529 if ( false !== $cached_data ) {
2530 return $cached_data;
2531 }
2532
2533 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared.
2534 $total_refunded = $this->wpdb->get_var(
2535 $this->wpdb->prepare(
2536 "SELECT SUM( order_item_meta.meta_value )
2537 FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta
2538 INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
2539 INNER JOIN {$this->wpdb->prefix}storeengine_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'tax' )
2540 WHERE order_item_meta.order_item_id = order_items.order_item_id
2541 AND order_item_meta.meta_key IN ('tax_amount', 'shipping_tax_amount')",
2542 $this->get_id()
2543 )
2544 ) ?? 0;
2545 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared
2546
2547 $total_refunded = floatval( $total_refunded );
2548
2549 wp_cache_set( $cache_key, $total_refunded, $this->cache_group );
2550
2551 return $total_refunded;
2552 }
2553
2554 /**
2555 * Get the total shipping refunded.
2556 *
2557 * @return float
2558 */
2559 public function get_total_shipping_refunded() {
2560 $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id();
2561 $cached_data = wp_cache_get( $cache_key, $this->cache_group );
2562
2563 if ( false !== $cached_data ) {
2564 return $cached_data;
2565 }
2566
2567 // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared
2568 $total_refunded = $this->wpdb->get_var(
2569 $this->wpdb->prepare(
2570 "SELECT SUM( order_item_meta.meta_value )
2571 FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta
2572 INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d )
2573 INNER JOIN {$this->wpdb->prefix}storeengine_order_items AS order_items ON ( order_items.order_id = orders.id AND order_items.order_item_type = 'shipping' )
2574 WHERE order_item_meta.order_item_id = order_items.order_item_id
2575 AND order_item_meta.meta_key IN ('cost')",
2576 $this->get_id()
2577 )
2578 ) ?? 0;
2579 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared
2580
2581 $total_refunded = floatval( $total_refunded );
2582
2583 wp_cache_set( $cache_key, $total_refunded, $this->cache_group );
2584
2585 return $total_refunded;
2586 }
2587
2588 /**
2589 * Gets the count of order items of a certain type that have been refunded.
2590 *
2591 * @param string $item_type Item type.
2592 *
2593 * @return int
2594 */
2595 public function get_item_count_refunded( $item_type = '' ): int {
2596 if ( empty( $item_type ) ) {
2597 $item_type = [ 'line_item' ];
2598 }
2599 if ( ! is_array( $item_type ) ) {
2600 $item_type = [ $item_type ];
2601 }
2602 $count = 0;
2603
2604 foreach ( $this->get_refunds() as $refund ) {
2605 foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
2606 $count += abs( $refunded_item->get_quantity() );
2607 }
2608 }
2609
2610 return apply_filters( 'storeengine/get_item_count_refunded', $count, $item_type, $this );
2611 }
2612
2613 /**
2614 * Get the total number of items refunded.
2615 *
2616 * @param string $item_type Type of the item we're checking, if not a line_item.
2617 *
2618 * @return int
2619 */
2620 public function get_total_qty_refunded( $item_type = 'line_item' ) {
2621 $qty = 0;
2622 foreach ( $this->get_refunds() as $refund ) {
2623 foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
2624 $qty += $refunded_item->get_quantity();
2625 }
2626 }
2627
2628 return $qty;
2629 }
2630
2631 /**
2632 * Get the refunded amount for a line item.
2633 *
2634 * @param int $item_id ID of the item we're checking.
2635 * @param string $item_type Type of the item we're checking, if not a line_item.
2636 *
2637 * @return int
2638 */
2639 public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) {
2640 $qty = 0;
2641 foreach ( $this->get_refunds() as $refund ) {
2642 foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
2643 if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) {
2644 $qty += $refunded_item->get_quantity();
2645 }
2646 }
2647 }
2648
2649 return $qty;
2650 }
2651
2652 /**
2653 * Get the refunded amount for a line item.
2654 *
2655 * @param int $item_id ID of the item we're checking.
2656 * @param string $item_type Type of the item we're checking, if not a line_item.
2657 *
2658 * @return int
2659 */
2660 public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) {
2661 $total = 0;
2662 foreach ( $this->get_refunds() as $refund ) {
2663 foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
2664 if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) {
2665 $total += $refunded_item->get_total();
2666 }
2667 }
2668 }
2669
2670 return $total * - 1;
2671 }
2672
2673 /**
2674 * Get the refunded tax amount for a line item.
2675 *
2676 * @param int $item_id ID of the item we're checking.
2677 * @param int $tax_id ID of the tax we're checking.
2678 * @param string $item_type Type of the item we're checking, if not a line_item.
2679 *
2680 * @return double
2681 */
2682 public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) {
2683 $total = 0;
2684 foreach ( $this->get_refunds() as $refund ) {
2685 foreach ( $refund->get_items( $item_type ) as $refunded_item ) {
2686 $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' );
2687 if ( $refunded_item_id === $item_id ) {
2688 $taxes = $refunded_item->get_taxes();
2689 // Add to total.
2690 $total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0;
2691 break;
2692 }
2693 }
2694 }
2695
2696 return Formatting::round_tax_total( $total ) * - 1;
2697 }
2698
2699 /**
2700 * Get total tax refunded by rate ID.
2701 *
2702 * @param int $rate_id Rate ID.
2703 *
2704 * @return float
2705 */
2706 public function get_total_tax_refunded_by_rate_id( $rate_id ) {
2707 $total = 0;
2708 foreach ( $this->get_refunds() as $refund ) {
2709 foreach ( $refund->get_items( 'tax' ) as $refunded_item ) {
2710 if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) {
2711 $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() );
2712 }
2713 }
2714 }
2715
2716 return $total;
2717 }
2718
2719 /**
2720 * How much money is left to refund?
2721 *
2722 * @return float
2723 */
2724 public function get_remaining_refund_amount(): float {
2725 return (float) Formatting::format_decimal( $this->get_total() - $this->get_total_refunded(), Formatting::get_price_decimals() );
2726 }
2727
2728 /**
2729 * How many items are left to refund?
2730 *
2731 * @return int
2732 */
2733 public function get_remaining_refund_items() {
2734 return absint( $this->get_item_count() - $this->get_item_count_refunded() );
2735 }
2736
2737 /**
2738 * Add total row for the payment method.
2739 *
2740 * @param array $total_rows Total rows.
2741 * @param string $tax_display Tax to display.
2742 */
2743 protected function add_order_item_totals_payment_method_row( array &$total_rows ) {
2744 if ( $this->get_total() > 0 && $this->get_payment_method_title() ) {
2745 $total_rows['payment_method'] = [
2746 'type' => 'payment_method',
2747 'label' => __( 'Payment method:', 'storeengine' ),
2748 'value' => $this->get_payment_method_to_display( 'customer' ),
2749 ];
2750 }
2751 }
2752
2753 /**
2754 * Add total row for refunds.
2755 *
2756 * @param array $total_rows Total rows.
2757 * @param string $tax_display Tax to display.
2758 */
2759 protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) {
2760 $refunds = $this->get_refunds();
2761 if ( $refunds ) {
2762 foreach ( $refunds as $id => $refund ) {
2763 $reason = trim( $refund->get_reason() );
2764
2765 if ( strlen( $reason ) > 0 ) {
2766 $reason = "<br><small>$reason</small>";
2767 }
2768
2769 $total_rows[ 'refund_' . $id ] = [
2770 'type' => 'refund',
2771 'label' => __( 'Refund', 'storeengine' ) . ':',
2772 'value' => Formatting::price( $refund->get_total_amount(), [ 'currency' => $this->get_currency() ] ) . $reason,
2773 ];
2774 }
2775 }
2776 }
2777
2778 /**
2779 * Get totals for display on pages and in emails.
2780 *
2781 * @param string $tax_display Tax to display.
2782 *
2783 * @return array
2784 */
2785 public function get_order_item_totals( $tax_display = '' ) {
2786 $tax_display = $tax_display ? $tax_display : Helper::get_settings( 'tax_display_cart' );
2787 $total_rows = [];
2788
2789 $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display );
2790 $this->add_order_item_totals_discount_row( $total_rows, $tax_display );
2791 $this->add_order_item_totals_shipping_row( $total_rows, $tax_display );
2792 $this->add_order_item_totals_fee_rows( $total_rows, $tax_display );
2793 $this->add_order_item_totals_tax_rows( $total_rows, $tax_display );
2794 $this->add_order_item_totals_refund_rows( $total_rows, $tax_display );
2795 $this->add_order_item_totals_total_row( $total_rows, $tax_display );
2796 $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display );
2797
2798 return apply_filters( 'storeengine/get_order_item_totals', $total_rows, $this, $tax_display );
2799 }
2800
2801 /**
2802 * Check if order has been created via admin, checkout, or in another way.
2803 *
2804 * @param string $modus Way of creating the order to test for.
2805 *
2806 * @return bool
2807 */
2808 public function is_created_via( $modus ) {
2809 return apply_filters( 'storeengine/order_is_created_via', $modus === $this->get_created_via(), $this, $modus );
2810 }
2811
2812 /**
2813 * Indicates that regular orders have an associated Cost of Goods Sold value.
2814 * Note that this is true even if the order has no line items with COGS values (in that case the COGS value for the order will be zero)-
2815 *
2816 * @return bool Always true.
2817 */
2818 public function has_cogs(): bool {
2819 return true;
2820 }
2821
2822 // -----------------------
2823
2824 /**
2825 * Coupons array.
2826 *
2827 * @var OrderItemCoupon[]
2828 */
2829 protected array $coupons = [];
2830
2831 /**
2832 * Determine how the payment method should be displayed for a subscription.
2833 *
2834 * @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'.
2835 */
2836 public function get_payment_method_to_display( string $context = 'admin' ) {
2837 $is_unknown = ! $this->get_payment_method() || 'other' === $this->get_payment_method();
2838 $payment_method_to_display = $this->get_payment_method_title();
2839
2840 if ( ! $is_unknown && $payment_method_to_display ) {
2841 $card_info = $this->get_payment_card_info();
2842 if ( isset( $card_info['last4'] ) && $card_info['last4'] ) {
2843 $payment_method_to_display .= sprintf(
2844 // translators: %1$s: Payment method title. %2$s: Last 4 digits of the card.
2845 _x('%1$s - %2$s', 'Card info with payment method name', 'storeengine' ),
2846 $payment_method_to_display,
2847 $card_info['last4']
2848 );
2849 }
2850 } elseif ( false !== ( $payment_gateway = Helper::get_payment_gateway_by_order( $this ) ) ) {
2851 $payment_method_to_display = $payment_gateway->get_title();
2852 } else {
2853 // Fallback to the title of the payment method when the order was created
2854 $payment_method_to_display = '';
2855 }
2856
2857 if ( 'customer' === $context ) {
2858 if ( $payment_method_to_display ) {
2859 // translators: %s: payment method.
2860 $payment_method_to_display = sprintf( __( 'Via %s', 'storeengine' ), $payment_method_to_display );
2861 }
2862
2863 $payment_method_to_display = PaymentUtil::maybe_display_my_payment_method( $payment_method_to_display, $this );
2864 }
2865
2866 return apply_filters(
2867 "storeengine/{$this->object_type}/payment_method_to_display",
2868 $payment_method_to_display,
2869 $this,
2870 $context
2871 );
2872 }
2873
2874 /**
2875 * @param int $customer_id
2876 * @param null $deprecated
2877 * @param bool $create
2878 *
2879 * @return $this|false|Order
2880 */
2881 public function get_recent_draft_order( int $customer_id = 0, $deprecated = null, bool $create = true ) {
2882 $cart_hash = Helper::get_cart_hash_from_cookie();
2883 if ( 0 === $customer_id ) {
2884 $customer_id = get_current_user_id();
2885 }
2886
2887 if ( ! $cart_hash && ! $customer_id ) {
2888 return $this;
2889 }
2890
2891 try {
2892 $cache_key = 'order:draft:' . $cart_hash;
2893 $id = wp_cache_get( $cache_key, $this->cache_group );
2894
2895 if ( false !== $id && false !== wp_cache_get( $id, $this->cache_group ) ) {
2896 $this->set_id( $id );
2897 $this->read();
2898
2899 return $this;
2900 }
2901
2902 $data = $this->read_db_data( [ $cart_hash, $customer_id ], 'cart_hash' );
2903
2904 wp_cache_set( $cache_key, $data['id'], $this->cache_group );
2905 wp_cache_set( $data['id'], $data, $this->cache_group );
2906
2907 $this->set_id( $data['id'] );
2908 $this->read();
2909
2910 return $this;
2911 } catch ( Exception $e ) {
2912 if ( 404 !== $e->getCode() ) {
2913 Helper::log_error( $e );
2914 }
2915 }
2916
2917 if ( $create ) {
2918 return self::create_draft_order( [
2919 'cart_hash' => $cart_hash,
2920 'customer_id' => $customer_id,
2921 ] );
2922 }
2923
2924 return false;
2925 }
2926
2927 public static function create_draft_order( array $args = [] ): Order {
2928 $args = wp_parse_args( $args, [
2929 'customer_id' => get_current_user_id(),
2930 'ip_address' => Helper::get_user_ip(),
2931 'user_agent' => Helper::get_user_agent(),
2932 'cart_hash' => Helper::get_cart_hash_from_cookie(),
2933 'prices_include_tax' => TaxUtil::prices_include_tax(),
2934 ] );
2935
2936 $order = new self();
2937 $order->set_props( $args );
2938 $order->set_prop( 'status', OrderStatus::DRAFT );
2939 $order->save();
2940
2941 if ( $order->get_id() ) {
2942 $cache_key = 'order:draft:' . Helper::get_cart_hash_from_cookie();
2943 wp_cache_set( $cache_key, $order->get_id(), 'storeengine_orders' );
2944 }
2945
2946 return $order;
2947 }
2948
2949 public function has_address( string $context = 'view' ): bool {
2950 return $this->has_shipping_address( $context ) || $this->has_billing_address( $context );
2951 }
2952
2953 /**
2954 * @return DownloadPermission[]
2955 */
2956 public function get_downloadable_permissions(): array {
2957 return Helper::get_download_permissions_by_order_id( $this->get_id() );
2958 }
2959
2960 public function get_tax_amount( string $context = 'view' ) {
2961 return $this->get_total_tax( $context );
2962 }
2963
2964 public function get_total_amount( string $context = 'view' ) {
2965 return $this->get_total( $context );
2966 }
2967
2968 public function set_total_amount( $amount ) {
2969 $this->set_total( $amount );
2970 }
2971
2972 public function set_order_placed_date_gmt( $value = null ) {
2973 if ( null === $value ) {
2974 $value = current_time( 'mysql', 1 );
2975 }
2976
2977 $this->set_date_prop( 'order_placed_date_gmt', $value );
2978 }
2979
2980 /**
2981 * @param $value
2982 *
2983 * @return void
2984 * @see get_date_from_gmt can be used.
2985 *
2986 */
2987 public function set_order_placed_date( $value = null ) {
2988 if ( null === $value ) {
2989 $value = current_time( 'mysql', false );
2990 }
2991
2992 $this->set_date_prop( 'order_placed_date', $value );
2993 }
2994
2995 public function maybe_set_digital_auto_complete() {
2996 $items= $this->get_line_product_items();
2997 $this->set_auto_complete_digital_order(
2998 $items &&
2999 ArrayUtil::every(
3000 $items,
3001 fn( $item ) => 'digital' === $item->get_shipping_type() && $item->get_digital_auto_complete()
3002 )
3003 );
3004 }
3005 }
3006