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

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

401 lines 11.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\Classes\Order;
4
5 use Exception;
6 use StoreEngine\Classes\AbstractEntity;
7 use StoreEngine\Classes\enums\ProductTaxStatus;
8 use StoreEngine\Classes\Exceptions\StoreEngineException;
9 use StoreEngine\Classes\Order;
10 use StoreEngine\Classes\Tax;
11 use StoreEngine\Utils\Helper;
12 use StoreEngine\Utils\TaxUtil;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Base model for a single order line item.
20 */
21 abstract class AbstractOrderItem extends AbstractEntity {
22
23 protected bool $read_extra_data_separately = false;
24
25 protected string $table = 'storeengine_order_items';
26
27 protected string $meta_type = 'order_item';
28
29 protected int $order_id = 0;
30
31 protected string $object_type = 'order_item';
32
33 protected array $data = [
34 'order_id' => 0,
35 'name' => '',
36 ];
37
38 protected string $primary_key = 'order_item_id';
39
40 protected array $readable_fields = [ 'order_item_id id', 'order_id', 'order_item_name name' ];
41
42 /**
43 * Create DB Record.
44 *
45 * @throws StoreEngineException
46 */
47 public function create() {
48 $data = [
49 'order_item_name' => $this->get_name(),
50 'order_item_type' => $this->get_type(),
51 'order_id' => $this->get_order_id(),
52 ];
53
54 if ( $this->wpdb->insert( $this->table, $data, [ '%s', '%s', '%d' ] ) ) {
55 $this->set_id( $this->wpdb->insert_id );
56 $this->save_item_data();
57 $this->save_meta_data();
58 $this->apply_changes();
59 $this->clear_cache();
60 }
61
62 if ( $this->wpdb->last_error ) {
63 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-insert-record' );
64 }
65 }
66
67 /**
68 * Update DB Record.
69 *
70 * @throws StoreEngineException
71 */
72 public function update() {
73 if ( ! $this->get_id() ) {
74 return;
75 }
76
77 $this->save_meta_data();
78
79 $changes = $this->get_changes();
80
81 if ( array_intersect( [ 'name', 'order_id' ], array_keys( $changes ) ) ) {
82 $data = [
83 'order_item_name' => $this->get_name(),
84 'order_item_type' => $this->get_type(),
85 'order_id' => $this->get_order_id(),
86 ];
87
88 $this->wpdb->update( $this->table, $data, [ $this->primary_key => $this->get_id() ], [
89 '%s',
90 '%s',
91 '%d',
92 ], [ '%d' ] );
93 if ( $this->wpdb->last_error ) {
94 throw new StoreEngineException( wp_kses_post( $this->wpdb->last_error ), 'db-error-update-record' );
95 }
96 }
97
98 $this->save_item_data();
99 $this->save_meta_data();
100 $this->apply_changes();
101 $this->clear_cache();
102 }
103
104 /**
105 * Get order ID by order item ID.
106 *
107 * @param int $item_id Item ID.
108 *
109 * @return int
110 */
111 public function get_order_id_by_order_item_id( int $item_id ): int {
112 global $wpdb;
113
114 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
115 return (int) $wpdb->get_var(
116 $wpdb->prepare(
117 "SELECT order_id FROM {$wpdb->prefix}storeengine_order_items WHERE order_item_id = %d",
118 $item_id
119 )
120 );
121 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
122 }
123
124 /**
125 * Clear meta cache.
126 *
127 * @param int $item_id Item ID.
128 * @param int|null $order_id Order ID. If not set, it will be loaded using the item ID.
129 */
130 protected function clear_caches( int $item_id, ?int $order_id ) {
131 wp_cache_delete( 'item-' . $item_id, 'order-items' );
132
133 if ( ! $order_id ) {
134 $order_id = $this->get_order_id_by_order_item_id( $item_id );
135 }
136 if ( $order_id ) {
137 wp_cache_delete( 'order-items-' . $order_id, 'orders' );
138 }
139 }
140
141 /**
142 * Get the order item type based on Item ID.
143 *
144 * @param int|string $item_id Item ID.
145 *
146 * @return string|null Order item type or null if no order item entry found.
147 */
148 public static function get_order_item_type( $item_id ): ?string {
149 global $wpdb;
150
151 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
152 return $wpdb->get_var(
153 $wpdb->prepare(
154 "SELECT order_item_type FROM {$wpdb->prefix}storeengine_order_items WHERE order_item_id = %d LIMIT 1;",
155 absint( $item_id )
156 )
157 );
158 }
159
160
161 /**
162 * Merge changes with data and clear.
163 * Overrides the base data object's apply_changes.
164 * array_replace_recursive does not work well for order items because it merges taxes instead
165 * of replacing them.
166 */
167 public function apply_changes() {
168 $this->data = array_replace( $this->data, $this->changes );
169 $this->changes = [];
170 }
171
172 /*
173 |--------------------------------------------------------------------------
174 | Getters
175 |--------------------------------------------------------------------------
176 */
177
178 /**
179 * Get order ID this meta belongs to.
180 *
181 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
182 *
183 * @return int
184 */
185 public function get_order_id( string $context = 'view' ): int {
186 return (int) $this->get_prop( 'order_id', $context );
187 }
188
189 /**
190 * Get order item name.
191 *
192 * @param string $context What the value is for. Valid values are 'view' and 'edit'.
193 *
194 * @return string
195 */
196 public function get_name( string $context = 'view' ): string {
197 return $this->get_prop( 'name', $context );
198 }
199
200 /**
201 * Get quantity.
202 *
203 * @return int
204 */
205 public function get_quantity(): int {
206 return 1;
207 }
208
209 /**
210 * Get tax status.
211 *
212 * @return string
213 */
214 public function get_tax_status(): string {
215 return ProductTaxStatus::TAXABLE;
216 }
217
218 /**
219 * Get tax class.
220 *
221 * @return string
222 */
223 public function get_tax_class(): string {
224 return '';
225 }
226
227 /**
228 * Get parent order object.
229 *
230 * @return Order|false
231 */
232 public function get_order(): Order {
233 $oder = Helper::get_order( $this->get_order_id() );
234
235 return $oder && ! is_wp_error( $oder ) ? $oder : false;
236 }
237
238 /*
239 |--------------------------------------------------------------------------
240 | Setters
241 |--------------------------------------------------------------------------
242 */
243
244 /**
245 * Set order ID.
246 *
247 * @param int|string $value Order ID.
248 */
249 public function set_order_id( $value ) {
250 $this->set_prop( 'order_id', absint( $value ) );
251 }
252
253 /**
254 * Set order item name.
255 *
256 * @param string $value Item name.
257 */
258 public function set_name( string $value ) {
259 $this->set_prop( 'name', wp_check_invalid_utf8( $value ) );
260 }
261
262 /*
263 |--------------------------------------------------------------------------
264 | Other Methods
265 |--------------------------------------------------------------------------
266 */
267
268 /**
269 * Check if an attribute is included in the attributes area of a variation name.
270 *
271 * @param string $attribute Attribute value to check for.
272 * @param string $name Product name to check in.
273 *
274 * @return bool
275 */
276 protected static function is_attribute_in_product_name( string $attribute, string $name ): bool {
277 $is_in_name = stristr( $name, ' ' . $attribute . ',' ) || 0 === stripos( strrev( $name ), strrev( ' ' . $attribute ) );
278
279 return apply_filters( 'storeengine/is_attribute_in_product_name', $is_in_name, $attribute, $name );
280 }
281
282 /**
283 * Wrapper for get_formatted_meta_data that includes all metadata by default.
284 *
285 * @param string $hide_prefix Meta data prefix, (default: _).
286 * @param bool $include_all Include all meta data, this stop skip items with values already in the product name.
287 *
288 * @return array
289 */
290 public function get_all_formatted_metadata( string $hide_prefix = '_', bool $include_all = true ): array {
291 return $this->get_formatted_metadata( $hide_prefix, $include_all );
292 }
293
294 /**
295 * Expands things like term slugs before return.
296 *
297 * @param string $hide_prefix Meta data prefix, (default: _).
298 * @param bool $include_all Include all meta data, this stop skip items with values already in the product name.
299 *
300 * @return array
301 */
302 public function get_formatted_metadata( string $hide_prefix = '_', bool $include_all = false ): array {
303 $formatted_meta = [];
304 $meta_data = $this->get_meta_data();
305 $hide_prefix_length = ! empty( $hide_prefix ) ? strlen( $hide_prefix ) : 0;
306 $order_item_name = $this->get_name();
307
308 try {
309 $product = is_callable( [ $this, 'get_product' ] ) ? $this->get_product() : false;
310 } catch ( Exception $e ) {
311 $product = false;
312 }
313
314 foreach ( $meta_data as $meta ) {
315 if ( empty( $meta->id ) || '' === $meta->value || ! is_scalar( $meta->value ) || ( $hide_prefix_length && substr( $meta->key, 0, $hide_prefix_length ) === $hide_prefix ) ) {
316 continue;
317 }
318
319 $meta->key = rawurldecode( (string) $meta->key );
320 $meta->value = rawurldecode( (string) $meta->value );
321 $display_value = $meta->value;
322 $display_key = trim( $meta->key );
323
324 if ( taxonomy_exists( $display_key ) ) {
325 $attribute_taxonomy = get_taxonomy( $display_key );
326 // Singular, not ->label: the latter is the admin plural
327 // ("Product Color"). See storeengine_get_cart_item_data().
328 $display_key = $attribute_taxonomy->labels->singular_name ?: $attribute_taxonomy->label;
329 $term = get_term_by( 'slug', $meta->value, $meta->key );
330
331 if ( ! is_wp_error( $term ) && is_object( $term ) && $term->name ) {
332 $display_value = $term->name;
333 }
334 } else {
335 if ( str_contains( $meta->key, '_' ) || str_contains( $meta->key, '-' ) ) {
336 $display_key = ucwords( str_replace( [ '_', '-' ], ' ', Helper::strip_attribute_taxonomy_name( $display_key ) ) );
337 }
338 }
339
340 // Skip items with values already in the product details area of the product name.
341 if ( ! $include_all && $product && $product->is_type( 'variable' ) && self::is_attribute_in_product_name( $display_value, $order_item_name ) ) {
342 continue;
343 }
344
345 $formatted_meta[ $meta->id ] = [
346 'key' => $meta->key,
347 'value' => $meta->value,
348 'display_key' => wp_kses_post( apply_filters( 'storeengine/order_item_display_meta_key', $display_key, $meta, $this ) ),
349 'display_value' => wp_kses_post( make_clickable( apply_filters( 'storeengine/order_item_display_meta_value', trim( $display_value ), $meta, $this ) ) ),
350 ];
351 }
352
353 return apply_filters( 'storeengine/order_item_get_formatted_meta_data', $formatted_meta, $this );
354 }
355
356 /**
357 * Calculate item taxes.
358 *
359 * @param array $calculate_tax_for Location data to get taxes for. Required.
360 *
361 * @return bool True if taxes were calculated.
362 */
363 public function calculate_taxes( array $calculate_tax_for = [] ): bool {
364 if ( ! method_exists( $this, 'set_taxes' ) ) {
365 return false;
366 }
367
368 if ( ! isset( $calculate_tax_for['country'], $calculate_tax_for['state'], $calculate_tax_for['postcode'], $calculate_tax_for['city'] ) ) {
369 return false;
370 }
371
372 if ( '0' !== $this->get_tax_class() && ProductTaxStatus::TAXABLE === $this->get_tax_status() && TaxUtil::is_tax_enabled() ) {
373 $calculate_tax_for['tax_class'] = $this->get_tax_class();
374 // Calc taxes.
375 $tax_rates = Tax::find_rates( $calculate_tax_for );
376 $taxes = Tax::calc_tax( $this->get_total(), $tax_rates, false );
377
378 if ( method_exists( $this, 'get_subtotal' ) ) {
379 $this->set_taxes( [
380 'total' => $taxes,
381 'subtotal' => Tax::calc_tax( $this->get_subtotal(), $tax_rates, false ),
382 ] );
383 } else {
384 $this->set_taxes( [ 'total' => $taxes ] );
385 }
386 } else {
387 $this->set_taxes( false );
388 }
389
390 /**
391 * Fires after calculating taxes for specific order item.
392 *
393 * @param self $this Order item object.
394 * @param array $calculate_tax_for Holds the address data.
395 */
396 do_action( 'storeengine/order_item/after_calculate_taxes', $this, $calculate_tax_for );
397
398 return true;
399 }
400 }
401