0, 'name' => '', ]; protected string $primary_key = 'order_item_id'; protected array $readable_fields = [ 'order_item_id id', 'order_id', 'order_item_name name' ]; /** * Create DB Record. * * @throws StoreEngineException */ public function create() { $data = [ 'order_item_name' => $this->get_name(), 'order_item_type' => $this->get_type(), 'order_id' => $this->get_order_id(), ]; if ( $this->wpdb->insert( $this->table, $data, [ '%s', '%s', '%d' ] ) ) { $this->set_id( $this->wpdb->insert_id ); $this->save_item_data(); $this->save_meta_data(); $this->apply_changes(); $this->clear_cache(); } if ( $this->wpdb->last_error ) { throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' ); } } /** * Update DB Record. * * @throws StoreEngineException */ public function update() { if ( ! $this->get_id() ) { return; } $this->save_meta_data(); $changes = $this->get_changes(); if ( array_intersect( [ 'name', 'order_id' ], array_keys( $changes ) ) ) { $data = [ 'order_item_name' => $this->get_name(), 'order_item_type' => $this->get_type(), 'order_id' => $this->get_order_id(), ]; $this->wpdb->update( $this->table, $data, [ $this->primary_key => $this->get_id() ], [ '%s', '%s', '%d', ], [ '%d' ] ); if ( $this->wpdb->last_error ) { throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' ); } } $this->save_item_data(); $this->save_meta_data(); $this->apply_changes(); $this->clear_cache(); } /** * Get order ID by order item ID. * * @param int $item_id Item ID. * * @return int */ public function get_order_id_by_order_item_id( int $item_id ): int { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return (int) $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}storeengine_order_items WHERE order_item_id = %d", $item_id ) ); // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Clear meta cache. * * @param int $item_id Item ID. * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID. */ protected function clear_caches( int $item_id, ?int $order_id ) { wp_cache_delete( 'item-' . $item_id, 'order-items' ); if ( ! $order_id ) { $order_id = $this->get_order_id_by_order_item_id( $item_id ); } if ( $order_id ) { wp_cache_delete( 'order-items-' . $order_id, 'orders' ); } } /** * Get the order item type based on Item ID. * * @param int|string $item_id Item ID. * * @return string|null Order item type or null if no order item entry found. */ public static function get_order_item_type( $item_id ): ?string { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching return $wpdb->get_var( $wpdb->prepare( "SELECT order_item_type FROM {$wpdb->prefix}storeengine_order_items WHERE order_item_id = %d LIMIT 1;", absint( $item_id ) ) ); } /** * Merge changes with data and clear. * Overrides the base data object's apply_changes. * array_replace_recursive does not work well for order items because it merges taxes instead * of replacing them. */ public function apply_changes() { $this->data = array_replace( $this->data, $this->changes ); $this->changes = []; } /* |-------------------------------------------------------------------------- | Getters |-------------------------------------------------------------------------- */ /** * Get order ID this meta belongs to. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * * @return int */ public function get_order_id( string $context = 'view' ): int { return (int) $this->get_prop( 'order_id', $context ); } /** * Get order item name. * * @param string $context What the value is for. Valid values are 'view' and 'edit'. * * @return string */ public function get_name( string $context = 'view' ): string { return $this->get_prop( 'name', $context ); } /** * Get quantity. * * @return int */ public function get_quantity(): int { return 1; } /** * Get tax status. * * @return string */ public function get_tax_status(): string { return ProductTaxStatus::TAXABLE; } /** * Get tax class. * * @return string */ public function get_tax_class(): string { return ''; } /** * Get parent order object. * * @return Order|false */ public function get_order(): Order { $oder = Helper::get_order( $this->get_order_id() ); return $oder && ! is_wp_error( $oder ) ? $oder : false; } /* |-------------------------------------------------------------------------- | Setters |-------------------------------------------------------------------------- */ /** * Set order ID. * * @param int|string $value Order ID. */ public function set_order_id( $value ) { $this->set_prop( 'order_id', absint( $value ) ); } /** * Set order item name. * * @param string $value Item name. */ public function set_name( string $value ) { $this->set_prop( 'name', wp_check_invalid_utf8( $value ) ); } /* |-------------------------------------------------------------------------- | Other Methods |-------------------------------------------------------------------------- */ /** * Check if an attribute is included in the attributes area of a variation name. * * @param string $attribute Attribute value to check for. * @param string $name Product name to check in. * * @return bool */ protected static function is_attribute_in_product_name( string $attribute, string $name ): bool { $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) ); return apply_filters( 'storeengine/is_attribute_in_product_name', $is_in_name, $attribute, $name ); } /** * Wrapper for get_formatted_meta_data that includes all metadata by default. * * @param string $hide_prefix Meta data prefix, (default: _). * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. * * @return array */ public function get_all_formatted_metadata( string $hide_prefix = '_', bool $include_all = true ): array { return $this->get_formatted_metadata( $hide_prefix, $include_all ); } /** * Expands things like term slugs before return. * * @param string $hide_prefix Meta data prefix, (default: _). * @param bool $include_all Include all meta data, this stop skip items with values already in the product name. * * @return array */ public function get_formatted_metadata( string $hide_prefix = '_', bool $include_all = false ): array { $formatted_meta = []; $meta_data = $this->get_meta_data(); $hide_prefix_length = ! empty( $hide_prefix ) ? strlen( $hide_prefix ) : 0; $order_item_name = $this->get_name(); try { $product = is_callable( [ $this, 'get_product' ] ) ? $this->get_product() : false; } catch ( Exception $e ) { $product = false; } foreach ( $meta_data as $meta ) { if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hide_prefix_length && substr( $meta->key, 0, $hide_prefix_length ) === $hide_prefix ) ) { continue; } $meta->key = rawurldecode( (string) $meta->key ); $meta->value = rawurldecode( (string) $meta->value ); $display_value = $meta->value; $display_key = trim( $meta->key ); if ( taxonomy_exists( $display_key ) ) { $attribute_taxonomy = get_taxonomy( $display_key ); // Singular, not ->label: the latter is the admin plural // ("Product Color"). See storeengine_get_cart_item_data(). $display_key = $attribute_taxonomy->labels->singular_name ?: $attribute_taxonomy->label; $term = get_term_by( 'slug', $meta->value, $meta->key ); if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) { $display_value = $term->name; } } else { if ( str_contains( $meta->key, '_' ) || str_contains( $meta->key, '-' ) ) { $display_key = ucwords( str_replace( [ '_', '-' ], ' ', Helper::strip_attribute_taxonomy_name( $display_key ) ) ); } } // Skip items with values already in the product details area of the product name. if ( ! $include_all && $product && $product->is_type( 'variable' ) && self::is_attribute_in_product_name( $display_value, $order_item_name ) ) { continue; } $formatted_meta[ $meta->id ] = [ 'key' => $meta->key, 'value' => $meta->value, 'display_key' => wp_kses_post( apply_filters( 'storeengine/order_item_display_meta_key', $display_key, $meta, $this ) ), 'display_value' => wp_kses_post( make_clickable( apply_filters( 'storeengine/order_item_display_meta_value', trim( $display_value ), $meta, $this ) ) ), ]; } return apply_filters( 'storeengine/order_item_get_formatted_meta_data', $formatted_meta, $this ); } /** * Calculate item taxes. * * @param array $calculate_tax_for Location data to get taxes for. Required. * * @return bool True if taxes were calculated. */ public function calculate_taxes( array $calculate_tax_for = [] ): bool { if ( ! method_exists( $this, 'set_taxes' ) ) { return false; } if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) { return false; } if ( '0' !== $this->get_tax_class() && ProductTaxStatus::TAXABLE === $this->get_tax_status() && TaxUtil::is_tax_enabled() ) { $calculate_tax_for['tax_class'] = $this->get_tax_class(); // Calc taxes. $tax_rates = Tax::find_rates( $calculate_tax_for ); $taxes = Tax::calc_tax( $this->get_total(), $tax_rates, false ); if ( method_exists( $this, 'get_subtotal' ) ) { $this->set_taxes( [ 'total' => $taxes, 'subtotal' => Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ), ] ); } else { $this->set_taxes( [ 'total' => $taxes ] ); } } else { $this->set_taxes( false ); } /** * Fires after calculating taxes for specific order item. * * @param self $this Order item object. * @param array $calculate_tax_for Holds the address data. */ do_action( 'storeengine/order_item/after_calculate_taxes', $this, $calculate_tax_for ); return true; } }