Completed
Branch TASK/update-about-page (5cee29)
by
unknown
34:34 queued 26:08
created

EEH_Line_Item::apply_taxes()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 10
nop 2
dl 0
loc 45
rs 8.2666
c 0
b 0
f 0
1
<?php
2
3
use EventEspresso\core\exceptions\InvalidDataTypeException;
4
use EventEspresso\core\exceptions\InvalidInterfaceException;
5
6
/**
7
 * EEH_Line_Item
8
 * This should be the main class which is aware of the line item tree structure, and
9
 * should take care of common operations like inserting items into it, updating
10
 * items in it based on what the line items are for, and removed line items.
11
 * All this logic was originally contained in EE_Cart, but because there are
12
 * actually other places that need to modify the record of what was purchased
13
 * (eg when a PayPal IPN is received, if PayPal changes the taxes, we need to update the line items;
14
 * or admin-side cancellations etc).
15
 * Generally all these functions will first take the total line item and figure things out from there
16
 *
17
 * @package               Event Espresso
18
 * @subpackage
19
 * @author                Mike Nelson
20
 */
21
class EEH_Line_Item
22
{
23
24
    /**
25
     * Adds a simple item (unrelated to any other model object) to the provided PARENT line item.
26
     * Does NOT automatically re-calculate the line item totals or update the related transaction.
27
     * You should call recalculate_total_including_taxes() on the grant total line item after this
28
     * to update the subtotals, and EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
29
     * to keep the registration final prices in-sync with the transaction's total.
30
     *
31
     * @param EE_Line_Item $parent_line_item
32
     * @param string       $name
33
     * @param float        $unit_price
34
     * @param string       $description
35
     * @param int          $quantity
36
     * @param boolean      $taxable
37
     * @param boolean      $code if set to a value, ensures there is only one line item with that code
38
     * @return boolean success
39
     * @throws EE_Error
40
     * @throws InvalidArgumentException
41
     * @throws InvalidDataTypeException
42
     * @throws InvalidInterfaceException
43
     * @throws ReflectionException
44
     */
45
    public static function add_unrelated_item(
46
        EE_Line_Item $parent_line_item,
47
        $name,
48
        $unit_price,
49
        $description = '',
50
        $quantity = 1,
51
        $taxable = false,
52
        $code = null
53
    ) {
54
        $items_subtotal = self::get_pre_tax_subtotal($parent_line_item);
55
        $line_item = EE_Line_Item::new_instance(array(
56
            'LIN_name'       => $name,
57
            'LIN_desc'       => $description,
58
            'LIN_unit_price' => $unit_price,
59
            'LIN_quantity'   => $quantity,
60
            'LIN_percent'    => null,
61
            'LIN_is_taxable' => $taxable,
62
            'LIN_order'      => $items_subtotal instanceof EE_Line_Item ? count($items_subtotal->children()) : 0,
63
            'LIN_total'      => (float) $unit_price * (int) $quantity,
64
            'LIN_type'       => EEM_Line_Item::type_line_item,
65
            'LIN_code'       => $code,
66
        ));
67
        $line_item = apply_filters(
68
            'FHEE__EEH_Line_Item__add_unrelated_item__line_item',
69
            $line_item,
70
            $parent_line_item
71
        );
72
        return self::add_item($parent_line_item, $line_item);
73
    }
74
75
76
    /**
77
     * Adds a simple item ( unrelated to any other model object) to the total line item,
78
     * in the correct spot in the line item tree. Does not automatically
79
     * re-calculate the line item totals, nor update the related transaction, nor upgrade the transaction's
80
     * registrations' final prices (which should probably change because of this).
81
     * You should call recalculate_total_including_taxes() on the grand total line item, then
82
     * update the transaction's total, and EE_Registration_Processor::update_registration_final_prices()
83
     * after using this, to keep the registration final prices in-sync with the transaction's total.
84
     *
85
     * @param EE_Line_Item $parent_line_item
86
     * @param string       $name
87
     * @param float        $percentage_amount
88
     * @param string       $description
89
     * @param boolean      $taxable
90
     * @return boolean success
91
     * @throws EE_Error
92
     */
93
    public static function add_percentage_based_item(
94
        EE_Line_Item $parent_line_item,
95
        $name,
96
        $percentage_amount,
97
        $description = '',
98
        $taxable = false
99
    ) {
100
        $line_item = EE_Line_Item::new_instance(array(
101
            'LIN_name'       => $name,
102
            'LIN_desc'       => $description,
103
            'LIN_unit_price' => 0,
104
            'LIN_percent'    => $percentage_amount,
105
            'LIN_quantity'   => 1,
106
            'LIN_is_taxable' => $taxable,
107
            'LIN_total'      => (float) ($percentage_amount * ($parent_line_item->total() / 100)),
108
            'LIN_type'       => EEM_Line_Item::type_line_item,
109
            'LIN_parent'     => $parent_line_item->ID(),
110
        ));
111
        $line_item = apply_filters(
112
            'FHEE__EEH_Line_Item__add_percentage_based_item__line_item',
113
            $line_item
114
        );
115
        return $parent_line_item->add_child_line_item($line_item, false);
116
    }
117
118
119
    /**
120
     * Returns the new line item created by adding a purchase of the ticket
121
     * ensures that ticket line item is saved, and that cart total has been recalculated.
122
     * If this ticket has already been purchased, just increments its count.
123
     * Automatically re-calculates the line item totals and updates the related transaction. But
124
     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
125
     * should probably change because of this).
126
     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
127
     * after using this, to keep the registration final prices in-sync with the transaction's total.
128
     *
129
     * @param EE_Line_Item $total_line_item grand total line item of type EEM_Line_Item::type_total
130
     * @param EE_Ticket    $ticket
131
     * @param int          $qty
132
     * @return EE_Line_Item
133
     * @throws EE_Error
134
     * @throws InvalidArgumentException
135
     * @throws InvalidDataTypeException
136
     * @throws InvalidInterfaceException
137
     * @throws ReflectionException
138
     */
139
    public static function add_ticket_purchase(EE_Line_Item $total_line_item, EE_Ticket $ticket, $qty = 1)
140
    {
141
        if (! $total_line_item instanceof EE_Line_Item || ! $total_line_item->is_total()) {
142
            throw new EE_Error(
143
                sprintf(
144
                    esc_html__(
145
                        'A valid line item total is required in order to add tickets. A line item of type "%s" was passed.',
146
                        'event_espresso'
147
                    ),
148
                    $ticket->ID(),
149
                    $total_line_item->ID()
150
                )
151
            );
152
        }
153
        // either increment the qty for an existing ticket
154
        $line_item = self::increment_ticket_qty_if_already_in_cart($total_line_item, $ticket, $qty);
155
        // or add a new one
156
        if (! $line_item instanceof EE_Line_Item) {
157
            $line_item = self::create_ticket_line_item($total_line_item, $ticket, $qty);
158
        }
159
        $total_line_item->recalculate_total_including_taxes();
160
        return $line_item;
161
    }
162
163
164
    /**
165
     * Returns the new line item created by adding a purchase of the ticket
166
     *
167
     * @param EE_Line_Item $total_line_item
168
     * @param EE_Ticket    $ticket
169
     * @param int          $qty
170
     * @return EE_Line_Item
171
     * @throws EE_Error
172
     * @throws InvalidArgumentException
173
     * @throws InvalidDataTypeException
174
     * @throws InvalidInterfaceException
175
     * @throws ReflectionException
176
     */
177
    public static function increment_ticket_qty_if_already_in_cart(
178
        EE_Line_Item $total_line_item,
179
        EE_Ticket $ticket,
180
        $qty = 1
181
    ) {
182
        $line_item = null;
183
        if ($total_line_item instanceof EE_Line_Item && $total_line_item->is_total()) {
184
            $ticket_line_items = EEH_Line_Item::get_ticket_line_items($total_line_item);
185
            foreach ((array) $ticket_line_items as $ticket_line_item) {
186
                if ($ticket_line_item instanceof EE_Line_Item
187
                    && (int) $ticket_line_item->OBJ_ID() === (int) $ticket->ID()
188
                ) {
189
                    $line_item = $ticket_line_item;
190
                    break;
191
                }
192
            }
193
        }
194
        if ($line_item instanceof EE_Line_Item) {
195
            EEH_Line_Item::increment_quantity($line_item, $qty);
196
            return $line_item;
197
        }
198
        return null;
199
    }
200
201
202
    /**
203
     * Increments the line item and all its children's quantity by $qty (but percent line items are unaffected).
204
     * Does NOT save or recalculate other line items totals
205
     *
206
     * @param EE_Line_Item $line_item
207
     * @param int          $qty
208
     * @return void
209
     * @throws EE_Error
210
     * @throws InvalidArgumentException
211
     * @throws InvalidDataTypeException
212
     * @throws InvalidInterfaceException
213
     * @throws ReflectionException
214
     */
215 View Code Duplication
    public static function increment_quantity(EE_Line_Item $line_item, $qty = 1)
216
    {
217
        if (! $line_item->is_percent()) {
218
            $qty += $line_item->quantity();
219
            $line_item->set_quantity($qty);
220
            $line_item->set_total($line_item->unit_price() * $qty);
221
            $line_item->save();
222
        }
223
        foreach ($line_item->children() as $child) {
224
            if ($child->is_sub_line_item()) {
225
                EEH_Line_Item::update_quantity($child, $qty);
226
            }
227
        }
228
    }
229
230
231
    /**
232
     * Decrements the line item and all its children's quantity by $qty (but percent line items are unaffected).
233
     * Does NOT save or recalculate other line items totals
234
     *
235
     * @param EE_Line_Item $line_item
236
     * @param int          $qty
237
     * @return void
238
     * @throws EE_Error
239
     * @throws InvalidArgumentException
240
     * @throws InvalidDataTypeException
241
     * @throws InvalidInterfaceException
242
     * @throws ReflectionException
243
     */
244
    public static function decrement_quantity(EE_Line_Item $line_item, $qty = 1)
245
    {
246
        if (! $line_item->is_percent()) {
247
            $qty = $line_item->quantity() - $qty;
248
            $qty = max($qty, 0);
249
            $line_item->set_quantity($qty);
250
            $line_item->set_total($line_item->unit_price() * $qty);
251
            $line_item->save();
252
        }
253
        foreach ($line_item->children() as $child) {
254
            if ($child->is_sub_line_item()) {
255
                EEH_Line_Item::update_quantity($child, $qty);
256
            }
257
        }
258
    }
259
260
261
    /**
262
     * Updates the line item and its children's quantities to the specified number.
263
     * Does NOT save them or recalculate totals.
264
     *
265
     * @param EE_Line_Item $line_item
266
     * @param int          $new_quantity
267
     * @throws EE_Error
268
     * @throws InvalidArgumentException
269
     * @throws InvalidDataTypeException
270
     * @throws InvalidInterfaceException
271
     * @throws ReflectionException
272
     */
273 View Code Duplication
    public static function update_quantity(EE_Line_Item $line_item, $new_quantity)
274
    {
275
        if (! $line_item->is_percent()) {
276
            $line_item->set_quantity($new_quantity);
277
            $line_item->set_total($line_item->unit_price() * $new_quantity);
278
            $line_item->save();
279
        }
280
        foreach ($line_item->children() as $child) {
281
            if ($child->is_sub_line_item()) {
282
                EEH_Line_Item::update_quantity($child, $new_quantity);
283
            }
284
        }
285
    }
286
287
288
    /**
289
     * Returns the new line item created by adding a purchase of the ticket
290
     *
291
     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
292
     * @param EE_Ticket    $ticket
293
     * @param int          $qty
294
     * @return EE_Line_Item
295
     * @throws EE_Error
296
     * @throws InvalidArgumentException
297
     * @throws InvalidDataTypeException
298
     * @throws InvalidInterfaceException
299
     * @throws ReflectionException
300
     */
301
    public static function create_ticket_line_item(EE_Line_Item $total_line_item, EE_Ticket $ticket, $qty = 1)
302
    {
303
        $datetimes = $ticket->datetimes();
304
        $first_datetime = reset($datetimes);
305
        $first_datetime_name = esc_html__('Event', 'event_espresso');
306
        if ($first_datetime instanceof EE_Datetime && $first_datetime->event() instanceof EE_Event) {
307
            $first_datetime_name = $first_datetime->event()->name();
308
        }
309
        $event = sprintf(_x('(For %1$s)', '(For Event Name)', 'event_espresso'), $first_datetime_name);
310
        // get event subtotal line
311
        $events_sub_total = self::get_event_line_item_for_ticket($total_line_item, $ticket);
312
        // add $ticket to cart
313
        $line_item = EE_Line_Item::new_instance(array(
314
            'LIN_name'       => $ticket->name(),
315
            'LIN_desc'       => $ticket->description() !== '' ? $ticket->description() . ' ' . $event : $event,
316
            'LIN_unit_price' => $ticket->price(),
317
            'LIN_quantity'   => $qty,
318
            'LIN_is_taxable' => $ticket->taxable(),
319
            'LIN_order'      => count($events_sub_total->children()),
320
            'LIN_total'      => $ticket->price() * $qty,
321
            'LIN_type'       => EEM_Line_Item::type_line_item,
322
            'OBJ_ID'         => $ticket->ID(),
323
            'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_TICKET,
324
        ));
325
        $line_item = apply_filters(
326
            'FHEE__EEH_Line_Item__create_ticket_line_item__line_item',
327
            $line_item
328
        );
329
        $events_sub_total->add_child_line_item($line_item);
330
        // now add the sub-line items
331
        $running_total_for_ticket = 0;
332
        foreach ($ticket->prices(array('order_by' => array('PRC_order' => 'ASC'))) as $price) {
333
            $sign = $price->is_discount() ? -1 : 1;
334
            $price_total = $price->is_percent()
335
                ? $running_total_for_ticket * $price->amount() / 100
336
                : $price->amount() * $qty;
337
            $sub_line_item = EE_Line_Item::new_instance(array(
338
                'LIN_name'       => $price->name(),
339
                'LIN_desc'       => $price->desc(),
340
                'LIN_quantity'   => $price->is_percent() ? null : $qty,
341
                'LIN_is_taxable' => false,
342
                'LIN_order'      => $price->order(),
343
                'LIN_total'      => $sign * $price_total,
344
                'LIN_type'       => EEM_Line_Item::type_sub_line_item,
345
                'OBJ_ID'         => $price->ID(),
346
                'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
347
            ));
348
            $sub_line_item = apply_filters(
349
                'FHEE__EEH_Line_Item__create_ticket_line_item__sub_line_item',
350
                $sub_line_item
351
            );
352
            if ($price->is_percent()) {
353
                $sub_line_item->set_percent($sign * $price->amount());
354
            } else {
355
                $sub_line_item->set_unit_price($sign * $price->amount());
356
            }
357
            $running_total_for_ticket += $price_total;
358
            $line_item->add_child_line_item($sub_line_item);
359
        }
360
        return $line_item;
361
    }
362
363
364
    /**
365
     * Adds the specified item under the pre-tax-sub-total line item. Automatically
366
     * re-calculates the line item totals and updates the related transaction. But
367
     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
368
     * should probably change because of this).
369
     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
370
     * after using this, to keep the registration final prices in-sync with the transaction's total.
371
     *
372
     * @param EE_Line_Item $total_line_item
373
     * @param EE_Line_Item $item to be added
374
     * @return boolean
375
     * @throws EE_Error
376
     * @throws InvalidArgumentException
377
     * @throws InvalidDataTypeException
378
     * @throws InvalidInterfaceException
379
     * @throws ReflectionException
380
     */
381
    public static function add_item(EE_Line_Item $total_line_item, EE_Line_Item $item)
382
    {
383
        $pre_tax_subtotal = self::get_pre_tax_subtotal($total_line_item);
384
        if ($pre_tax_subtotal instanceof EE_Line_Item) {
385
            $success = $pre_tax_subtotal->add_child_line_item($item);
386
        } else {
387
            return false;
388
        }
389
        $total_line_item->recalculate_total_including_taxes();
390
        return $success;
391
    }
392
393
394
    /**
395
     * cancels an existing ticket line item,
396
     * by decrementing it's quantity by 1 and adding a new "type_cancellation" sub-line-item.
397
     * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
398
     *
399
     * @param EE_Line_Item $ticket_line_item
400
     * @param int          $qty
401
     * @return bool success
402
     * @throws EE_Error
403
     * @throws InvalidArgumentException
404
     * @throws InvalidDataTypeException
405
     * @throws InvalidInterfaceException
406
     * @throws ReflectionException
407
     */
408
    public static function cancel_ticket_line_item(EE_Line_Item $ticket_line_item, $qty = 1)
409
    {
410
        // validate incoming line_item
411 View Code Duplication
        if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
412
            throw new EE_Error(
413
                sprintf(
414
                    esc_html__(
415
                        'The supplied line item must have an Object Type of "Ticket", not %1$s.',
416
                        'event_espresso'
417
                    ),
418
                    $ticket_line_item->type()
419
                )
420
            );
421
        }
422
        if ($ticket_line_item->quantity() < $qty) {
423
            throw new EE_Error(
424
                sprintf(
425
                    esc_html__(
426
                        'Can not cancel %1$d ticket(s) because the supplied line item has a quantity of %2$d.',
427
                        'event_espresso'
428
                    ),
429
                    $qty,
430
                    $ticket_line_item->quantity()
431
                )
432
            );
433
        }
434
        // decrement ticket quantity; don't rely on auto-fixing when recalculating totals to do this
435
        $ticket_line_item->set_quantity($ticket_line_item->quantity() - $qty);
436
        foreach ($ticket_line_item->children() as $child_line_item) {
437
            if ($child_line_item->is_sub_line_item()
438
                && ! $child_line_item->is_percent()
439
                && ! $child_line_item->is_cancellation()
440
            ) {
441
                $child_line_item->set_quantity($child_line_item->quantity() - $qty);
442
            }
443
        }
444
        // get cancellation sub line item
445
        $cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
446
            $ticket_line_item,
447
            EEM_Line_Item::type_cancellation
448
        );
449
        $cancellation_line_item = reset($cancellation_line_item);
450
        // verify that this ticket was indeed previously cancelled
451
        if ($cancellation_line_item instanceof EE_Line_Item) {
452
            // increment cancelled quantity
453
            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() + $qty);
454
        } else {
455
            // create cancellation sub line item
456
            $cancellation_line_item = EE_Line_Item::new_instance(array(
457
                'LIN_name'       => esc_html__('Cancellation', 'event_espresso'),
458
                'LIN_desc'       => sprintf(
459
                    esc_html_x(
460
                        'Cancelled %1$s : %2$s',
461
                        'Cancelled Ticket Name : 2015-01-01 11:11',
462
                        'event_espresso'
463
                    ),
464
                    $ticket_line_item->name(),
465
                    current_time(get_option('date_format') . ' ' . get_option('time_format'))
466
                ),
467
                'LIN_unit_price' => 0, // $ticket_line_item->unit_price()
468
                'LIN_quantity'   => $qty,
469
                'LIN_is_taxable' => $ticket_line_item->is_taxable(),
470
                'LIN_order'      => count($ticket_line_item->children()),
471
                'LIN_total'      => 0, // $ticket_line_item->unit_price()
472
                'LIN_type'       => EEM_Line_Item::type_cancellation,
473
            ));
474
            $ticket_line_item->add_child_line_item($cancellation_line_item);
475
        }
476 View Code Duplication
        if ($ticket_line_item->save_this_and_descendants() > 0) {
477
            // decrement parent line item quantity
478
            $event_line_item = $ticket_line_item->parent();
479
            if ($event_line_item instanceof EE_Line_Item
480
                && $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
481
            ) {
482
                $event_line_item->set_quantity($event_line_item->quantity() - $qty);
483
                $event_line_item->save();
484
            }
485
            EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
486
            return true;
487
        }
488
        return false;
489
    }
490
491
492
    /**
493
     * reinstates (un-cancels?) a previously canceled ticket line item,
494
     * by incrementing it's quantity by 1, and decrementing it's "type_cancellation" sub-line-item.
495
     * ALL totals and subtotals will NEED TO BE UPDATED after performing this action
496
     *
497
     * @param EE_Line_Item $ticket_line_item
498
     * @param int          $qty
499
     * @return bool success
500
     * @throws EE_Error
501
     * @throws InvalidArgumentException
502
     * @throws InvalidDataTypeException
503
     * @throws InvalidInterfaceException
504
     * @throws ReflectionException
505
     */
506
    public static function reinstate_canceled_ticket_line_item(EE_Line_Item $ticket_line_item, $qty = 1)
507
    {
508
        // validate incoming line_item
509 View Code Duplication
        if ($ticket_line_item->OBJ_type() !== EEM_Line_Item::OBJ_TYPE_TICKET) {
510
            throw new EE_Error(
511
                sprintf(
512
                    esc_html__(
513
                        'The supplied line item must have an Object Type of "Ticket", not %1$s.',
514
                        'event_espresso'
515
                    ),
516
                    $ticket_line_item->type()
517
                )
518
            );
519
        }
520
        // get cancellation sub line item
521
        $cancellation_line_item = EEH_Line_Item::get_descendants_of_type(
522
            $ticket_line_item,
523
            EEM_Line_Item::type_cancellation
524
        );
525
        $cancellation_line_item = reset($cancellation_line_item);
526
        // verify that this ticket was indeed previously cancelled
527
        if (! $cancellation_line_item instanceof EE_Line_Item) {
528
            return false;
529
        }
530
        if ($cancellation_line_item->quantity() > $qty) {
531
            // decrement cancelled quantity
532
            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
533
        } elseif ($cancellation_line_item->quantity() === $qty) {
534
            // decrement cancelled quantity in case anyone still has the object kicking around
535
            $cancellation_line_item->set_quantity($cancellation_line_item->quantity() - $qty);
536
            // delete because quantity will end up as 0
537
            $cancellation_line_item->delete();
538
            // and attempt to destroy the object,
539
            // even though PHP won't actually destroy it until it needs the memory
540
            unset($cancellation_line_item);
541
        } else {
542
            // what ?!?! negative quantity ?!?!
543
            throw new EE_Error(
544
                sprintf(
545
                    esc_html__(
546
                        'Can not reinstate %1$d cancelled ticket(s) because the cancelled ticket quantity is only %2$d.',
547
                        'event_espresso'
548
                    ),
549
                    $qty,
550
                    $cancellation_line_item->quantity()
551
                )
552
            );
553
        }
554
        // increment ticket quantity
555
        $ticket_line_item->set_quantity($ticket_line_item->quantity() + $qty);
556 View Code Duplication
        if ($ticket_line_item->save_this_and_descendants() > 0) {
557
            // increment parent line item quantity
558
            $event_line_item = $ticket_line_item->parent();
559
            if ($event_line_item instanceof EE_Line_Item
560
                && $event_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
561
            ) {
562
                $event_line_item->set_quantity($event_line_item->quantity() + $qty);
563
            }
564
            EEH_Line_Item::get_grand_total_and_recalculate_everything($ticket_line_item);
565
            return true;
566
        }
567
        return false;
568
    }
569
570
571
    /**
572
     * calls EEH_Line_Item::find_transaction_grand_total_for_line_item()
573
     * then EE_Line_Item::recalculate_total_including_taxes() on the result
574
     *
575
     * @param EE_Line_Item $line_item
576
     * @return float
577
     * @throws EE_Error
578
     * @throws InvalidArgumentException
579
     * @throws InvalidDataTypeException
580
     * @throws InvalidInterfaceException
581
     * @throws ReflectionException
582
     */
583
    public static function get_grand_total_and_recalculate_everything(EE_Line_Item $line_item)
584
    {
585
        $grand_total_line_item = EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item);
586
        return $grand_total_line_item->recalculate_total_including_taxes();
587
    }
588
589
590
    /**
591
     * Gets the line item which contains the subtotal of all the items
592
     *
593
     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
594
     * @return EE_Line_Item
595
     * @throws EE_Error
596
     * @throws InvalidArgumentException
597
     * @throws InvalidDataTypeException
598
     * @throws InvalidInterfaceException
599
     * @throws ReflectionException
600
     */
601
    public static function get_pre_tax_subtotal(EE_Line_Item $total_line_item)
602
    {
603
        $pre_tax_subtotal = $total_line_item->get_child_line_item('pre-tax-subtotal');
604
        return $pre_tax_subtotal instanceof EE_Line_Item
605
            ? $pre_tax_subtotal
606
            : self::create_pre_tax_subtotal($total_line_item);
607
    }
608
609
610
    /**
611
     * Gets the line item for the taxes subtotal
612
     *
613
     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
614
     * @return EE_Line_Item
615
     * @throws EE_Error
616
     * @throws InvalidArgumentException
617
     * @throws InvalidDataTypeException
618
     * @throws InvalidInterfaceException
619
     * @throws ReflectionException
620
     */
621
    public static function get_taxes_subtotal(EE_Line_Item $total_line_item)
622
    {
623
        $taxes = $total_line_item->get_child_line_item('taxes');
624
        return $taxes ? $taxes : self::create_taxes_subtotal($total_line_item);
625
    }
626
627
628
    /**
629
     * sets the TXN ID on an EE_Line_Item if passed a valid EE_Transaction object
630
     *
631
     * @param EE_Line_Item   $line_item
632
     * @param EE_Transaction $transaction
633
     * @return void
634
     * @throws EE_Error
635
     * @throws InvalidArgumentException
636
     * @throws InvalidDataTypeException
637
     * @throws InvalidInterfaceException
638
     * @throws ReflectionException
639
     */
640
    public static function set_TXN_ID(EE_Line_Item $line_item, $transaction = null)
641
    {
642
        if ($transaction) {
643
            /** @type EEM_Transaction $EEM_Transaction */
644
            $EEM_Transaction = EE_Registry::instance()->load_model('Transaction');
645
            $TXN_ID = $EEM_Transaction->ensure_is_ID($transaction);
646
            $line_item->set_TXN_ID($TXN_ID);
647
        }
648
    }
649
650
651
    /**
652
     * Creates a new default total line item for the transaction,
653
     * and its tickets subtotal and taxes subtotal line items (and adds the
654
     * existing taxes as children of the taxes subtotal line item)
655
     *
656
     * @param EE_Transaction $transaction
657
     * @return EE_Line_Item of type total
658
     * @throws EE_Error
659
     * @throws InvalidArgumentException
660
     * @throws InvalidDataTypeException
661
     * @throws InvalidInterfaceException
662
     * @throws ReflectionException
663
     */
664 View Code Duplication
    public static function create_total_line_item($transaction = null)
665
    {
666
        $total_line_item = EE_Line_Item::new_instance(array(
667
            'LIN_code' => 'total',
668
            'LIN_name' => esc_html__('Grand Total', 'event_espresso'),
669
            'LIN_type' => EEM_Line_Item::type_total,
670
            'OBJ_type' => EEM_Line_Item::OBJ_TYPE_TRANSACTION,
671
        ));
672
        $total_line_item = apply_filters(
673
            'FHEE__EEH_Line_Item__create_total_line_item__total_line_item',
674
            $total_line_item
675
        );
676
        self::set_TXN_ID($total_line_item, $transaction);
677
        self::create_pre_tax_subtotal($total_line_item, $transaction);
678
        self::create_taxes_subtotal($total_line_item, $transaction);
679
        return $total_line_item;
680
    }
681
682
683
    /**
684
     * Creates a default items subtotal line item
685
     *
686
     * @param EE_Line_Item   $total_line_item
687
     * @param EE_Transaction $transaction
688
     * @return EE_Line_Item
689
     * @throws EE_Error
690
     * @throws InvalidArgumentException
691
     * @throws InvalidDataTypeException
692
     * @throws InvalidInterfaceException
693
     * @throws ReflectionException
694
     */
695 View Code Duplication
    protected static function create_pre_tax_subtotal(EE_Line_Item $total_line_item, $transaction = null)
696
    {
697
        $pre_tax_line_item = EE_Line_Item::new_instance(array(
698
            'LIN_code' => 'pre-tax-subtotal',
699
            'LIN_name' => esc_html__('Pre-Tax Subtotal', 'event_espresso'),
700
            'LIN_type' => EEM_Line_Item::type_sub_total,
701
        ));
702
        $pre_tax_line_item = apply_filters(
703
            'FHEE__EEH_Line_Item__create_pre_tax_subtotal__pre_tax_line_item',
704
            $pre_tax_line_item
705
        );
706
        self::set_TXN_ID($pre_tax_line_item, $transaction);
707
        $total_line_item->add_child_line_item($pre_tax_line_item);
708
        self::create_event_subtotal($pre_tax_line_item, $transaction);
709
        return $pre_tax_line_item;
710
    }
711
712
713
    /**
714
     * Creates a line item for the taxes subtotal and finds all the tax prices
715
     * and applies taxes to it
716
     *
717
     * @param EE_Line_Item   $total_line_item of type EEM_Line_Item::type_total
718
     * @param EE_Transaction $transaction
719
     * @return EE_Line_Item
720
     * @throws EE_Error
721
     * @throws InvalidArgumentException
722
     * @throws InvalidDataTypeException
723
     * @throws InvalidInterfaceException
724
     * @throws ReflectionException
725
     */
726 View Code Duplication
    protected static function create_taxes_subtotal(EE_Line_Item $total_line_item, $transaction = null)
727
    {
728
        $tax_line_item = EE_Line_Item::new_instance(array(
729
            'LIN_code'  => 'taxes',
730
            'LIN_name'  => esc_html__('Taxes', 'event_espresso'),
731
            'LIN_type'  => EEM_Line_Item::type_tax_sub_total,
732
            'LIN_order' => 1000,// this should always come last
733
        ));
734
        $tax_line_item = apply_filters(
735
            'FHEE__EEH_Line_Item__create_taxes_subtotal__tax_line_item',
736
            $tax_line_item
737
        );
738
        self::set_TXN_ID($tax_line_item, $transaction);
739
        $total_line_item->add_child_line_item($tax_line_item);
740
        // and lastly, add the actual taxes
741
        self::apply_taxes($total_line_item);
742
        return $tax_line_item;
743
    }
744
745
746
    /**
747
     * Creates a default items subtotal line item
748
     *
749
     * @param EE_Line_Item   $pre_tax_line_item
750
     * @param EE_Transaction $transaction
751
     * @param EE_Event       $event
752
     * @return EE_Line_Item
753
     * @throws EE_Error
754
     * @throws InvalidArgumentException
755
     * @throws InvalidDataTypeException
756
     * @throws InvalidInterfaceException
757
     * @throws ReflectionException
758
     */
759
    public static function create_event_subtotal(EE_Line_Item $pre_tax_line_item, $transaction = null, $event = null)
760
    {
761
        $event_line_item = EE_Line_Item::new_instance(array(
762
            'LIN_code' => self::get_event_code($event),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 759 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...
763
            'LIN_name' => self::get_event_name($event),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 759 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...
764
            'LIN_desc' => self::get_event_desc($event),
0 ignored issues
show
Bug introduced by
It seems like $event defined by parameter $event on line 759 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...
765
            'LIN_type' => EEM_Line_Item::type_sub_total,
766
            'OBJ_type' => EEM_Line_Item::OBJ_TYPE_EVENT,
767
            'OBJ_ID'   => $event instanceof EE_Event ? $event->ID() : 0,
768
        ));
769
        $event_line_item = apply_filters(
770
            'FHEE__EEH_Line_Item__create_event_subtotal__event_line_item',
771
            $event_line_item
772
        );
773
        self::set_TXN_ID($event_line_item, $transaction);
774
        $pre_tax_line_item->add_child_line_item($event_line_item);
775
        return $event_line_item;
776
    }
777
778
779
    /**
780
     * Gets what the event ticket's code SHOULD be
781
     *
782
     * @param EE_Event $event
783
     * @return string
784
     * @throws EE_Error
785
     */
786
    public static function get_event_code($event)
787
    {
788
        return 'event-' . ($event instanceof EE_Event ? $event->ID() : '0');
789
    }
790
791
792
    /**
793
     * Gets the event name
794
     *
795
     * @param EE_Event $event
796
     * @return string
797
     * @throws EE_Error
798
     */
799
    public static function get_event_name($event)
800
    {
801
        return $event instanceof EE_Event
802
            ? mb_substr($event->name(), 0, 245)
803
            : esc_html__('Event', 'event_espresso');
804
    }
805
806
807
    /**
808
     * Gets the event excerpt
809
     *
810
     * @param EE_Event $event
811
     * @return string
812
     * @throws EE_Error
813
     */
814
    public static function get_event_desc($event)
815
    {
816
        return $event instanceof EE_Event ? $event->short_description() : '';
817
    }
818
819
820
    /**
821
     * Given the grand total line item and a ticket, finds the event sub-total
822
     * line item the ticket's purchase should be added onto
823
     *
824
     * @access public
825
     * @param EE_Line_Item $grand_total the grand total line item
826
     * @param EE_Ticket    $ticket
827
     * @return EE_Line_Item
828
     * @throws EE_Error
829
     * @throws InvalidArgumentException
830
     * @throws InvalidDataTypeException
831
     * @throws InvalidInterfaceException
832
     * @throws ReflectionException
833
     */
834
    public static function get_event_line_item_for_ticket(EE_Line_Item $grand_total, EE_Ticket $ticket)
835
    {
836
        $first_datetime = $ticket->first_datetime();
837
        if (! $first_datetime instanceof EE_Datetime) {
838
            throw new EE_Error(
839
                sprintf(
840
                    __('The supplied ticket (ID %d) has no datetimes', 'event_espresso'),
841
                    $ticket->ID()
842
                )
843
            );
844
        }
845
        $event = $first_datetime->event();
846
        if (! $event instanceof EE_Event) {
847
            throw new EE_Error(
848
                sprintf(
849
                    esc_html__(
850
                        'The supplied ticket (ID %d) has no event data associated with it.',
851
                        'event_espresso'
852
                    ),
853
                    $ticket->ID()
854
                )
855
            );
856
        }
857
        $events_sub_total = EEH_Line_Item::get_event_line_item($grand_total, $event);
858
        if (! $events_sub_total instanceof EE_Line_Item) {
859
            throw new EE_Error(
860
                sprintf(
861
                    esc_html__(
862
                        'There is no events sub-total for ticket %s on total line item %d',
863
                        'event_espresso'
864
                    ),
865
                    $ticket->ID(),
866
                    $grand_total->ID()
867
                )
868
            );
869
        }
870
        return $events_sub_total;
871
    }
872
873
874
    /**
875
     * Gets the event line item
876
     *
877
     * @param EE_Line_Item $grand_total
878
     * @param EE_Event     $event
879
     * @return EE_Line_Item for the event subtotal which is a child of $grand_total
880
     * @throws EE_Error
881
     * @throws InvalidArgumentException
882
     * @throws InvalidDataTypeException
883
     * @throws InvalidInterfaceException
884
     * @throws ReflectionException
885
     */
886
    public static function get_event_line_item(EE_Line_Item $grand_total, $event)
887
    {
888
        /** @type EE_Event $event */
889
        $event = EEM_Event::instance()->ensure_is_obj($event, true);
890
        $event_line_item = null;
891
        $found = false;
892
        foreach (EEH_Line_Item::get_event_subtotals($grand_total) as $event_line_item) {
893
            // default event subtotal, we should only ever find this the first time this method is called
894
            if (! $event_line_item->OBJ_ID()) {
895
                // let's use this! but first... set the event details
896
                EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
897
                $found = true;
898
                break;
899
            }
900
            if ($event_line_item->OBJ_ID() === $event->ID()) {
901
                // found existing line item for this event in the cart, so break out of loop and use this one
902
                $found = true;
903
                break;
904
            }
905
        }
906
        if (! $found) {
907
            // there is no event sub-total yet, so add it
908
            $pre_tax_subtotal = EEH_Line_Item::get_pre_tax_subtotal($grand_total);
909
            // create a new "event" subtotal below that
910
            $event_line_item = EEH_Line_Item::create_event_subtotal($pre_tax_subtotal, null, $event);
911
            // and set the event details
912
            EEH_Line_Item::set_event_subtotal_details($event_line_item, $event);
913
        }
914
        return $event_line_item;
915
    }
916
917
918
    /**
919
     * Creates a default items subtotal line item
920
     *
921
     * @param EE_Line_Item   $event_line_item
922
     * @param EE_Event       $event
923
     * @param EE_Transaction $transaction
924
     * @return void
925
     * @throws EE_Error
926
     * @throws InvalidArgumentException
927
     * @throws InvalidDataTypeException
928
     * @throws InvalidInterfaceException
929
     * @throws ReflectionException
930
     */
931
    public static function set_event_subtotal_details(
932
        EE_Line_Item $event_line_item,
933
        EE_Event $event,
934
        $transaction = null
935
    ) {
936
        if ($event instanceof EE_Event) {
937
            $event_line_item->set_code(self::get_event_code($event));
938
            $event_line_item->set_name(self::get_event_name($event));
939
            $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...
940
            $event_line_item->set_OBJ_ID($event->ID());
941
        }
942
        self::set_TXN_ID($event_line_item, $transaction);
943
    }
944
945
946
    /**
947
     * Finds what taxes should apply, adds them as tax line items under the taxes sub-total,
948
     * and recalculates the taxes sub-total and the grand total. Resets the taxes, so
949
     * any old taxes are removed
950
     *
951
     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
952
     * @param bool         $update_txn_status
953
     * @return bool
954
     * @throws EE_Error
955
     * @throws InvalidArgumentException
956
     * @throws InvalidDataTypeException
957
     * @throws InvalidInterfaceException
958
     * @throws ReflectionException
959
     * @throws RuntimeException
960
     */
961
    public static function apply_taxes(EE_Line_Item $total_line_item, $update_txn_status = false)
962
    {
963
        /** @type EEM_Price $EEM_Price */
964
        $EEM_Price = EE_Registry::instance()->load_model('Price');
965
        // get array of taxes via Price Model
966
        $ordered_taxes = $EEM_Price->get_all_prices_that_are_taxes();
967
        ksort($ordered_taxes);
968
        $taxes_line_item = self::get_taxes_subtotal($total_line_item);
969
        // just to be safe, remove its old tax line items
970
        $deleted = $taxes_line_item->delete_children_line_items();
971
        $updates = false;
972
        // loop thru taxes
973
        foreach ($ordered_taxes as $order => $taxes) {
974
            foreach ($taxes as $tax) {
975
                if ($tax instanceof EE_Price) {
976
                    $tax_line_item = EE_Line_Item::new_instance(
977
                        array(
978
                            'LIN_name'       => $tax->name(),
979
                            'LIN_desc'       => $tax->desc(),
980
                            'LIN_percent'    => $tax->amount(),
981
                            'LIN_is_taxable' => false,
982
                            'LIN_order'      => $order,
983
                            'LIN_total'      => 0,
984
                            'LIN_type'       => EEM_Line_Item::type_tax,
985
                            'OBJ_type'       => EEM_Line_Item::OBJ_TYPE_PRICE,
986
                            'OBJ_ID'         => $tax->ID(),
987
                        )
988
                    );
989
                    $tax_line_item = apply_filters(
990
                        'FHEE__EEH_Line_Item__apply_taxes__tax_line_item',
991
                        $tax_line_item
992
                    );
993
                    $updates = $taxes_line_item->add_child_line_item($tax_line_item) ?
994
                        true :
995
                        $updates;
996
                }
997
            }
998
        }
999
        // only recalculate totals if something changed
1000
        if ($deleted || $updates) {
1001
            $total_line_item->recalculate_total_including_taxes($update_txn_status);
1002
            return true;
1003
        }
1004
        return false;
1005
    }
1006
1007
1008
    /**
1009
     * Ensures that taxes have been applied to the order, if not applies them.
1010
     * Returns the total amount of tax
1011
     *
1012
     * @param EE_Line_Item $total_line_item of type EEM_Line_Item::type_total
1013
     * @return float
1014
     * @throws EE_Error
1015
     * @throws InvalidArgumentException
1016
     * @throws InvalidDataTypeException
1017
     * @throws InvalidInterfaceException
1018
     * @throws ReflectionException
1019
     */
1020
    public static function ensure_taxes_applied($total_line_item)
1021
    {
1022
        $taxes_subtotal = self::get_taxes_subtotal($total_line_item);
1023
        if (! $taxes_subtotal->children()) {
1024
            self::apply_taxes($total_line_item);
1025
        }
1026
        return $taxes_subtotal->total();
1027
    }
1028
1029
1030
    /**
1031
     * Deletes ALL children of the passed line item
1032
     *
1033
     * @param EE_Line_Item $parent_line_item
1034
     * @return bool
1035
     * @throws EE_Error
1036
     * @throws InvalidArgumentException
1037
     * @throws InvalidDataTypeException
1038
     * @throws InvalidInterfaceException
1039
     * @throws ReflectionException
1040
     */
1041
    public static function delete_all_child_items(EE_Line_Item $parent_line_item)
1042
    {
1043
        $deleted = 0;
1044
        foreach ($parent_line_item->children() as $child_line_item) {
1045
            if ($child_line_item instanceof EE_Line_Item) {
1046
                $deleted += EEH_Line_Item::delete_all_child_items($child_line_item);
1047
                if ($child_line_item->ID()) {
1048
                    $child_line_item->delete();
1049
                    unset($child_line_item);
1050
                } else {
1051
                    $parent_line_item->delete_child_line_item($child_line_item->code());
1052
                }
1053
                $deleted++;
1054
            }
1055
        }
1056
        return $deleted;
1057
    }
1058
1059
1060
    /**
1061
     * Deletes the line items as indicated by the line item code(s) provided,
1062
     * regardless of where they're found in the line item tree. Automatically
1063
     * re-calculates the line item totals and updates the related transaction. But
1064
     * DOES NOT automatically upgrade the transaction's registrations' final prices (which
1065
     * should probably change because of this).
1066
     * You should call EE_Registration_Processor::calculate_reg_final_prices_per_line_item()
1067
     * after using this, to keep the registration final prices in-sync with the transaction's total.
1068
     *
1069
     * @param EE_Line_Item      $total_line_item of type EEM_Line_Item::type_total
1070
     * @param array|bool|string $line_item_codes
1071
     * @return int number of items successfully removed
1072
     * @throws EE_Error
1073
     */
1074
    public static function delete_items(EE_Line_Item $total_line_item, $line_item_codes = false)
1075
    {
1076
1077
        if ($total_line_item->type() !== EEM_Line_Item::type_total) {
1078
            EE_Error::doing_it_wrong(
1079
                'EEH_Line_Item::delete_items',
1080
                esc_html__(
1081
                    'This static method should only be called with a TOTAL line item, otherwise we won\'t recalculate the totals correctly',
1082
                    'event_espresso'
1083
                ),
1084
                '4.6.18'
1085
            );
1086
        }
1087
        do_action('AHEE_log', __FILE__, __FUNCTION__, '');
1088
1089
        // check if only a single line_item_id was passed
1090
        if (! empty($line_item_codes) && ! is_array($line_item_codes)) {
1091
            // place single line_item_id in an array to appear as multiple line_item_ids
1092
            $line_item_codes = array($line_item_codes);
1093
        }
1094
        $removals = 0;
1095
        // cycle thru line_item_ids
1096
        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...
1097
            $removals += $total_line_item->delete_child_line_item($line_item_id);
1098
        }
1099
1100
        if ($removals > 0) {
1101
            $total_line_item->recalculate_taxes_and_tax_total();
1102
            return $removals;
1103
        } else {
1104
            return false;
1105
        }
1106
    }
1107
1108
1109
    /**
1110
     * Overwrites the previous tax by clearing out the old taxes, and creates a new
1111
     * tax and updates the total line item accordingly
1112
     *
1113
     * @param EE_Line_Item $total_line_item
1114
     * @param float        $amount
1115
     * @param string       $name
1116
     * @param string       $description
1117
     * @param string       $code
1118
     * @param boolean      $add_to_existing_line_item
1119
     *                          if true, and a duplicate line item with the same code is found,
1120
     *                          $amount will be added onto it; otherwise will simply set the taxes to match $amount
1121
     * @return EE_Line_Item the new tax line item created
1122
     * @throws EE_Error
1123
     * @throws InvalidArgumentException
1124
     * @throws InvalidDataTypeException
1125
     * @throws InvalidInterfaceException
1126
     * @throws ReflectionException
1127
     */
1128
    public static function set_total_tax_to(
1129
        EE_Line_Item $total_line_item,
1130
        $amount,
1131
        $name = null,
1132
        $description = null,
1133
        $code = null,
1134
        $add_to_existing_line_item = false
1135
    ) {
1136
        $tax_subtotal = self::get_taxes_subtotal($total_line_item);
1137
        $taxable_total = $total_line_item->taxable_total();
1138
1139
        if ($add_to_existing_line_item) {
1140
            $new_tax = $tax_subtotal->get_child_line_item($code);
1141
            EEM_Line_Item::instance()->delete(
1142
                array(array('LIN_code' => array('!=', $code), 'LIN_parent' => $tax_subtotal->ID()))
1143
            );
1144
        } else {
1145
            $new_tax = null;
1146
            $tax_subtotal->delete_children_line_items();
1147
        }
1148
        if ($new_tax) {
1149
            $new_tax->set_total($new_tax->total() + $amount);
1150
            $new_tax->set_percent($taxable_total ? $new_tax->total() / $taxable_total * 100 : 0);
1151
        } else {
1152
            // no existing tax item. Create it
1153
            $new_tax = EE_Line_Item::new_instance(array(
1154
                'TXN_ID'      => $total_line_item->TXN_ID(),
1155
                'LIN_name'    => $name ? $name : esc_html__('Tax', 'event_espresso'),
1156
                'LIN_desc'    => $description ? $description : '',
1157
                'LIN_percent' => $taxable_total ? ($amount / $taxable_total * 100) : 0,
1158
                'LIN_total'   => $amount,
1159
                'LIN_parent'  => $tax_subtotal->ID(),
1160
                'LIN_type'    => EEM_Line_Item::type_tax,
1161
                'LIN_code'    => $code,
1162
            ));
1163
        }
1164
1165
        $new_tax = apply_filters(
1166
            'FHEE__EEH_Line_Item__set_total_tax_to__new_tax_subtotal',
1167
            $new_tax,
1168
            $total_line_item
1169
        );
1170
        $new_tax->save();
1171
        $tax_subtotal->set_total($new_tax->total());
1172
        $tax_subtotal->save();
1173
        $total_line_item->recalculate_total_including_taxes();
1174
        return $new_tax;
1175
    }
1176
1177
1178
    /**
1179
     * Makes all the line items which are children of $line_item taxable (or not).
1180
     * Does NOT save the line items
1181
     *
1182
     * @param EE_Line_Item $line_item
1183
     * @param boolean      $taxable
1184
     * @param string       $code_substring_for_whitelist if this string is part of the line item's code
1185
     *                                                   it will be whitelisted (ie, except from becoming taxable)
1186
     * @throws EE_Error
1187
     */
1188
    public static function set_line_items_taxable(
1189
        EE_Line_Item $line_item,
1190
        $taxable = true,
1191
        $code_substring_for_whitelist = null
1192
    ) {
1193
        $whitelisted = false;
1194
        if ($code_substring_for_whitelist !== null) {
1195
            $whitelisted = strpos($line_item->code(), $code_substring_for_whitelist) !== false;
1196
        }
1197
        if (! $whitelisted && $line_item->is_line_item()) {
1198
            $line_item->set_is_taxable($taxable);
1199
        }
1200
        foreach ($line_item->children() as $child_line_item) {
1201
            EEH_Line_Item::set_line_items_taxable(
1202
                $child_line_item,
1203
                $taxable,
1204
                $code_substring_for_whitelist
1205
            );
1206
        }
1207
    }
1208
1209
1210
    /**
1211
     * Gets all descendants that are event subtotals
1212
     *
1213
     * @uses  EEH_Line_Item::get_subtotals_of_object_type()
1214
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1215
     * @return EE_Line_Item[]
1216
     * @throws EE_Error
1217
     */
1218
    public static function get_event_subtotals(EE_Line_Item $parent_line_item)
1219
    {
1220
        return self::get_subtotals_of_object_type($parent_line_item, EEM_Line_Item::OBJ_TYPE_EVENT);
1221
    }
1222
1223
1224
    /**
1225
     * Gets all descendants subtotals that match the supplied object type
1226
     *
1227
     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1228
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1229
     * @param string       $obj_type
1230
     * @return EE_Line_Item[]
1231
     * @throws EE_Error
1232
     */
1233
    public static function get_subtotals_of_object_type(EE_Line_Item $parent_line_item, $obj_type = '')
1234
    {
1235
        return self::_get_descendants_by_type_and_object_type(
1236
            $parent_line_item,
1237
            EEM_Line_Item::type_sub_total,
1238
            $obj_type
1239
        );
1240
    }
1241
1242
1243
    /**
1244
     * Gets all descendants that are tickets
1245
     *
1246
     * @uses  EEH_Line_Item::get_line_items_of_object_type()
1247
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1248
     * @return EE_Line_Item[]
1249
     * @throws EE_Error
1250
     */
1251
    public static function get_ticket_line_items(EE_Line_Item $parent_line_item)
1252
    {
1253
        return self::get_line_items_of_object_type(
1254
            $parent_line_item,
1255
            EEM_Line_Item::OBJ_TYPE_TICKET
1256
        );
1257
    }
1258
1259
1260
    /**
1261
     * Gets all descendants subtotals that match the supplied object type
1262
     *
1263
     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1264
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1265
     * @param string       $obj_type
1266
     * @return EE_Line_Item[]
1267
     * @throws EE_Error
1268
     */
1269
    public static function get_line_items_of_object_type(EE_Line_Item $parent_line_item, $obj_type = '')
1270
    {
1271
        return self::_get_descendants_by_type_and_object_type(
1272
            $parent_line_item,
1273
            EEM_Line_Item::type_line_item,
1274
            $obj_type
1275
        );
1276
    }
1277
1278
1279
    /**
1280
     * Gets all the descendants (ie, children or children of children etc) that are of the type 'tax'
1281
     *
1282
     * @uses  EEH_Line_Item::get_descendants_of_type()
1283
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1284
     * @return EE_Line_Item[]
1285
     * @throws EE_Error
1286
     */
1287
    public static function get_tax_descendants(EE_Line_Item $parent_line_item)
1288
    {
1289
        return EEH_Line_Item::get_descendants_of_type(
1290
            $parent_line_item,
1291
            EEM_Line_Item::type_tax
1292
        );
1293
    }
1294
1295
1296
    /**
1297
     * Gets all the real items purchased which are children of this item
1298
     *
1299
     * @uses  EEH_Line_Item::get_descendants_of_type()
1300
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1301
     * @return EE_Line_Item[]
1302
     * @throws EE_Error
1303
     */
1304
    public static function get_line_item_descendants(EE_Line_Item $parent_line_item)
1305
    {
1306
        return EEH_Line_Item::get_descendants_of_type(
1307
            $parent_line_item,
1308
            EEM_Line_Item::type_line_item
1309
        );
1310
    }
1311
1312
1313
    /**
1314
     * Gets all descendants of supplied line item that match the supplied line item type
1315
     *
1316
     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1317
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1318
     * @param string       $line_item_type   one of the EEM_Line_Item constants
1319
     * @return EE_Line_Item[]
1320
     * @throws EE_Error
1321
     */
1322
    public static function get_descendants_of_type(EE_Line_Item $parent_line_item, $line_item_type)
1323
    {
1324
        return self::_get_descendants_by_type_and_object_type(
1325
            $parent_line_item,
1326
            $line_item_type,
1327
            null
1328
        );
1329
    }
1330
1331
1332
    /**
1333
     * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1334
     * as well
1335
     *
1336
     * @param EE_Line_Item  $parent_line_item - the line item to find descendants of
1337
     * @param string        $line_item_type   one of the EEM_Line_Item constants
1338
     * @param string | NULL $obj_type         object model class name (minus prefix) or NULL to ignore object type when
1339
     *                                        searching
1340
     * @return EE_Line_Item[]
1341
     * @throws EE_Error
1342
     */
1343
    protected static function _get_descendants_by_type_and_object_type(
1344
        EE_Line_Item $parent_line_item,
1345
        $line_item_type,
1346
        $obj_type = null
1347
    ) {
1348
        $objects = array();
1349
        foreach ($parent_line_item->children() as $child_line_item) {
1350
            if ($child_line_item instanceof EE_Line_Item) {
1351
                if ($child_line_item->type() === $line_item_type
1352
                    && (
1353
                        $child_line_item->OBJ_type() === $obj_type || $obj_type === null
1354
                    )
1355
                ) {
1356
                    $objects[] = $child_line_item;
1357
                } else {
1358
                    // go-through-all-its children looking for more matches
1359
                    $objects = array_merge(
1360
                        $objects,
1361
                        self::_get_descendants_by_type_and_object_type(
1362
                            $child_line_item,
1363
                            $line_item_type,
1364
                            $obj_type
1365
                        )
1366
                    );
1367
                }
1368
            }
1369
        }
1370
        return $objects;
1371
    }
1372
1373
1374
    /**
1375
     * Gets all descendants subtotals that match the supplied object type
1376
     *
1377
     * @uses  EEH_Line_Item::_get_descendants_by_type_and_object_type()
1378
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1379
     * @param string       $OBJ_type         object type (like Event)
1380
     * @param array        $OBJ_IDs          array of OBJ_IDs
1381
     * @return EE_Line_Item[]
1382
     * @throws EE_Error
1383
     */
1384
    public static function get_line_items_by_object_type_and_IDs(
1385
        EE_Line_Item $parent_line_item,
1386
        $OBJ_type = '',
1387
        $OBJ_IDs = array()
1388
    ) {
1389
        return self::_get_descendants_by_object_type_and_object_ID(
1390
            $parent_line_item,
1391
            $OBJ_type,
1392
            $OBJ_IDs
1393
        );
1394
    }
1395
1396
1397
    /**
1398
     * Gets all descendants of supplied line item that match the supplied line item type and possibly the object type
1399
     * as well
1400
     *
1401
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1402
     * @param string       $OBJ_type         object type (like Event)
1403
     * @param array        $OBJ_IDs          array of OBJ_IDs
1404
     * @return EE_Line_Item[]
1405
     * @throws EE_Error
1406
     */
1407
    protected static function _get_descendants_by_object_type_and_object_ID(
1408
        EE_Line_Item $parent_line_item,
1409
        $OBJ_type,
1410
        $OBJ_IDs
1411
    ) {
1412
        $objects = array();
1413
        foreach ($parent_line_item->children() as $child_line_item) {
1414
            if ($child_line_item instanceof EE_Line_Item) {
1415
                if ($child_line_item->OBJ_type() === $OBJ_type
1416
                    && is_array($OBJ_IDs)
1417
                    && in_array($child_line_item->OBJ_ID(), $OBJ_IDs)
1418
                ) {
1419
                    $objects[] = $child_line_item;
1420
                } else {
1421
                    // go-through-all-its children looking for more matches
1422
                    $objects = array_merge(
1423
                        $objects,
1424
                        self::_get_descendants_by_object_type_and_object_ID(
1425
                            $child_line_item,
1426
                            $OBJ_type,
1427
                            $OBJ_IDs
1428
                        )
1429
                    );
1430
                }
1431
            }
1432
        }
1433
        return $objects;
1434
    }
1435
1436
1437
    /**
1438
     * Uses a breadth-first-search in order to find the nearest descendant of
1439
     * the specified type and returns it, else NULL
1440
     *
1441
     * @uses  EEH_Line_Item::_get_nearest_descendant()
1442
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1443
     * @param string       $type             like one of the EEM_Line_Item::type_*
1444
     * @return EE_Line_Item
1445
     * @throws EE_Error
1446
     * @throws InvalidArgumentException
1447
     * @throws InvalidDataTypeException
1448
     * @throws InvalidInterfaceException
1449
     * @throws ReflectionException
1450
     */
1451
    public static function get_nearest_descendant_of_type(EE_Line_Item $parent_line_item, $type)
1452
    {
1453
        return self::_get_nearest_descendant($parent_line_item, 'LIN_type', $type);
1454
    }
1455
1456
1457
    /**
1458
     * Uses a breadth-first-search in order to find the nearest descendant
1459
     * having the specified LIN_code and returns it, else NULL
1460
     *
1461
     * @uses  EEH_Line_Item::_get_nearest_descendant()
1462
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1463
     * @param string       $code             any value used for LIN_code
1464
     * @return EE_Line_Item
1465
     * @throws EE_Error
1466
     * @throws InvalidArgumentException
1467
     * @throws InvalidDataTypeException
1468
     * @throws InvalidInterfaceException
1469
     * @throws ReflectionException
1470
     */
1471
    public static function get_nearest_descendant_having_code(EE_Line_Item $parent_line_item, $code)
1472
    {
1473
        return self::_get_nearest_descendant($parent_line_item, 'LIN_code', $code);
1474
    }
1475
1476
1477
    /**
1478
     * Uses a breadth-first-search in order to find the nearest descendant
1479
     * having the specified LIN_code and returns it, else NULL
1480
     *
1481
     * @param EE_Line_Item $parent_line_item - the line item to find descendants of
1482
     * @param string       $search_field     name of EE_Line_Item property
1483
     * @param string       $value            any value stored in $search_field
1484
     * @return EE_Line_Item
1485
     * @throws EE_Error
1486
     * @throws InvalidArgumentException
1487
     * @throws InvalidDataTypeException
1488
     * @throws InvalidInterfaceException
1489
     * @throws ReflectionException
1490
     */
1491
    protected static function _get_nearest_descendant(EE_Line_Item $parent_line_item, $search_field, $value)
1492
    {
1493
        foreach ($parent_line_item->children() as $child) {
1494
            if ($child->get($search_field) == $value) {
1495
                return $child;
1496
            }
1497
        }
1498
        foreach ($parent_line_item->children() as $child) {
1499
            $descendant_found = self::_get_nearest_descendant(
1500
                $child,
1501
                $search_field,
1502
                $value
1503
            );
1504
            if ($descendant_found) {
1505
                return $descendant_found;
1506
            }
1507
        }
1508
        return null;
1509
    }
1510
1511
1512
    /**
1513
     * if passed line item has a TXN ID, uses that to jump directly to the grand total line item for the transaction,
1514
     * else recursively walks up the line item tree until a parent of type total is found,
1515
     *
1516
     * @param EE_Line_Item $line_item
1517
     * @return EE_Line_Item
1518
     * @throws EE_Error
1519
     */
1520
    public static function find_transaction_grand_total_for_line_item(EE_Line_Item $line_item)
1521
    {
1522
        if ($line_item->TXN_ID()) {
1523
            $total_line_item = $line_item->transaction()->total_line_item(false);
1524
            if ($total_line_item instanceof EE_Line_Item) {
1525
                return $total_line_item;
1526
            }
1527
        } else {
1528
            $line_item_parent = $line_item->parent();
1529
            if ($line_item_parent instanceof EE_Line_Item) {
1530
                if ($line_item_parent->is_total()) {
1531
                    return $line_item_parent;
1532
                }
1533
                return EEH_Line_Item::find_transaction_grand_total_for_line_item($line_item_parent);
1534
            }
1535
        }
1536
        throw new EE_Error(
1537
            sprintf(
1538
                esc_html__(
1539
                    'A valid grand total for line item %1$d was not found.',
1540
                    'event_espresso'
1541
                ),
1542
                $line_item->ID()
1543
            )
1544
        );
1545
    }
1546
1547
1548
    /**
1549
     * Prints out a representation of the line item tree
1550
     *
1551
     * @param EE_Line_Item $line_item
1552
     * @param int          $indentation
1553
     * @return void
1554
     * @throws EE_Error
1555
     */
1556
    public static function visualize(EE_Line_Item $line_item, $indentation = 0)
1557
    {
1558
        echo defined('EE_TESTS_DIR') ? "\n" : '<br />';
1559
        if (! $indentation) {
1560
            echo defined('EE_TESTS_DIR') ? "\n" : '<br />';
1561
        }
1562
        for ($i = 0; $i < $indentation; $i++) {
1563
            echo '. ';
1564
        }
1565
        $breakdown = '';
1566
        if ($line_item->is_line_item()) {
1567
            if ($line_item->is_percent()) {
1568
                $breakdown = "{$line_item->percent()}%";
1569
            } else {
1570
                $breakdown = '$' . "{$line_item->unit_price()} x {$line_item->quantity()}";
1571
            }
1572
        }
1573
        echo $line_item->name();
1574
        echo " [ ID:{$line_item->ID()} | qty:{$line_item->quantity()} ] {$line_item->type()} : ";
1575
        echo '$' . (string) $line_item->total();
1576
        if ($breakdown) {
1577
            echo " ( {$breakdown} )";
1578
        }
1579
        if ($line_item->is_taxable()) {
1580
            echo '  * taxable';
1581
        }
1582
        if ($line_item->children()) {
1583
            foreach ($line_item->children() as $child) {
1584
                self::visualize($child, $indentation + 1);
1585
            }
1586
        }
1587
    }
1588
1589
1590
    /**
1591
     * Calculates the registration's final price, taking into account that they
1592
     * need to not only help pay for their OWN ticket, but also any transaction-wide surcharges and taxes,
1593
     * and receive a portion of any transaction-wide discounts.
1594
     * eg1, if I buy a $1 ticket and brent buys a $9 ticket, and we receive a $5 discount
1595
     * then I'll get 1/10 of that $5 discount, which is $0.50, and brent will get
1596
     * 9/10ths of that $5 discount, which is $4.50. So my final price should be $0.50
1597
     * and brent's final price should be $5.50.
1598
     * In order to do this, we basically need to traverse the line item tree calculating
1599
     * the running totals (just as if we were recalculating the total), but when we identify
1600
     * regular line items, we need to keep track of their share of the grand total.
1601
     * Also, we need to keep track of the TAXABLE total for each ticket purchase, so
1602
     * we can know how to apply taxes to it. (Note: "taxable total" does not equal the "pretax total"
1603
     * when there are non-taxable items; otherwise they would be the same)
1604
     *
1605
     * @param EE_Line_Item $line_item
1606
     * @param array        $billable_ticket_quantities  array of EE_Ticket IDs and their corresponding quantity that
1607
     *                                                  can be included in price calculations at this moment
1608
     * @return array        keys are line items for tickets IDs and values are their share of the running total,
1609
     *                                                  plus the key 'total', and 'taxable' which also has keys of all
1610
     *                                                  the ticket IDs.
1611
     *                                                  Eg array(
1612
     *                                                      12 => 4.3
1613
     *                                                      23 => 8.0
1614
     *                                                      'total' => 16.6,
1615
     *                                                      'taxable' => array(
1616
     *                                                          12 => 10,
1617
     *                                                          23 => 4
1618
     *                                                      ).
1619
     *                                                  So to find which registrations have which final price, we need
1620
     *                                                  to find which line item is theirs, which can be done with
1621
     *                                                  `EEM_Line_Item::instance()->get_line_item_for_registration(
1622
     *                                                  $registration );`
1623
     * @throws EE_Error
1624
     * @throws InvalidArgumentException
1625
     * @throws InvalidDataTypeException
1626
     * @throws InvalidInterfaceException
1627
     * @throws ReflectionException
1628
     */
1629
    public static function calculate_reg_final_prices_per_line_item(
1630
        EE_Line_Item $line_item,
1631
        $billable_ticket_quantities = array()
1632
    ) {
1633
        $running_totals = [
1634
            'total'   => 0,
1635
            'taxable' => ['total' => 0]
1636
        ];
1637
        foreach ($line_item->children() as $child_line_item) {
1638
            switch ($child_line_item->type()) {
1639
                case EEM_Line_Item::type_sub_total:
1640
                    $running_totals_from_subtotal = EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1641
                        $child_line_item,
1642
                        $billable_ticket_quantities
1643
                    );
1644
                    // combine arrays but preserve numeric keys
1645
                    $running_totals = array_replace_recursive($running_totals_from_subtotal, $running_totals);
1646
                    $running_totals['total'] += $running_totals_from_subtotal['total'];
1647
                    $running_totals['taxable']['total'] += $running_totals_from_subtotal['taxable']['total'];
1648
                    break;
1649
1650
                case EEM_Line_Item::type_tax_sub_total:
1651
                    // find how much the taxes percentage is
1652
                    if ($child_line_item->percent() !== 0) {
1653
                        $tax_percent_decimal = $child_line_item->percent() / 100;
1654
                    } else {
1655
                        $tax_percent_decimal = EE_Taxes::get_total_taxes_percentage() / 100;
1656
                    }
1657
                    // and apply to all the taxable totals, and add to the pretax totals
1658
                    foreach ($running_totals as $line_item_id => $this_running_total) {
1659
                        // "total" and "taxable" array key is an exception
1660
                        if ($line_item_id === 'taxable') {
1661
                            continue;
1662
                        }
1663
                        $taxable_total = $running_totals['taxable'][ $line_item_id ];
1664
                        $running_totals[ $line_item_id ] += ($taxable_total * $tax_percent_decimal);
1665
                    }
1666
                    break;
1667
1668
                case EEM_Line_Item::type_line_item:
1669
                    // ticket line items or ????
1670
                    if ($child_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET) {
1671
                        // kk it's a ticket
1672
                        if (isset($running_totals[ $child_line_item->ID() ])) {
1673
                            // huh? that shouldn't happen.
1674
                            $running_totals['total'] += $child_line_item->total();
1675
                        } else {
1676
                            // its not in our running totals yet. great.
1677
                            if ($child_line_item->is_taxable()) {
1678
                                $taxable_amount = $child_line_item->unit_price();
1679
                            } else {
1680
                                $taxable_amount = 0;
1681
                            }
1682
                            // are we only calculating totals for some tickets?
1683
                            if (isset($billable_ticket_quantities[ $child_line_item->OBJ_ID() ])) {
1684
                                $quantity = $billable_ticket_quantities[ $child_line_item->OBJ_ID() ];
1685
                                $running_totals[ $child_line_item->ID() ] = $quantity
1686
                                    ? $child_line_item->unit_price()
1687
                                    : 0;
1688
                                $running_totals['taxable'][ $child_line_item->ID() ] = $quantity
1689
                                    ? $taxable_amount
1690
                                    : 0;
1691
                            } else {
1692
                                $quantity = $child_line_item->quantity();
1693
                                $running_totals[ $child_line_item->ID() ] = $child_line_item->unit_price();
1694
                                $running_totals['taxable'][ $child_line_item->ID() ] = $taxable_amount;
1695
                            }
1696
                            $running_totals['taxable']['total'] += $taxable_amount * $quantity;
1697
                            $running_totals['total'] += $child_line_item->unit_price() * $quantity;
1698
                        }
1699
                    } else {
1700
                        // it's some other type of item added to the cart
1701
                        // it should affect the running totals
1702
                        // basically we want to convert it into a PERCENT modifier. Because
1703
                        // more clearly affect all registration's final price equally
1704
                        $line_items_percent_of_running_total = $running_totals['total'] > 0
1705
                            ? ($child_line_item->total() / $running_totals['total']) + 1
1706
                            : 1;
1707
                        foreach ($running_totals as $line_item_id => $this_running_total) {
1708
                            // the "taxable" array key is an exception
1709
                            if ($line_item_id === 'taxable') {
1710
                                continue;
1711
                            }
1712
                            // update the running totals
1713
                            // yes this actually even works for the running grand total!
1714
                            $running_totals[ $line_item_id ] =
1715
                                $line_items_percent_of_running_total * $this_running_total;
1716
1717
                            if ($child_line_item->is_taxable()) {
1718
                                $running_totals['taxable'][ $line_item_id ] =
1719
                                    $line_items_percent_of_running_total * $running_totals['taxable'][ $line_item_id ];
1720
                            }
1721
                        }
1722
                    }
1723
                    break;
1724
            }
1725
        }
1726
        return $running_totals;
1727
    }
1728
1729
1730
    /**
1731
     * @param EE_Line_Item $total_line_item
1732
     * @param EE_Line_Item $ticket_line_item
1733
     * @return float | null
1734
     * @throws EE_Error
1735
     * @throws InvalidArgumentException
1736
     * @throws InvalidDataTypeException
1737
     * @throws InvalidInterfaceException
1738
     * @throws OutOfRangeException
1739
     * @throws ReflectionException
1740
     */
1741
    public static function calculate_final_price_for_ticket_line_item(
1742
        EE_Line_Item $total_line_item,
1743
        EE_Line_Item $ticket_line_item
1744
    ) {
1745
        static $final_prices_per_ticket_line_item = array();
1746
        if (empty($final_prices_per_ticket_line_item)) {
1747
            $final_prices_per_ticket_line_item = EEH_Line_Item::calculate_reg_final_prices_per_line_item(
1748
                $total_line_item
1749
            );
1750
        }
1751
        // ok now find this new registration's final price
1752
        if (isset($final_prices_per_ticket_line_item[ $ticket_line_item->ID() ])) {
1753
            return $final_prices_per_ticket_line_item[ $ticket_line_item->ID() ];
1754
        }
1755
        $message = sprintf(
1756
            esc_html__(
1757
                'The final price for the ticket line item (ID:%1$d) could not be calculated.',
1758
                'event_espresso'
1759
            ),
1760
            $ticket_line_item->ID()
1761
        );
1762
        if (WP_DEBUG) {
1763
            $message .= '<br>' . print_r($final_prices_per_ticket_line_item, true);
1764
            throw new OutOfRangeException($message);
1765
        }
1766
        EE_Log::instance()->log(__CLASS__, __FUNCTION__, $message);
1767
        return null;
1768
    }
1769
1770
1771
    /**
1772
     * Creates a duplicate of the line item tree, except only includes billable items
1773
     * and the portion of line items attributed to billable things
1774
     *
1775
     * @param EE_Line_Item      $line_item
1776
     * @param EE_Registration[] $registrations
1777
     * @return EE_Line_Item
1778
     * @throws EE_Error
1779
     * @throws InvalidArgumentException
1780
     * @throws InvalidDataTypeException
1781
     * @throws InvalidInterfaceException
1782
     * @throws ReflectionException
1783
     */
1784
    public static function billable_line_item_tree(EE_Line_Item $line_item, $registrations)
1785
    {
1786
        $copy_li = EEH_Line_Item::billable_line_item($line_item, $registrations);
1787
        foreach ($line_item->children() as $child_li) {
1788
            $copy_li->add_child_line_item(
1789
                EEH_Line_Item::billable_line_item_tree($child_li, $registrations)
1790
            );
1791
        }
1792
        // if this is the grand total line item, make sure the totals all add up
1793
        // (we could have duplicated this logic AS we copied the line items, but
1794
        // it seems DRYer this way)
1795
        if ($copy_li->type() === EEM_Line_Item::type_total) {
1796
            $copy_li->recalculate_total_including_taxes();
1797
        }
1798
        return $copy_li;
1799
    }
1800
1801
1802
    /**
1803
     * Creates a new, unsaved line item from $line_item that factors in the
1804
     * number of billable registrations on $registrations.
1805
     *
1806
     * @param EE_Line_Item      $line_item
1807
     * @param EE_Registration[] $registrations
1808
     * @return EE_Line_Item
1809
     * @throws EE_Error
1810
     * @throws InvalidArgumentException
1811
     * @throws InvalidDataTypeException
1812
     * @throws InvalidInterfaceException
1813
     * @throws ReflectionException
1814
     */
1815
    public static function billable_line_item(EE_Line_Item $line_item, $registrations)
1816
    {
1817
        $new_li_fields = $line_item->model_field_array();
1818
        if ($line_item->type() === EEM_Line_Item::type_line_item &&
1819
            $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1820
        ) {
1821
            $count = 0;
1822
            foreach ($registrations as $registration) {
1823
                if ($line_item->OBJ_ID() === $registration->ticket_ID() &&
1824
                    in_array(
1825
                        $registration->status_ID(),
1826
                        EEM_Registration::reg_statuses_that_allow_payment(),
1827
                        true
1828
                    )
1829
                ) {
1830
                    $count++;
1831
                }
1832
            }
1833
            $new_li_fields['LIN_quantity'] = $count;
1834
        }
1835
        // don't set the total. We'll leave that up to the code that calculates it
1836
        unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent'], $new_li_fields['LIN_total']);
1837
        return EE_Line_Item::new_instance($new_li_fields);
1838
    }
1839
1840
1841
    /**
1842
     * Returns a modified line item tree where all the subtotals which have a total of 0
1843
     * are removed, and line items with a quantity of 0
1844
     *
1845
     * @param EE_Line_Item $line_item |null
1846
     * @return EE_Line_Item|null
1847
     * @throws EE_Error
1848
     * @throws InvalidArgumentException
1849
     * @throws InvalidDataTypeException
1850
     * @throws InvalidInterfaceException
1851
     * @throws ReflectionException
1852
     */
1853
    public static function non_empty_line_items(EE_Line_Item $line_item)
1854
    {
1855
        $copied_li = EEH_Line_Item::non_empty_line_item($line_item);
1856
        if ($copied_li === null) {
1857
            return null;
1858
        }
1859
        // if this is an event subtotal, we want to only include it if it
1860
        // has a non-zero total and at least one ticket line item child
1861
        $ticket_children = 0;
1862
        foreach ($line_item->children() as $child_li) {
1863
            $child_li_copy = EEH_Line_Item::non_empty_line_items($child_li);
1864
            if ($child_li_copy !== null) {
1865
                $copied_li->add_child_line_item($child_li_copy);
1866
                if ($child_li_copy->type() === EEM_Line_Item::type_line_item &&
1867
                    $child_li_copy->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1868
                ) {
1869
                    $ticket_children++;
1870
                }
1871
            }
1872
        }
1873
        // if this is an event subtotal with NO ticket children
1874
        // we basically want to ignore it
1875
        if ($ticket_children === 0
1876
            && $line_item->type() === EEM_Line_Item::type_sub_total
1877
            && $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_EVENT
1878
            && $line_item->total() === 0
1879
        ) {
1880
            return null;
1881
        }
1882
        return $copied_li;
1883
    }
1884
1885
1886
    /**
1887
     * Creates a new, unsaved line item, but if it's a ticket line item
1888
     * with a total of 0, or a subtotal of 0, returns null instead
1889
     *
1890
     * @param EE_Line_Item $line_item
1891
     * @return EE_Line_Item
1892
     * @throws EE_Error
1893
     * @throws InvalidArgumentException
1894
     * @throws InvalidDataTypeException
1895
     * @throws InvalidInterfaceException
1896
     * @throws ReflectionException
1897
     */
1898
    public static function non_empty_line_item(EE_Line_Item $line_item)
1899
    {
1900 View Code Duplication
        if ($line_item->type() === EEM_Line_Item::type_line_item
1901
            && $line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1902
            && $line_item->quantity() === 0
1903
        ) {
1904
            return null;
1905
        }
1906
        $new_li_fields = $line_item->model_field_array();
1907
        // don't set the total. We'll leave that up to the code that calculates it
1908
        unset($new_li_fields['LIN_ID'], $new_li_fields['LIN_parent']);
1909
        return EE_Line_Item::new_instance($new_li_fields);
1910
    }
1911
1912
1913
    /**
1914
     * Cycles through all of the ticket line items for the supplied total line item
1915
     * and ensures that the line item's "is_taxable" field matches that of its corresponding ticket
1916
     *
1917
     * @param EE_Line_Item $total_line_item
1918
     * @since 4.9.79.p
1919
     * @throws EE_Error
1920
     * @throws InvalidArgumentException
1921
     * @throws InvalidDataTypeException
1922
     * @throws InvalidInterfaceException
1923
     * @throws ReflectionException
1924
     */
1925
    public static function resetIsTaxableForTickets(EE_Line_Item $total_line_item)
1926
    {
1927
        $ticket_line_items = self::get_ticket_line_items($total_line_item);
1928
        foreach ($ticket_line_items as $ticket_line_item) {
1929
            if ($ticket_line_item instanceof EE_Line_Item
1930
                && $ticket_line_item->OBJ_type() === EEM_Line_Item::OBJ_TYPE_TICKET
1931
            ) {
1932
                $ticket = $ticket_line_item->ticket();
1933
                if ($ticket instanceof EE_Ticket && $ticket->taxable() !== $ticket_line_item->is_taxable()) {
1934
                    $ticket_line_item->set_is_taxable($ticket->taxable());
1935
                    $ticket_line_item->save();
1936
                }
1937
            }
1938
        }
1939
    }
1940
1941
1942
1943
    /**************************************** @DEPRECATED METHODS *************************************** */
1944
    /**
1945
     * @deprecated
1946
     * @param EE_Line_Item $total_line_item
1947
     * @return EE_Line_Item
1948
     * @throws EE_Error
1949
     * @throws InvalidArgumentException
1950
     * @throws InvalidDataTypeException
1951
     * @throws InvalidInterfaceException
1952
     * @throws ReflectionException
1953
     */
1954 View Code Duplication
    public static function get_items_subtotal(EE_Line_Item $total_line_item)
1955
    {
1956
        EE_Error::doing_it_wrong(
1957
            'EEH_Line_Item::get_items_subtotal()',
1958
            sprintf(
1959
                esc_html__('Method replaced with %1$s', 'event_espresso'),
1960
                'EEH_Line_Item::get_pre_tax_subtotal()'
1961
            ),
1962
            '4.6.0'
1963
        );
1964
        return self::get_pre_tax_subtotal($total_line_item);
1965
    }
1966
1967
1968
    /**
1969
     * @deprecated
1970
     * @param EE_Transaction $transaction
1971
     * @return EE_Line_Item
1972
     * @throws EE_Error
1973
     * @throws InvalidArgumentException
1974
     * @throws InvalidDataTypeException
1975
     * @throws InvalidInterfaceException
1976
     * @throws ReflectionException
1977
     */
1978
    public static function create_default_total_line_item($transaction = null)
1979
    {
1980
        EE_Error::doing_it_wrong(
1981
            'EEH_Line_Item::create_default_total_line_item()',
1982
            sprintf(
1983
                esc_html__('Method replaced with %1$s', 'event_espresso'),
1984
                'EEH_Line_Item::create_total_line_item()'
1985
            ),
1986
            '4.6.0'
1987
        );
1988
        return self::create_total_line_item($transaction);
1989
    }
1990
1991
1992
    /**
1993
     * @deprecated
1994
     * @param EE_Line_Item   $total_line_item
1995
     * @param EE_Transaction $transaction
1996
     * @return EE_Line_Item
1997
     * @throws EE_Error
1998
     * @throws InvalidArgumentException
1999
     * @throws InvalidDataTypeException
2000
     * @throws InvalidInterfaceException
2001
     * @throws ReflectionException
2002
     */
2003 View Code Duplication
    public static function create_default_tickets_subtotal(EE_Line_Item $total_line_item, $transaction = null)
2004
    {
2005
        EE_Error::doing_it_wrong(
2006
            'EEH_Line_Item::create_default_tickets_subtotal()',
2007
            sprintf(
2008
                esc_html__('Method replaced with %1$s', 'event_espresso'),
2009
                'EEH_Line_Item::create_pre_tax_subtotal()'
2010
            ),
2011
            '4.6.0'
2012
        );
2013
        return self::create_pre_tax_subtotal($total_line_item, $transaction);
2014
    }
2015
2016
2017
    /**
2018
     * @deprecated
2019
     * @param EE_Line_Item   $total_line_item
2020
     * @param EE_Transaction $transaction
2021
     * @return EE_Line_Item
2022
     * @throws EE_Error
2023
     * @throws InvalidArgumentException
2024
     * @throws InvalidDataTypeException
2025
     * @throws InvalidInterfaceException
2026
     * @throws ReflectionException
2027
     */
2028 View Code Duplication
    public static function create_default_taxes_subtotal(EE_Line_Item $total_line_item, $transaction = null)
2029
    {
2030
        EE_Error::doing_it_wrong(
2031
            'EEH_Line_Item::create_default_taxes_subtotal()',
2032
            sprintf(
2033
                esc_html__('Method replaced with %1$s', 'event_espresso'),
2034
                'EEH_Line_Item::create_taxes_subtotal()'
2035
            ),
2036
            '4.6.0'
2037
        );
2038
        return self::create_taxes_subtotal($total_line_item, $transaction);
2039
    }
2040
2041
2042
    /**
2043
     * @deprecated
2044
     * @param EE_Line_Item   $total_line_item
2045
     * @param EE_Transaction $transaction
2046
     * @return EE_Line_Item
2047
     * @throws EE_Error
2048
     * @throws InvalidArgumentException
2049
     * @throws InvalidDataTypeException
2050
     * @throws InvalidInterfaceException
2051
     * @throws ReflectionException
2052
     */
2053 View Code Duplication
    public static function create_default_event_subtotal(EE_Line_Item $total_line_item, $transaction = null)
2054
    {
2055
        EE_Error::doing_it_wrong(
2056
            'EEH_Line_Item::create_default_event_subtotal()',
2057
            sprintf(
2058
                esc_html__('Method replaced with %1$s', 'event_espresso'),
2059
                'EEH_Line_Item::create_event_subtotal()'
2060
            ),
2061
            '4.6.0'
2062
        );
2063
        return self::create_event_subtotal($total_line_item, $transaction);
2064
    }
2065
}
2066