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 / utils / traits / order.php

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

629 lines 20.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Utils\traits;
4
5 use Exception;
6 use StoreEngine\Classes\AbstractOrder;
7 use StoreEngine\Classes\Exceptions\StoreEngineException;
8 use StoreEngine\Classes\Exceptions\StoreEngineInvalidArgumentException;
9 use StoreEngine\Classes\Exceptions\StoreEngineNotFoundException;
10 use StoreEngine\Classes\Order as OrderClass;
11 use StoreEngine\Classes\Order\OrderItemProduct;
12 use StoreEngine\Classes\OrderCollection;
13 use StoreEngine\Classes\Orders;
14 use StoreEngine\Classes\OrderStatus\OrderStatus;
15 use StoreEngine\Classes\Refund;
16 use StoreEngine\Payment_Gateways;
17 use StoreEngine\Utils\Caching;
18 use StoreEngine\Utils\Formatting;
19 use StoreEngine\Utils\Helper;
20 use StoreEngine\Utils\StringUtil;
21 use WP_Error;
22
23 trait Order {
24
25 public static function order_type_classes() {
26 return apply_filters( 'storeengine/order/classes', [
27 'order' => OrderClass::class,
28 'refund_order' => Refund::class,
29 ] );
30 }
31
32 public static function get_order_type_class( string $type ): ?string {
33 return self::order_type_classes()[ $type ] ?? null;
34 }
35
36 public static function get_order_paid_statuses(): array {
37 return apply_filters( 'storeengine/order_paid_statuses', [ 'completed', 'payment_confirmed' ] );
38 }
39
40 /**
41 * @param false|int|AbstractOrder $order_id
42 *
43 * @return AbstractOrder|WP_Error
44 */
45 public static function get_order( $order_id ) {
46 try {
47 $order_id = self::get_order_id( $order_id );
48
49 if ( ! $order_id ) {
50 return new WP_Error( 'order-not-found', __( 'Order not found.', 'storeengine' ), [ 'status' => 404 ] );
51 }
52
53 $order = wp_cache_get( 'orders_' . $order_id, 'storeengine_orders' );
54
55 if ( $order ) {
56 if ( 0 === $order->get_id() ) {
57 wp_cache_delete( 'orders_' . $order_id, 'storeengine_orders' );
58
59 return new WP_Error( 'order-not-found', __( 'Order not found.', 'storeengine' ), [ 'status' => 404 ] );
60 }
61
62 return $order;
63 }
64
65 $classname = self::get_class_name_for_order_id( $order_id );
66
67 if ( ! $classname ) {
68 return new WP_Error( 'order-class-not-found', __( 'Invalid order type.', 'storeengine' ), [ 'status' => 404 ] );
69 }
70
71 $order = new $classname( $order_id );
72
73 if ( $order instanceof AbstractOrder ) {
74 wp_cache_set( 'orders_' . $order_id, $order, 'storeengine_orders', HOUR_IN_SECONDS );
75 }
76
77 return $order;
78 } catch ( StoreEngineException $e ) {
79 Helper::log_error( $e );
80
81 return $e->toWpError();
82 } catch ( Exception $e ) {
83 Helper::log_error( $e );
84
85 return new WP_Error( 'unknown-error', $e->getMessage(), [ 'status' => 500 ] );
86 }
87 }
88
89 /**
90 * @param mixed $order
91 *
92 * @return false|int
93 */
94 public static function get_order_id( $order ) {
95 if ( false === $order ) {
96 return self::get_global_order_id();
97 } elseif ( is_numeric( $order ) ) {
98 return absint( $order );
99 } elseif ( $order instanceof AbstractOrder ) {
100 return $order->get_id();
101 } elseif ( ! empty( $order->ID ) ) {
102 return (int) $order->ID;
103 } else {
104 return false;
105 }
106 }
107
108 private static function get_global_order_id() {
109 global $order;
110 global $refund;
111 global $subscription;
112
113 if ( $order instanceof AbstractOrder ) {
114 return $order->get_id();
115 }
116 if ( $refund instanceof AbstractOrder ) {
117 return $refund->get_id();
118 }
119 if ( $subscription instanceof AbstractOrder ) {
120 return $subscription->get_id();
121 }
122
123 return false;
124 }
125
126 /**
127 * Gets the class name an order instance should have based on its ID.
128 *
129 * @param int $order_id The order ID.
130 *
131 * @return string|false The class name or FALSE if the class does not exist.
132 */
133 public static function get_class_name_for_order_id( int $order_id ) {
134 $classnames = self::get_class_names_for_order_ids( [ $order_id ] );
135
136 return $classnames[ $order_id ] ?? false;
137 }
138
139 /**
140 * Gets the class name bunch of order instances should have based on their IDs.
141 *
142 * @param array $order_ids Order IDs to get the class name for.
143 *
144 * @return array Array of order_id => class_name.
145 * @throws StoreEngineNotFoundException
146 */
147 public static function get_class_names_for_order_ids( array $order_ids ): array {
148 $order_types = self::get_orders_type( $order_ids );
149
150 if ( empty( $order_types ) && 1 === count( $order_ids ) ) {
151 throw new StoreEngineNotFoundException( esc_html__( 'Entry not found!', 'storeengine' ) );
152 }
153
154 return array_map( function ( $order_type ) {
155 return self::get_order_type_class( $order_type );
156 }, $order_types );
157 }
158
159 public static function get_order_type( $order_id ): string {
160 return self::get_orders_type( [ $order_id ] )[ $order_id ] ?? '';
161 }
162 public static function get_orders_type( array $order_ids ): array {
163 global $wpdb;
164
165 if ( empty( $order_ids ) ) {
166 return [];
167 }
168
169 $order_types = [];
170 $key_map = array_combine( $order_ids, array_map( fn( $key ) => 'oder_type_' . $key, $order_ids ) );
171 $cached_values = wp_cache_get_multiple( array_values( $key_map ), 'storeengine_orders' );
172
173 foreach ( $key_map as $key => $prefixed_key ) {
174 if ( isset( $cached_values[ $prefixed_key ] ) && false !== $cached_values[ $prefixed_key ] ) {
175 $order_types[ $key ] = $cached_values[ $prefixed_key ];
176 }
177 }
178
179 // Remaining order ids.
180 $order_ids = array_diff( $order_ids, array_keys( $order_types ) );
181
182 if ( $order_ids ) {
183 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Data cached.
184 $order_ids_placeholder = implode( ', ', array_fill( 0, count( $order_ids ), '%d' ) );
185 $results = $wpdb->get_results( $wpdb->prepare( "SELECT id, type FROM {$wpdb->prefix}storeengine_orders WHERE id IN ( $order_ids_placeholder );", $order_ids ) );
186 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.DirectDatabaseQuery.DirectQuery -- Data cached.
187
188 foreach ( $results as $row ) {
189 $order_types[ $row->id ] = $row->type;
190 }
191
192 $objects = array_combine( array_map( fn( $key ) => 'oder_type_' . $key, array_keys( $order_types ) ), $order_types );
193
194 wp_cache_set_multiple( $objects, 'storeengine_orders', HOUR_IN_SECONDS );
195 }
196
197 return $order_types;
198 }
199
200 /**
201 * Finds an Order ID based on an order key.
202 *
203 * @param string $order_key An order key has generated by.
204 *
205 * @return int The ID of an order, or 0 if the order could not be found
206 */
207 public static function get_order_id_by_order_key( string $order_key ): int {
208 global $wpdb;
209 if ( empty( $order_key ) ) {
210 return 0;
211 }
212
213 $id = wp_cache_get( 'order:key:' . $order_key, 'storeengine_orders' );
214
215 if ( false !== $id ) {
216 return (int) $id;
217 }
218
219 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Data cached.
220 $id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}storeengine_order_operational_data WHERE order_key = %s LIMIT 1;", $order_key ) );
221 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery -- Data cached.
222
223 wp_cache_set( 'order:key:' . $order_key, $id, 'storeengine_orders', DAY_IN_SECONDS );
224
225 return $id;
226 }
227
228 public static function get_order_by_key( string $key ) {
229 return self::get_order( self::get_order_id_by_order_key( $key ) );
230 }
231
232 public static function get_order_id_by_meta( string $key, $value = null ): int {
233 global $wpdb;
234
235 if ( ! is_scalar( $value ) ) {
236 StoreEngineInvalidArgumentException::throw(
237 sprintf(
238 /* translators: %s: Argument type. */
239 __( 'Invalid argument provided. Value (meta_value) must be string, int or float, %s provided.', 'storeengine' ),
240 gettype( $value )
241 )
242 );
243 }
244
245 if ( is_bool( $value ) ) {
246 $value = (int) $value;
247 }
248
249 $cache_key = 'order:meta' . $key . '_' . $value;
250 $id = wp_cache_get( $cache_key, 'storeengine_orders' );
251
252 if ( false !== $id ) {
253 return (int) $id;
254 }
255
256 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Prepared for value type and cached the result.
257
258 $format = is_float( $value ) ? '%s' : ( is_numeric( $value ) ? '%d' : '%s' );
259 $id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}storeengine_orders_meta WHERE meta_key = %s AND meta_value = {$format} ORDER BY order_id DESC LIMIT 1;", $key, $value ) );
260
261 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
262
263 wp_cache_set( $cache_key, $id, 'storeengine_orders', DAY_IN_SECONDS );
264
265 return $id;
266 }
267
268 public static function get_order_by_meta( string $key, $value ) {
269 return self::get_order( self::get_order_id_by_meta( $key, $value ) );
270 }
271
272 /**
273 * Get orders of customer.
274 *
275 * @param int $customer_id [Optional] Customer id. Default to zero (current user).
276 *
277 * @return OrderClass[]
278 * @see OrderCollection
279 *
280 * @deprecated Use Order collection class to query and count results together.
281 */
282 public static function get_customer_orders( int $customer_id = 0, int $page = 1, int $per_page = 10 ): array {
283 return ( new Orders( $page, $per_page ) )->get( array(
284 'customer_id' => array(
285 'condition' => '=',
286 'formatter' => '%d',
287 'value' => 0 !== $customer_id ? $customer_id : get_current_user_id(),
288 ),
289 ) );
290 }
291
292 public static function get_payment_data( OrderClass $order ): array {
293 $data = [
294 'id' => $order->get_id(),
295 'status' => $order->get_status(),
296 'customer_id' => $order->get_customer_id(),
297 'total_amount' => $order->get_total(),
298 'date_created_gmt' => $order->get_date_created_gmt() ? $order->get_date_created_gmt()->format( 'Y-m-d H:i:s' ) : null,
299 'date_updated_gmt' => $order->get_date_updated_gmt() ? $order->get_date_updated_gmt()->format( 'Y-m-d H:i:s' ) : null,
300 'payment_method' => $order->get_payment_method(),
301 'payment_method_title' => $order->get_payment_method_title(),
302 'refunds_total' => $order->get_total_refunded(),
303 'refunded_amount' => $order->get_total_refunded(),
304 'can_refund' => false,
305 'gateway_can_refund_order' => false,
306 'currency' => $order->get_currency(),
307 'is_paid' => $order->is_paid(),
308 ];
309
310 if ( $order->is_paid() && 'refunded' !== $order->get_status() ) {
311 $data['can_refund'] = (bool) apply_filters(
312 'storeengine/refund/can_admin_refund_order',
313 (
314 0 < $order->get_total() - $order->get_total_refunded() ||
315 0 < absint( $order->get_item_count() - $order->get_item_count_refunded() )
316 ),
317 $order->get_id(),
318 $order
319 );
320
321 $payment_gateway = Helper::get_payment_gateway_by_order( $order );
322
323 if ( false !== $payment_gateway ) {
324 $data['gateway_name'] = ( ! empty( $payment_gateway->method_title ) ? $payment_gateway->method_title : $payment_gateway->get_title() );
325
326 if ( $payment_gateway->can_refund_order( $order ) ) {
327 $data['gateway_can_refund_order'] = true;
328 }
329 } else {
330 $data['gateway_name'] = __( 'Payment gateway', 'storeengine' );
331 }
332 }
333
334 return $data;
335 }
336
337 /**
338 * Get orders by page and conditions.
339 *
340 * @param array $args Conditional Args.
341 * @param array $pagination Pagination array.
342 *
343 * @return OrderClass[]
344 * @see OrderCollection
345 *
346 * @deprecated Use Order collection class to query and count results together.
347 */
348 public static function get_orders( array $args = [], array $pagination = [] ): array {
349 $pagination = wp_parse_args( $pagination, [
350 'page' => 1,
351 'per_page' => 10,
352 ] );
353
354 return ( new Orders( $pagination['page'], $pagination['per_page'] ) )->get( $args );
355 }
356
357 /**
358 * @param array $conditions
359 *
360 * @return int
361 * @see OrderCollection
362 *
363 * @deprecated Use Order collection class to query and count results together.
364 */
365 public static function get_total_orders_count( array $conditions = [] ): int {
366 return ( new Orders() )->get_total_orders_count( $conditions );
367 }
368
369 public static function get_recent_draft_order( int $customer_id = 0, ?string $cart_hash = null, bool $create = true ) {
370 return ( new OrderClass() )->get_recent_draft_order( $customer_id, null, $create );
371 }
372
373 public static function create_refund( $args = [] ) {
374 $default_args = [
375 'amount' => 0,
376 'reason' => null,
377 'order_id' => 0,
378 'refund_id' => 0,
379 'line_items' => [],
380 'refund_payment' => false,
381 'restock_items' => false,
382 ];
383
384 try {
385 $args = wp_parse_args( $args, $default_args );
386 $order = self::get_order( absint( $args['order_id'] ) );
387
388 if ( is_wp_error( $order ) ) {
389 throw new StoreEngineException( esc_html__( 'Invalid order ID.', 'storeengine' ), 'invalid-order-id' );
390 }
391
392 $remaining_refund_amount = $order->get_remaining_refund_amount();
393 $remaining_refund_items = $order->get_remaining_refund_items();
394 $refund_item_count = 0;
395 $refund = new Refund( $args['refund_id'] ); // @TODO should use self::get_order( $args['refund_id'] );
396 $refunded_order_and_products = [];
397
398 if ( 0 > $args['amount'] || $args['amount'] > $remaining_refund_amount ) {
399 throw new StoreEngineException( esc_html__( 'Invalid refund amount.', 'storeengine' ), 'invalid-refund-amount' );
400 }
401
402 $refund->set_currency( $order->get_currency() );
403 $refund->set_amount( $args['amount'] );
404 $refund->set_status( OrderStatus::COMPLETED );
405 $refund->set_parent_order_id( $order->get_id() );
406 $refund->set_customer_id( $order->get_customer_id() );
407 $refund->set_refunded_by( get_current_user_id() );
408 $refund->set_prices_include_tax( $order->get_prices_include_tax() );
409
410 if ( ! StringUtil::is_null_or_whitespace( $args['reason'] ) ) {
411 $refund->set_reason( (string) $args['reason'] );
412 }
413
414 // Negative line items.
415 if ( is_array( $args['line_items'] ) && count( $args['line_items'] ) > 0 ) {
416 $items = $order->get_items( [ 'line_item', 'fee', 'shipping' ] );
417
418 foreach ( $items as $item_id => $item ) {
419 if ( ! isset( $args['line_items'][ $item_id ] ) ) {
420 continue;
421 }
422
423 $qty = $args['line_items'][ $item_id ]['qty'] ?? 0;
424 $refund_total = $args['line_items'][ $item_id ]['refund_total'];
425 $refund_tax = isset( $args['line_items'][ $item_id ]['refund_tax'] ) ? array_filter( (array) $args['line_items'][ $item_id ]['refund_tax'] ) : [];
426
427 if ( empty( $qty ) && empty( $refund_total ) && empty( $args['line_items'][ $item_id ]['refund_tax'] ) ) {
428 continue;
429 }
430
431 // array of order id and product id which were refunded.
432 // later to be used for revoking download permission.
433 // checking if the item is a product, as we only need to revoke download permission for products.
434 if ( $item->is_type( 'line_item' ) ) {
435 $refunded_order_and_products[ $item_id ] = [
436 'order_id' => $order->get_id(),
437 'product_id' => $item->get_product_id(),
438 ];
439 }
440
441 $class = get_class( $item );
442 $refunded_item = new $class( $item );
443 $refunded_item->set_id( 0 );
444 $refunded_item->add_meta_data( '_refunded_item_id', $item_id, true );
445 $refunded_item->set_total( Formatting::format_refund_total( $refund_total ) );
446 $refunded_item->set_taxes( [
447 'total' => array_map( [ Formatting::class, 'format_refund_total' ], $refund_tax ),
448 'subtotal' => array_map( [ Formatting::class, 'format_refund_total' ], $refund_tax ),
449 ] );
450
451 if ( is_callable( [ $refunded_item, 'set_subtotal' ] ) ) {
452 $refunded_item->set_subtotal( Formatting::format_refund_total( $refund_total ) );
453 }
454
455 if ( is_callable( [ $refunded_item, 'set_quantity' ] ) ) {
456 $refunded_item->set_quantity( $qty * - 1 );
457 }
458
459 $refund->add_item( $refunded_item );
460 $refund_item_count += $qty;
461 }
462 }
463
464 $refund->update_taxes();
465 $refund->calculate_totals( false );
466 $refund->set_total( $args['amount'] * - 1 );
467
468 // this should remain after update_taxes(), as this will save the order, and write the current date to the db
469 // so we must wait until the order is persisted to set the date.
470 if ( isset( $args['date_created'] ) ) {
471 $refund->set_date_created( $args['date_created'] );
472 }
473
474 /**
475 * Action hook to adjust refund before save.
476 */
477 do_action( 'storeengine/create_refund', $refund, $args );
478
479 if ( ! $refund->save() ) {
480 return new WP_Error( 'unknown-error', __( 'Something went wrong. Please try again after sometime.', 'storeengine' ) );
481 }
482
483 if ( $args['refund_payment'] ) {
484 $result = self::refund_payment( $order, $refund->get_amount(), $refund->get_reason() );
485
486 if ( is_wp_error( $result ) ) {
487 $refund->delete();
488
489 return $result;
490 }
491
492 $refund->set_refunded_payment( true );
493 $refund->save();
494 }
495
496 $cache_key = Caching::get_cache_prefix( 'orders' ) . 'refunds' . $order->get_id();
497 wp_cache_delete( $cache_key, 'storeengine_orders' );
498 wp_cache_delete( Caching::get_cache_prefix( 'orders' ) . 'total_refunded' . $order->get_id(), 'storeengine_orders' );
499
500 if ( $args['restock_items'] ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
501 // @TODO restock items.
502 }
503
504 // delete downloads that were refunded using order and product id, if present.
505 // @TODO remove download permission.
506
507 /**
508 * Trigger notification emails.
509 *
510 * Filter hook to modify the partially-refunded status conditions.
511 *
512 * @param bool $is_partially_refunded Whether the order is partially refunded.
513 * @param int $order_id The order id.
514 * @param int $refund_id The refund id.
515 */
516 if ( apply_filters( 'storeengine/order_is_partially_refunded', ( $remaining_refund_amount - $args['amount'] ) > 0 || ( $order->has_free_item() && ( $remaining_refund_items - $refund_item_count ) > 0 ), $order->get_id(), $refund->get_id() ) ) {
517 do_action( 'storeengine/order/partially_refunded', $order->get_id(), $refund->get_id(), $remaining_refund_amount - $args['amount'] );
518 } else {
519 do_action( 'storeengine/order/fully_refunded', $order->get_id(), $refund->get_id() );
520
521 /**
522 * Filter the status to set the order to when fully refunded.
523 *
524 * @param string $parent_status The status to set the order to when fully refunded.
525 * @param int $order_id The order ID.
526 * @param int $refund_id The refund ID.
527 */
528 $parent_status = apply_filters( 'storeengine/order/fully_refunded_status', OrderStatus::REFUNDED, $order->get_id(), $refund->get_id() );
529
530 if ( $parent_status ) {
531 $order->update_status( $parent_status );
532 }
533 }
534
535 if ( ! $order->get_remaining_refund_amount() ) {
536 $order->set_paid_status( 'refunded' );
537 }
538
539 $order->set_date_modified( time() );
540 $order->save();
541
542 do_action( 'storeengine/order/refund_created', $refund, $args );
543 do_action( 'storeengine/order/order_refunded', $order->get_id(), $refund->get_id() );
544 } catch ( StoreEngineException $e ) {
545 Helper::log_error( $e );
546
547 try {
548 if ( isset( $refund ) && is_a( $refund, Refund::class ) ) {
549 $refund->delete( true );
550 }
551 } catch ( StoreEngineException $ex ) {
552 Helper::log_error( $ex );
553 }
554
555 return $e->toWpError();
556 }
557
558 return $refund;
559 }
560
561 /**
562 * Try to refund the payment for an order via the gateway.
563 *
564 * @param OrderClass $order Order instance.
565 * @param string|float $amount Amount to refund.
566 * @param string $reason Refund reason.
567 *
568 * @return bool|WP_Error
569 */
570 public static function refund_payment( OrderClass $order, $amount, string $reason = '' ) {
571 try {
572 $gateway = Payment_Gateways::get_instance()->get_gateway( $order->get_payment_method() ) ?? false;
573
574 if ( ! $gateway ) {
575 throw new StoreEngineException( esc_html__( 'The payment gateway for this order does not exist.', 'storeengine' ), 'gateway_not_found_for_order' );
576 }
577
578 if ( ! $gateway->supports( 'refunds' ) ) {
579 throw new StoreEngineException( esc_html__( 'The payment gateway for this order does not support automatic refunds.', 'storeengine' ), 'gateway_does_not_support_refund' );
580 }
581
582 $result = $gateway->process_refund( $order->get_id(), $amount, $reason );
583
584 if ( ! $result ) {
585 throw new StoreEngineException( esc_html__( 'An error occurred while attempting to create the refund using the payment gateway API.', 'storeengine' ) );
586 }
587
588 if ( is_wp_error( $result ) ) {
589 throw StoreEngineException::from_wp_error( $result );
590 }
591
592 return true;
593 } catch ( StoreEngineException $e ) {
594 Helper::log_error( $e );
595
596 return $e->toWpError();
597 }
598 }
599
600 public static function get_order_item( int $id ) {
601 return ( new OrderItemProduct( $id ) );
602 }
603
604 public static function get_first_order() {
605 static $order;
606
607 if ( null === $order ) {
608 $query = new OrderCollection( [
609 'per_page' => 1,
610 'orderby' => 'id',
611 'order' => 'ASC',
612 'where' => [
613 'key' => 'type',
614 'value' => 'order',
615 ]
616 ] );
617 $order = $query->next_result();
618 }
619
620 return $order;
621 }
622
623 public static function get_first_order_date( $format = 'Y-m-d H:i:s' ): string {
624 $firstOrder = Helper::get_first_order();
625
626 return $firstOrder ? $firstOrder->get_date_created_gmt()->format( $format ) : gmdate( 'Y-m-d H:i:s' );
627 }
628 }
629