Completed
Branch TASK-9118-extensions-page (04eaec)
by
unknown
38:06 queued 23:39
created

EEH_Line_Item::visualize()   B

Complexity

Conditions 6
Paths 24

Size

Total Lines 20
Code Lines 14

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 20
rs 8.8571
cc 6
eloc 14
nc 24
nop 2
1
<?php if (!defined('EVENT_ESPRESSO_VERSION')) { exit('No direct script access allowed'); }
2
/**
3
 *
4
 * EEH_Line_Item
5
 *
6
 * This should be the main class which is aware of the line item tree structure, and
7
 * should take care of common operations like inserting items into it, updating
8
 * items in it based on what the line items are for, and removed line items.
9
 * All this logic was originally contained in EE_Cart, but because there are
10
 * actually other places that need to modify the record of what was purchased
11
 * (eg when a PayPal IPN is received, if PayPal changes the taxes, we need to update the line items;
12
 * or admin-side cancellations etc).
13
 * Generally all these functions will first take the total line item and figure things out from there
14
 *
15
 * @package			Event Espresso
16
 * @subpackage
17
 * @author				Mike Nelson
18
 *
19
 */
20
class EEH_Line_Item {
21
22
	//other functions: cancel ticket purchase
23
	//delete ticket purchase
24
	//add promotion
25
	/**
26
	 * Adds a simple item ( unrelated to any other model object) to the total line item
27
	 * in the correct spot in the line item tree (also verifying it doesn't add a duplicate based on the LIN_code)
28
	 * beneath the pre-tax-total (alongside event subtotals).
29
	 * Automatically re-calculates the line item totals and updates the related transaction. But
30
	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
31
	 * should probably change because of this).
32
	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
33
	 * after using this, to keep the registration final prices in-sync with the transaction's total.
34
	 * @param EE_Line_Item $parent_line_item
35
	 * @param string $name
36
	 * @param float $unit_price
37
	 * @param string $description
38
	 * @param int $quantity
39
	 * @param boolean $taxable
40
	 * @param boolean $code if set to a value, ensures there is only one line item with that code
41
	 * @return boolean success
42
	 */
43
	public static function add_unrelated_item( EE_Line_Item $parent_line_item, $name, $unit_price, $description = '', $quantity = 1, $taxable = FALSE, $code = NULL  ){
44
		$items_subtotal = self::get_pre_tax_subtotal( $parent_line_item );
45
		$line_item = EE_Line_Item::new_instance(array(
46
			'LIN_name' => $name,
47
			'LIN_desc' => $description,
48
			'LIN_unit_price' => $unit_price,
49
			'LIN_quantity' => $quantity,
50
			'LIN_percent' => null,
51
			'LIN_is_taxable' => $taxable,
52
			'LIN_order' => $items_subtotal instanceof EE_Line_Item ? count( $items_subtotal->children() ) : 0,
53
			'LIN_total' => floatval( $unit_price ) * intval( $quantity ),
54
			'LIN_type'=>  EEM_Line_Item::type_line_item,
55
			'LIN_code' => $code,
56
		));
57
		$line_item = apply_filters(
58
			'FHEE__EEH_Line_Item__add_unrelated_item__line_item',
59
			$line_item,
60
			$parent_line_item
61
		);
62
		return self::add_item( $parent_line_item, $line_item );
63
	}
64
65
66
67
	/**
68
	 * Adds a simple item ( unrelated to any other model object) to the total line item,
69
	 * in the correct spot in the line item tree. Automatically
70
	 * re-calculates the line item totals and updates the related transaction. But
71
	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
72
	 * should probably change because of this).
73
	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
74
	 * after using this, to keep the registration final prices in-sync with the transaction's total.
75
	 *
76
	 * @param EE_Line_Item $parent_line_item
77
	 * @param string       $name
78
	 * @param float        $percentage_amount
79
	 * @param string       $description
80
	 * @param boolean      $taxable
81
	 * @return boolean success
82
	 */
83
	public static function add_percentage_based_item( EE_Line_Item $parent_line_item, $name, $percentage_amount, $description = '', $taxable = FALSE ){
84
		$line_item = EE_Line_Item::new_instance(array(
85
			'LIN_name' => $name,
86
			'LIN_desc' => $description,
87
			'LIN_unit_price' => 0,
88
			'LIN_percent' => $percentage_amount,
89
			'LIN_quantity' => NULL,
90
			'LIN_is_taxable' => $taxable,
91
			'LIN_total' => floatval( $percentage_amount * ( $parent_line_item->total() / 100 )),
92
			'LIN_type'=>  EEM_Line_Item::type_line_item,
93
			'LIN_parent' => $parent_line_item->ID()
94
		));
95
		$line_item = apply_filters(
96
			'FHEE__EEH_Line_Item__add_percentage_based_item__line_item',
97
			$line_item
98
		);
99
		return self::add_item( $parent_line_item, $line_item );
100
	}
101
102
103
104
	/**
105
	 * Returns the new line item created by adding a purchase of the ticket
106
	 * ensures that ticket line item is saved, and that cart total has been recalculated.
107
	 * If this ticket has already been purchased, just increments its count.
108
	 * Automatically re-calculates the line item totals and updates the related transaction. But
109
	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
110
	 * should probably change because of this).
111
	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
112
	 * after using this, to keep the registration final prices in-sync with the transaction's total.
113
	 *
114
	 * @param EE_Line_Item $total_line_item grand total line item of type EEM_Line_Item::type_total
115
	 * @param EE_Ticket $ticket
116
	 * @param int $qty
117
	 * @return \EE_Line_Item
118
	 * @throws \EE_Error
119
	 */
120
	public static function add_ticket_purchase( EE_Line_Item $total_line_item, EE_Ticket $ticket, $qty = 1 ){
121
		if ( ! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total() ) {
122
			throw new EE_Error( sprintf( __( 'A valid line item total is required in order to add tickets. A line item of type "%s" was passed.', 'event_espresso' ), $ticket->ID(), $total_line_item->ID() ) );
123
		}
124
		// either increment the qty for an existing ticket
125
		$line_item = self::increment_ticket_qty_if_already_in_cart( $total_line_item, $ticket, $qty );
126
		// or add a new one
127
		if ( ! $line_item instanceof EE_Line_Item ) {
128
			$line_item = self::create_ticket_line_item( $total_line_item, $ticket, $qty );
129
		}
130
		$total_line_item->recalculate_total_including_taxes();
131
		return $line_item;
132
	}
133
134
135
136
	/**
137
	 * Returns the new line item created by adding a purchase of the ticket
138
	 * @param \EE_Line_Item $total_line_item
139
	 * @param EE_Ticket $ticket
140
	 * @param int $qty
141
	 * @return \EE_Line_Item
142
	 * @throws \EE_Error
143
	 */
144
	public static function increment_ticket_qty_if_already_in_cart( EE_Line_Item $total_line_item, EE_Ticket $ticket, $qty = 1 ) {
145
		$line_item = null;
146
		if ( $total_line_item instanceof EE_Line_Item && $total_line_item->is_total() ) {
147
			$ticket_line_items = EEH_Line_Item::get_ticket_line_items( $total_line_item );
148
			foreach ( (array)$ticket_line_items as $ticket_line_item ) {
149
				if ( $ticket_line_item instanceof EE_Line_Item && $ticket_line_item->OBJ_ID() == $ticket->ID() ) {
150
					$line_item = $ticket_line_item;
151
					break;
152
				}
153
			}
154
		}
155
		if ( $line_item instanceof EE_Line_Item ) {
156
			EEH_Line_Item::increment_quantity( $line_item, $qty );
157
			return $line_item;
158
		}
159
		return null;
160
	}
161
162
	/**
163
	 * Increments the line item and all its children's quantity by $qty (but percent line items are unaffected).
164
	 * Does NOT save or recalculate other line items totals
165
	 * @param EE_Line_Item $line_item
166
	 * @param int $qty
167
	 * @return void
168
	 */
169
	public static function increment_quantity( EE_Line_Item $line_item, $qty = 1 ) {
170
		if( ! $line_item->is_percent() ) {
171
			$qty += $line_item->quantity();
172
			$line_item->set_quantity( $qty );
173
			$line_item->set_total( $line_item->unit_price() * $qty );
174
		}
175
		foreach( $line_item->children() as $child ) {
176
			if( $child->is_sub_line_item() ) {
177
				EEH_Line_Item::update_quantity( $child, $line_item->quantity() );
0 ignored issues
show
Documentation introduced by
$line_item->quantity() is of type boolean, but the function expects a integer.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
178
			}
179
		}
180
	}
181
182
	/**
183
	 * Updates the line item and its children's quantities to the specified number.
184
	 * Does NOT save them or recalculate totals.
185
	 * @param EE_Line_Item $line_item
186
	 * @param int $new_quantity
187
	 */
188
	public static function update_quantity( EE_Line_Item $line_item, $new_quantity ) {
189
		if( ! $line_item->is_percent() ) {
190
			$line_item->set_quantity( $new_quantity );
191
			$line_item->set_total( $line_item->unit_price() * $new_quantity );
192
		}
193
		foreach( $line_item->children() as $child ) {
194
			if( $child->is_sub_line_item() ) {
195
				EEH_Line_Item::update_quantity( $child, $new_quantity );
196
			}
197
		}
198
	}
199
200
201
202
	/**
203
	 * Returns the new line item created by adding a purchase of the ticket
204
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
205
	 * @param EE_Ticket $ticket
206
	 * @param int $qty
207
	 * @return \EE_Line_Item
208
	 * @throws \EE_Error
209
	 */
210
	public static function create_ticket_line_item( EE_Line_Item $total_line_item, EE_Ticket $ticket, $qty = 1 ) {
211
		$datetimes = $ticket->datetimes();
212
		$first_datetime = reset( $datetimes );
213
		if( $first_datetime instanceof EE_Datetime &&
214
				$first_datetime->event() instanceof EE_Event ) {
215
			$first_datetime_name = $first_datetime->event()->name();
216
		} else {
217
			$first_datetime_name = __( 'Event', 'event_espresso' );
218
		}
219
		$event = sprintf( _x( '(For %1$s)', '(For Event Name)', 'event_espresso' ), $first_datetime_name );
220
		// get event subtotal line
221
		$events_sub_total = self::get_event_line_item_for_ticket( $total_line_item, $ticket );
222
		if ( ! $events_sub_total instanceof EE_Line_Item ) {
223
			throw new EE_Error( sprintf( __( 'There is no events sub-total for ticket %s on total line item %d', 'event_espresso' ), $ticket->ID(), $total_line_item->ID() ) );
224
		}
225
		// add $ticket to cart
226
		$line_item = EE_Line_Item::new_instance( array(
227
			'LIN_name'       	=> $ticket->name(),
228
			'LIN_desc'       		=> $ticket->description() != '' ? $ticket->description() . ' ' . $event : $event,
229
			'LIN_unit_price' 	=> $ticket->price(),
230
			'LIN_quantity'   	=> $qty,
231
			'LIN_is_taxable' 	=> $ticket->taxable(),
232
			'LIN_order'      	=> count( $events_sub_total->children() ),
233
			'LIN_total'      		=> $ticket->price() * $qty,
234
			'LIN_type'       		=> EEM_Line_Item::type_line_item,
235
			'OBJ_ID'         		=> $ticket->ID(),
236
			'OBJ_type'       	=> 'Ticket'
237
		) );
238
		$line_item = apply_filters(
239
			'FHEE__EEH_Line_Item__create_ticket_line_item__line_item',
240
			$line_item
241
		);
242
		$events_sub_total->add_child_line_item( $line_item );
243
		//now add the sub-line items
244
		$running_total_for_ticket = 0;
245
		foreach ( $ticket->prices( array( 'order_by' => array( 'PRC_order' => 'ASC' ) ) ) as $price ) {
246
			$sign = $price->is_discount() ? -1 : 1;
0 ignored issues
show
Documentation Bug introduced by
The method is_discount does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
247
			$price_total = $price->is_percent() ? $running_total_for_ticket * $price->amount() / 100 : $price->amount() * $qty;
0 ignored issues
show
Documentation Bug introduced by
The method is_percent does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
Documentation Bug introduced by
The method amount does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
248
			$sub_line_item = EE_Line_Item::new_instance( array(
249
				'LIN_name'       	=> $price->name(),
250
				'LIN_desc'       		=> $price->desc(),
0 ignored issues
show
Documentation Bug introduced by
The method desc does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
251
				'LIN_quantity'   	=> $price->is_percent() ? null : $qty,
0 ignored issues
show
Documentation Bug introduced by
The method is_percent does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
252
				'LIN_is_taxable' 	=> false,
253
				'LIN_order'      	=> $price->order(),
0 ignored issues
show
Documentation Bug introduced by
The method order does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
254
				'LIN_total'      		=> $sign * $price_total,
255
				'LIN_type'       		=> EEM_Line_Item::type_sub_line_item,
256
				'OBJ_ID'         		=> $price->ID(),
257
				'OBJ_type'       	=> 'Price'
258
			) );
259
			$sub_line_item = apply_filters(
260
				'FHEE__EEH_Line_Item__create_ticket_line_item__sub_line_item',
261
				$sub_line_item
262
			);
263
			if ( $price->is_percent() ) {
0 ignored issues
show
Documentation Bug introduced by
The method is_percent does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
264
				$sub_line_item->set_percent( $sign * $price->amount() );
0 ignored issues
show
Documentation Bug introduced by
The method amount does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
265
			} else {
266
				$sub_line_item->set_unit_price( $sign * $price->amount() );
0 ignored issues
show
Documentation Bug introduced by
The method amount does not exist on object<EE_Base_Class>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
267
			}
268
			$running_total_for_ticket += $price_total;
269
			$line_item->add_child_line_item( $sub_line_item );
270
		}
271
		return $line_item;
272
	}
273
274
275
276
	/**
277
	 * Adds the specified item under the pre-tax-sub-total line item. Automatically
278
	 * re-calculates the line item totals and updates the related transaction. But
279
	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
280
	 * should probably change because of this).
281
	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
282
	 * after using this, to keep the registration final prices in-sync with the transaction's total.
283
	 * @param EE_Line_Item $total_line_item
284
	 * @param EE_Line_Item $item to be added
285
	 * @return boolean
286
	 */
287
	public static function add_item( EE_Line_Item $total_line_item, EE_Line_Item $item ){
288
		$pre_tax_subtotal = self::get_pre_tax_subtotal( $total_line_item );
289
		if ( $pre_tax_subtotal instanceof EE_Line_Item ){
290
			$success = $pre_tax_subtotal->add_child_line_item($item);
291
		}else{
292
			return FALSE;
293
		}
294
		$total_line_item->recalculate_total_including_taxes();
295
		return $success;
296
	}
297
298
299
300
	/**
301
	 * Gets the line item which contains the subtotal of all the items
302
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
303
	 *	@return \EE_Line_Item
304
	 */
305
	public static function get_pre_tax_subtotal( EE_Line_Item $total_line_item ){
306
		$pre_tax_subtotal = $total_line_item->get_child_line_item( 'pre-tax-subtotal' );
307
		return $pre_tax_subtotal instanceof EE_Line_Item ? $pre_tax_subtotal : self::create_pre_tax_subtotal( $total_line_item );
308
	}
309
310
311
312
	/**
313
	 * Gets the line item for the taxes subtotal
314
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
315
	 * @return \EE_Line_Item
316
	 */
317
	public static function get_taxes_subtotal( EE_Line_Item $total_line_item ){
318
		$taxes = $total_line_item->get_child_line_item( 'taxes' );
319
		return $taxes ? $taxes : self::create_taxes_subtotal( $total_line_item );
320
	}
321
322
323
324
	/**
325
	 * sets the TXN ID on an EE_Line_Item if passed a valid EE_Transaction object
326
	 * @param EE_Line_Item $line_item
327
	 * @param EE_Transaction $transaction
328
	 * @return void
329
	 */
330
	public static function set_TXN_ID( EE_Line_Item $line_item, $transaction = NULL ){
331
		if( $transaction ){
332
			/** @type EEM_Transaction $EEM_Transaction */
333
			$EEM_Transaction = EE_Registry::instance()->load_model( 'Transaction' );
334
			$transaction = $EEM_Transaction->ensure_is_ID( $transaction );
335
			$line_item->set_TXN_ID( $transaction );
336
		}
337
	}
338
339
340
341
	/**
342
	 * Creates a new default total line item for the transaction,
343
	 * and its tickets subtotal and taxes subtotal line items (and adds the
344
	 * existing taxes as children of the taxes subtotal line item)
345
	 * @param EE_Transaction $transaction
346
	 * @return \EE_Line_Item of type total
347
	 */
348 View Code Duplication
	public static function create_total_line_item( $transaction = NULL ){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
349
		$total_line_item = EE_Line_Item::new_instance( array(
350
			'LIN_code'	=> 'total',
351
			'LIN_name'	=> __('Grand Total', 'event_espresso'),
352
			'LIN_type'	=> EEM_Line_Item::type_total,
353
			'OBJ_type'	=>'Transaction'
354
		));
355
		$total_line_item = apply_filters(
356
			'FHEE__EEH_Line_Item__create_total_line_item__total_line_item',
357
			$total_line_item
358
		);
359
		self::set_TXN_ID( $total_line_item, $transaction );
360
		self::create_pre_tax_subtotal( $total_line_item, $transaction );
361
		self::create_taxes_subtotal( $total_line_item, $transaction );
362
		return $total_line_item;
363
	}
364
365
366
367
	/**
368
	 * Creates a default items subtotal line item
369
	 * @param EE_Line_Item $total_line_item
370
	 * @param EE_Transaction $transaction
371
	 * @return EE_Line_Item
372
	 */
373 View Code Duplication
	protected static function create_pre_tax_subtotal( EE_Line_Item $total_line_item, $transaction = NULL ){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
374
		$pre_tax_line_item = EE_Line_Item::new_instance( array(
375
			'LIN_code' 	=> 'pre-tax-subtotal',
376
			'LIN_name' 	=> __( 'Pre-Tax Subtotal', 'event_espresso' ),
377
			'LIN_type' 	=> EEM_Line_Item::type_sub_total
378
		) );
379
		$pre_tax_line_item = apply_filters(
380
			'FHEE__EEH_Line_Item__create_pre_tax_subtotal__pre_tax_line_item',
381
			$pre_tax_line_item
382
		);
383
		self::set_TXN_ID( $pre_tax_line_item, $transaction );
384
		$total_line_item->add_child_line_item( $pre_tax_line_item );
385
		self::create_event_subtotal( $pre_tax_line_item, $transaction );
386
		return $pre_tax_line_item;
387
	}
388
389
390
391
	/**
392
	 * Creates a line item for the taxes subtotal and finds all the tax prices
393
	 * and applies taxes to it
394
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
395
	 * @param EE_Transaction $transaction
396
	 * @return EE_Line_Item
397
	 */
398 View Code Duplication
	protected static function create_taxes_subtotal( EE_Line_Item $total_line_item, $transaction = NULL ){
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
399
		$tax_line_item = EE_Line_Item::new_instance(array(
400
			'LIN_code'	=> 'taxes',
401
			'LIN_name' 	=> __('Taxes', 'event_espresso'),
402
			'LIN_type'	=> EEM_Line_Item::type_tax_sub_total,
403
			'LIN_order' => 1000,//this should always come last
404
		));
405
		$tax_line_item = apply_filters(
406
			'FHEE__EEH_Line_Item__create_taxes_subtotal__tax_line_item',
407
			$tax_line_item
408
		);
409
		self::set_TXN_ID( $tax_line_item, $transaction );
410
		$total_line_item->add_child_line_item( $tax_line_item );
411
		//and lastly, add the actual taxes
412
		self::apply_taxes( $total_line_item );
413
		return $tax_line_item;
414
	}
415
416
417
418
	/**
419
	 * Creates a default items subtotal line item
420
	 * @param EE_Line_Item $pre_tax_line_item
421
	 * @param EE_Transaction $transaction
422
	 * @param EE_Event $event
423
	 * @return EE_Line_Item
424
	 */
425
	public static function create_event_subtotal( EE_Line_Item $pre_tax_line_item, $transaction = NULL, $event = NULL ){
426
		$event_line_item = EE_Line_Item::new_instance(array(
427
			'LIN_code'	=> self::get_event_code( $event ),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 425 can be null; however, EEH_Line_Item::get_event_code() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
428
			'LIN_name' 	=> self::get_event_name( $event ),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 425 can be null; however, EEH_Line_Item::get_event_name() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
429
			'LIN_desc' 	=> self::get_event_desc( $event ),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 425 can be null; however, EEH_Line_Item::get_event_desc() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
430
			'LIN_type'	=> EEM_Line_Item::type_sub_total,
431
			'OBJ_type' 	=> 'Event',
432
			'OBJ_ID' 		=>  $event instanceof EE_Event ? $event->ID() : 0
433
		));
434
		$event_line_item = apply_filters(
435
			'FHEE__EEH_Line_Item__create_event_subtotal__event_line_item',
436
			$event_line_item
437
		);
438
		self::set_TXN_ID( $event_line_item, $transaction );
439
		$pre_tax_line_item->add_child_line_item( $event_line_item );
440
		return $event_line_item;
441
	}
442
443
	/**
444
	 * Gets what the event ticket's code SHOULD be
445
	 * @param EE_Event $event
446
	 * @return string
447
	 */
448
	public static function get_event_code( $event ) {
449
		return 'event-' . ( $event instanceof EE_Event ? $event->ID() : '0' );
450
	}
451
452
	/**
453
	 * Gets the event name
454
	 * @param EE_Event $event
455
	 * @return string
456
	 */
457
	public static function get_event_name( $event ) {
458
		return $event instanceof EE_Event ? $event->name() : __( 'Event', 'event_espresso' );
459
	}
460
461
	/**
462
	 * Gets the event excerpt
463
	 * @param EE_Event $event
464
	 * @return string
465
	 */
466
	public static function get_event_desc( $event ) {
467
		return $event instanceof EE_Event ? $event->short_description() : '';
468
	}
469
470
	/**
471
	  * Given the grand total line item and a ticket, finds the event sub-total
472
	  * line item the ticket's purchase should be added onto
473
	  *
474
	  * @access public
475
	  * @param EE_Line_Item $grand_total the grand total line item
476
	  * @param EE_Ticket $ticket
477
	  * @throws \EE_Error
478
	  * @return EE_Line_Item
479
	  */
480
	public static function get_event_line_item_for_ticket( EE_Line_Item $grand_total, EE_Ticket $ticket ) {
481
		$first_datetime = $ticket->first_datetime();
482
		if( ! $first_datetime instanceof EE_Datetime ){
483
			throw new EE_Error( sprintf( __( 'The supplied ticket (ID %d) has no datetimes', 'event_espresso' ), $ticket->ID() ) );
484
		}
485
		$event = $first_datetime->event();
486
		if ( ! $event instanceof EE_Event ) {
487
			throw new EE_Error( sprintf( __( 'The supplied ticket (ID %d) has no event data associated with it.','event_espresso' ), $ticket->ID() ) );
488
		}
489
		return EEH_Line_Item::get_event_line_item( $grand_total, $event );
490
	}
491
492
	/**
493
	 * Gets the event line item
494
	 * @param EE_Line_Item $grand_total
495
	 * @param EE_Event $event
496
	 * @return EE_Line_Item for the event subtotal which is a child of $grand_total
497
	 */
498
	public static function get_event_line_item( EE_Line_Item $grand_total, $event ) {
499
		/** @type EE_Event $event */
500
		$event = EEM_Event::instance()->ensure_is_obj( $event, true );
501
		$event_line_item = NULL;
502
		$found = false;
503
		foreach ( EEH_Line_Item::get_event_subtotals( $grand_total ) as $event_line_item ) {
504
			// default event subtotal, we should only ever find this the first time this method is called
505
			if ( ! $event_line_item->OBJ_ID() ) {
506
				// let's use this! but first... set the event details
507
				EEH_Line_Item::set_event_subtotal_details( $event_line_item, $event );
508
				$found = true;
509
				break;
510
			} else if ( $event_line_item->OBJ_ID() === $event->ID() ) {
511
				// found existing line item for this event in the cart, so break out of loop and use this one
512
				$found = true;
513
				break;
514
			}
515
		}
516
		if ( ! $found ) {
517
			//there is no event sub-total yet, so add it
518
			$pre_tax_subtotal = EEH_Line_Item::get_pre_tax_subtotal( $grand_total );
519
			// create a new "event" subtotal below that
520
			$event_line_item = EEH_Line_Item::create_event_subtotal( $pre_tax_subtotal, null, $event );
521
			// and set the event details
522
			EEH_Line_Item::set_event_subtotal_details( $event_line_item, $event );
523
		}
524
		return $event_line_item;
525
	}
526
527
528
529
	/**
530
	 * Creates a default items subtotal line item
531
	 * @param EE_Line_Item $event_line_item
532
	 * @param EE_Event $event
533
	 * @param EE_Transaction $transaction
534
	 * @return EE_Line_Item
535
	 */
536
	public static function set_event_subtotal_details( EE_Line_Item $event_line_item, EE_Event $event, $transaction = NULL ){
537
		if ( $event instanceof EE_Event ) {
538
			$event_line_item->set_code( self::get_event_code( $event ) );
539
			$event_line_item->set_name( self::get_event_name( $event ) );
540
			$event_line_item->set_desc( self::get_event_desc( $event ) );
0 ignored issues
show
Bug introduced by
It seems like self::get_event_desc($event) targeting EEH_Line_Item::get_event_desc() can also be of type boolean; however, EE_Line_Item::set_desc() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
541
			$event_line_item->set_OBJ_ID( $event->ID() );
542
		}
543
		self::set_TXN_ID( $event_line_item, $transaction );
544
	}
545
546
547
548
	/**
549
	 * Finds what taxes should apply, adds them as tax line items under the taxes sub-total,
550
	 * and recalculates the taxes sub-total and the grand total. Resets the taxes, so
551
	 * any old taxes are removed
552
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
553
	 */
554
	public static function apply_taxes( EE_Line_Item $total_line_item ){
555
		/** @type EEM_Price $EEM_Price */
556
		$EEM_Price = EE_Registry::instance()->load_model( 'Price' );
557
		// get array of taxes via Price Model
558
		$ordered_taxes = $EEM_Price->get_all_prices_that_are_taxes();
559
		ksort( $ordered_taxes );
560
		$taxes_line_item = self::get_taxes_subtotal( $total_line_item );
561
		//just to be safe, remove its old tax line items
562
		$taxes_line_item->delete_children_line_items();
563
		//loop thru taxes
564
		foreach ( $ordered_taxes as $order => $taxes ) {
565
			foreach ( $taxes as $tax ) {
566
				if ( $tax instanceof EE_Price ) {
567
					$tax_line_item = EE_Line_Item::new_instance(
568
						array(
569
							'LIN_name'       => $tax->name(),
570
							'LIN_desc'       => $tax->desc(),
571
							'LIN_percent'    => $tax->amount(),
572
							'LIN_is_taxable' => false,
573
							'LIN_order'      => $order,
574
							'LIN_total'      => 0,
575
							'LIN_type'       => EEM_Line_Item::type_tax,
576
							'OBJ_type'       => 'Price',
577
							'OBJ_ID'         => $tax->ID()
578
						)
579
					);
580
					$tax_line_item = apply_filters(
581
						'FHEE__EEH_Line_Item__apply_taxes__tax_line_item',
582
						$tax_line_item
583
					);
584
					$taxes_line_item->add_child_line_item( $tax_line_item );
585
				}
586
			}
587
		}
588
		$total_line_item->recalculate_total_including_taxes();
589
	}
590
591
592
593
	/**
594
	 * Ensures that taxes have been applied to the order, if not applies them.
595
	 * Returns the total amount of tax
596
	 * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
597
	 * @return float
598
	 */
599
	public static function ensure_taxes_applied( $total_line_item ){
600
		$taxes_subtotal = self::get_taxes_subtotal( $total_line_item );
601
		if( ! $taxes_subtotal->children()){
602
			self::apply_taxes( $total_line_item );
603
		}
604
		return $taxes_subtotal->total();
605
	}
606
607
608
609
	/**
610
	 * Deletes ALL children of the passed line item
611
	 *
612
	 * @param EE_Line_Item $parent_line_item
613
	 * @return bool
614
	 */
615
	public static function delete_all_child_items( EE_Line_Item $parent_line_item ) {
616
		$deleted = 0;
617
		foreach ( $parent_line_item->children() as $child_line_item ) {
618
			if ( $child_line_item instanceof EE_Line_Item ) {
619
				$deleted += EEH_Line_Item::delete_all_child_items( $child_line_item );
620
				if ( $child_line_item->ID() ) {
621
					$child_line_item->delete();
622
					unset( $child_line_item );
623
				} else {
624
					$parent_line_item->delete_child_line_item( $child_line_item->code() );
0 ignored issues
show
Documentation introduced by
$child_line_item->code() is of type boolean, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
625
				}
626
				$deleted++;
627
			}
628
		}
629
		return $deleted;
630
	}
631
632
633
634
	/**
635
	 * Deletes the line items as indicated by the line item code(s) provided,
636
	 * regardless of where they're found in the line item tree. Automatically
637
	 * re-calculates the line item totals and updates the related transaction. But
638
	 * DOES NOT automatically upgrade the transaction's registrations' final prices (which
639
	 * should probably change because of this).
640
	 * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
641
	 * after using this, to keep the registration final prices in-sync with the transaction's total.
642
	 * @param EE_Line_Item      $total_line_item of type EEM_Line_Item::type_total
643
	 * @param array|bool|string $line_item_codes
644
	 * @return int number of items successfully removed
645
	 */
646
	public static function delete_items( EE_Line_Item $total_line_item, $line_item_codes = FALSE ) {
647
648
		if( $total_line_item->type() != EEM_Line_Item::type_total ){
649
			EE_Error::doing_it_wrong('EEH_Line_Item::delete_items', __( 'This static method should only be called with a TOTAL line item, otherwise we won\'t recalculate the totals correctly', 'event_espresso' ), '4.6.18' );
650
		}
651
		do_action( 'AHEE_log', __FILE__, __FUNCTION__, '' );
652
653
		// check if only a single line_item_id was passed
654
		if ( ! empty( $line_item_codes ) && ! is_array( $line_item_codes )) {
655
			// place single line_item_id in an array to appear as multiple line_item_ids
656
			$line_item_codes = array ( $line_item_codes );
657
		}
658
		$removals = 0;
659
		// cycle thru line_item_ids
660
		foreach ( $line_item_codes as $line_item_id ) {
0 ignored issues
show
Bug introduced by
The expression $line_item_codes of type array|boolean|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
661
			$removals += $total_line_item->delete_child_line_item($line_item_id);
662
		}
663
664
		if ( $removals > 0 ) {
665
			$total_line_item->recalculate_taxes_and_tax_total();
666
			return $removals;
667
		} else {
668
			return FALSE;
669
		}
670
	}
671
672
673
674
	/**
675
	 * Overwrites the previous tax by clearing out the old taxes, and creates a new
676
	 * tax and updates the total line item accordingly
677
	 * @param EE_Line_Item $total_line_item
678
	 * @param float $amount
679
	 * @param string $name
680
	 * @param string $description
681
	 * @param string $code
682
	 * @param boolean $add_to_existing_line_item
683
	 *                                           if true, and a duplicate line item with the same code is found,
684
	 *                                           $amount will be added onto it; otherwise will simply set the taxes to match $amount
685
	 * @return EE_Line_Item the new tax line item created
686
	 */
687
	public static function set_total_tax_to( EE_Line_Item $total_line_item, $amount, $name = NULL, $description = NULL, $code = NULL, $add_to_existing_line_item = false ){
688
            $tax_subtotal = self::get_taxes_subtotal( $total_line_item );
689
            $taxable_total = $total_line_item->taxable_total();
690
691
            if( $add_to_existing_line_item ) {
692
                $new_tax = $tax_subtotal->get_child_line_item( $code );
693
                EEM_Line_Item::instance()->delete( array( array( 'LIN_code' => array( '!=', $code ), 'LIN_parent' => $tax_subtotal->ID() ) ) );
694
            } else {
695
                $new_tax = null;
696
                $tax_subtotal->delete_children_line_items();
697
            }
698
            if( $new_tax ) {
699
                $new_tax->set_total( $new_tax->total() + $amount );
700
                $new_tax->set_percent( $taxable_total ? ( $new_tax->total() ) / $taxable_total * 100 : 0 );
701
            } else {
702
                //no existing tax item. Create it
703
				$new_tax = EE_Line_Item::new_instance( array(
704
					'TXN_ID'      => $total_line_item->TXN_ID(),
705
					'LIN_name'    => $name ? $name : __( 'Tax', 'event_espresso' ),
706
					'LIN_desc'    => $description ? $description : '',
707
					'LIN_percent' => $taxable_total ? ( $amount / $taxable_total * 100 ) : 0,
708
					'LIN_total'   => $amount,
709
					'LIN_parent'  => $tax_subtotal->ID(),
710
					'LIN_type'    => EEM_Line_Item::type_tax,
711
					'LIN_code'    => $code
712
				) );
713
			}
714
715
            $new_tax = apply_filters(
716
				'FHEE__EEH_Line_Item__set_total_tax_to__new_tax_subtotal',
717
				$new_tax,
718
				$total_line_item
719
            );
720
            $new_tax->save();
721
            $tax_subtotal->set_total( $new_tax->total() );
722
            $tax_subtotal->save();
723
            $total_line_item->recalculate_total_including_taxes();
724
            return $new_tax;
725
	}
726
727
728
729
	/**
730
	 * Makes all the line items which are children of $line_item taxable (or not).
731
	 * Does NOT save the line items
732
	 * @param EE_Line_Item $line_item
733
	 * @param string $code_substring_for_whitelist if this string is part of the line item's code
734
	 *  it will be whitelisted (ie, except from becoming taxable)
735
	 * @param boolean $taxable
736
	 */
737
	public static function set_line_items_taxable( EE_Line_Item $line_item, $taxable = true, $code_substring_for_whitelist = null ) {
738
		if( $code_substring_for_whitelist !== null ) {
739
			$whitelisted = strpos( $line_item->code(), $code_substring_for_whitelist ) !== false ? true : false;
740
		} else {
741
			$whitelisted = false;
742
		}
743
		if( $line_item->is_line_item() && ! $whitelisted ) {
744
			$line_item->set_is_taxable( $taxable );
745
		}
746
		foreach( $line_item->children() as $child_line_item ) {
747
			EEH_Line_Item::set_line_items_taxable( $child_line_item, $taxable, $code_substring_for_whitelist );
748
		}
749
	}
750
751
752
753
	/**
754
	 * Gets all descendants that are event subtotals
755
	 *
756
	 * @uses  EEH_Line_Item::get_subtotals_of_object_type()
757
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
758
	 * @return EE_Line_Item[]
759
	 */
760
	public static function get_event_subtotals( EE_Line_Item $parent_line_item ) {
761
		return self::get_subtotals_of_object_type( $parent_line_item, 'Event' );
762
	}
763
764
765
766
	/**
767
	 * Gets all descendants subtotals that match the supplied object type
768
	 *
769
	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
770
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
771
	 * @param string $obj_type
772
	 * @return EE_Line_Item[]
773
	 */
774
	public static function get_subtotals_of_object_type( EE_Line_Item $parent_line_item, $obj_type = '' ) {
775
		return self::_get_descendants_by_type_and_object_type( $parent_line_item, EEM_Line_Item::type_sub_total, $obj_type );
776
	}
777
778
779
780
	/**
781
	 * Gets all descendants that are tickets
782
	 *
783
	 * @uses  EEH_Line_Item::get_line_items_of_object_type()
784
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
785
	 * @return EE_Line_Item[]
786
	 */
787
	public static function get_ticket_line_items( EE_Line_Item $parent_line_item ) {
788
		return self::get_line_items_of_object_type( $parent_line_item, 'Ticket' );
789
	}
790
791
792
793
	/**
794
	 * Gets all descendants subtotals that match the supplied object type
795
	 *
796
	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
797
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
798
	 * @param string $obj_type
799
	 * @return EE_Line_Item[]
800
	 */
801
	public static function get_line_items_of_object_type( EE_Line_Item $parent_line_item, $obj_type = '' ) {
802
		return self::_get_descendants_by_type_and_object_type( $parent_line_item, EEM_Line_Item::type_line_item, $obj_type );
803
	}
804
805
806
807
	/**
808
	 * Gets all the descendants (ie, children or children of children etc) that are of the type 'tax'
809
	 * @uses  EEH_Line_Item::get_descendants_of_type()
810
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
811
	 * @return EE_Line_Item[]
812
	 */
813
	public static function get_tax_descendants( EE_Line_Item $parent_line_item ) {
814
		return EEH_Line_Item::get_descendants_of_type( $parent_line_item, EEM_Line_Item::type_tax );
815
	}
816
817
818
819
	/**
820
	 * Gets all the real items purchased which are children of this item
821
	 * @uses  EEH_Line_Item::get_descendants_of_type()
822
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
823
	 * @return EE_Line_Item[]
824
	 */
825
	public static function get_line_item_descendants( EE_Line_Item $parent_line_item ) {
826
		return EEH_Line_Item::get_descendants_of_type( $parent_line_item, EEM_Line_Item::type_line_item );
827
	}
828
829
830
831
	/**
832
	 * Gets all descendants of supplied line item that match the supplied line item type
833
	 *
834
	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
835
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
836
	 * @param string $line_item_type one of the EEM_Line_Item constants
837
	 * @return EE_Line_Item[]
838
	 */
839
	public static function get_descendants_of_type( EE_Line_Item $parent_line_item, $line_item_type ) {
840
		return self::_get_descendants_by_type_and_object_type( $parent_line_item, $line_item_type, NULL );
841
	}
842
843
844
845
	/**
846
	 * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type as well
847
	 *
848
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
849
	 * @param string $line_item_type one of the EEM_Line_Item constants
850
	 * @param string | NULL $obj_type object model class name (minus prefix) or NULL to ignore object type when searching
851
	 * @return EE_Line_Item[]
852
	 */
853
	protected static function _get_descendants_by_type_and_object_type( EE_Line_Item $parent_line_item, $line_item_type, $obj_type = NULL ) {
854
		$objects = array();
855
		foreach ( $parent_line_item->children() as $child_line_item ) {
856
			if ( $child_line_item instanceof EE_Line_Item ) {
857
				if ( $child_line_item->type() == $line_item_type && ( $child_line_item->OBJ_type() == $obj_type || $obj_type === NULL )) {
858
					$objects[] = $child_line_item;
859
				} else {
860
					//go-through-all-its children looking for more matches
861
					$objects = array_merge( $objects, self::_get_descendants_by_type_and_object_type( $child_line_item, $line_item_type, $obj_type ));
862
				}
863
			}
864
		}
865
		return $objects;
866
	}
867
868
869
870
	/**
871
	 * Gets all descendants subtotals that match the supplied object type
872
	 *
873
	 * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
874
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
875
	 * @param string $OBJ_type object type (like Event)
876
	 * @param array $OBJ_IDs array of OBJ_IDs
877
	 * @return EE_Line_Item[]
878
	 */
879
	public static function get_line_items_by_object_type_and_IDs( EE_Line_Item $parent_line_item, $OBJ_type = '', $OBJ_IDs = array() ) {
880
		return self::_get_descendants_by_object_type_and_object_ID( $parent_line_item, $OBJ_type, $OBJ_IDs );
881
	}
882
883
884
885
	/**
886
	 * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type as well
887
	 *
888
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
889
	 * @param string $OBJ_type object type (like Event)
890
	 * @param array $OBJ_IDs array of OBJ_IDs
891
	 * @return EE_Line_Item[]
892
	 */
893
	protected static function _get_descendants_by_object_type_and_object_ID( EE_Line_Item $parent_line_item, $OBJ_type, $OBJ_IDs ) {
894
		$objects = array();
895
		foreach ( $parent_line_item->children() as $child_line_item ) {
896
			if ( $child_line_item instanceof EE_Line_Item ) {
897
				if ( is_array( $OBJ_IDs ) && $child_line_item->OBJ_type() == $OBJ_type && in_array( $child_line_item->OBJ_ID(), $OBJ_IDs )) {
898
					$objects[] = $child_line_item;
899
				} else {
900
					//go-through-all-its children looking for more matches
901
					$objects = array_merge( $objects, self::_get_descendants_by_object_type_and_object_ID( $child_line_item, $OBJ_type, $OBJ_IDs ));
902
				}
903
			}
904
		}
905
		return $objects;
906
	}
907
908
909
910
	/**
911
	 * Uses a breadth-first-search in order to find the nearest descendant of
912
	 * the specified type and returns it, else NULL
913
	 *
914
	 * @uses  EEH_Line_Item::_get_nearest_descendant()
915
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
916
	 * @param string $type like one of the EEM_Line_Item::type_*
917
	 * @return EE_Line_Item
918
	 */
919
	public static function get_nearest_descendant_of_type( EE_Line_Item $parent_line_item, $type ) {
920
		return self::_get_nearest_descendant( $parent_line_item, 'LIN_type' , $type );
921
	}
922
923
924
925
	/**
926
	 * Uses a breadth-first-search in order to find the nearest descendant having the specified LIN_code and returns it, else NULL
927
	 *
928
	 * @uses  EEH_Line_Item::_get_nearest_descendant()
929
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
930
	 * @param string $code any value used for LIN_code
931
	 * @return EE_Line_Item
932
	 */
933
	public static function get_nearest_descendant_having_code( EE_Line_Item $parent_line_item, $code ) {
934
		return self::_get_nearest_descendant( $parent_line_item, 'LIN_code' , $code );
935
	}
936
937
938
939
	/**
940
	 * Uses a breadth-first-search in order to find the nearest descendant having the specified LIN_code and returns it, else NULL
941
	 *
942
	 * @param \EE_Line_Item $parent_line_item - the line item to find descendants of
943
	 * @param string $search_field  name of EE_Line_Item property
944
	 * @param string $value any value stored in $search_field
945
	 * @return EE_Line_Item
946
	 */
947
	protected static function _get_nearest_descendant( EE_Line_Item $parent_line_item, $search_field, $value ) {
948
		foreach( $parent_line_item->children() as $child ){
949
			if ( $child->get( $search_field ) == $value ){
950
				return $child;
951
			}
952
		}
953
		foreach( $parent_line_item->children() as $child ){
954
			$descendant_found = self::_get_nearest_descendant( $child, $search_field, $value );
955
			if ( $descendant_found ){
956
				return $descendant_found;
957
			}
958
		}
959
		return NULL;
960
	}
961
962
963
964
965
	/**
966
	 * Prints out a representation of the line item tree
967
	 * @param EE_Line_Item $line_item
968
	 * @param int $indentation
969
	 * @return void
970
	 */
971
	public static function visualize( EE_Line_Item $line_item, $indentation = 0 ){
972
		echo "\n<br />";
973
		for( $i = 0; $i < $indentation; $i++ ){
974
			echo " - ";
975
		}
976
		if( $line_item->is_percent() ) {
977
			$breakdown = $line_item->percent() . '%';
978
		} else {
979
			$breakdown = '$' . $line_item->unit_price() . "x" . $line_item->quantity();
980
		}
981
		echo $line_item->name() . "( " . $line_item->ID() . " ) : " . $line_item->type() . " $" . $line_item->total() . "(" . $breakdown . ")";
982
		if( $line_item->is_taxable() ){
983
			echo "  * taxable";
984
		}
985
		if( $line_item->children() ){
986
			foreach($line_item->children() as $child){
987
				self::visualize($child, $indentation + 1);
988
			}
989
		}
990
	}
991
992
993
994
	/**
995
	 * Calculates the registration's final price, taking into account that they
996
	 * need to not only help pay for their OWN ticket, but also any transaction-wide surcharges and taxes,
997
	 * and receive a portion of any transaction-wide discounts.
998
	 * eg1, if I buy a $1 ticket and brent buys a $9 ticket, and we receive a $5 discount
999
	 * then I'll get 1/10 of that $5 discount, which is $0.50, and brent will get
1000
	 * 9/10ths of that $5 discount, which is $4.50. So my final price should be $0.50
1001
	 * and brent's final price should be $5.50.
1002
	 *
1003
	 * In order to do this, we basically need to traverse the line item tree calculating
1004
	 * the running totals (just as if we were recalculating the total), but when we identify
1005
	 * regular line items, we need to keep track of their share of the grand total.
1006
	 * Also, we need to keep track of the TAXABLE total for each ticket purchase, so
1007
	 * we can know how to apply taxes to it. (Note: "taxable total" does not equal the "pretax total"
1008
	 * when there are non-taxable items; otherwise they would be the same)
1009
	 *
1010
	 * @param EE_Line_Item $line_item
1011
	 * @param array $billable_ticket_quantities 		array of EE_Ticket IDs and their corresponding quantity that
1012
	 *                                          									can be included in price calculations at this moment
1013
	 * @return array 		keys are line items for tickets IDs and values are their share of the running total,
1014
	 *                                          plus the key 'total', and 'taxable' which also has keys of all the ticket IDs. Eg
1015
	 *                                          array(
1016
	 *                                          12 => 4.3
1017
	 *                                          23 => 8.0
1018
	 *                                          'total' => 16.6,
1019
	 *                                          'taxable' => array(
1020
	 *                                          12 => 10,
1021
	 *                                          23 => 4
1022
	 *                                          ).
1023
	 *                                          So to find which registrations have which final price, we need to find which line item
1024
	 *                                          is theirs, which can be done with
1025
	 *                                          `EEM_Line_Item::instance()->get_line_item_for_registration( $registration );`
1026
	 */
1027
	public static function calculate_reg_final_prices_per_line_item( EE_Line_Item $line_item, $billable_ticket_quantities = array() ) {
1028
		//init running grand total if not already
1029
		if ( ! isset( $running_totals[ 'total' ] ) ) {
0 ignored issues
show
Bug introduced by
The variable $running_totals seems only to be defined at a later point. As such the call to isset() seems to always evaluate to false.

This check marks calls to isset(...) or empty(...) that are found before the variable itself is defined. These will always have the same result.

This is likely the result of code being shifted around. Consider removing these calls.

Loading history...
1030
			$running_totals[ 'total' ] = 0;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$running_totals was never initialized. Although not strictly required by PHP, it is generally a good practice to add $running_totals = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1031
		}
1032
		if( ! isset( $running_totals[ 'taxable' ] ) ) {
1033
			$running_totals[ 'taxable' ] = array( 'total' => 0 );
1034
		}
1035
		foreach ( $line_item->children() as $child_line_item ) {
1036
			switch ( $child_line_item->type() ) {
1037
1038
				case EEM_Line_Item::type_sub_total :
1039
					$running_totals_from_subtotal = EEH_Line_Item::calculate_reg_final_prices_per_line_item( $child_line_item, $billable_ticket_quantities );
1040
					//combine arrays but preserve numeric keys
1041
					$running_totals = array_replace_recursive( $running_totals_from_subtotal, $running_totals );
1042
					$running_totals[ 'total' ] += $running_totals_from_subtotal[ 'total' ];
1043
					$running_totals[ 'taxable'][ 'total' ] += $running_totals_from_subtotal[ 'taxable' ][ 'total' ];
1044
					break;
1045
1046
				case EEM_Line_Item::type_tax_sub_total :
1047
1048
					//find how much the taxes percentage is
1049
					if ( $child_line_item->percent() != 0 ) {
1050
						$tax_percent_decimal = $child_line_item->percent() / 100;
1051
					} else {
1052
						$tax_percent_decimal = EE_Taxes::get_total_taxes_percentage() / 100;
1053
					}
1054
					//and apply to all the taxable totals, and add to the pretax totals
1055
					foreach ( $running_totals as $line_item_id => $this_running_total ) {
1056
						//"total" and "taxable" array key is an exception
1057
						if ( $line_item_id === 'taxable' ) {
1058
							continue;
1059
						}
1060
						$taxable_total = $running_totals[ 'taxable' ][ $line_item_id ];
1061
						$running_totals[ $line_item_id ] += ( $taxable_total * $tax_percent_decimal );
1062
					}
1063
					break;
1064
1065
				case EEM_Line_Item::type_line_item :
1066
1067
					// ticket line items or ????
1068
					if ( $child_line_item->OBJ_type() === 'Ticket' ) {
1069
						// kk it's a ticket
1070
						if ( isset( $running_totals[ $child_line_item->ID() ] ) ) {
1071
							//huh? that shouldn't happen.
1072
							$running_totals[ 'total' ] += $child_line_item->total();
1073
						} else {
1074
							//its not in our running totals yet. great.
1075
							if ( $child_line_item->is_taxable() ) {
1076
								$taxable_amount = $child_line_item->unit_price();
1077
							} else {
1078
								$taxable_amount = 0;
1079
							}
1080
							// are we only calculating totals for some tickets?
1081
							if ( isset( $billable_ticket_quantities[ $child_line_item->OBJ_ID() ] ) ) {
1082
								$quantity = $billable_ticket_quantities[ $child_line_item->OBJ_ID() ];
1083
								$running_totals[ $child_line_item->ID() ] = $quantity ? $child_line_item->unit_price() : 0;
1084
								$running_totals[ 'taxable' ][ $child_line_item->ID() ] = $quantity ? $taxable_amount : 0;
1085
							} else {
1086
								$quantity = $child_line_item->quantity();
1087
								$running_totals[ $child_line_item->ID() ] = $child_line_item->unit_price();
1088
								$running_totals[ 'taxable' ][ $child_line_item->ID() ] = $taxable_amount;
1089
							}
1090
							$running_totals[ 'taxable' ][ 'total' ] += $taxable_amount * $quantity;
1091
							$running_totals[ 'total' ] += $child_line_item->unit_price() * $quantity;
1092
						}
1093
					} else {
1094
						// it's some other type of item added to the cart
1095
						// it should affect the running totals
1096
						// basically we want to convert it into a PERCENT modifier. Because
1097
						// more clearly affect all registration's final price equally
1098
						$line_items_percent_of_running_total = $running_totals[ 'total' ] > 0 ? ( $child_line_item->total() / $running_totals[ 'total' ] ) + 1 : 1;
1099
						foreach ( $running_totals as $line_item_id => $this_running_total ) {
1100
							//the "taxable" array key is an exception
1101
							if ( $line_item_id === 'taxable' ) {
1102
								continue;
1103
							}
1104
							// update the running totals
1105
							// yes this actually even works for the running grand total!
1106
							$running_totals[ $line_item_id ] = $line_items_percent_of_running_total * $this_running_total;
1107
							if ( $child_line_item->is_taxable() ) {
1108
								$running_totals[ 'taxable' ][ $line_item_id ] = $line_items_percent_of_running_total * $running_totals[ 'taxable' ][ $line_item_id ];
1109
							}
1110
						}
1111
					}
1112
					break;
1113
			}
1114
		}
1115
		return $running_totals;
1116
	}
1117
1118
1119
1120
	/**
1121
	 * Creates a duplicate of the line item tree, except only includes billable items
1122
	 * and the portion of line items attributed to billable things
1123
	 * @param EE_Line_Item      $line_item
1124
	 * @param EE_Registration[] $registrations
1125
	 * @return \EE_Line_Item
1126
	 */
1127
	public static function billable_line_item_tree( EE_Line_Item $line_item, $registrations ) {
1128
		$copy_li = EEH_Line_Item::billable_line_item( $line_item, $registrations );
1129
		foreach ( $line_item->children() as $child_li ) {
1130
			$copy_li->add_child_line_item( EEH_Line_Item::billable_line_item_tree( $child_li, $registrations ) );
1131
		}
1132
		//if this is the grand total line item, make sure the totals all add up
1133
		//(we could have duplicated this logic AS we copied the line items, but
1134
		//it seems DRYer this way)
1135
		if ( $copy_li->type() === EEM_Line_Item::type_total ) {
1136
			$copy_li->recalculate_total_including_taxes();
1137
		}
1138
		return $copy_li;
1139
	}
1140
1141
1142
1143
	/**
1144
	 * Creates a new, unsaved line item from $line_item that factors in the
1145
	 * number of billable registrations on $registrations.
1146
	 * @param EE_Line_Item      $line_item
1147
	 * @return EE_Line_Item
1148
	 * @param EE_Registration[] $registrations
1149
	 */
1150
	public static function billable_line_item( EE_Line_Item $line_item, $registrations ) {
1151
		$new_li_fields = $line_item->model_field_array();
1152
		if ( $line_item->type() === EEM_Line_Item::type_line_item &&
1153
			$line_item->OBJ_type() === 'Ticket'
1154
		) {
1155
			$count = 0;
1156
			foreach ( $registrations as $registration ) {
1157
				if ( $line_item->OBJ_ID() === $registration->ticket_ID() &&
1158
					in_array( $registration->status_ID(), EEM_Registration::reg_statuses_that_allow_payment() )
1159
				) {
1160
					$count++;
1161
				}
1162
			}
1163
			$new_li_fields[ 'LIN_quantity' ] = $count;
1164
		}
1165
		//don't set the total. We'll leave that up to the code that calculates it
1166
		unset( $new_li_fields[ 'LIN_ID' ] );
1167
		unset( $new_li_fields[ 'LIN_parent' ] );
1168
		unset( $new_li_fields[ 'LIN_total' ] );
1169
		return EE_Line_Item::new_instance( $new_li_fields );
1170
	}
1171
1172
1173
1174
	/**
1175
	 * Returns a modified line item tree where all the subtotals which have a total of 0
1176
	 * are removed, and line items with a quantity of 0
1177
	 *
1178
	 * @param EE_Line_Item $line_item |null
1179
	 * @return \EE_Line_Item|null
1180
	 */
1181
	public static function non_empty_line_items( EE_Line_Item $line_item ) {
1182
		$copied_li = EEH_Line_Item::non_empty_line_item( $line_item );
1183
		if ( $copied_li === null ) {
1184
			return null;
1185
		}
1186
		//if this is an event subtotal, we want to only include it if it
1187
		//has a non-zero total and at least one ticket line item child
1188
		$ticket_children = 0;
1189
		foreach ( $line_item->children() as $child_li ) {
1190
			$child_li_copy = EEH_Line_Item::non_empty_line_items( $child_li );
1191
			if ( $child_li_copy !== null ) {
1192
				$copied_li->add_child_line_item( $child_li_copy );
1193
				if ( $child_li_copy->type() === EEM_Line_Item::type_line_item &&
1194
					$child_li_copy->OBJ_type() === 'Ticket'
1195
				) {
1196
					$ticket_children++;
1197
				}
1198
			}
1199
		}
1200
		//if this is an event subtotal with NO ticket children
1201
		//we basically want to ignore it
1202
		if ( $line_item->type() === EEM_Line_Item::type_sub_total &&
1203
			$line_item->OBJ_type() === 'Event' &&
1204
			$ticket_children === 0 &&
1205
			$line_item->total() === 0
1206
		) {
1207
			return null;
1208
		}
1209
		return $copied_li;
1210
	}
1211
1212
1213
1214
	/**
1215
	 * Creates a new, unsaved line item, but if it's a ticket line item
1216
	 * with a total of 0, or a subtotal of 0, returns null instead
1217
	 * @param EE_Line_Item      $line_item
1218
	 * @return EE_Line_Item
1219
	 */
1220
	public static function non_empty_line_item( EE_Line_Item $line_item ) {
1221 View Code Duplication
		if ( $line_item->type() === EEM_Line_Item::type_line_item &&
1222
			$line_item->OBJ_type() === 'Ticket' &&
1223
			$line_item->quantity() == 0
1224
		) {
1225
			return null;
1226
		}
1227
		$new_li_fields = $line_item->model_field_array();
1228
		//don't set the total. We'll leave that up to the code that calculates it
1229
		unset( $new_li_fields[ 'LIN_ID' ] );
1230
		unset( $new_li_fields[ 'LIN_parent' ] );
1231
		return EE_Line_Item::new_instance( $new_li_fields );
1232
	}
1233
1234
1235
1236
	/**************************************** @DEPRECATED METHODS ****************************************/
1237
1238
1239
1240
	/**
1241
	 * @deprecated
1242
	 * @param EE_Line_Item $total_line_item
1243
	 *	@return \EE_Line_Item
1244
	 */
1245
	public static function get_items_subtotal( EE_Line_Item $total_line_item ){
1246
		EE_Error::doing_it_wrong( 'EEH_Line_Item::get_items_subtotal()', __('Method replaced with EEH_Line_Item::get_pre_tax_subtotal()', 'event_espresso'), '4.6.0' );
1247
		return self::get_pre_tax_subtotal( $total_line_item );
1248
	}
1249
1250
1251
1252
	/**
1253
	 * @deprecated
1254
	 * @param EE_Transaction $transaction
1255
	 *	@return \EE_Line_Item
1256
	 */
1257
	public static function create_default_total_line_item( $transaction = NULL) {
1258
		EE_Error::doing_it_wrong( 'EEH_Line_Item::create_default_total_line_item()', __('Method replaced with EEH_Line_Item::create_total_line_item()', 'event_espresso'), '4.6.0' );
1259
		return self::create_total_line_item( $transaction );
1260
	}
1261
1262
1263
1264
	/**
1265
	 * @deprecated
1266
	 * @param EE_Line_Item $total_line_item
1267
	 * @param EE_Transaction $transaction
1268
	 *	@return \EE_Line_Item
1269
	 */
1270
	public static function create_default_tickets_subtotal( EE_Line_Item $total_line_item, $transaction = NULL) {
1271
		EE_Error::doing_it_wrong( 'EEH_Line_Item::create_default_tickets_subtotal()', __('Method replaced with EEH_Line_Item::create_pre_tax_subtotal()', 'event_espresso'), '4.6.0' );
1272
		return self::create_pre_tax_subtotal( $total_line_item, $transaction );
1273
	}
1274
1275
1276
1277
	/**
1278
	 * @deprecated
1279
	 * @param EE_Line_Item $total_line_item
1280
	 * @param EE_Transaction $transaction
1281
	 *	@return \EE_Line_Item
1282
	 */
1283
	public static function create_default_taxes_subtotal( EE_Line_Item $total_line_item, $transaction = NULL) {
1284
		EE_Error::doing_it_wrong( 'EEH_Line_Item::create_default_taxes_subtotal()', __('Method replaced with EEH_Line_Item::create_taxes_subtotal()', 'event_espresso'), '4.6.0' );
1285
		return self::create_taxes_subtotal( $total_line_item, $transaction );
1286
	}
1287
1288
1289
1290
	/**
1291
	 * @deprecated
1292
	 * @param EE_Line_Item $total_line_item
1293
	 * @param EE_Transaction $transaction
1294
	 *	@return \EE_Line_Item
1295
	 */
1296
	public static function create_default_event_subtotal( EE_Line_Item $total_line_item, $transaction = NULL) {
1297
		EE_Error::doing_it_wrong( 'EEH_Line_Item::create_default_event_subtotal()', __('Method replaced with EEH_Line_Item::create_event_subtotal()', 'event_espresso'), '4.6.0' );
1298
		return self::create_event_subtotal( $total_line_item, $transaction );
1299
	}
1300
1301
1302
1303
}
1304
// End of file EEH_Line_Item.helper.php
1305