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

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

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