Completed
Branch FET-9222-rest-api-writes (6820fb)
by
unknown
124:42 queued 113:43
created

EE_Line_Item   F

Complexity

Total Complexity 190

Size/Duplication

Total Lines 1265
Duplicated Lines 1.19 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 15
loc 1265
rs 3.9999
wmc 190
lcom 1
cbo 11

79 Methods

Rating   Name   Duplication   Size   Complexity  
A new_instance() 0 4 2
A new_instance_from_db() 0 3 1
A __construct() 0 6 2
A ID() 0 3 1
A TXN_ID() 0 3 1
A set_TXN_ID() 0 3 1
A name() 0 7 2
A set_name() 0 3 1
A desc() 0 3 1
A set_desc() 0 3 1
A quantity() 0 3 1
A set_quantity() 0 3 1
A OBJ_ID() 0 3 1
A set_OBJ_ID() 0 3 1
A OBJ_type() 0 3 1
B OBJ_type_i18n() 0 21 6
A set_OBJ_type() 0 3 1
A unit_price() 0 3 1
A set_unit_price() 0 3 1
B is_percent() 0 19 8
A percent() 0 3 1
A set_percent() 0 3 1
A total() 0 3 1
A set_total() 0 3 1
A order() 0 3 1
A set_order() 0 3 1
A parent_ID() 0 3 1
A set_parent_ID() 0 3 1
A type() 0 3 1
A set_type() 0 3 1
A parent() 0 7 2
A children() 0 13 3
A code() 0 3 1
A set_code() 0 3 1
A is_taxable() 0 3 1
A set_is_taxable() 0 3 1
A get_object() 0 4 2
A ticket() 0 6 1
A get_ticket_datetime() 9 12 4
A ticket_event_name() 0 8 2
A ticket_event() 6 11 3
A ticket_datetime_start() 0 8 2
C add_child_line_item() 0 24 8
A set_parent() 0 12 3
A get_child_line_item() 0 7 3
A delete_children_line_items() 0 9 2
B delete_child_line_item() 0 23 6
A delete_if_childless_subtotal() 0 9 4
A generate_code() 0 4 1
A is_tax() 0 3 1
A is_tax_sub_total() 0 3 1
A is_line_item() 0 3 1
A is_sub_line_item() 0 3 1
A is_sub_total() 0 3 1
A is_cancellation() 0 3 1
A is_total() 0 3 1
A is_cancelled() 0 3 1
A unit_price_no_code() 0 3 1
A total_no_code() 0 3 1
B recalculate_total_including_taxes() 0 22 5
C recalculate_pre_tax_total() 0 49 21
C _recalculate_pretax_total_for_subtotal() 0 36 8
C _recalculate_pretax_total_for_line_item() 0 40 7
A recalculate_taxes_and_tax_total() 0 15 2
B _recalculate_tax_sub_total() 0 21 7
A get_total_tax() 0 10 3
B get_items_total() 0 16 5
A tax_descendants() 0 3 1
A get_items() 0 3 1
B taxable_total() 0 19 7
A transaction() 0 3 1
B save_this_and_descendants_to_txn() 0 16 5
A save_this_and_descendants() 0 12 4
A get_cancellations() 0 4 1
A maybe_save() 0 6 2
A clear_related_line_item_cache() 0 4 1
A timestamp() 0 4 2
A _get_descendants_of_type() 0 6 1
A get_nearest_descendant_of_type() 0 6 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like EE_Line_Item often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EE_Line_Item, and based on these observations, apply Extract Interface, too.

1
<?php if ( !defined( 'EVENT_ESPRESSO_VERSION' ) ) {
2
	exit( 'No direct script access allowed' );
3
}
4
/**
5
 * Event Espresso
6
 *
7
 * Event Registration and Management Plugin for WordPress
8
 *
9
 * @ package        Event Espresso
10
 * @ author        Event Espresso
11
 * @ copyright    (c) 2008-2011 Event Espresso  All Rights Reserved.
12
 * @ license        {@link http://eventespresso.com/support/terms-conditions/}   * see Plugin Licensing *
13
 * @ link                {@link http://www.eventespresso.com}
14
 * @ since            4.0
15
 *
16
 */
17
18
19
20
/**
21
 * EE_Line_Item class
22
 * see EEM_Line_Item for description
23
 *
24
 * @package            Event Espresso
25
 * @subpackage         includes/classes/EE_Checkin.class.php
26
 * @author             Michael Nelson
27
 */
28
class EE_Line_Item extends EE_Base_Class implements EEI_Line_Item {
29
30
	/**
31
	 * for children line items (currently not a normal relation)
32
	 * @type EE_Line_Item[]
33
	 */
34
	protected $_children;
35
36
	/**
37
	 * for the parent line item
38
	 * @var EE_Line_Item
39
	 */
40
	protected $_parent;
41
42
43
44
	/**
45
	 *
46
	 * @param array $props_n_values  incoming values
47
	 * @param string $timezone  incoming timezone (if not set the timezone set for the website will be
48
	 *                          		used.)
49
	 * @param array $date_formats  incoming date_formats in an array where the first value is the
50
	 *                             		    date_format and the second value is the time format
51
	 * @return EE_Line_Item
52
	 */
53
	public static function new_instance( $props_n_values = array(), $timezone = null, $date_formats = array() ) {
54
		$has_object = parent::_check_for_object( $props_n_values, __CLASS__, $timezone, $date_formats );
55
		return $has_object ? $has_object : new self( $props_n_values, false, $timezone, $date_formats );
56
	}
57
58
59
60
	/**
61
	 * @param array $props_n_values  incoming values from the database
62
	 * @param string $timezone  incoming timezone as set by the model.  If not set the timezone for
63
	 *                          		the website will be used.
64
	 * @return EE_Line_Item
65
	 */
66
	public static function new_instance_from_db( $props_n_values = array(), $timezone = null ) {
67
		return new self( $props_n_values, TRUE, $timezone );
68
	}
69
70
71
72
	/**
73
	 * Adds some defaults if they're not specified
74
	 * @param array  $fieldValues
75
	 * @param bool   $bydb
76
	 * @param string $timezone
77
	 */
78
	protected function __construct( $fieldValues = array(), $bydb = FALSE, $timezone = '' ) {
79
		parent::__construct( $fieldValues, $bydb, $timezone );
80
		if ( ! $this->get( 'LIN_code' ) ) {
81
			$this->set_code( $this->generate_code() );
82
		}
83
	}
84
85
86
87
	/**
88
	 * Gets ID
89
	 * @return int
90
	 */
91
	public function ID() {
92
		return $this->get( 'LIN_ID' );
93
	}
94
95
96
97
	/**
98
	 * Gets TXN_ID
99
	 * @return int
100
	 */
101
	public function TXN_ID() {
102
		return $this->get( 'TXN_ID' );
103
	}
104
105
106
107
	/**
108
	 * Sets TXN_ID
109
	 * @param int $TXN_ID
110
	 */
111
	public function set_TXN_ID( $TXN_ID ) {
112
		$this->set( 'TXN_ID', $TXN_ID );
113
	}
114
115
116
117
	/**
118
	 * Gets name
119
	 * @return string
120
	 */
121
	public function name() {
122
		$name =  $this->get( 'LIN_name' );
123
		if( ! $name ){
124
			$name = ucwords( str_replace( '-', ' ', $this->type() ) );
125
		}
126
		return $name;
127
	}
128
129
130
131
	/**
132
	 * Sets name
133
	 * @param string $name
134
	 */
135
	public function set_name( $name ) {
136
		$this->set( 'LIN_name', $name );
137
	}
138
139
140
141
	/**
142
	 * Gets desc
143
	 * @return string
144
	 */
145
	public function desc() {
146
		return $this->get( 'LIN_desc' );
147
	}
148
149
150
151
	/**
152
	 * Sets desc
153
	 * @param string $desc
154
	 */
155
	public function set_desc( $desc ) {
156
		$this->set( 'LIN_desc', $desc );
157
	}
158
159
160
161
	/**
162
	 * Gets quantity
163
	 * @return int
164
	 */
165
	public function quantity() {
166
		return $this->get( 'LIN_quantity' );
167
	}
168
169
170
171
	/**
172
	 * Sets quantity
173
	 * @param int $quantity
174
	 */
175
    public function set_quantity( $quantity ) {
176
		$this->set( 'LIN_quantity', max( $quantity, 0 ) );
177
	}
178
179
180
181
	/**
182
	 * Gets item_id
183
	 * @return string
184
	 */
185
	public function OBJ_ID() {
186
		return $this->get( 'OBJ_ID' );
187
	}
188
189
190
191
	/**
192
	 * Sets item_id
193
	 * @param string $item_id
194
	 */
195
	public function set_OBJ_ID( $item_id ) {
196
		$this->set( 'OBJ_ID', $item_id );
197
	}
198
199
200
201
	/**
202
	 * Gets item_type
203
	 * @return string
204
	 */
205
	public function OBJ_type() {
206
		return $this->get( 'OBJ_type' );
207
	}
208
209
210
211
	/**
212
	 * Gets item_type
213
	 * @return string
214
	 */
215
    public function OBJ_type_i18n() {
216
	    $obj_type = $this->OBJ_type();
217
        switch ($obj_type) {
218
            case 'Event':
219
                $obj_type = __('Event', 'event_espresso');
220
                break;
221
            case 'Price':
222
                $obj_type = __('Price', 'event_espresso');
223
                break;
224
            case 'Promotion':
225
                $obj_type = __('Promotion', 'event_espresso');
226
                break;
227
            case 'Ticket':
228
                $obj_type = __('Ticket', 'event_espresso');
229
                break;
230
            case 'Transaction':
231
                $obj_type = __('Transaction', 'event_espresso');
232
                break;
233
        }
234
		return apply_filters('FHEE__EE_Line_Item__OBJ_type_i18n', $obj_type, $this);
235
	}
236
237
238
239
	/**
240
	 * Sets item_type
241
	 * @param string $OBJ_type
242
	 */
243
	public function set_OBJ_type( $OBJ_type ) {
244
		$this->set( 'OBJ_type', $OBJ_type );
245
	}
246
247
248
249
	/**
250
	 * Gets unit_price
251
	 * @return float
252
	 */
253
	public function unit_price() {
254
		return $this->get( 'LIN_unit_price' );
255
	}
256
257
258
259
	/**
260
	 * Sets unit_price
261
	 *
262
	 * @param float $unit_price
263
	 */
264
	public function set_unit_price( $unit_price ) {
265
		$this->set( 'LIN_unit_price', $unit_price );
266
	}
267
268
269
270
	/**
271
	 * Checks if this item is a percentage modifier or not
272
	 * @throws EE_Error
273
	 * @return boolean
274
	 */
275
	public function is_percent() {
276
		if( $this->is_tax_sub_total() ) {
277
			//tax subtotals HAVE a percent on them, that percentage only applies
278
			//to taxable items, so its' an exception. Treat it like a flat line item
279
			return false;
280
		}
281
		$unit_price = abs( $this->get( 'LIN_unit_price' ) );
282
		$percent = abs( $this->get( 'LIN_percent' ) );
283
		if ( $unit_price < .001 && $percent ) {
284
			return TRUE;
285
		} elseif ( $unit_price >= .001 && !$percent ) {
286
			return FALSE;
287
		} elseif ( $unit_price >= .001 && $percent ) {
288
			throw new EE_Error( sprintf( __( "A Line Item can not have a unit price of (%s) AND a percent (%s)!", "event_espresso" ), $unit_price, $percent ) );
289
		} else {
290
			// if they're both 0, assume its not a percent item
291
			return FALSE;
292
		}
293
	}
294
295
296
297
	/**
298
	 * Gets percent (between 100-.001)
299
	 * @return float
300
	 */
301
	public function percent() {
302
		return $this->get( 'LIN_percent' );
303
	}
304
305
306
307
	/**
308
	 * Sets percent (between 100-0.01)
309
	 * @param float $percent
310
	 */
311
	public function set_percent( $percent ) {
312
		$this->set( 'LIN_percent', $percent );
313
	}
314
315
316
317
	/**
318
	 * Gets total
319
	 * @return float
320
	 */
321
	public function total() {
322
		return $this->get( 'LIN_total' );
323
	}
324
325
326
327
	/**
328
	 * Sets total
329
	 * @param float $total
330
	 */
331
	public function set_total( $total ) {
332
		$this->set( 'LIN_total', $total );
333
	}
334
335
336
337
	/**
338
	 * Gets order
339
	 * @return int
340
	 */
341
	public function order() {
342
		return $this->get( 'LIN_order' );
343
	}
344
345
346
347
	/**
348
	 * Sets order
349
	 * @param int $order
350
	 */
351
	public function set_order( $order ) {
352
		$this->set( 'LIN_order', $order );
353
	}
354
355
356
357
	/**
358
	 * Gets parent
359
	 * @return int
360
	 */
361
	public function parent_ID() {
362
		return $this->get( 'LIN_parent' );
363
	}
364
365
366
367
	/**
368
	 * Sets parent
369
	 * @param int $parent
370
	 */
371
	public function set_parent_ID( $parent ) {
372
		$this->set( 'LIN_parent', $parent );
373
	}
374
375
376
377
	/**
378
	 * Gets type
379
	 * @return string
380
	 */
381
	public function type() {
382
		return $this->get( 'LIN_type' );
383
	}
384
385
386
387
	/**
388
	 * Sets type
389
	 * @param string $type
390
	 */
391
	public function set_type( $type ) {
392
		$this->set( 'LIN_type', $type );
393
	}
394
395
396
397
	/**
398
	 * Gets the line item of which this item is a composite. Eg, if this is a subtotal, the parent might be a total\
399
	 * If this line item is saved to the DB, fetches the parent from the DB. However, if this line item isn't in the DB
400
	 * it uses its cached reference to its parent line item (which would have been set by `EE_Line_Item::set_parent()` or
401
	 * indirectly by `EE_Line_item::add_child_line_item()`)
402
	 * @return EE_Line_Item
403
	 */
404
	public function parent() {
405
		if( $this->ID() ) {
406
			return $this->get_model()->get_one_by_ID( $this->parent_ID() );
407
		} else {
408
			return $this->_parent;
409
		}
410
	}
411
412
413
414
	/**
415
	 * Gets ALL the children of this line item (ie, all the parts that contribute towards this total).
416
	 * @return EE_Line_Item[]
417
	 */
418
	public function children() {
419
		if ( $this->ID() ) {
420
			return $this->get_model()->get_all(
421
					array(
422
						array( 'LIN_parent' => $this->ID() ),
423
						'order_by' => array( 'LIN_order' => 'ASC' ) ) );
424
		} else {
425
			if ( ! is_array( $this->_children ) ) {
426
				$this->_children = array();
427
			}
428
			return $this->_children;
429
		}
430
	}
431
432
433
434
	/**
435
	 * Gets code
436
	 * @return string
437
	 */
438
	public function code() {
439
		return $this->get( 'LIN_code' );
440
	}
441
442
443
444
	/**
445
	 * Sets code
446
	 * @param string $code
447
	 */
448
	public function set_code( $code ) {
449
		$this->set( 'LIN_code', $code );
450
	}
451
452
453
454
	/**
455
	 * Gets is_taxable
456
	 * @return boolean
457
	 */
458
	public function is_taxable() {
459
		return $this->get( 'LIN_is_taxable' );
460
	}
461
462
463
464
	/**
465
	 * Sets is_taxable
466
	 * @param boolean $is_taxable
467
	 */
468
	public function set_is_taxable( $is_taxable ) {
469
		$this->set( 'LIN_is_taxable', $is_taxable );
470
	}
471
472
473
474
	/**
475
	 * Gets the object that this model-joins-to.
476
	 * returns one of the model objects that the field OBJ_ID can point to... see the 'OBJ_ID' field on EEM_Promotion_Object
477
	 *
478
	 * 		Eg, if this line item join model object is for a ticket, this will return the EE_Ticket object
479
	 *
480
	 * @return EE_Base_Class | NULL
481
	 */
482
	public function get_object() {
483
		$model_name_of_related_obj = $this->OBJ_type();
484
		return $this->get_model()->has_relation(  $model_name_of_related_obj ) ? $this->get_first_related( $model_name_of_related_obj ) : NULL;
485
	}
486
487
488
489
	/**
490
	 * Like EE_Line_Item::get_object(), but can only ever actually return an EE_Ticket.
491
	 * (IE, if this line item is for a price or something else, will return NULL)
492
	 * @param array $query_params
493
	 * @return EE_Ticket
494
	 */
495
	public function ticket( $query_params = array() ) {
496
		//we're going to assume that when this method is called we always want to receive the attached ticket EVEN if that ticket is archived.  This can be overridden via the incoming $query_params argument
497
		$remove_defaults = array( 'default_where_conditions' => 'none' );
498
		$query_params = array_merge( $remove_defaults, $query_params );
499
		return $this->get_first_related( 'Ticket', $query_params );
500
	}
501
502
503
504
	/**
505
	 * Gets the EE_Datetime that's related to the ticket, IF this is for a ticket
506
	 * @return EE_Datetime | NULL
507
	 */
508
	public function get_ticket_datetime() {
509 View Code Duplication
		if ( $this->OBJ_type() === 'Ticket' ) {
510
			$ticket = $this->ticket();
511
			if ( $ticket instanceof EE_Ticket ) {
512
				$datetime = $ticket->first_datetime();
513
				if ( $datetime instanceof EE_Datetime ) {
514
					return $datetime;
515
				}
516
			}
517
		}
518
		return NULL;
519
	}
520
521
522
523
	/**
524
	 * Gets the event's name that's related to the ticket, if this is for
525
	 * a ticket
526
	 * @return string
527
	 */
528
	public function ticket_event_name() {
529
		$event_name = __( "Unknown", "event_espresso" );
530
		$event = $this->ticket_event();
531
		if ( $event instanceof EE_Event ) {
532
			$event_name = $event->name();
533
		}
534
		return $event_name;
535
	}
536
537
538
	/**
539
	 * Gets the event that's related to the ticket, if this line item represents a ticket.
540
	 * @return EE_Event|null
541
	 */
542
	public function ticket_event() {
543
		$event = null;
544
		$ticket = $this->ticket();
545 View Code Duplication
		if ( $ticket instanceof EE_Ticket ) {
546
			$datetime = $ticket->first_datetime();
547
			if ( $datetime instanceof EE_Datetime ) {
548
				$event = $datetime->event();
549
			}
550
		}
551
		return $event;
552
	}
553
554
555
556
	/**
557
	 * Gets the first datetime for this lien item, assuming it's for a ticket
558
	 * @param string $date_format
559
	 * @param string $time_format
560
	 * @return string
561
	 */
562
	public function ticket_datetime_start( $date_format = '', $time_format = '' ) {
563
		$first_datetime_string = __( "Unknown", "event_espresso" );
564
		$datetime = $this->get_ticket_datetime();
565
		if ( $datetime ) {
566
			$first_datetime_string = $datetime->start_date_and_time( $date_format, $time_format );
567
		}
568
		return $first_datetime_string;
569
	}
570
571
572
573
	/**
574
	 * Adds the line item as a child to this line item. If there is another child line
575
	 * item with the same LIN_code, it is overwritten by this new one
576
	 * @param EEI_Line_Item $line_item
577
	 * @param bool         $set_order
578
	 * @return bool success
579
	 * @throws \EE_Error
580
	 */
581
	public function add_child_line_item( EEI_Line_Item $line_item, $set_order = true ) {
582
		// should we calculate the LIN_order for this line item ?
583
		if ( $set_order || $line_item->order() === null ) {
584
			$line_item->set_order( count( $this->children() ) );
585
		}
586
		if ( $this->ID() ) {
587
			//check for any duplicate line items (with the same code), if so, this replaces it
588
			$line_item_with_same_code = $this->get_child_line_item(  $line_item->code() );
589
			if( $line_item_with_same_code instanceof EE_Line_Item && $line_item_with_same_code !== $line_item ) {
590
				$this->delete_child_line_item( $line_item_with_same_code->code() );
591
			}
592
			$line_item->set_parent_ID( $this->ID() );
593
			if( $this->TXN_ID() ){
594
				$line_item->set_TXN_ID( $this->TXN_ID() );
595
			}
596
			return $line_item->save();
597
		} else {
598
			$this->_children[ $line_item->code() ] = $line_item;
599
			if( $line_item->parent() != $this ) {
600
				$line_item->set_parent( $this );
601
			}
602
			return TRUE;
603
		}
604
	}
605
606
	/**
607
	 * Similar to EE_Base_Class::_add_relation_to, except this isn't a normal relation.
608
	 * If this line item is saved to the DB, this is just a wrapper for set_parent_ID() and save()
609
	 * However, if this line item is NOT saved to the DB, this just caches the parent on
610
	 * the EE_Line_Item::_parent property.
611
	 * @param EE_Line_Item $line_item
612
	 *
613
	 */
614
	public function set_parent( $line_item ) {
615
		if ( $this->ID() ) {
616
			if( ! $line_item->ID() ) {
617
				$line_item->save();
618
			}
619
			$this->set_parent_ID( $line_item->ID() );
620
			$this->save();
621
		} else {
622
			$this->_parent = $line_item;
623
			$this->set_parent_ID( $line_item->ID() );
624
		}
625
	}
626
627
628
629
	/**
630
	 * Gets the child line item as specified by its code. Because this returns an object (by reference)
631
	 * you can modify this child line item and the parent (this object) can know about them
632
	 * because it also has a reference to that line item
633
	 * @param string $code
634
	 * @return EE_Line_Item
635
	 */
636
	public function get_child_line_item( $code ) {
637
		if ( $this->ID() ) {
638
			return $this->get_model()->get_one( array( array( 'LIN_parent' => $this->ID(), 'LIN_code' => $code ) ) );
639
		} else {
640
			return isset( $this->_children[ $code ] ) ? $this->_children[ $code ] : null;
641
		}
642
	}
643
644
645
646
	/**
647
	 * Returns how many items are deleted (or, if this item has not been saved ot the DB yet, just how many it HAD cached on it)
648
	 * @return int
649
	 */
650
	public function delete_children_line_items() {
651
		if ( $this->ID() ) {
652
			return $this->get_model()->delete( array( array( 'LIN_parent' => $this->ID() ) ) );
653
		} else {
654
			$count = count( $this->_children );
655
			$this->_children = array();
656
			return $count;
657
		}
658
	}
659
660
661
662
	/**
663
	 * If this line item has been saved to the DB, deletes its child with LIN_code == $code. If this line
664
	 * HAS NOT been saved to the DB, removes the child line item with index $code.
665
	 * Also searches through the child's children for a matching line item. However, once a line item has been found
666
	 * and deleted, stops searching (so if there are line items with duplicate codes, only the first one found will be deleted)
667
	 * @param string $code
668
	 * @param bool $stop_search_once_found
669
	 * @return int count of items deleted (or simply removed from the line item's cache, if not has not been saved to the DB yet)
670
	 */
671
	public function delete_child_line_item( $code, $stop_search_once_found = true ) {
672
		if ( $this->ID() ) {
673
			$items_deleted = 0;
674
			if( $this->code() == $code ) {
675
				$items_deleted += EEH_Line_Item::delete_all_child_items( $this );
676
				$items_deleted += (int) $this->delete();
677
				if( $stop_search_once_found ){
678
					return $items_deleted;
679
				}
680
			}
681
			foreach( $this->children() as $child_line_item ) {
682
				$items_deleted += $child_line_item->delete_child_line_item( $code, $stop_search_once_found );
683
			}
684
			return $items_deleted;
685
		} else {
686
			if( isset( $this->_children[ $code ] ) ) {
687
				unset( $this->_children[ $code ] );
688
				return 1;
689
			}else{
690
				return 0;
691
			}
692
		}
693
	}
694
695
	/**
696
	 * If this line item is in the database, is of the type subtotal, and
697
	 * has no children, why do we have it? It should be deleted so this function
698
	 * does that
699
	 * @return boolean
700
	 */
701
	public function delete_if_childless_subtotal() {
702
		if( $this->ID() &&
703
				$this->type() == EEM_Line_Item::type_sub_total &&
704
				! $this->children() ) {
705
			return $this->delete();
706
		} else {
707
			return false;
708
		}
709
	}
710
711
712
713
	/**
714
	 * Creates a code and returns a string. doesn't assign the code to this model object
715
	 * @return string
716
	 */
717
	public function generate_code() {
718
		// each line item in the cart requires a unique identifier
719
		return md5( $this->get( 'OBJ_type' ) . $this->get( 'OBJ_ID' ) . microtime() );
720
	}
721
722
723
724
	/**
725
	 * @return bool
726
	 */
727
	public function is_tax() {
728
		return $this->type() === EEM_Line_Item::type_tax;
729
	}
730
731
732
733
	/**
734
	 * @return bool
735
	 */
736
	public function is_tax_sub_total() {
737
		return $this->type() === EEM_Line_Item::type_tax_sub_total;
738
	}
739
740
741
742
	/**
743
	 * @return bool
744
	 */
745
	public function is_line_item() {
746
		return $this->type() === EEM_Line_Item::type_line_item;
747
	}
748
749
750
751
	/**
752
	 * @return bool
753
	 */
754
	public function is_sub_line_item() {
755
		return $this->type() === EEM_Line_Item::type_sub_line_item;
756
	}
757
758
759
760
	/**
761
	 * @return bool
762
	 */
763
	public function is_sub_total() {
764
		return $this->type() === EEM_Line_Item::type_sub_total;
765
	}
766
767
768
769
	/**
770
	 * Whether or not this line item is a cancellation line item
771
	 * @return boolean
772
	 */
773
	public function is_cancellation() {
774
		return EEM_Line_Item::type_cancellation === $this->type();
775
	}
776
777
778
779
	/**
780
	 * @return bool
781
	 */
782
	public function is_total() {
783
		return $this->type() === EEM_Line_Item::type_total;
784
	}
785
786
787
788
	/**
789
	 * @return bool
790
	 */
791
	public function is_cancelled() {
792
		return $this->type() === EEM_Line_Item::type_cancellation;
793
	}
794
795
796
797
	/**
798
	 *
799
	 * @return string like '2, 004.00', formatted according to the localized currency
800
	 */
801
	public function unit_price_no_code() {
802
		return $this->get_pretty( 'LIN_unit_price', 'no_currency_code' );
803
	}
804
805
806
807
	/**
808
	 *
809
	 * @return string like '2, 004.00', formatted according to the localized currency
810
	 */
811
	public function total_no_code() {
812
		return $this->get_pretty( 'LIN_total', 'no_currency_code' );
813
	}
814
815
816
817
	/**
818
	 * Gets the final total on this item, taking taxes into account.
819
	 * Has the side-effect of setting the sub-total as it was just calculated.
820
	 * If this is used on a grand-total line item, also updates the transaction's
821
	 * TXN_total (provided this line item is allowed to persist, otherwise we don't
822
	 * want to change a persistable transaction with info from a non-persistent line item)
823
	 *
824
	 * @return float
825
	 * @throws \EE_Error
826
	 */
827
	public function recalculate_total_including_taxes() {
828
		$pre_tax_total = $this->recalculate_pre_tax_total();
829
		$tax_total = $this->recalculate_taxes_and_tax_total();
830
		$total = $pre_tax_total + $tax_total;
831
		// no negative totals plz
832
		$total = max( $total, 0 );
833
		$this->set_total( $total );
834
		//only update the related transaction's total
835
		//if we intend to save this line item and its a grand total
836
		if(
837
			$this->allow_persist() &&
838
			$this->type() === EEM_Line_Item::type_total &&
839
			$this->transaction() instanceof EE_Transaction
840
		){
841
			$this->transaction()->set_total( $total );
842
			if ( $this->transaction()->ID() ) {
843
				$this->transaction()->save();
844
			}
845
		}
846
		$this->maybe_save();
847
		return $total;
848
	}
849
850
851
	/**
852
	 * Recursively goes through all the children and recalculates sub-totals EXCEPT for
853
	 * tax-sub-totals (they're a an odd beast). Updates the 'total' on each line item according to either its
854
	 * unit price * quantity or the total of all its children EXCEPT when we're only calculating the taxable total and when this is called on the grand total
855
	 * @return float
856
	 * @throws \EE_Error
857
	 */
858
	public function recalculate_pre_tax_total() {
859
		$total = 0;
860
		$my_children = $this->children();
861
		$has_children = ! empty( $my_children );
862
		if ( $has_children && $this->is_line_item() ) {
863
			$total = $this->_recalculate_pretax_total_for_line_item( $total, $my_children );
864
		} elseif ( ! $has_children && ( $this->is_sub_line_item() || $this->is_line_item() ) ) {
865
			$total = $this->unit_price() * $this->quantity();
866
		} elseif( $this->is_sub_total() || $this->is_total() ) {
867
			$total = $this->_recalculate_pretax_total_for_subtotal( $total, $my_children );
868
		} elseif ( $this->is_tax_sub_total() || $this->is_tax() || $this->is_cancelled() ) {
869
			// completely ignore tax totals, tax sub-totals, and cancelled line items, when calculating the pre-tax-total
870
			return 0;
871
		}
872
		// ensure all non-line items and non-sub-line-items have a quantity of 1 (except for Events)
873
		if(
874
			! $this->is_line_item() &&
875
			! $this->is_sub_line_item() &&
876
			! $this->is_cancellation()
877
		) {
878
			if ( $this->OBJ_type() !== 'Event' ) {
879
				$this->set_quantity( 1 );
880
			}
881
			if( ! $this->is_percent() ) {
882
				$this->set_unit_price( $this->total() );
883
			}
884
		}
885
886
		//we don't want to bother saving grand totals, because that needs to factor in taxes anyways
887
		//so it ought to be
888
		if( ! $this->is_total() ) {
889
			$this->set_total( $total );
890
			//if not a percent line item, make sure we keep the unit price in sync
891
			if(
892
				$has_children
893
				&& $this->is_line_item()
894
				&& ! $this->is_percent()
895
			) {
896
				if( $this->quantity() === 0 ){
897
					$new_unit_price = 0;
898
				} else {
899
					$new_unit_price = $this->total() / $this->quantity();
900
				}
901
				$this->set_unit_price( $new_unit_price );
902
			}
903
			$this->maybe_save();
904
		}
905
		return $total;
906
	}
907
908
909
910
	/**
911
	 * Calculates the pretax total when this line item is a subtotal or total line item.
912
	 * Basically does a sum-then-round approach (ie, any percent line item that are children
913
	 * will calculate their total based on the un-rounded total we're working with so far, and
914
	 * THEN round the result; instead of rounding as we go like with sub-line-items)
915
	 *
916
	 * @param float          $calculated_total_so_far
917
	 * @param EE_Line_Item[] $my_children
918
	 * @return float
919
	 * @throws \EE_Error
920
	 */
921
	protected function _recalculate_pretax_total_for_subtotal( $calculated_total_so_far, $my_children = null ) {
922
		if( $my_children === null ) {
923
			$my_children = $this->children();
924
		}
925
		//get the total of all its children
926
		foreach ( $my_children as $child_line_item ) {
927
			if ( $child_line_item instanceof EE_Line_Item && ! $child_line_item->is_cancellation() ) {
928
				// percentage line items are based on total so far
929
				if ( $child_line_item->is_percent() ) {
930
					//round as we go so that the line items add up ok
931
					$percent_total = round(
932
						$calculated_total_so_far * $child_line_item->percent() / 100,
933
						EE_Registry::instance()->CFG->currency->dec_plc
934
					);
935
					$child_line_item->set_total( $percent_total );
936
					//so far all percent line items should have a quantity of 1
937
					//(ie, no double percent discounts. Although that might be requested someday)
938
					$child_line_item->set_quantity( 1 );
939
					$child_line_item->maybe_save();
940
					$calculated_total_so_far += $percent_total;
941
				} else {
942
					//verify flat sub-line-item quantities match their parent
943
					if( $child_line_item->is_sub_line_item() ) {
944
						$child_line_item->set_quantity( $this->quantity() );
945
					}
946
					$calculated_total_so_far += $child_line_item->recalculate_pre_tax_total();
947
				}
948
			}
949
		}
950
951
		if( $this->is_sub_total() ){
952
			// no negative totals plz
953
			$calculated_total_so_far = max( $calculated_total_so_far, 0 );
954
		}
955
		return $calculated_total_so_far;
956
	}
957
958
959
960
	/**
961
	 * Calculates the pretax total for a normal line item, in a round-then-sum approach
962
	 * (where each sub-line-item is applied to the base price for the line item
963
	 * and the result is immediately rounded, rather than summing all the sub-line-items
964
	 * then rounding, like we do when recalculating pretax totals on totals and subtotals).
965
	 *
966
	 * @param float          $calculated_total_so_far
967
	 * @param EE_Line_Item[] $my_children
968
	 * @return float
969
	 * @throws \EE_Error
970
	 */
971
	protected function _recalculate_pretax_total_for_line_item( $calculated_total_so_far, $my_children = null ) {
972
		if( $my_children === null ) {
973
			$my_children = $this->children();
974
		}
975
		//we need to keep track of the running total for a single item,
976
		//because we need to round as we go
977
		$unit_price_for_total = 0;
978
		$quantity_for_total = 1;
979
		//get the total of all its children
980
		foreach ( $my_children as $child_line_item ) {
981
			if ( $child_line_item instanceof EE_Line_Item &&
982
					! $child_line_item->is_cancellation()) {
983
				if ( $child_line_item->is_percent() ) {
984
					//it should be the unit-price-so-far multiplied by teh percent multiplied by the quantity
985
					//not total multiplied by percent, because that ignores rounding along-the-way
986
					$percent_unit_price = round(
987
						$unit_price_for_total * $child_line_item->percent() / 100,
988
						EE_Registry::instance()->CFG->currency->dec_plc
989
					);
990
					$percent_total = $percent_unit_price * $quantity_for_total;
991
					$child_line_item->set_total( $percent_total );
992
					//so far all percent line items should have a quantity of 1
993
					//(ie, no double percent discounts. Although that might be requested someday)
994
					$child_line_item->set_quantity( 1 );
995
					$child_line_item->maybe_save();
996
					$calculated_total_so_far += $percent_total;
997
					$unit_price_for_total += $percent_unit_price;
998
				} else {
999
					//verify flat sub-line-item quantities match their parent
1000
					if( $child_line_item->is_sub_line_item() ) {
1001
						$child_line_item->set_quantity( $this->quantity() );
1002
					}
1003
					$quantity_for_total = $child_line_item->quantity();
1004
					$calculated_total_so_far += $child_line_item->recalculate_pre_tax_total();
1005
					$unit_price_for_total += $child_line_item->unit_price();
1006
				}
1007
			}
1008
		}
1009
		return $calculated_total_so_far;
1010
	}
1011
1012
1013
1014
	/**
1015
	 * Recalculates the total on each individual tax (based on a recalculation of the pre-tax total), sets
1016
	 * the totals on each tax calculated, and returns the final tax total
1017
	 * @return float
1018
	 */
1019
	public function recalculate_taxes_and_tax_total() {
1020
		//get all taxes
1021
		$taxes = $this->tax_descendants();
1022
		//calculate the pretax total
1023
		$taxable_total = $this->taxable_total();
1024
		$tax_total = 0;
1025
		foreach ( $taxes as $tax ) {
1026
			$total_on_this_tax = $taxable_total * $tax->percent() / 100;
1027
			//remember the total on this line item
1028
			$tax->set_total( $total_on_this_tax );
1029
			$tax_total += $tax->total();
1030
		}
1031
		$this->_recalculate_tax_sub_total();
1032
		return $tax_total;
1033
	}
1034
1035
1036
1037
	/**
1038
	 * Simply forces all the tax-sub-totals to recalculate. Assumes the taxes have been calculated
1039
	 * @return void
1040
	 */
1041
	private function _recalculate_tax_sub_total() {
1042
		if ( $this->is_tax_sub_total() ) {
1043
			$total = 0;
1044
			$total_percent = 0;
1045
			//simply loop through all its children (which should be taxes) and sum their total
1046
			foreach ( $this->children() as $child_tax ) {
1047
				if ( $child_tax instanceof EE_Line_Item ) {
1048
					$total += $child_tax->total();
1049
					$total_percent += $child_tax->percent();
1050
				}
1051
			}
1052
			$this->set_total( $total );
1053
			$this->set_percent( $total_percent );
1054
		} elseif ( $this->is_total() ) {
1055
			foreach ( $this->children() as $maybe_tax_subtotal ) {
1056
				if ( $maybe_tax_subtotal instanceof EE_Line_Item ) {
1057
					$maybe_tax_subtotal->_recalculate_tax_sub_total();
1058
				}
1059
			}
1060
		}
1061
	}
1062
1063
1064
1065
	/**
1066
	 * Gets the total tax on this line item. Assumes taxes have already been calculated using recalculate_taxes_and_total
1067
	 * @return float
1068
	 */
1069
	public function get_total_tax() {
1070
		$this->_recalculate_tax_sub_total();
1071
		$total = 0;
1072
		foreach ( $this->tax_descendants() as $tax_line_item ) {
1073
			if ( $tax_line_item instanceof EE_Line_Item ) {
1074
				$total += $tax_line_item->total();
1075
			}
1076
		}
1077
		return $total;
1078
	}
1079
1080
1081
	/**
1082
	 * Gets the total for all the items purchased only
1083
	 * @return float
1084
	 */
1085
	public function get_items_total() {
1086
		//by default, let's make sure we're consistent with the existing line item
1087
		if( $this->is_total() ) {
1088
			$pretax_subtotal_li = EEH_Line_Item::get_pre_tax_subtotal( $this );
1089
			if( $pretax_subtotal_li instanceof EE_Line_Item ) {
1090
				return $pretax_subtotal_li->total();
1091
			}
1092
		}
1093
		$total = 0;
1094
		foreach ( $this->get_items() as $item ) {
1095
			if ( $item instanceof EE_Line_Item ) {
1096
				$total += $item->total();
1097
			}
1098
		}
1099
		return $total;
1100
	}
1101
1102
1103
1104
	/**
1105
	 * Gets all the descendants (ie, children or children of children etc) that
1106
	 * are of the type 'tax'
1107
	 * @return EE_Line_Item[]
1108
	 */
1109
	public function tax_descendants() {
1110
		return EEH_Line_Item::get_tax_descendants( $this );
1111
	}
1112
1113
1114
1115
	/**
1116
	 * Gets all the real items purchased which are children of this item
1117
	 * @return EE_Line_Item[]
1118
	 */
1119
	public function get_items() {
1120
		return EEH_Line_Item::get_line_item_descendants( $this );
1121
	}
1122
1123
1124
1125
	/**
1126
	 * Returns the amount taxable among this line item's children (or if it has no children,
1127
	 * how much of it is taxable). Does not recalculate totals or subtotals.
1128
	 * If the taxable total is negative, (eg, if none of the tickets were taxable,
1129
	 * but there is a "Taxable" discount), returns 0.
1130
	 *
1131
	 * @return float
1132
	 * @throws \EE_Error
1133
	 */
1134
	public function taxable_total() {
1135
		$total = 0;
1136
		if ( $this->children() ) {
1137
			foreach ( $this->children() as $child_line_item ) {
1138
				if ( $child_line_item->type() === EEM_Line_Item::type_line_item && $child_line_item->is_taxable()) {
1139
					//if it's a percent item, only take into account the percent
1140
					//that's taxable too (the taxable total so far)
1141
					if( $child_line_item->is_percent() ) {
1142
						$total = $total + ( $total * $child_line_item->percent() / 100 );
1143
					}else{
1144
						$total += $child_line_item->total();
1145
					}
1146
				}elseif( $child_line_item->type() === EEM_Line_Item::type_sub_total ){
1147
					$total += $child_line_item->taxable_total();
1148
				}
1149
			}
1150
		}
1151
		return max( $total, 0 );
1152
	}
1153
1154
1155
1156
	/**
1157
	 * Gets the transaction for this line item
1158
	 * @return EE_Transaction
1159
	 */
1160
	public function transaction() {
1161
		return $this->get_first_related( 'Transaction' );
1162
	}
1163
1164
1165
1166
	/**
1167
	 * Saves this line item to the DB, and recursively saves its descendants.
1168
	 * Because there currently is no proper parent-child relation on the model,
1169
	 * save_this_and_cached() will NOT save the descendants.
1170
	 * Also sets the transaction on this line item and all its descendants before saving
1171
	 * @param int $txn_id if none is provided, assumes $this->TXN_ID()
1172
	 * @return int count of items saved
1173
	 */
1174
	public function save_this_and_descendants_to_txn( $txn_id = NULL ) {
1175
		$count = 0;
1176
		if ( ! $txn_id ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $txn_id of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1177
			$txn_id = $this->TXN_ID();
1178
		}
1179
		$this->set_TXN_ID( $txn_id );
1180
		$children = $this->children();
1181
		$count += $this->save() ? 1 : 0;
1182
		foreach ( $children as $child_line_item ) {
1183
			if ( $child_line_item instanceof EE_Line_Item ) {
1184
				$child_line_item->set_parent_ID( $this->ID() );
1185
				$count += $child_line_item->save_this_and_descendants_to_txn( $txn_id );
1186
			}
1187
		}
1188
		return $count;
1189
	}
1190
1191
1192
1193
	/**
1194
	 * Saves this line item to the DB, and recursively saves its descendants.
1195
	 *
1196
	 * @return int count of items saved
1197
	 */
1198
	public function save_this_and_descendants() {
1199
		$count = 0;
1200
		$children = $this->children();
1201
		$count += $this->save() ? 1 : 0;
1202
		foreach ( $children as $child_line_item ) {
1203
			if ( $child_line_item instanceof EE_Line_Item ) {
1204
				$child_line_item->set_parent_ID( $this->ID() );
1205
				$count += $child_line_item->save_this_and_descendants();
1206
			}
1207
		}
1208
		return $count;
1209
	}
1210
1211
1212
1213
	/**
1214
	 * returns the cancellation line item if this item was cancelled
1215
	 * @return EE_Line_Item[]
1216
	 */
1217
	public function get_cancellations() {
1218
		EE_Registry::instance()->load_helper( 'Line_Item' );
1219
		return EEH_Line_Item::get_descendants_of_type( $this, EEM_Line_Item::type_cancellation );
1220
	}
1221
1222
1223
1224
	/**
1225
	 * If this item has an ID, then this saves it again to update the db
1226
	 *
1227
	 * @return int count of items saved
1228
	 */
1229
	public function maybe_save() {
1230
		if ( $this->ID() ) {
1231
			return $this->save();
1232
		}
1233
		return false;
1234
	}
1235
1236
	/**
1237
	 * clears the cached children and parent from the line item
1238
	 * @return void
1239
	 */
1240
	public function clear_related_line_item_cache() {
1241
		$this->_children = array();
1242
		$this->_parent = null;
1243
	}
1244
1245
1246
1247
    /**
1248
     * @param bool $raw
1249
     * @return int
1250
     * @throws \EE_Error
1251
     */
1252
    public function timestamp($raw = false)
1253
    {
1254
        return $raw ? $this->get_raw('LIN_timestamp') : $this->get('LIN_timestamp');
1255
    }
1256
1257
1258
1259
1260
    /************************* DEPRECATED *************************/
1261
1262
1263
1264
    /**
1265
     * @deprecated 4.6.0
1266
     * @param string $type one of the constants on EEM_Line_Item
1267
     * @return EE_Line_Item[]
1268
     */
1269
    protected function _get_descendants_of_type($type)
1270
    {
1271
        EE_Error::doing_it_wrong('EE_Line_Item::_get_descendants_of_type()',
1272
            __('Method replaced with EEH_Line_Item::get_descendants_of_type()', 'event_espresso'), '4.6.0');
1273
        return EEH_Line_Item::get_descendants_of_type($this, $type);
1274
    }
1275
1276
1277
1278
    /**
1279
     * @deprecated 4.6.0
1280
     * @param string $type like one of the EEM_Line_Item::type_*
1281
     * @return EE_Line_Item
1282
     */
1283
    public function get_nearest_descendant_of_type($type)
1284
    {
1285
        EE_Error::doing_it_wrong('EE_Line_Item::get_nearest_descendant_of_type()',
1286
            __('Method replaced with EEH_Line_Item::get_nearest_descendant_of_type()', 'event_espresso'), '4.6.0');
1287
        return EEH_Line_Item::get_nearest_descendant_of_type($this, $type);
1288
    }
1289
1290
1291
1292
}
1293