null, 'order_placed_date' => null, 'paid_status' => null, // Addresses 'billing' => [ 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', 'email' => '', 'phone' => '', 'address_type' => 'billing', ], 'shipping' => [ 'first_name' => '', 'last_name' => '', 'company' => '', 'address_1' => '', 'address_2' => '', 'city' => '', 'state' => '', 'postcode' => '', 'country' => '', 'email' => '', 'phone' => '', 'address_type' => 'shipping', ], 'download_permissions_granted' => false, 'auto_complete_digital_order' => false, ]; /** * Stores data about status changes so relevant hooks can be fired. * * @var bool|array */ protected $status_transition = false; public function __construct( $read = 0 ) { $this->internal_meta_keys[] = '_order_placed_date_gmt'; $this->internal_meta_keys[] = '_order_placed_date'; $this->internal_meta_keys[] = '_download_permissions_granted'; $this->internal_meta_keys[] = '_auto_complete_digital_order'; $this->meta_key_to_props['_order_placed_date_gmt'] = 'order_placed_date_gmt'; $this->meta_key_to_props['_order_placed_date'] = 'order_placed_date'; $this->meta_key_to_props['_paid_status'] = 'paid_status'; $this->meta_key_to_props['_download_permissions_granted'] = 'download_permissions_granted'; $this->meta_key_to_props['_auto_complete_digital_order'] = 'auto_complete_digital_order'; parent::__construct( $read ); } protected function read_db_data( $value, string $field = 'id' ): array { return array_merge( parent::read_db_data( $value, $field ), [ 'order_placed_date_gmt' => $this->get_metadata( '_order_placed_date_gmt' ), 'order_placed_date' => $this->get_metadata( '_order_placed_date' ), 'paid_status' => $this->get_metadata( '_paid_status' ), 'download_permissions_granted' => $this->get_metadata( '_download_permissions_granted' ), 'auto_complete_digital_order' => $this->get_metadata( '_auto_complete_digital_order' ), ] ); } public function save() { $this->maybe_set_user_billing_email(); $saved = parent::save(); if ( is_wp_error( $saved ) ) { return $saved; } $this->status_transition(); return $this->get_id(); } public function create() { parent::create(); if ( ! $this->is_type( 'refund' ) && ( array_key_exists( 'billing', $this->data ) || array_key_exists( 'shipping', $this->data ) ) ) { foreach ( [ 'billing', 'shipping' ] as $type ) { $address = $this->get_address( $type ); if ( ! $this->{'has_' . $type . '_address'}( 'edit' ) ) { continue; } $address['order_id'] = $this->get_id(); $formats = array_fill( 0, count( $address ), '%s' ); $this->wpdb->insert( "{$this->wpdb->prefix}storeengine_order_addresses", $address, $formats ); if ( $this->wpdb->last_error ) { throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' ); } } } } public function update() { parent::update(); foreach ( [ 'billing', 'shipping' ] as $type ) { $address = $this->get_address( $type ); // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared. $this->wpdb->query( $this->wpdb->prepare( " INSERT INTO `{$this->wpdb->prefix}storeengine_order_addresses` (`order_id`, `address_type`, `first_name`, `last_name`, `company`, `address_1`, `address_2`, `city`, `state`, `postcode`, `country`, `email`, `phone`) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) ON DUPLICATE KEY UPDATE `order_id` = VALUES(`order_id`), `first_name` = VALUES(`first_name`), `last_name` = VALUES(`last_name`), `company` = VALUES(`company`), `address_1` = VALUES(`address_1`), `address_2` = VALUES(`address_2`), `city` = VALUES(`city`), `state` = VALUES(`state`), `postcode` = VALUES(`postcode`), `country` = VALUES(`country`), `email` = VALUES(`email`), `phone` = VALUES(`phone`); ", $this->get_id(), $type, $address['first_name'], $address['last_name'], $address['company'], $address['address_1'], $address['address_2'], $address['city'], $address['state'], $address['postcode'], $address['country'], $address['email'], $address['phone'] ) ); // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query prepared. if ( $this->wpdb->last_error ) { throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' ); } } } public function delete( bool $force_delete = false ): bool { if ( ! $force_delete && $this->is_trashable() ) { $this->set_status( 'trash' ); $this->save(); return true; } $refunds = $this->get_refunds(); if ( ! empty( $refunds ) ) { foreach ( $refunds as $refund ) { $refund->delete( true ); } } if ( $this->has_address( 'edit' ) ) { $this->wpdb->delete( "{$this->wpdb->prefix}storeengine_order_addresses", [ 'order_id' => $this->get_id() ], [ '%d' ] ); if ( $this->wpdb->last_error ) { throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-delete-order_addresses' ); } } return parent::delete( true ); } /** * Log an error about this order is exception is encountered. * * @param StoreEngineException $e Exception object. * @param string $message Message regarding exception thrown. */ protected function handle_exception( StoreEngineException $e, string $message = 'Error' ) { $this->add_order_note( $message . ' ' . $e->getMessage() ); } /** * When a payment is complete this function is called. * * Most of the time this should mark an order as 'processing' so that admin can process/post the items. * If the cart contains only downloadable items then the order is 'completed' since the admin needs to take no action. * Stock levels are reduced at this point. * Sales are also recorded for products. * Finally, record the date of payment. * * @param string $transaction_id Optional transaction id to store in post meta. * * @return bool success */ public function payment_complete( string $transaction_id = '' ): bool { if ( ! $this->get_id() ) { // Order must exist. return false; } try { $order_id = $this->get_id(); /** * Fires before payment complete process of an order. * * @param int $order_id Order id. * @param string $transaction_id Transaction id. */ do_action( 'storeengine/pre_payment_complete', $order_id, $transaction_id ); /** * Filters the valid order statuses for payment complete. * * @param array $valid_completed_statuses Array of valid order statuses for payment complete. * @param Order $this Order object. */ $valid_completed_statuses = apply_filters( 'storeengine/valid_order_statuses_for_payment_complete', [ OrderStatus::ON_HOLD, OrderStatus::PAYMENT_PENDING, OrderStatus::PAYMENT_FAILED, OrderStatus::CANCELLED, ], $this ); if ( $this->has_status( $valid_completed_statuses ) ) { if ( ! empty( $transaction_id ) ) { $this->set_transaction_id( $transaction_id ); } if ( ! $this->get_date_paid_gmt( 'edit' ) ) { $this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); } /** * Filters the order status to set after payment complete. * * @param string $status Order status. * @param int $order_id Order ID. * @param Order $this Order object. */ $this->set_status( apply_filters( 'storeengine/payment_complete_order_status', $this->needs_processing() ? OrderStatus::PROCESSING : OrderStatus::COMPLETED, $this->get_id(), $this ) ); $this->save(); /** * Fires after payment complete process of an order. * * @param int $order_id Order id. * @param string $transaction_id Transaction id. */ do_action( 'storeengine/payment_complete', $order_id, $transaction_id ); } else { $status = $this->get_status(); /** * If order status isn't valid for mark order as processing/completed, then fire this hook. * * @param int $order_id Order ID. * @param array $transaction_id Transaction ID. */ do_action( "storeengine/payment_complete_order_status_{$status}", $order_id, $transaction_id ); } } catch ( Exception $e ) { Helper::log_error( $e ); $this->add_order_note( __( 'Payment complete event failed.', 'storeengine' ) . ' ' . $e->getMessage() ); return false; } return true; } /** * Forcibly mark an order as paid and advance it to a paid state, running the * full payment-complete flow (status transition, digital auto-complete, paid * status + date, downstream hooks/emails). * * Unlike payment_complete() this handles EVERY starting status — including * `draft`/`auto-draft` (a Paddle order stranded by the client-side flow) — by * first "placing" the order, then advancing it. Idempotent: a no-op that * returns true if the order is already paid. * * Shared seam used by both the Paddle webhook reconciliation * (GatewayPaddle::complete_order_from_transaction) and the admin * "Mark as paid" action (Ajax\Order::mark_order_as_paid). * * @param string $note Order note attached to the status transition. * * @return bool True if the order ended in a paid state. */ public function mark_as_paid_force( string $note = '' ): bool { if ( ! $this->get_id() ) { return false; } if ( $this->is_paid() ) { return true; } try { $status = $this->get_status(); // Draft orders must be "placed" first (draft -> pending_payment) before // they can be processed. order_placed resets paid_status to unpaid, so // we (re)assert paid below for the pending_payment branch. if ( in_array( $status, [ OrderStatus::DRAFT, OrderStatus::AUTO_DRAFT ], true ) ) { ( new OrderContext( $status ) )->proceed_to_next_status( 'order_placed', $this, [ 'note' => $note ] ); $status = $this->get_status(); } if ( OrderStatus::PAYMENT_PENDING === $status ) { // Mark paid BEFORE advancing so OrderContext's digital auto-complete // branch (which checks paid_status === 'paid') fires correctly. $this->set_paid_status( 'paid' ); ( new OrderContext( $status ) )->proceed_to_next_status( 'process_order', $this, [ 'note' => $note ] ); } elseif ( in_array( $status, [ OrderStatus::ON_HOLD, OrderStatus::PAYMENT_FAILED, OrderStatus::CANCELLED ], true ) ) { // payment_complete() accepts exactly these statuses and moves the // order to processing/completed based on needs_processing(). $this->payment_complete( $this->get_transaction_id() ); // set_status() only auto-marks paid for COMPLETED/PAYMENT_CONFIRMED, // so assert it for the processing case too. if ( 'paid' !== $this->get_paid_status( 'edit' ) ) { $this->set_paid_status( 'paid' ); } } else { // Already in a post-payment status (processing/payment_confirmed/ // completed) but flagged unpaid — just assert the paid flag. $this->set_paid_status( 'paid' ); } $this->save(); } catch ( \Throwable $e ) { Helper::log_error( $e ); $this->add_order_note( __( 'Force mark as paid failed.', 'storeengine' ) . ' ' . $e->getMessage() ); return false; } return $this->is_paid(); } /** * Gets order total - formatted for display. * * @param string $tax_display Type of tax display. * @param bool $display_refunded If should include refunded value. * * @return string */ public function get_formatted_order_total( string $tax_display = '', bool $display_refunded = true ): string { $formatted_total = Formatting::price( $this->get_total(), [ 'currency' => $this->get_currency() ] ); $order_total = $this->get_total(); $total_refunded = $this->get_total_refunded(); $tax_string = ''; // Tax for inclusive prices. if ( TaxUtil::is_tax_enabled() && 'incl' === $tax_display ) { $tax_string_array = []; $tax_totals = $this->get_tax_totals(); if ( 'itemized' === Helper::get_settings( 'tax_total_display' ) ) { foreach ( $tax_totals as $code => $tax ) { $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; $tax_string_array[] = sprintf( '%s %s', $tax_amount, $tax->label ); } } elseif ( ! empty( $tax_totals ) ) { $tax_amount = ( $total_refunded && $display_refunded ) ? $this->get_total_tax() - $this->get_total_tax_refunded() : $this->get_total_tax(); $tax_string_array[] = sprintf( '%s %s', Formatting::price( $tax_amount, [ 'currency' => $this->get_currency() ] ), Countries::init()->tax_or_vat() ); } if ( ! empty( $tax_string_array ) ) { /* translators: %s: tax amounts */ $tax_string = ' ' . sprintf( __( '(includes %s)', 'storeengine' ), implode( ', ', $tax_string_array ) ) . ''; } } if ( $total_refunded && $display_refunded ) { $current_total = Formatting::price( $order_total - $total_refunded, [ 'currency' => $this->get_currency() ] ); // Strikethrough pricing. $formatted_total = ' '; // For accessibility (a11y) we'll also display that information to screen readers. $formatted_total .= ' '; // translators: %s is total order amount without refund. $formatted_total .= esc_html( sprintf( __( 'Original amount was: %s.', 'storeengine' ), wp_strip_all_tags( $formatted_total ) ) ); $formatted_total .= ''; // Add the sale price. $formatted_total .= ' '; // For accessibility (a11y) we'll also display that information to screen readers. $formatted_total .= ' '; // translators: %s is total order amount after refund. $formatted_total .= esc_html( sprintf( __( 'Current amount is: %s.', 'storeengine' ), wp_strip_all_tags( $current_total ) ) ); $formatted_total .= ''; } else { $formatted_total .= $tax_string; } /** * Filter StoreEngine formatted order total. * * @param string $formatted_total Total to display. * @param Order $order Order data. * @param string $tax_display Type of tax display. * @param bool $display_refunded If should include refunded value. */ return apply_filters( 'storeengine/get_formatted_order_total', $formatted_total, $this, $tax_display, $display_refunded ); } /** * Set order status. * * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @param string $note * @param bool $manual_update * * @return array */ public function set_status( string $new_status, string $note = '', bool $manual_update = false ): array { $result = parent::set_status( $new_status ); if ( true === $this->object_read && ! empty( $result['from'] ) && $result['from'] !== $result['to'] ) { $this->status_transition = [ 'from' => ! empty( $this->status_transition['from'] ) ? $this->status_transition['from'] : $result['from'], 'to' => $result['to'], 'note' => $note, 'manual' => $manual_update, ]; if ( $manual_update ) { $order_id = $this->get_id(); $new_order_status = $result['to']; /** * Fires during set new status when manual update is set to true. * * @param int $order_id Order ID. * @param string $new_order_status New Order Status. */ do_action( 'storeengine/order_edit_status', $order_id, $new_order_status ); } if ( $this->is_type( 'order' ) ) { // Maybe set order-placed-date if ( ! $this->get_order_placed_date_gmt( 'edit' ) && $this->has_status( [ OrderStatus::PROCESSING, OrderStatus::PAYMENT_CONFIRMED, OrderStatus::COMPLETED ] ) ) { $this->set_order_placed_date_gmt(); $this->set_order_placed_date(); } // Maybe set paid status. if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::COMPLETED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) { $this->set_paid_status( 'paid' ); $this->add_order_note( __( 'Order marked as paid automatically as order is completed.', 'storeengine' ) ); } if ( ! $this->is_paid( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_CONFIRMED ) && 'paid' !== $this->get_paid_status( 'edit' ) ) { $this->set_paid_status( 'paid' ); $this->add_order_note( __( 'Order marked as paid as order status set to payment confirmed.', 'storeengine' ) ); } if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::PAYMENT_PENDING ) ) { $this->set_paid_status( 'unpaid' ); $this->set_date_paid_gmt( 0 ); $this->add_order_note( __( 'Payment status marked as unpaid as order status set to payment pending.', 'storeengine' ) ); } if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( OrderStatus::ON_HOLD ) ) { $this->set_paid_status( 'on_hold' ); $this->set_date_paid_gmt( 0 ); $this->add_order_note( __( 'Payment status marked as oh-hold as order status set to payment on-hold.', 'storeengine' ) ); } if ( 'paid' === $this->get_paid_status( 'edit' ) && $this->has_status( 'refunded' ) ) { $this->set_paid_status( 'refunded' ); $this->set_date_paid_gmt( 0 ); $this->add_order_note( __( 'Payment status marked as refunded as order status set to payment refunded.', 'storeengine' ) ); } $this->maybe_set_date_completed(); } } return $result; } /** * Maybe set date paid. * * Sets the date paid variable when transitioning to the payment complete * order status. This is either processing or completed. This is not filtered * to avoid infinite loops e.g. if loading an order via the filter. * * Date paid is set once in this manner - only when it is not already set. * This ensures the data exists even if a gateway does not use the * `payment_complete` method. * * @deprecated use paid_status */ public function maybe_set_date_paid() { // This logic only runs if the date_paid prop has not been set yet. if ( ! $this->get_date_paid_gmt( 'edit' ) ) { $paid_statuses = [ OrderStatus::PAYMENT_CONFIRMED, OrderStatus::PROCESSING, OrderStatus::COMPLETED ]; if ( $this->has_status( $paid_statuses ) ) { // If payment complete status is reached, set paid now. $this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); $this->set_prop( 'paid_status', 'paid' ); } else { $this->set_date_paid_gmt( 0 ); $this->set_prop( 'paid_status', 'unpaid' ); } } $unpaid_statuses = [ OrderStatus::PAYMENT_PENDING, OrderStatus::AUTO_DRAFT, OrderStatus::DRAFT ]; if ( $this->get_date_paid_gmt( 'edit' ) && $this->has_status( $unpaid_statuses ) ) { $this->set_date_paid_gmt( 0 ); $this->set_prop( 'paid_status', 'unpaid' ); } } /** * @param string $status * @param string|null $transaction_id * * @return void * @throws StoreEngineException */ public function set_paid_status( string $status, ?string $transaction_id = null ) { $paid_stati = [ 'paid', 'partially_paid', 'unpaid', 'failed', 'on_hold', 'refunded' ]; if ( ! in_array( $status, $paid_stati, true ) ) { throw StoreEngineInvalidArgumentException::create( 1, 'status', $paid_stati, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped } if ( $transaction_id ) { $this->set_transaction_id( $transaction_id ); } $old_status = $this->get_paid_status(); if ( 'paid' === $status ) { if ( ! $this->get_date_paid_gmt( 'edit' ) || 'partially_paid' === $old_status ) { $this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); } } elseif ( 'partially_paid' === $status ) { $this->set_date_paid_gmt( current_time( 'mysql', 1 ) ); } else { $this->set_date_paid_gmt( 0 ); } $this->set_prop( 'paid_status', $status ); if ( true === $this->object_read && ! empty( $old_status ) && $old_status !== $status ) { do_action_ref_array( 'storeengine/order/payment_status_changed', [ &$this, $status, $old_status ] ); } } public function get_paid_status( string $context = 'view' ): ?string { $status = $this->get_prop( 'paid_status', $context ); if ( ! $status && 'view' === $context ) { $status = 'unpaid'; } return $status; } public function is_paid( $context = 'view' ): bool { return (bool) apply_filters( 'storeengine/order/is_paid', 'paid' === $this->get_paid_status( $context ) ); } // /** // * Returns if an order has been paid for based on the order status. // * // * @return bool // */ // public function is_paid(): bool { // return apply_filters( 'storeengine/order_is_paid', $this->has_status( OrderStatus::get_is_paid_statuses() ), $this ); // } /** * Maybe set date completed. * * Sets the date completed variable when transitioning to completed status. */ protected function maybe_set_date_completed() { if ( $this->has_status( OrderStatus::COMPLETED ) ) { $this->set_date_completed_gmt( time() ); } } /** * Updates status of order immediately. * * @param string $new_status Status to change the order to. No internal wc- prefix is required. * @param string $note Optional note to add. * @param bool $manual Is this a manual order status change?. * * @return bool * @uses self::set_status() */ public function update_status( string $new_status, string $note = '', bool $manual = false ): bool { if ( ! $this->get_id() ) { // Order must exist. return false; } try { $this->set_status( $new_status, $note, $manual ); $this->save(); } catch ( Exception $e ) { Helper::log_error( $e ); $this->add_order_note( __( 'Update status event failed.', 'storeengine' ) . ' ' . $e->getMessage() ); return false; } return true; } /** * Handle the status transition. */ protected function status_transition() { $status_transition = $this->status_transition; // Reset status transition variable. $this->status_transition = false; if ( $status_transition ) { try { $new_status = $status_transition['to']; $order_id = $this->get_id(); /** * Fires when order status is changed. * * @param int $order_id Order ID. * @param Order $this Order object. * @param array $status_transition Status transition data. */ do_action( "storeengine/order_status_{$new_status}", $order_id, $this, $status_transition ); if ( ! empty( $status_transition['from'] ) ) { /* translators: 1: old order status 2: new order status */ $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'] ) ); // Note the transition occurred. $this->add_status_transition_note( $transition_note, $status_transition ); $old_status = $status_transition['from']; /** * Fires when order status is changed. * * @param int $order_id Order ID. * @param Order $this Order object. */ do_action( "storeengine/order_status_{$old_status}_to_{$new_status}", $order_id, $this ); /** * Fires when order status is changed. * * @param int $order_id Order ID. * @param string $old_status Old Status. * @param string $new_status New Status. * @param Order $this Order object. */ do_action( 'storeengine/order/status_changed', $order_id, $old_status, $new_status, $this ); /** * Fires when order status is changed. * * @param int $order_id Order ID. * @param string $old_status Old Status. * @param Order $this Order object. */ do_action( "storeengine/order/status_{$new_status}", $order_id, $old_status, $this ); // Work out if this was for a payment, and trigger a payment_status hook instead. /** * Filter the valid order statuses for payment. * * @param array $valid_order_statuses Array of valid order statuses for payment. * @param Order $order Order object. */ $check_transition_from = in_array( $status_transition['from'], apply_filters( 'storeengine/valid_order_statuses_for_payment', [ OrderStatus::PAYMENT_PENDING, OrderStatus::PAYMENT_FAILED, ], $this ), true ); $check_transition_to = in_array( $status_transition['to'], OrderStatus::get_is_paid_statuses(), true ); if ( $check_transition_from && $check_transition_to ) { /** * Fires when the order progresses from a pending payment status to a paid one. * * @param int $order_id Order ID. * @param Order $this Order object. */ do_action( 'storeengine/order_payment_status_changed', $order_id, $this ); } } else { /* translators: %s: new order status */ $transition_note = sprintf( __( 'Order status set to %s.', 'storeengine' ), OrderStatus::get_order_status_name( $status_transition['to'] ) ); // Note the transition occurred. $this->add_status_transition_note( $transition_note, $status_transition ); } } catch ( Exception $e ) { Helper::log_error( $e ); $this->add_order_note( __( 'Error during status transition.', 'storeengine' ) . ' ' . $e->getMessage() ); } } } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- | | Methods for getting data from the order object. | */ /** * Get basic order data in array format. * * @return array */ public function get_base_data(): array { return array_merge( [ 'id' => $this->get_id() ], $this->data, [ 'number' => $this->get_order_number() ] ); } /** * Get all class data in array format. * * @return array */ public function get_data(): array { return array_merge( $this->get_base_data(), [ 'meta_data' => $this->get_meta_data(), 'line_items' => $this->get_items( 'line_item' ), 'tax_lines' => $this->get_items( 'tax' ), 'shipping_lines' => $this->get_items( 'shipping' ), 'fee_lines' => $this->get_items( 'fee' ), 'coupon_lines' => $this->get_items( 'coupon' ), ] ); } /** * Expands the shipping and billing information in the changes array. */ public function get_changes(): array { $changed_props = parent::get_changes(); $subs = [ 'shipping', 'billing' ]; foreach ( $subs as $sub ) { if ( ! empty( $changed_props[ $sub ] ) ) { foreach ( $changed_props[ $sub ] as $sub_prop => $value ) { $changed_props[ $sub . '_' . $sub_prop ] = $value; } } } if ( isset( $changed_props['customer_note'] ) ) { $changed_props['post_excerpt'] = $changed_props['customer_note']; } return $changed_props; } /** * Gets the order number for display (by default, order ID). * * @return string */ public function get_order_number(): string { return (string) apply_filters( 'storeengine/order_number', $this->get_id(), $this ); } /** * Gets a prop for a getter method. * * @param string $prop Name of prop to get. * @param string $address_type Type of address; 'billing' or 'shipping'. * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ protected function get_address_prop( string $prop, string $address_type = 'billing', string $context = 'view' ): ?string { $value = null; if ( array_key_exists( $prop, $this->data[ $address_type ] ) ) { $value = $this->changes[ $address_type ][ $prop ] ?? $this->data[ $address_type ][ $prop ]; if ( 'view' === $context ) { /** * Filter: 'storeengine/order_get_[billing|shipping]_[prop]' * * Allow developers to change the returned value for any order address property. * * @param string $value The address property value. * @param Order $order The order object being read. * * @ignore Ignore from HookParser. */ $value = apply_filters( $this->get_hook_prefix( $address_type . '_' . $prop ), $value, $this ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound } } return $value; } /** * Get billing first name. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_first_name( string $context = 'view' ): ?string { return $this->get_address_prop( 'first_name', 'billing', $context ); } /** * Get billing last name. * * @param ?string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_billing_last_name( string $context = 'view' ): ?string { return $this->get_address_prop( 'last_name', 'billing', $context ); } /** * Get billing company. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_company( string $context = 'view' ): ?string { return $this->get_address_prop( 'company', 'billing', $context ); } /** * Get billing address line 1. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_address_1( string $context = 'view' ): ?string { return $this->get_address_prop( 'address_1', 'billing', $context ); } /** * Get billing address line 2. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_address_2( string $context = 'view' ): ?string { return $this->get_address_prop( 'address_2', 'billing', $context ); } /** * Get billing city. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_city( string $context = 'view' ): ?string { return $this->get_address_prop( 'city', 'billing', $context ); } /** * Get billing state. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_state( string $context = 'view' ): ?string { return $this->get_address_prop( 'state', 'billing', $context ); } /** * Get billing postcode. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_postcode( string $context = 'view' ): ?string { return $this->get_address_prop( 'postcode', 'billing', $context ); } /** * Get billing country. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_country( string $context = 'view' ): ?string { return $this->get_address_prop( 'country', 'billing', $context ); } /** * Get billing email. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_email( string $context = 'view' ): ?string { return $this->get_address_prop( 'email', 'billing', $context ); } /** * Get billing phone. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_billing_phone( string $context = 'view' ): ?string { return $this->get_address_prop( 'phone', 'billing', $context ); } /** * Get shipping first name. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_first_name( string $context = 'view' ): ?string { return $this->get_address_prop( 'first_name', 'shipping', $context ); } /** * Get shipping_last_name. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_last_name( string $context = 'view' ): ?string { return $this->get_address_prop( 'last_name', 'shipping', $context ); } /** * Get shipping company. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_company( string $context = 'view' ): ?string { return $this->get_address_prop( 'company', 'shipping', $context ); } /** * Get shipping address line 1. * * @param string $context What the value is for. Valid values are view and edit. * * @return? string */ public function get_shipping_address_1( string $context = 'view' ): ?string { return $this->get_address_prop( 'address_1', 'shipping', $context ); } /** * Get shipping address line 2. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_address_2( string $context = 'view' ): ?string { return $this->get_address_prop( 'address_2', 'shipping', $context ); } /** * Get shipping city. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_shipping_city( string $context = 'view' ): ?string { return $this->get_address_prop( 'city', 'shipping', $context ); } /** * Get shipping state. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_state( string $context = 'view' ): ?string { return $this->get_address_prop( 'state', 'shipping', $context ); } /** * Get shipping postcode. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_postcode( string $context = 'view' ): ?string { return $this->get_address_prop( 'postcode', 'shipping', $context ); } /** * Get shipping country. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_country( string $context = 'view' ): ?string { return $this->get_address_prop( 'country', 'shipping', $context ); } /** * Get shipping country. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_email( string $context = 'view' ): ?string { return $this->get_address_prop( 'email', 'shipping', $context ); } /** * Get shipping phone. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_shipping_phone( string $context = 'view' ): ?string { return $this->get_address_prop( 'phone', 'shipping', $context ); } /** * Get the payment method. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_payment_method( string $context = 'view' ) { return $this->get_prop( 'payment_method', $context ); } /** * Get payment method title. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_payment_method_title( string $context = 'view' ) { return $this->get_prop( 'payment_method_title', $context ); } /** * Get transaction id. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_transaction_id( string $context = 'view' ) { return $this->get_prop( 'transaction_id', $context ); } /** * Get customer ip address. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_ip_address( string $context = 'view' ) { return $this->get_prop( 'ip_address', $context ); } /** * Get customer user agent. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_user_agent( string $context = 'view' ) { return $this->get_prop( 'user_agent', $context ); } /** * Get created via. * * @param string $context What the value is for. Valid values are view and edit. * * @return ?string */ public function get_created_via( string $context = 'view' ) { return $this->get_prop( 'created_via', $context ); } /** * Get customer note. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_customer_note( string $context = 'view' ) { return $this->get_prop( 'customer_note', $context ); } /** * Get cart hash. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_cart_hash( string $context = 'view' ) { return $this->get_hash( $context ); } /** * Get cart hash. * * @param string $context What the value is for. Valid values are view and edit. * * @return string */ public function get_hash( string $context = 'view' ) { return $this->get_prop( 'hash', $context ); } /** * Returns the requested address in raw, non-formatted way. * Note: Merges raw data with get_prop data so changes are returned too. * * @param string $address_type Type of address; 'billing' or 'shipping'. * * @return array The stored address after filter. */ public function get_address( $address_type = 'billing' ) { /** * Filter: 'storeengine/get_order_address' * * Allow developers to change the returned value for an order's billing or shipping address. * * @param array $address_data The raw address data merged with the data from get_prop. * @param string $address_type Type of address; 'billing' or 'shipping'. */ return apply_filters( 'storeengine/get_order_address', array_merge( $this->data[ $address_type ], $this->get_prop( $address_type, 'view' ) ), $address_type, $this ); } /** * Get a formatted shipping address for the order. * * @return string */ public function get_shipping_address_map_url() { $address = $this->get_address( 'shipping' ); // Remove name and company before generate the Google Maps URL. unset( $address['first_name'], $address['last_name'], $address['company'], $address['phone'] ); $address = apply_filters( 'storeengine/shipping_address_map_url_parts', $address, $this ); return apply_filters( 'storeengine/shipping_address_map_url', 'https://maps.google.com/maps?&q=' . rawurlencode( implode( ', ', $address ) ) . '&z=16', $this ); } /** * Get a formatted billing full name. * * @return string */ public function get_formatted_billing_full_name() { /* translators: 1: first name 2: last name */ return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_billing_first_name(), $this->get_billing_last_name() ); } /** * Get a formatted shipping full name. * * @return string */ public function get_formatted_shipping_full_name() { /* translators: 1: first name 2: last name */ return sprintf( _x( '%1$s %2$s', 'full name', 'storeengine' ), $this->get_shipping_first_name(), $this->get_shipping_last_name() ); } /** * Get a formatted billing address for the order. * * @param string $empty_content Content to show if no address is present. * * @return string */ public function get_formatted_billing_address( $empty_content = '' ) { $raw_address = apply_filters( 'storeengine/order_formatted_billing_address', $this->get_address( 'billing' ), $this ); $address = Countries::init()->get_formatted_address( $raw_address ); /** * Filter orders formatted billing address. * * @param string $address Formatted billing address string. * @param array $raw_address Raw billing address. * @param Order $order Order data. */ return apply_filters( 'storeengine/order_get_formatted_billing_address', $address ? $address : $empty_content, $raw_address, $this ); } /** * Get a formatted shipping address for the order. * * @param string $empty_content Content to show if no address is present. * * @return string */ public function get_formatted_shipping_address( $empty_content = '' ) { $address = ''; $raw_address = $this->get_address( 'shipping' ); if ( $this->has_shipping_address() ) { $raw_address = apply_filters( 'storeengine/order_formatted_shipping_address', $raw_address, $this ); $address = Countries::init()->get_formatted_address( $raw_address ); } /** * Filter orders formatted shipping address. * * @param string $address Formatted shipping address string. * @param array $raw_address Raw shipping address. * @param Order $order Order data. */ return apply_filters( 'storeengine/order_get_formatted_shipping_address', $address ? $address : $empty_content, $raw_address, $this ); } /** * Returns true if the order has a billing address. * * @param string $context * * @return boolean */ public function has_billing_address( string $context = 'view' ): bool { return $this->get_billing_address_1( $context ) || $this->get_billing_address_2( $context ); } /** * Returns true if the order has a shipping address. * * @param string $context * * @return boolean */ public function has_shipping_address( string $context = 'view' ): bool { return $this->get_shipping_address_1( $context ) || $this->get_shipping_address_2( $context ); } /** * Gets information about whether stock was reduced. * * @param string $context What the value is for. Valid values are view and edit. * * @return bool */ public function get_order_stock_reduced( string $context = 'view' ): bool { return Formatting::string_to_bool( $this->get_prop( 'order_stock_reduced', $context ) ); } /** * Gets information about whether permissions were generated yet. * * @param string $context What the value is for. Valid values are view and edit. * * @return bool True if permissions were generated, false otherwise. */ public function get_download_permissions_granted( string $context = 'view' ): bool { return Formatting::string_to_bool( $this->get_prop( 'download_permissions_granted', $context ) ); } public function get_auto_complete_digital_order( string $context = 'view' ): bool { return Formatting::string_to_bool( $this->get_prop( 'auto_complete_digital_order', $context ) ); } /** * Whether email have been sent for this order. * * @param string $context What the value is for. Valid values are view and edit. * * @return bool */ public function get_new_order_email_sent( string $context = 'view' ): bool { return Formatting::string_to_bool( $this->get_prop( 'new_order_email_sent', $context ) ); } /** * Gets information about whether sales were recorded. * * @param string $context What the value is for. Valid values are view and edit. * * @return bool True if sales were recorded, false otherwise. */ public function get_recorded_sales( string $context = 'view' ): bool { return Formatting::string_to_bool( $this->get_prop( 'recorded_sales', $context ) ); } /** * @param string $context * * @return null|StoreengineDatetime */ public function get_order_placed_date_gmt( string $context = 'view' ): ?StoreengineDatetime { return $this->get_prop( 'order_placed_date_gmt', $context ); } /** * @param string $context * * @return null|StoreengineDatetime */ public function get_order_placed_date( string $context = 'view' ): ?StoreengineDatetime { return $this->get_prop( 'order_placed_date', $context ); } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- | | Functions for setting order data. These should not update anything in the | database itself and should only change what is stored in the class | object. However, for backwards compatibility pre 3.0.0 some of these | setters may handle both. | */ /** * Sets a prop for a setter method. * * @param string $prop Name of prop to set. * @param string $address_type Type of address; 'billing' or 'shipping'. * @param ?string $value Value of the prop. */ protected function set_address_prop( $prop, string $address_type, ?string $value ) { if ( isset( $this->data[ $address_type ] ) && array_key_exists( $prop, $this->data[ $address_type ] ) ) { if ( true === $this->object_read ) { if ( $value !== $this->data[ $address_type ][ $prop ] || ( isset( $this->changes[ $address_type ] ) && array_key_exists( $prop, $this->changes[ $address_type ] ) ) ) { $this->changes[ $address_type ][ $prop ] = $value; } } else { $this->data[ $address_type ][ $prop ] = $value; } } } /** * Setter for billing address, expects the $address parameter to be key value pairs for individual address props. * * @param array $address Address to set. * * @return void */ public function set_billing_address( array $address ) { foreach ( $address as $key => $value ) { $this->set_address_prop( $key, 'billing', $value ); } } /** * Shortcut for calling set_billing_address. * * 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. * * @param array $address Address to set. * * @return void */ public function set_billing( array $address ) { $this->set_billing_address( $address ); } /** * Setter for shipping address, expects the $address parameter to be key value pairs for individual address props. * * @param array $address Address to set. * * @return void */ public function set_shipping_address( array $address ) { foreach ( $address as $key => $value ) { $this->set_address_prop( $key, 'shipping', $value ); } } /** * 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. * * @param array $address Address to set. * * @return void */ public function set_shipping( array $address ) { $this->set_shipping_address( $address ); } /** * Set billing first name. * * @param ?string $value Billing first name. */ public function set_billing_first_name( ?string $value ) { $this->set_address_prop( 'first_name', 'billing', $value ); } /** * Set billing last name. * * @param ?string $value Billing last name. */ public function set_billing_last_name( ?string $value ) { $this->set_address_prop( 'last_name', 'billing', $value ); } /** * Set billing company. * * @param ?string $value Billing company. */ public function set_billing_company( ?string $value ) { $this->set_address_prop( 'company', 'billing', $value ); } /** * Set billing address line 1. * * @param ?string $value Billing address line 1. */ public function set_billing_address_1( ?string $value ) { $this->set_address_prop( 'address_1', 'billing', $value ); } /** * Set billing address line 2. * * @param ?string $value Billing address line 2. */ public function set_billing_address_2( ?string $value ) { $this->set_address_prop( 'address_2', 'billing', $value ); } /** * Set billing city. * * @param ?string $value Billing city. */ public function set_billing_city( ?string $value ) { $this->set_address_prop( 'city', 'billing', $value ); } /** * Set billing state. * * @param ?string $value Billing state. */ public function set_billing_state( ?string $value ) { $this->set_address_prop( 'state', 'billing', $value ); } /** * Set billing postcode. * * @param ?string $value Billing postcode. */ public function set_billing_postcode( ?string $value ) { $this->set_address_prop( 'postcode', 'billing', $value ); } /** * Set billing country. * * @param ?string $value Billing country. */ public function set_billing_country( ?string $value ) { $this->set_address_prop( 'country', 'billing', $value ); } /** * Maybe set empty billing email to that of the user who owns the order. */ protected function maybe_set_user_billing_email() { $user = $this->get_user(); if ( ! $this->get_billing_email() && $user ) { try { $this->set_billing_email( $user->user_email ); } catch ( Exception $e ) { unset( $e ); } } } /** * Set billing email. * * @param ?string $value Billing email. * * @throws StoreEngineException */ public function set_billing_email( ?string $value = '' ) { $value = $value ?? ''; if ( $value && ! is_email( $value ) ) { $this->error( 'order_invalid_billing_email', __( 'Invalid billing email address', 'storeengine' ) ); } $this->set_address_prop( 'email', 'billing', sanitize_email( $value ) ); } /** * Set billing phone. * * @param ?string $value Billing phone. */ public function set_billing_phone( ?string $value ) { $this->set_address_prop( 'phone', 'billing', $value ); } /** * Set shipping first name. * * @param ?string $value Shipping first name. */ public function set_shipping_first_name( ?string $value ) { $this->set_address_prop( 'first_name', 'shipping', $value ); } /** * Set shipping last name. * * @param ?string $value Shipping last name. */ public function set_shipping_last_name( ?string $value ) { $this->set_address_prop( 'last_name', 'shipping', $value ); } /** * Set shipping company. * * @param ?string $value Shipping company. */ public function set_shipping_company( ?string $value ) { $this->set_address_prop( 'company', 'shipping', $value ); } /** * Set shipping address line 1. * * @param ?string $value Shipping address line 1. */ public function set_shipping_address_1( ?string $value ) { $this->set_address_prop( 'address_1', 'shipping', $value ); } /** * Set shipping address line 2. * * @param ?string $value Shipping address line 2. */ public function set_shipping_address_2( ?string $value ) { $this->set_address_prop( 'address_2', 'shipping', $value ); } /** * Set shipping city. * * @param ?string $value Shipping city. */ public function set_shipping_city( ?string $value ) { $this->set_address_prop( 'city', 'shipping', $value ); } /** * Set shipping state. * * @param ?string $value Shipping state. */ public function set_shipping_state( ?string $value ) { $this->set_address_prop( 'state', 'shipping', $value ); } /** * Set shipping postcode. * * @param ?string $value Shipping postcode. */ public function set_shipping_postcode( ?string $value ) { $this->set_address_prop( 'postcode', 'shipping', $value ); } /** * Set shipping country. * * @param ?string $value Shipping country. */ public function set_shipping_country( ?string $value ) { $this->set_address_prop( 'country', 'shipping', $value ); } /** * Set shipping phone. * * @param ?string $value Shipping phone. */ public function set_shipping_phone( ?string $value ) { $this->set_address_prop( 'phone', 'shipping', $value ); } /** * Set shipping phone. * * @param ?string $value Shipping phone. * * @throws StoreEngineException */ public function set_shipping_email( ?string $value ) { $value = $value ?? ''; if ( $value && ! is_email( $value ) ) { $this->error( 'order_invalid_shipping_email', __( 'Invalid shipping email address', 'storeengine' ) ); } $this->set_address_prop( 'email', 'shipping', sanitize_email( $value ) ); } /** * Set the payment method. * * @param string|PaymentGateway $payment_method Supports a payment-gateway object for bw compatibility with < 3.0. */ public function set_payment_method( $payment_method = '' ) { if ( is_object( $payment_method ) ) { $this->set_payment_method( $payment_method->id ); $this->set_payment_method_title( $payment_method->get_title() ); } elseif ( '' === $payment_method ) { $this->set_prop( 'payment_method', '' ); $this->set_prop( 'payment_method_title', '' ); } else { $this->set_prop( 'payment_method', $payment_method ); } } /** * Set payment method title. * * @param ?string $value Payment method title. */ public function set_payment_method_title( ?string $value ) { $this->set_prop( 'payment_method_title', $value ); } /** * Check if the subscription has a payment gateway. * * @return bool */ public function has_payment_gateway(): bool { return (bool) Helper::get_payment_gateway_by_order( $this ); } /** * Set transaction id. * * @param ?string $value Transaction id. */ public function set_transaction_id( ?string $value ) { $this->set_prop( 'transaction_id', $value ); } /** * Set customer ip address. * * @param ?string $value Customer ip address. */ public function set_ip_address( ?string $value ) { $this->set_prop( 'ip_address', $value ); } /** * Set customer user agent. * * @param ?string $value Customer user agent. */ public function set_user_agent( ?string $value ) { $this->set_prop( 'user_agent', $value ); } /** * Set created via. * * @param ?string $value Created via. */ public function set_created_via( ?string $value ) { $this->set_prop( 'created_via', $value ); } /** * Set customer note. * * @param ?string $value Customer note. */ public function set_customer_note( ?string $value ) { $this->set_prop( 'customer_note', $value ); } /** * Set cart hash. * * @param string $value Cart hash. */ public function set_cart_hash( $value ) { $this->set_hash( $value ); } /** * Set cart hash. * * @param string $value Cart hash. */ public function set_hash( $value ) { $this->set_prop( 'hash', $value ); } /** * Stores information about whether stock was reduced. * * @param bool|string $value True if stock was reduced, false if not. * * @return void */ public function set_order_stock_reduced( $value ) { $this->set_prop( 'order_stock_reduced', Formatting::string_to_bool( $value ) ); } /** * Stores information about whether permissions were generated yet. * * @param bool|string $value True if permissions were generated, false if not. * * @return void */ public function set_download_permissions_granted( $value ) { $this->set_prop( 'download_permissions_granted', Formatting::string_to_bool( $value ) ); } public function set_auto_complete_digital_order( $value ) { $this->set_prop( 'auto_complete_digital_order', Formatting::string_to_bool( $value ) ); } /** * Stores information about whether email was sent. * * @param bool|string $value True if email was sent, false if not. * * @return void */ public function set_new_order_email_sent( $value ) { $this->set_prop( 'new_order_email_sent', Formatting::string_to_bool( $value ) ); } /** * Stores information about whether sales were recorded. * * @param bool|string $value True if sales were recorded, false if not. * * @return void */ public function set_recorded_sales( $value ) { $this->set_prop( 'recorded_sales', Formatting::string_to_bool( $value ) ); } /* |-------------------------------------------------------------------------- | Conditionals |-------------------------------------------------------------------------- | | Checks if a condition is true or false. | */ /** * Check if an order key is valid. * * @param string $key Order key. * * @return bool */ public function key_is_valid( $key ) { return hash_equals( $this->get_order_key(), $key ); } /** * See if order matches cart_hash. * * @param string $cart_hash Cart hash. * * @return bool */ public function has_cart_hash( $cart_hash = '' ) { return hash_equals( $this->get_cart_hash(), $cart_hash ); } /** * Checks if an order can be edited, specifically for use on the Edit Order screen. * * @return bool */ public function is_editable(): bool { $editable_statuses = [ OrderStatus::PAYMENT_PENDING, OrderStatus::ON_HOLD, OrderStatus::AUTO_DRAFT, ]; /** * Filter to check if an order is editable. * * @param bool $is_editable Is the order editable. * @param Order $order Order object. */ return apply_filters( 'storeengine/order/is_editable', in_array( $this->get_status(), $editable_statuses, true ), $this ); } /** * Checks if product download is permitted. * * @return bool */ public function is_download_permitted(): bool { /** * Filter to check if an order is downloadable. * * @param bool $is_download_permitted Is the order downloadable. * @param Order $this Order object. */ 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 ); } /** * Checks if an order needs display the shipping address, based on shipping method. * * @return bool */ public function needs_shipping_address(): bool { if ( 'no' === get_option( 'storeengine/calc_shipping' ) ) { return false; } $hide = apply_filters( 'storeengine/order_hide_shipping_address', [ 'local_pickup' ], $this ); $needs_address = false; foreach ( $this->get_shipping_methods() as $shipping_method ) { $shipping_method_id = $shipping_method->get_method_id(); if ( ! in_array( $shipping_method_id, $hide, true ) ) { $needs_address = true; break; } } return apply_filters( 'storeengine/order_needs_shipping_address', $needs_address, $hide, $this ); } /** * Returns true if the order contains a downloadable product. * * @return bool */ public function has_downloadable_item() { foreach ( $this->get_items() as $item ) { if ( $item->is_type( 'line_item' ) ) { $product = $item->get_product(); if ( $product && $product->has_file() ) { return true; } } } return false; } /** * Get downloads from all line items for this order. * * @return array */ public function get_downloadable_items(): array { $downloads = []; foreach ( $this->get_items() as $item ) { if ( ! is_object( $item ) ) { continue; } // Check item refunds. $refunded_qty = abs( $this->get_qty_refunded_for_item( $item->get_id() ) ); if ( $refunded_qty && $item->get_quantity() === $refunded_qty ) { continue; } if ( $item->is_type( 'line_item' ) ) { $item_downloads = $item->get_item_downloads(); $product = $item->get_product(); if ( $product && $item_downloads ) { foreach ( $item_downloads as $file ) { $downloads[] = [ 'download_url' => $file['download_url'], 'download_id' => $file['id'], 'product_id' => $product->get_id(), 'product_name' => $product->get_name(), 'product_url' => $product->is_visible() ? $product->get_permalink() : '', 'download_name' => $file['name'], 'order_id' => $this->get_id(), 'order_key' => $this->get_order_key(), 'downloads_remaining' => $file['downloads_remaining'], 'access_expires' => $file['access_expires'], 'file' => [ 'name' => $file['name'], 'file' => $file['file'], ], ]; } } } } return apply_filters( 'storeengine/order_get_downloadable_items', $downloads, $this ); } /** * Checks if an order needs payment, based on status and order total. * * @return bool */ public function needs_payment(): bool { /** * Filter the valid order statuses for payment. * * @param array $valid_order_statuses Array of valid order statuses for payment. * @param Order $order Order object. */ $paid_status = $this->get_paid_status(); $valid_unpaid_statuses = [ 'unpaid', 'failed' ]; $valid_order_statuses = apply_filters( 'storeengine/order/valid_unpaid_statuses', $valid_unpaid_statuses, $this ); $valid_statuses_for_payment = [ OrderStatus::PAYMENT_PENDING, OrderStatus::PAYMENT_FAILED ]; $valid_statuses_for_payment = apply_filters( 'storeengine/order/valid_statuses_for_payment', $valid_statuses_for_payment, $this ); $need_payment = ( in_array( $paid_status, $valid_order_statuses, true ) && in_array( $this->get_status(), $valid_statuses_for_payment, true ) && $this->get_total() > 0 ); return apply_filters( 'storeengine/order_needs_payment', $need_payment, $this, $valid_order_statuses ); } /** * See if the order needs processing before it can be completed. * * Orders which only contain virtual, downloadable items do not need admin * intervention. * * Uses a transient so these calls are not repeated multiple times, and because * once the order is processed this code/transient does not need to persist. * * @return bool */ public function needs_processing(): bool { $transient_name = 'storeengine/order_' . $this->get_id() . '_needs_processing'; $needs_processing = get_transient( $transient_name ); if ( false === $needs_processing ) { $needs_processing = 0; if ( count( $this->get_items() ) > 0 ) { foreach ( $this->get_items() as $item ) { if ( $item->is_type( 'line_item' ) ) { /** @var $product AbstractProduct */ $product = $item->get_product(); if ( ! $product ) { continue; } $virtual_downloadable_item = $product->is_downloadable() && $product->is_virtual(); if ( apply_filters( 'storeengine/order/item_needs_processing', ! $virtual_downloadable_item, $product, $this->get_id() ) ) { $needs_processing = 1; break; } } } } set_transient( $transient_name, $needs_processing, DAY_IN_SECONDS ); } return 1 === absint( $needs_processing ); } /* |-------------------------------------------------------------------------- | URLs and Endpoints |-------------------------------------------------------------------------- */ /** * 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. * * @param bool $on_checkout If on checkout. * * @return string */ public function get_checkout_payment_url( bool $on_checkout = false ): string { $pay_url = Helper::get_endpoint_url( 'order-pay', $this->get_id(), Helper::get_checkout_url() ); if ( $on_checkout ) { $pay_url = add_query_arg( 'key', $this->get_order_key(), $pay_url ); } else { $pay_url = add_query_arg( [ 'pay_for_order' => 'true', 'key' => $this->get_order_key(), ], $pay_url ); } return apply_filters( 'storeengine/get_checkout_payment_url', $pay_url, $this ); } /** * Generates a URL for the thanks page (order received). * * @return string */ public function get_checkout_order_received_url(): string { $order_received_url = add_query_arg( 'order_hash', $this->get_order_key(), Helper::get_thankyou_page_url() ); return apply_filters( 'storeengine/order/get_checkout_order_received_url', $order_received_url, $this ); } /** * Generates a URL so that a customer can cancel their (unpaid - pending) order. * * @param string $redirect Redirect URL. * * @return string */ public function get_cancel_order_url( string $redirect = '' ): string { /** * Filter the URL to cancel the order in the frontend. * * @param string $url * @param Order $order Order data. * @param string $redirect Redirect URL. */ return apply_filters( 'storeengine/order/get_cancel_order_url', wp_nonce_url( add_query_arg( [ 'cancel_order' => 'true', 'order' => $this->get_order_key(), 'order_id' => $this->get_id(), 'redirect' => $redirect, ], $this->get_cancel_endpoint() ), 'storeengine-cancel_order' ), $this, $redirect ); } /** * Generates a raw (unescaped) cancel-order URL for use by payment gateways. * * @param string $redirect Redirect URL. * * @return string The unescaped cancel-order URL. */ public function get_cancel_order_url_raw( string $redirect = '' ): string { /** * Filter the raw URL to cancel the order in the frontend. * * @param string $url * @param Order $order Order data. * @param string $redirect Redirect URL. */ return apply_filters( 'storeengine/order/get_cancel_order_url_raw', add_query_arg( [ 'cancel_order' => 'true', 'order' => $this->get_order_key(), 'order_id' => $this->get_id(), 'redirect' => $redirect, '_wpnonce' => wp_create_nonce( 'storeengine-cancel_order' ), ], $this->get_cancel_endpoint() ), $this, $redirect ); } /** * Helper method to return the cancel endpoint. * * @return string the cancel endpoint; either the cart page or the home page. */ public function get_cancel_endpoint(): string { $cancel_endpoint = Helper::get_cart_url(); if ( ! $cancel_endpoint ) { $cancel_endpoint = home_url(); } if ( false === strpos( $cancel_endpoint, '?' ) ) { $cancel_endpoint = trailingslashit( $cancel_endpoint ); } return $cancel_endpoint; } /** * Generates a URL to view an order from the myaccount page. * * @return string */ public function get_view_order_url(): string { return apply_filters( 'storeengine/order/get_view_url', Helper::get_account_endpoint_url( 'orders', $this->get_id() ), $this ); } /** * Get the URL to edit the order in the backend. * * @return string */ public function get_edit_order_url(): string { $edit_url = admin_url( 'admin.php?page=storeengine-orders&id=' . $this->get_id() . '&action=edit' ); /** * Filter the URL to edit the order in the backend. */ return apply_filters( 'storeengine/order/get_edit_url', $edit_url, $this ); } /* |-------------------------------------------------------------------------- | Order notes. |-------------------------------------------------------------------------- */ /** * Adds a note (comment) to the order. Order must exist. * * @param string $note Note to add. * @param int|string $is_customer_note Is this a note for the customer?. * @param bool $added_by_user Was the note added by a user?. * * @return int|false Comment ID. */ public function add_order_note( string $note, $is_customer_note = 0, bool $added_by_user = false ) { if ( ! $this->get_id() ) { return 0; } $is_customer_note = absint( $is_customer_note ); // @TODO edit_shop_orders cap doesn't exists in storeengine. if ( is_user_logged_in() && current_user_can( 'edit_shop_orders', $this->get_id() ) && $added_by_user ) { $user = get_user_by( 'id', get_current_user_id() ); $comment_author = $user->display_name; $comment_author_email = $user->user_email; } else { $comment_author = _x( 'StoreEngine', 'System Comment Author', 'storeengine' ); $comment_author_email = strtolower( $comment_author ) . '@' . wp_parse_url( get_site_url(), PHP_URL_HOST ); $comment_author_email = sanitize_email( $comment_author_email ); } $commentdata = apply_filters( 'storeengine/new_order_note_data', [ 'comment_post_ID' => $this->get_id(), 'comment_author' => $comment_author, 'comment_author_email' => $comment_author_email, 'comment_author_url' => '', 'comment_content' => $note, 'comment_agent' => 'StoreEngine', 'comment_type' => 'order_note', 'comment_parent' => 0, 'comment_approved' => 1, ], [ 'order_id' => $this->get_id(), 'is_customer_note' => $is_customer_note, ] ); $comment_id = wp_insert_comment( $commentdata ); if ( ! $comment_id ) { return false; } if ( $is_customer_note ) { add_comment_meta( $comment_id, 'is_customer_note', 1 ); /** * Action hook fired after an order note is added for the customer. * * @param string $note Comment data. * @param Order $this Comment data. */ do_action( 'storeengine/order/new_customer_note', $note, $this ); } /** * Action hook fired after an order note is added. * * @param int $comment_id Order note ID. * @param Order $this Order object. */ do_action( 'storeengine/order/note_added', $comment_id, $this ); return $comment_id; } /** * Add an order note for status transition * * @param string $note Note to be added giving status transition from and to details. * @param bool $transition Details of the status transition. * * @return int Comment ID. * @uses self::add_order_note() */ protected function add_status_transition_note( $note, $transition ) { return $this->add_order_note( trim( $transition['note'] . ' ' . $note ), 0, $transition['manual'] ); } /** * List order notes (public) for the customer. * * @return WP_Comment[] */ public function get_customer_order_notes(): array { return $this->get_order_notes( 'customer' ); } /** * List order notes. * * @param string $customer_notes switch for customer (public) notes or internal (admin) notes. Default all notes. * @param bool $ids * * @return WP_Comment[] */ public function get_order_notes( string $customer_notes = '', bool $ids = false ): array { $notes = []; if ( ! $this->get_id() ) { return $notes; } $args = [ 'post_id' => $this->get_id(), 'orderby' => 'comment_ID', 'order' => 'DESC', 'approve' => 'approve', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'relation' => 'AND', ], ]; // type->order_note & author->StoreEngine conditions are added via filter below. if ( $ids ) { $args['fields'] = 'ids'; } if ( 'customer' === $customer_notes ) { $args['meta_query'][] = [ 'key' => 'is_customer_note', 'value' => 1, 'compare' => '=', 'type' => 'UNSIGNED', ]; } elseif ( 'internal' === $customer_notes ) { $args['meta_query'][] = [ 'key' => 'is_customer_note', 'compare' => 'NOT EXISTS', ]; } remove_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] ); add_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] ); $comments = get_comments( $args ); foreach ( $comments as $comment ) { $comment->comment_content = make_clickable( $comment->comment_content ); $notes[] = $comment; } remove_filter( 'comments_clauses', [ Hooks::class, 'include_order_comments' ] ); add_filter( 'comments_clauses', [ Hooks::class, 'exclude_order_comments' ] ); return array_filter( array_map( [ __CLASS__, 'get_order_note' ], $notes ) ); } /** * Get an order note. * * @param int|WP_Comment $data Note ID (or WP_Comment instance for internal use only). * * @return stdClass|null Object with order note details or null when does not exists. * @throws StoreEngineException */ public static function get_order_note( $data ) { if ( is_numeric( $data ) ) { $data = get_comment( $data ); } if ( ! is_a( $data, 'WP_Comment' ) ) { return null; } // @TODO use OrderNote object. return (object) apply_filters( 'storeengine/order/get_order_note', [ 'id' => (int) $data->comment_ID, 'date_created' => $data->comment_date_gmt, //'date_created' => Formatting::string_to_datetime( $data->comment_date ), 'content' => $data->comment_content, 'customer_note' => (bool) get_comment_meta( $data->comment_ID, 'is_customer_note', true ), 'added_by' => __( 'StoreEngine', 'storeengine' ) === $data->comment_author ? 'system' : $data->comment_author, 'order_id' => absint( $data->comment_post_ID ), ], $data ); } /** * Delete an order note. * * @param int $note_id Order note. * * @return bool True on success, false on failure. * @throws StoreEngineException */ public static function delete_order_note( int $note_id ): bool { $note = self::get_order_note( $note_id ); if ( $note && wp_delete_comment( $note_id, true ) ) { /** * Action hook fired after an order note is deleted. * * @param int $note_id Order note ID. * @param stdClass $note Object with the deleted order note details. */ do_action( 'storeengine/order/note_deleted', $note_id, $note ); return true; } return false; } /* |-------------------------------------------------------------------------- | Refunds |-------------------------------------------------------------------------- */ /** * Get order refunds. * * @return Refund[] of Order_Refund objects * @throws StoreEngineException */ public function get_refunds(): array { $cache_key = Caching::get_cache_prefix( 'orders' ) . 'refunds' . $this->get_id(); $ids = wp_cache_get( $cache_key, $this->cache_group ); $refunds = []; if ( false === $ids || ! is_array( $ids ) ) { $query = ( new Refund() )->query(); // @TODO cache properly to prevent 2x query while creating refund object. // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class. $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 // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared in refund class. if ( ! $results ) { return $refunds; } $ids = array_unique( array_filter( array_map( 'absint', array_column( $results, 'o_id' ) ) ) ); wp_cache_set( $cache_key, $ids, $this->cache_group ); } foreach ( $ids as $id ) { $refunds[] = new Refund( $id ); } return $refunds; } /** * Get amount already refunded. * * @param bool $refresh * * @return float|int */ public function get_total_refunded( bool $refresh = false ) { $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data && ! $refresh ) { return (float) $cached_data; } // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded. $total_refunded = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT SUM( total_amount ) FROM $this->table WHERE type = %s AND parent_order_id = %d;", 'refund_order', $this->get_id() ) ) ?? 0; // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $this->table is hardcoded. $total_refunded = - 1 * floatval( $total_refunded ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Get the total tax refunded. * * @return float */ public function get_total_tax_refunded(): float { $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_tax_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared. $total_refunded = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT SUM( order_item_meta.meta_value ) FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d ) 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' ) WHERE order_item_meta.order_item_id = order_items.order_item_id AND order_item_meta.meta_key IN ('tax_amount', 'shipping_tax_amount')", $this->get_id() ) ) ?? 0; // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared $total_refunded = floatval( $total_refunded ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Get the total shipping refunded. * * @return float */ public function get_total_shipping_refunded() { $cache_key = Caching::get_cache_prefix( 'orders' ) . 'total_shipping_refunded' . $this->get_id(); $cached_data = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $cached_data ) { return $cached_data; } // phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared $total_refunded = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT SUM( order_item_meta.meta_value ) FROM {$this->wpdb->prefix}storeengine_order_item_meta AS order_item_meta INNER JOIN $this->table AS orders ON ( orders.type = 'shop_order_refund' AND orders.parent_order_id = %d ) 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' ) WHERE order_item_meta.order_item_id = order_items.order_item_id AND order_item_meta.meta_key IN ('cost')", $this->get_id() ) ) ?? 0; // phpcs:enable PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- query prepared $total_refunded = floatval( $total_refunded ); wp_cache_set( $cache_key, $total_refunded, $this->cache_group ); return $total_refunded; } /** * Gets the count of order items of a certain type that have been refunded. * * @param string $item_type Item type. * * @return int */ public function get_item_count_refunded( $item_type = '' ): int { if ( empty( $item_type ) ) { $item_type = [ 'line_item' ]; } if ( ! is_array( $item_type ) ) { $item_type = [ $item_type ]; } $count = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $count += abs( $refunded_item->get_quantity() ); } } return apply_filters( 'storeengine/get_item_count_refunded', $count, $item_type, $this ); } /** * Get the total number of items refunded. * * @param string $item_type Type of the item we're checking, if not a line_item. * * @return int */ public function get_total_qty_refunded( $item_type = 'line_item' ) { $qty = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $qty += $refunded_item->get_quantity(); } } return $qty; } /** * Get the refunded amount for a line item. * * @param int $item_id ID of the item we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * * @return int */ public function get_qty_refunded_for_item( $item_id, $item_type = 'line_item' ) { $qty = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { $qty += $refunded_item->get_quantity(); } } } return $qty; } /** * Get the refunded amount for a line item. * * @param int $item_id ID of the item we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * * @return int */ public function get_total_refunded_for_item( $item_id, $item_type = 'line_item' ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { if ( absint( $refunded_item->get_meta( '_refunded_item_id' ) ) === $item_id ) { $total += $refunded_item->get_total(); } } } return $total * - 1; } /** * Get the refunded tax amount for a line item. * * @param int $item_id ID of the item we're checking. * @param int $tax_id ID of the tax we're checking. * @param string $item_type Type of the item we're checking, if not a line_item. * * @return double */ public function get_tax_refunded_for_item( $item_id, $tax_id, $item_type = 'line_item' ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( $item_type ) as $refunded_item ) { $refunded_item_id = (int) $refunded_item->get_meta( '_refunded_item_id' ); if ( $refunded_item_id === $item_id ) { $taxes = $refunded_item->get_taxes(); // Add to total. $total += isset( $taxes['total'][ $tax_id ] ) ? (float) $taxes['total'][ $tax_id ] : 0; break; } } } return Formatting::round_tax_total( $total ) * - 1; } /** * Get total tax refunded by rate ID. * * @param int $rate_id Rate ID. * * @return float */ public function get_total_tax_refunded_by_rate_id( $rate_id ) { $total = 0; foreach ( $this->get_refunds() as $refund ) { foreach ( $refund->get_items( 'tax' ) as $refunded_item ) { if ( absint( $refunded_item->get_rate_id() ) === $rate_id ) { $total += abs( $refunded_item->get_tax_total() ) + abs( $refunded_item->get_shipping_tax_total() ); } } } return $total; } /** * How much money is left to refund? * * @return float */ public function get_remaining_refund_amount(): float { return (float) Formatting::format_decimal( $this->get_total() - $this->get_total_refunded(), Formatting::get_price_decimals() ); } /** * How many items are left to refund? * * @return int */ public function get_remaining_refund_items() { return absint( $this->get_item_count() - $this->get_item_count_refunded() ); } /** * Add total row for the payment method. * * @param array $total_rows Total rows. * @param string $tax_display Tax to display. */ protected function add_order_item_totals_payment_method_row( array &$total_rows ) { if ( $this->get_total() > 0 && $this->get_payment_method_title() ) { $total_rows['payment_method'] = [ 'type' => 'payment_method', 'label' => __( 'Payment method:', 'storeengine' ), 'value' => $this->get_payment_method_to_display( 'customer' ), ]; } } /** * Add total row for refunds. * * @param array $total_rows Total rows. * @param string $tax_display Tax to display. */ protected function add_order_item_totals_refund_rows( &$total_rows, $tax_display ) { $refunds = $this->get_refunds(); if ( $refunds ) { foreach ( $refunds as $id => $refund ) { $reason = trim( $refund->get_reason() ); if ( strlen( $reason ) > 0 ) { $reason = "
$reason"; } $total_rows[ 'refund_' . $id ] = [ 'type' => 'refund', 'label' => __( 'Refund', 'storeengine' ) . ':', 'value' => Formatting::price( $refund->get_total_amount(), [ 'currency' => $this->get_currency() ] ) . $reason, ]; } } } /** * Get totals for display on pages and in emails. * * @param string $tax_display Tax to display. * * @return array */ public function get_order_item_totals( $tax_display = '' ) { $tax_display = $tax_display ? $tax_display : Helper::get_settings( 'tax_display_cart' ); $total_rows = []; $this->add_order_item_totals_subtotal_row( $total_rows, $tax_display ); $this->add_order_item_totals_discount_row( $total_rows, $tax_display ); $this->add_order_item_totals_shipping_row( $total_rows, $tax_display ); $this->add_order_item_totals_fee_rows( $total_rows, $tax_display ); $this->add_order_item_totals_tax_rows( $total_rows, $tax_display ); $this->add_order_item_totals_refund_rows( $total_rows, $tax_display ); $this->add_order_item_totals_total_row( $total_rows, $tax_display ); $this->add_order_item_totals_payment_method_row( $total_rows, $tax_display ); return apply_filters( 'storeengine/get_order_item_totals', $total_rows, $this, $tax_display ); } /** * Check if order has been created via admin, checkout, or in another way. * * @param string $modus Way of creating the order to test for. * * @return bool */ public function is_created_via( $modus ) { return apply_filters( 'storeengine/order_is_created_via', $modus === $this->get_created_via(), $this, $modus ); } /** * Indicates that regular orders have an associated Cost of Goods Sold value. * 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)- * * @return bool Always true. */ public function has_cogs(): bool { return true; } // ----------------------- /** * Coupons array. * * @var OrderItemCoupon[] */ protected array $coupons = []; /** * Determine how the payment method should be displayed for a subscription. * * @param string $context The context the payment method is being displayed in. Can be 'admin' or 'customer'. Default 'admin'. */ public function get_payment_method_to_display( string $context = 'admin' ) { $is_unknown = ! $this->get_payment_method() || 'other' === $this->get_payment_method(); $payment_method_to_display = $this->get_payment_method_title(); if ( ! $is_unknown && $payment_method_to_display ) { $card_info = $this->get_payment_card_info(); if ( isset( $card_info['last4'] ) && $card_info['last4'] ) { $payment_method_to_display .= sprintf( // translators: %1$s: Payment method title. %2$s: Last 4 digits of the card. _x('%1$s - %2$s', 'Card info with payment method name', 'storeengine' ), $payment_method_to_display, $card_info['last4'] ); } } elseif ( false !== ( $payment_gateway = Helper::get_payment_gateway_by_order( $this ) ) ) { $payment_method_to_display = $payment_gateway->get_title(); } else { // Fallback to the title of the payment method when the order was created $payment_method_to_display = ''; } if ( 'customer' === $context ) { if ( $payment_method_to_display ) { // translators: %s: payment method. $payment_method_to_display = sprintf( __( 'Via %s', 'storeengine' ), $payment_method_to_display ); } $payment_method_to_display = PaymentUtil::maybe_display_my_payment_method( $payment_method_to_display, $this ); } return apply_filters( "storeengine/{$this->object_type}/payment_method_to_display", $payment_method_to_display, $this, $context ); } /** * @param int $customer_id * @param null $deprecated * @param bool $create * * @return $this|false|Order */ public function get_recent_draft_order( int $customer_id = 0, $deprecated = null, bool $create = true ) { $cart_hash = Helper::get_cart_hash_from_cookie(); if ( 0 === $customer_id ) { $customer_id = get_current_user_id(); } if ( ! $cart_hash && ! $customer_id ) { return $this; } try { $cache_key = 'order:draft:' . $cart_hash; $id = wp_cache_get( $cache_key, $this->cache_group ); if ( false !== $id && false !== wp_cache_get( $id, $this->cache_group ) ) { $this->set_id( $id ); $this->read(); return $this; } $data = $this->read_db_data( [ $cart_hash, $customer_id ], 'cart_hash' ); wp_cache_set( $cache_key, $data['id'], $this->cache_group ); wp_cache_set( $data['id'], $data, $this->cache_group ); $this->set_id( $data['id'] ); $this->read(); return $this; } catch ( Exception $e ) { if ( 404 !== $e->getCode() ) { Helper::log_error( $e ); } } if ( $create ) { return self::create_draft_order( [ 'cart_hash' => $cart_hash, 'customer_id' => $customer_id, ] ); } return false; } public static function create_draft_order( array $args = [] ): Order { $args = wp_parse_args( $args, [ 'customer_id' => get_current_user_id(), 'ip_address' => Helper::get_user_ip(), 'user_agent' => Helper::get_user_agent(), 'cart_hash' => Helper::get_cart_hash_from_cookie(), 'prices_include_tax' => TaxUtil::prices_include_tax(), ] ); $order = new self(); $order->set_props( $args ); $order->set_prop( 'status', OrderStatus::DRAFT ); $order->save(); if ( $order->get_id() ) { $cache_key = 'order:draft:' . Helper::get_cart_hash_from_cookie(); wp_cache_set( $cache_key, $order->get_id(), 'storeengine_orders' ); } return $order; } public function has_address( string $context = 'view' ): bool { return $this->has_shipping_address( $context ) || $this->has_billing_address( $context ); } /** * @return DownloadPermission[] */ public function get_downloadable_permissions(): array { return Helper::get_download_permissions_by_order_id( $this->get_id() ); } public function get_tax_amount( string $context = 'view' ) { return $this->get_total_tax( $context ); } public function get_total_amount( string $context = 'view' ) { return $this->get_total( $context ); } public function set_total_amount( $amount ) { $this->set_total( $amount ); } public function set_order_placed_date_gmt( $value = null ) { if ( null === $value ) { $value = current_time( 'mysql', 1 ); } $this->set_date_prop( 'order_placed_date_gmt', $value ); } /** * @param $value * * @return void * @see get_date_from_gmt can be used. * */ public function set_order_placed_date( $value = null ) { if ( null === $value ) { $value = current_time( 'mysql', false ); } $this->set_date_prop( 'order_placed_date', $value ); } public function maybe_set_digital_auto_complete() { $items= $this->get_line_product_items(); $this->set_auto_complete_digital_order( $items && ArrayUtil::every( $items, fn( $item ) => 'digital' === $item->get_shipping_type() && $item->get_digital_auto_complete() ) ); } }