Completed
Branch FET/conditional-update-queries (d7ff88)
by
unknown
53:03 queued 44:18
created

EE_Ticket::decreaseReservedForDatetimes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
3
use EventEspresso\core\exceptions\InvalidDataTypeException;
4
use EventEspresso\core\exceptions\InvalidInterfaceException;
5
use EventEspresso\core\exceptions\UnexpectedEntityException;
6
7
/**
8
 * EE_Ticket class
9
 *
10
 * @package            Event Espresso
11
 * @subpackage         includes/classes/EE_Ticket.class.php
12
 * @author             Darren Ethier
13
 */
14
class EE_Ticket extends EE_Soft_Delete_Base_Class implements EEI_Line_Item_Object, EEI_Event_Relation, EEI_Has_Icon
15
{
16
17
    /**
18
     * The following constants are used by the ticket_status() method to indicate whether a ticket is on sale or not.
19
     */
20
    const sold_out = 'TKS';
21
22
    /**
23
     *
24
     */
25
    const expired = 'TKE';
26
27
    /**
28
     *
29
     */
30
    const archived = 'TKA';
31
32
    /**
33
     *
34
     */
35
    const pending = 'TKP';
36
37
    /**
38
     *
39
     */
40
    const onsale = 'TKO';
41
42
    /**
43
     * extra meta key for tracking ticket reservations
44
     *
45
     * @type string
46
     */
47
    const META_KEY_TICKET_RESERVATIONS = 'ticket_reservations';
48
49
    /**
50
     * cached result from method of the same name
51
     *
52
     * @var float $_ticket_total_with_taxes
53
     */
54
    private $_ticket_total_with_taxes;
55
56
57
    /**
58
     * @param array  $props_n_values          incoming values
59
     * @param string $timezone                incoming timezone (if not set the timezone set for the website will be
60
     *                                        used.)
61
     * @param array  $date_formats            incoming date_formats in an array where the first value is the
62
     *                                        date_format and the second value is the time format
63
     * @return EE_Ticket
64
     * @throws \EE_Error
65
     */
66
    public static function new_instance($props_n_values = array(), $timezone = null, $date_formats = array())
67
    {
68
        $has_object = parent::_check_for_object($props_n_values, __CLASS__, $timezone, $date_formats);
69
        return $has_object ? $has_object : new self($props_n_values, false, $timezone, $date_formats);
70
    }
71
72
73
    /**
74
     * @param array  $props_n_values  incoming values from the database
75
     * @param string $timezone        incoming timezone as set by the model.  If not set the timezone for
76
     *                                the website will be used.
77
     * @return EE_Ticket
78
     * @throws \EE_Error
79
     */
80
    public static function new_instance_from_db($props_n_values = array(), $timezone = null)
81
    {
82
        return new self($props_n_values, true, $timezone);
83
    }
84
85
86
    /**
87
     * @return bool
88
     * @throws \EE_Error
89
     */
90
    public function parent()
91
    {
92
        return $this->get('TKT_parent');
93
    }
94
95
96
    /**
97
     * return if a ticket has quantities available for purchase
98
     *
99
     * @param  int $DTT_ID the primary key for a particular datetime
100
     * @return boolean
101
     * @throws \EE_Error
102
     */
103
    public function available($DTT_ID = 0)
104
    {
105
        // are we checking availability for a particular datetime ?
106
        if ($DTT_ID) {
107
            // get that datetime object
108
            $datetime = $this->get_first_related('Datetime', array(array('DTT_ID' => $DTT_ID)));
109
            // if  ticket sales for this datetime have exceeded the reg limit...
110
            if ($datetime instanceof EE_Datetime && $datetime->sold_out()) {
111
                return false;
112
            }
113
        }
114
        // datetime is still open for registration, but is this ticket sold out ?
115
        return $this->qty() < 1 || $this->qty() > $this->sold() ? true : false;
116
    }
117
118
119
    /**
120
     * Using the start date and end date this method calculates whether the ticket is On Sale, Pending, or Expired
121
     *
122
     * @param bool        $display   true = we'll return a localized string, otherwise we just return the value of the
123
     *                               relevant status const
124
     * @param bool | null $remaining if it is already known that tickets are available, then simply pass a bool to save
125
     *               further processing
126
     * @return mixed status int if the display string isn't requested
127
     * @throws \EE_Error
128
     */
129
    public function ticket_status($display = false, $remaining = null)
130
    {
131
        $remaining = is_bool($remaining) ? $remaining : $this->is_remaining();
132
        if (! $remaining) {
133
            return $display ? EEH_Template::pretty_status(EE_Ticket::sold_out, false, 'sentence') : EE_Ticket::sold_out;
134
        }
135
        if ($this->get('TKT_deleted')) {
136
            return $display ? EEH_Template::pretty_status(EE_Ticket::archived, false, 'sentence') : EE_Ticket::archived;
137
        }
138
        if ($this->is_expired()) {
139
            return $display ? EEH_Template::pretty_status(EE_Ticket::expired, false, 'sentence') : EE_Ticket::expired;
140
        }
141
        if ($this->is_pending()) {
142
            return $display ? EEH_Template::pretty_status(EE_Ticket::pending, false, 'sentence') : EE_Ticket::pending;
143
        }
144
        if ($this->is_on_sale()) {
145
            return $display ? EEH_Template::pretty_status(EE_Ticket::onsale, false, 'sentence') : EE_Ticket::onsale;
146
        }
147
        return '';
148
    }
149
150
151
    /**
152
     * The purpose of this method is to simply return a boolean for whether there are any tickets remaining for sale
153
     * considering ALL the factors used for figuring that out.
154
     *
155
     * @access public
156
     * @param  int $DTT_ID if an int above 0 is included here then we get a specific dtt.
157
     * @return boolean         true = tickets remaining, false not.
158
     * @throws \EE_Error
159
     */
160
    public function is_remaining($DTT_ID = 0)
161
    {
162
        $num_remaining = $this->remaining($DTT_ID);
163
        if ($num_remaining === 0) {
164
            return false;
165
        }
166
        if ($num_remaining > 0 && $num_remaining < $this->min()) {
167
            return false;
168
        }
169
        return true;
170
    }
171
172
173
    /**
174
     * return the total number of tickets available for purchase
175
     *
176
     * @param  int $DTT_ID the primary key for a particular datetime.
177
     *                     set to 0 for all related datetimes
178
     * @return int
179
     * @throws \EE_Error
180
     */
181
    public function remaining($DTT_ID = 0)
182
    {
183
        return $this->real_quantity_on_ticket('saleable', $DTT_ID);
184
    }
185
186
187
    /**
188
     * Gets min
189
     *
190
     * @return int
191
     * @throws \EE_Error
192
     */
193
    public function min()
194
    {
195
        return $this->get('TKT_min');
196
    }
197
198
199
    /**
200
     * return if a ticket is no longer available cause its available dates have expired.
201
     *
202
     * @return boolean
203
     * @throws \EE_Error
204
     */
205
    public function is_expired()
206
    {
207
        return ($this->get_raw('TKT_end_date') < time());
208
    }
209
210
211
    /**
212
     * Return if a ticket is yet to go on sale or not
213
     *
214
     * @return boolean
215
     * @throws \EE_Error
216
     */
217
    public function is_pending()
218
    {
219
        return ($this->get_raw('TKT_start_date') > time());
220
    }
221
222
223
    /**
224
     * Return if a ticket is on sale or not
225
     *
226
     * @return boolean
227
     * @throws \EE_Error
228
     */
229
    public function is_on_sale()
230
    {
231
        return ($this->get_raw('TKT_start_date') < time() && $this->get_raw('TKT_end_date') > time());
232
    }
233
234
235
    /**
236
     * This returns the chronologically last datetime that this ticket is associated with
237
     *
238
     * @param string $dt_frmt
239
     * @param string $conjunction - conjunction junction what's your function ? this string joins the start date with
240
     *                            the end date ie: Jan 01 "to" Dec 31
241
     * @return string
242
     * @throws \EE_Error
243
     */
244
    public function date_range($dt_frmt = '', $conjunction = ' - ')
245
    {
246
        $first_date = $this->first_datetime() instanceof EE_Datetime ? $this->first_datetime()->start_date($dt_frmt)
247
            : '';
248
        $last_date = $this->last_datetime() instanceof EE_Datetime ? $this->last_datetime()->end_date($dt_frmt) : '';
249
250
        return $first_date && $last_date ? $first_date . $conjunction . $last_date : '';
251
    }
252
253
254
    /**
255
     * This returns the chronologically first datetime that this ticket is associated with
256
     *
257
     * @return EE_Datetime
258
     * @throws \EE_Error
259
     */
260
    public function first_datetime()
261
    {
262
        $datetimes = $this->datetimes(array('limit' => 1));
263
        return reset($datetimes);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($datetimes); of type EE_Base_Class|false adds false to the return on line 263 which is incompatible with the return type documented by EE_Ticket::first_datetime of type EE_Datetime. It seems like you forgot to handle an error condition.
Loading history...
264
    }
265
266
267
    /**
268
     * Gets all the datetimes this ticket can be used for attending.
269
     * Unless otherwise specified, orders datetimes by start date.
270
     *
271
     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
272
     * @return EE_Datetime[]|EE_Base_Class[]
273
     * @throws \EE_Error
274
     */
275
    public function datetimes($query_params = array())
276
    {
277
        if (! isset($query_params['order_by'])) {
278
            $query_params['order_by']['DTT_order'] = 'ASC';
279
        }
280
        return $this->get_many_related('Datetime', $query_params);
281
    }
282
283
284
    /**
285
     * This returns the chronologically last datetime that this ticket is associated with
286
     *
287
     * @return EE_Datetime
288
     * @throws \EE_Error
289
     */
290
    public function last_datetime()
291
    {
292
        $datetimes = $this->datetimes(array('limit' => 1, 'order_by' => array('DTT_EVT_start' => 'DESC')));
293
        return end($datetimes);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression end($datetimes); of type EE_Base_Class|false adds false to the return on line 293 which is incompatible with the return type documented by EE_Ticket::last_datetime of type EE_Datetime. It seems like you forgot to handle an error condition.
Loading history...
294
    }
295
296
297
    /**
298
     * This returns the total tickets sold depending on the given parameters.
299
     *
300
     * @param  string $what   Can be one of two options: 'ticket', 'datetime'.
301
     *                        'ticket' = total ticket sales for all datetimes this ticket is related to
302
     *                        'datetime' = total ticket sales for a specified datetime (required $dtt_id)
303
     *                        'datetime' = total ticket sales in the datetime_ticket table.
304
     *                        If $dtt_id is not given then we return an array of sales indexed by datetime.
305
     *                        If $dtt_id IS given then we return the tickets sold for that given datetime.
306
     * @param  int    $dtt_id [optional] include the dtt_id with $what = 'datetime'.
307
     * @return mixed (array|int)          how many tickets have sold
308
     * @throws \EE_Error
309
     */
310
    public function tickets_sold($what = 'ticket', $dtt_id = null)
311
    {
312
        $total = 0;
313
        $tickets_sold = $this->_all_tickets_sold();
314
        switch ($what) {
315
            case 'ticket':
316
                return $tickets_sold['ticket'];
317
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
318
            case 'datetime':
319
                if (empty($tickets_sold['datetime'])) {
320
                    return $total;
321
                }
322
                if (! empty($dtt_id) && ! isset($tickets_sold['datetime'][ $dtt_id ])) {
323
                    EE_Error::add_error(
324
                        __(
325
                            'You\'ve requested the amount of tickets sold for a given ticket and datetime, however there are no records for the datetime id you included.  Are you SURE that is a datetime related to this ticket?',
326
                            'event_espresso'
327
                        ),
328
                        __FILE__,
329
                        __FUNCTION__,
330
                        __LINE__
331
                    );
332
                    return $total;
333
                }
334
                return empty($dtt_id) ? $tickets_sold['datetime'] : $tickets_sold['datetime'][ $dtt_id ];
335
                break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
336
            default:
337
                return $total;
338
        }
339
    }
340
341
342
    /**
343
     * This returns an array indexed by datetime_id for tickets sold with this ticket.
344
     *
345
     * @return EE_Ticket[]
346
     * @throws \EE_Error
347
     */
348
    protected function _all_tickets_sold()
349
    {
350
        $datetimes = $this->get_many_related('Datetime');
351
        $tickets_sold = array();
352
        if (! empty($datetimes)) {
353
            foreach ($datetimes as $datetime) {
354
                $tickets_sold['datetime'][ $datetime->ID() ] = $datetime->get('DTT_sold');
355
            }
356
        }
357
        // Tickets sold
358
        $tickets_sold['ticket'] = $this->sold();
359
        return $tickets_sold;
360
    }
361
362
363
    /**
364
     * This returns the base price object for the ticket.
365
     *
366
     * @param  bool $return_array whether to return as an array indexed by price id or just the object.
367
     * @return EE_Price|EE_Base_Class|EE_Price[]|EE_Base_Class[]
368
     * @throws \EE_Error
369
     */
370
    public function base_price($return_array = false)
371
    {
372
        $_where = array('Price_Type.PBT_ID' => EEM_Price_Type::base_type_base_price);
373
        return $return_array
374
            ? $this->get_many_related('Price', array($_where))
375
            : $this->get_first_related('Price', array($_where));
376
    }
377
378
379
    /**
380
     * This returns ONLY the price modifiers for the ticket (i.e. no taxes or base price)
381
     *
382
     * @access public
383
     * @return EE_Price[]
384
     * @throws \EE_Error
385
     */
386
    public function price_modifiers()
387
    {
388
        $query_params = array(
389
            0 => array(
390
                'Price_Type.PBT_ID' => array(
391
                    'NOT IN',
392
                    array(EEM_Price_Type::base_type_base_price, EEM_Price_Type::base_type_tax),
393
                ),
394
            ),
395
        );
396
        return $this->prices($query_params);
397
    }
398
399
400
    /**
401
     * Gets all the prices that combine to form the final price of this ticket
402
     *
403
     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
404
     * @return EE_Price[]|EE_Base_Class[]
405
     * @throws \EE_Error
406
     */
407
    public function prices($query_params = array())
408
    {
409
        return $this->get_many_related('Price', $query_params);
410
    }
411
412
413
    /**
414
     * Gets all the ticket applicabilities (ie, relations between datetimes and tickets)
415
     *
416
     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
417
     * @return EE_Datetime_Ticket|EE_Base_Class[]
418
     * @throws \EE_Error
419
     */
420
    public function datetime_tickets($query_params = array())
421
    {
422
        return $this->get_many_related('Datetime_Ticket', $query_params);
423
    }
424
425
426
    /**
427
     * Gets all the datetimes from the db ordered by DTT_order
428
     *
429
     * @param boolean $show_expired
430
     * @param boolean $show_deleted
431
     * @return EE_Datetime[]
432
     * @throws \EE_Error
433
     */
434
    public function datetimes_ordered($show_expired = true, $show_deleted = false)
435
    {
436
        return EEM_Datetime::instance($this->_timezone)->get_datetimes_for_ticket_ordered_by_DTT_order(
437
            $this->ID(),
438
            $show_expired,
439
            $show_deleted
440
        );
441
    }
442
443
444
    /**
445
     * Gets ID
446
     *
447
     * @return string
448
     * @throws \EE_Error
449
     */
450
    public function ID()
451
    {
452
        return $this->get('TKT_ID');
453
    }
454
455
456
    /**
457
     * get the author of the ticket.
458
     *
459
     * @since 4.5.0
460
     * @return int
461
     * @throws \EE_Error
462
     */
463
    public function wp_user()
464
    {
465
        return $this->get('TKT_wp_user');
466
    }
467
468
469
    /**
470
     * Gets the template for the ticket
471
     *
472
     * @return EE_Ticket_Template|EE_Base_Class
473
     * @throws \EE_Error
474
     */
475
    public function template()
476
    {
477
        return $this->get_first_related('Ticket_Template');
478
    }
479
480
481
    /**
482
     * Simply returns an array of EE_Price objects that are taxes.
483
     *
484
     * @return EE_Price[]
485
     * @throws \EE_Error
486
     */
487
    public function get_ticket_taxes_for_admin()
488
    {
489
        return EE_Taxes::get_taxes_for_admin();
490
    }
491
492
493
    /**
494
     * @return float
495
     * @throws \EE_Error
496
     */
497
    public function ticket_price()
498
    {
499
        return $this->get('TKT_price');
500
    }
501
502
503
    /**
504
     * @return mixed
505
     * @throws \EE_Error
506
     */
507
    public function pretty_price()
508
    {
509
        return $this->get_pretty('TKT_price');
510
    }
511
512
513
    /**
514
     * @return bool
515
     * @throws \EE_Error
516
     */
517
    public function is_free()
518
    {
519
        return $this->get_ticket_total_with_taxes() === (float) 0;
520
    }
521
522
523
    /**
524
     * get_ticket_total_with_taxes
525
     *
526
     * @param bool $no_cache
527
     * @return float
528
     * @throws \EE_Error
529
     */
530
    public function get_ticket_total_with_taxes($no_cache = false)
531
    {
532
        if ($this->_ticket_total_with_taxes === null || $no_cache) {
533
            $this->_ticket_total_with_taxes = $this->get_ticket_subtotal() + $this->get_ticket_taxes_total_for_admin();
534
        }
535
        return (float) $this->_ticket_total_with_taxes;
536
    }
537
538
539
    public function ensure_TKT_Price_correct()
540
    {
541
        $this->set('TKT_price', EE_Taxes::get_subtotal_for_admin($this));
542
        $this->save();
543
    }
544
545
546
    /**
547
     * @return float
548
     * @throws \EE_Error
549
     */
550
    public function get_ticket_subtotal()
551
    {
552
        return EE_Taxes::get_subtotal_for_admin($this);
553
    }
554
555
556
    /**
557
     * Returns the total taxes applied to this ticket
558
     *
559
     * @return float
560
     * @throws \EE_Error
561
     */
562
    public function get_ticket_taxes_total_for_admin()
563
    {
564
        return EE_Taxes::get_total_taxes_for_admin($this);
565
    }
566
567
568
    /**
569
     * Sets name
570
     *
571
     * @param string $name
572
     * @throws \EE_Error
573
     */
574
    public function set_name($name)
575
    {
576
        $this->set('TKT_name', $name);
577
    }
578
579
580
    /**
581
     * Gets description
582
     *
583
     * @return string
584
     * @throws \EE_Error
585
     */
586
    public function description()
587
    {
588
        return $this->get('TKT_description');
589
    }
590
591
592
    /**
593
     * Sets description
594
     *
595
     * @param string $description
596
     * @throws \EE_Error
597
     */
598
    public function set_description($description)
599
    {
600
        $this->set('TKT_description', $description);
601
    }
602
603
604
    /**
605
     * Gets start_date
606
     *
607
     * @param string $dt_frmt
608
     * @param string $tm_frmt
609
     * @return string
610
     * @throws \EE_Error
611
     */
612
    public function start_date($dt_frmt = '', $tm_frmt = '')
613
    {
614
        return $this->_get_datetime('TKT_start_date', $dt_frmt, $tm_frmt);
615
    }
616
617
618
    /**
619
     * Sets start_date
620
     *
621
     * @param string $start_date
622
     * @return void
623
     * @throws \EE_Error
624
     */
625
    public function set_start_date($start_date)
626
    {
627
        $this->_set_date_time('B', $start_date, 'TKT_start_date');
628
    }
629
630
631
    /**
632
     * Gets end_date
633
     *
634
     * @param string $dt_frmt
635
     * @param string $tm_frmt
636
     * @return string
637
     * @throws \EE_Error
638
     */
639
    public function end_date($dt_frmt = '', $tm_frmt = '')
640
    {
641
        return $this->_get_datetime('TKT_end_date', $dt_frmt, $tm_frmt);
642
    }
643
644
645
    /**
646
     * Sets end_date
647
     *
648
     * @param string $end_date
649
     * @return void
650
     * @throws \EE_Error
651
     */
652
    public function set_end_date($end_date)
653
    {
654
        $this->_set_date_time('B', $end_date, 'TKT_end_date');
655
    }
656
657
658
    /**
659
     * Sets sell until time
660
     *
661
     * @since 4.5.0
662
     * @param string $time a string representation of the sell until time (ex 9am or 7:30pm)
663
     * @throws \EE_Error
664
     */
665
    public function set_end_time($time)
666
    {
667
        $this->_set_time_for($time, 'TKT_end_date');
668
    }
669
670
671
    /**
672
     * Sets min
673
     *
674
     * @param int $min
675
     * @return void
676
     * @throws \EE_Error
677
     */
678
    public function set_min($min)
679
    {
680
        $this->set('TKT_min', $min);
681
    }
682
683
684
    /**
685
     * Gets max
686
     *
687
     * @return int
688
     * @throws \EE_Error
689
     */
690
    public function max()
691
    {
692
        return $this->get('TKT_max');
693
    }
694
695
696
    /**
697
     * Sets max
698
     *
699
     * @param int $max
700
     * @return void
701
     * @throws \EE_Error
702
     */
703
    public function set_max($max)
704
    {
705
        $this->set('TKT_max', $max);
706
    }
707
708
709
    /**
710
     * Sets price
711
     *
712
     * @param float $price
713
     * @return void
714
     * @throws \EE_Error
715
     */
716
    public function set_price($price)
717
    {
718
        $this->set('TKT_price', $price);
719
    }
720
721
722
    /**
723
     * Gets sold
724
     *
725
     * @return int
726
     * @throws \EE_Error
727
     */
728
    public function sold()
729
    {
730
        return $this->get_raw('TKT_sold');
731
    }
732
733
734
    /**
735
     * Sets sold
736
     *
737
     * @param int $sold
738
     * @return void
739
     * @throws \EE_Error
740
     */
741
    public function set_sold($sold)
742
    {
743
        // sold can not go below zero
744
        $sold = max(0, $sold);
745
        $this->set('TKT_sold', $sold);
746
    }
747
748
749
    /**
750
     * increments sold by amount passed by $qty
751
     *
752
     * @param int $qty
753
     * @return void
754
     * @throws \EE_Error
755
     */
756
    public function increase_sold($qty = 1)
757
    {
758
        $sold = $this->sold() + $qty;
759
        // remove ticket reservation, but don't adjust datetime reservations,  because that will happen
760
        // via \EE_Datetime::increase_sold() when \EE_Ticket::_increase_sold_for_datetimes() is called
761
        $this->decrease_reserved($qty, false, "TKT: {$this->ID()} (ln:" . __LINE__ . ')');
762
        $this->_increase_sold_for_datetimes($qty);
763
        $this->set_sold($sold);
764
        do_action(
765
            'AHEE__EE_Ticket__increase_sold',
766
            $this,
767
            $qty,
768
            $sold
769
        );
770
    }
771
772
773
    /**
774
     * Increases sold on related datetimes
775
     *
776
     * @param int $qty
777
     * @return void
778
     * @throws \EE_Error
779
     */
780 View Code Duplication
    protected function _increase_sold_for_datetimes($qty = 1)
781
    {
782
        $datetimes = $this->datetimes();
783
        if (is_array($datetimes)) {
784
            foreach ($datetimes as $datetime) {
785
                if ($datetime instanceof EE_Datetime) {
786
                    $datetime->increase_sold($qty);
787
                    $datetime->save();
788
                }
789
            }
790
        }
791
    }
792
793
794
    /**
795
     * decrements (subtracts) sold by amount passed by $qty
796
     *
797
     * @param int $qty
798
     * @return void
799
     * @throws \EE_Error
800
     */
801
    public function decrease_sold($qty = 1)
802
    {
803
        $sold = $this->sold() - $qty;
804
        $this->_decrease_sold_for_datetimes($qty);
805
        $this->set_sold($sold);
806
        do_action(
807
            'AHEE__EE_Ticket__decrease_sold',
808
            $this,
809
            $qty,
810
            $sold
811
        );
812
    }
813
814
815
    /**
816
     * Decreases sold on related datetimes
817
     *
818
     * @param int $qty
819
     * @return void
820
     * @throws \EE_Error
821
     */
822 View Code Duplication
    protected function _decrease_sold_for_datetimes($qty = 1)
823
    {
824
        $datetimes = $this->datetimes();
825
        if (is_array($datetimes)) {
826
            foreach ($datetimes as $datetime) {
827
                if ($datetime instanceof EE_Datetime) {
828
                    $datetime->decrease_sold($qty);
829
                    $datetime->save();
830
                }
831
            }
832
        }
833
    }
834
835
836
    /**
837
     * Gets qty of reserved tickets
838
     *
839
     * @return int
840
     * @throws \EE_Error
841
     */
842
    public function reserved()
843
    {
844
        return $this->get_raw('TKT_reserved');
845
    }
846
847
848
    /**
849
     * Sets reserved
850
     *
851
     * @param int $reserved
852
     * @return void
853
     * @throws \EE_Error
854
     */
855
    public function set_reserved($reserved)
856
    {
857
        // reserved can not go below zero
858
        $reserved = max(0, (int) $reserved);
859
        $this->set('TKT_reserved', $reserved);
860
    }
861
862
863
    /**
864
     * increments reserved by amount passed by $qty
865
     *
866
     * @param int    $qty
867
     * @param string $source
868
     * @return bool whether we successfully reserved the ticket or not.
869
     * @throws EE_Error
870
     * @throws InvalidArgumentException
871
     * @throws ReflectionException
872
     * @throws InvalidDataTypeException
873
     * @throws InvalidInterfaceException
874
     */
875
    public function increase_reserved($qty = 1, $source = 'unknown')
876
    {
877
        do_action(
878
            'AHEE__EE_Ticket__increase_reserved__begin',
879
            $this,
880
            $qty,
881
            $source
882
        );
883
        if (! $this->add_extra_meta(
884
                EE_Ticket::META_KEY_TICKET_RESERVATIONS,
885
                "{$qty} from {$source}"
886
            )
887
        ) {
888
            return false;
889
        }
890
        $datetimes_adjusted_successfully = $this->_increase_reserved_for_datetimes($qty);
891
        if( $datetimes_adjusted_successfully ) {
892
            $successful_bump = $this->bumpConditionally(
893
                'TKT_reserved',
894
                'TKT_sold',
895
                'TKT_qty',
896
                absint($qty)
897
            );
898
            if (! $successful_bump) {
899
                // The datetimes were successfully bumped, but not the
900
                // ticket. So we need to manually rollback the datetimes.
901
                $this->_decrease_reserved_for_datetimes($qty);
902
            }
903
        } else {
904
            $successful_bump = false;
905
        }
906
        do_action(
907
            'AHEE__EE_Ticket__increase_reserved',
908
            $this,
909
            $qty,
910
            $this->reserved(),
911
            $successful_bump
912
        );
913
        return $successful_bump;
914
    }
915
916
917
    /**
918
     * Increases sold on related datetimes
919
     *
920
     * @param int $qty
921
     * @return boolean indicating success
922
     * @throws \EE_Error
923
     */
924
    protected function _increase_reserved_for_datetimes($qty = 1)
925
    {
926
        $datetimes = $this->datetimes();
927
        $datetimes_updated = [];
928
        $limit_exceeded = false;
929
        if (is_array($datetimes)) {
930
            foreach ($datetimes as $datetime) {
931
                if ($datetime instanceof EE_Datetime) {
932
                    if ($datetime->increase_reserved($qty)) {
933
                        $datetimes_updated[] = $datetime;
934
                    } else {
935
                        $limit_exceeded = true;
936
                        break;
937
                    }
938
                }
939
            }
940
            // If somewhere along the way we detected a datetime whose
941
            // limit was exceeded, do a manual rollback.
942
            if( $limit_exceeded ) {
943
                $this->decreaseReservedForDatetimes($datetimes_updated, $qty);
944
                return false;
945
            }
946
        }
947
        return true;
948
    }
949
950
    /**
951
     * Decreases the reserved count on the specified datetimes.
952
     * @since $VID:$
953
     * @param EE_Datetime[] $datetimes
954
     * @param int $qty
955
     * @throws EE_Error
956
     * @throws InvalidArgumentException
957
     * @throws ReflectionException
958
     * @throws InvalidDataTypeException
959
     * @throws InvalidInterfaceException
960
     */
961
    protected function decreaseReservedForDatetimes($datetimes, $qty = 1) {
962
        foreach($datetimes as $datetime) {
963
            if ($datetime instanceof EE_Datetime) {
964
                $datetime->decrease_reserved($qty);
965
            }
966
        }
967
    }
968
969
970
    /**
971
     * decrements (subtracts) reserved by amount passed by $qty
972
     *
973
     * @param int    $qty
974
     * @param bool   $adjust_datetimes
975
     * @param string $source
976
     * @return void
977
     * @throws EE_Error
978
     * @throws InvalidArgumentException
979
     * @throws ReflectionException
980
     * @throws InvalidDataTypeException
981
     * @throws InvalidInterfaceException
982
     */
983
    public function decrease_reserved($qty = 1, $adjust_datetimes = true, $source = 'unknown')
984
    {
985
        $reserved = $this->reserved() - absint($qty);
986
        if (! $this->add_extra_meta(
987
            EE_Ticket::META_KEY_TICKET_RESERVATIONS,
988
            "-{$qty} from {$source}"
989
        )) {
990
            return false;
991
        }
992
        if ($adjust_datetimes) {
993
            $this->_decrease_reserved_for_datetimes($qty);
994
        }
995
        $this->bump(
996
            'TKT_reserved',
997
            $qty * -1
998
        );
999
        do_action(
1000
            'AHEE__EE_Ticket__decrease_reserved',
1001
            $this,
1002
            $qty,
1003
            $reserved
1004
        );
1005
    }
1006
1007
1008
    /**
1009
     * Decreases reserved on related datetimes
1010
     *
1011
     * @param int $qty
1012
     * @return void
1013
     * @throws EE_Error
1014
     * @throws InvalidArgumentException
1015
     * @throws ReflectionException
1016
     * @throws InvalidDataTypeException
1017
     * @throws InvalidInterfaceException
1018
     */
1019
    protected function _decrease_reserved_for_datetimes($qty = 1)
1020
    {
1021
        $this->decreaseReservedForDatetimes($this->datetimes(), $qty);
1022
    }
1023
1024
1025
    /**
1026
     * Gets ticket quantity
1027
     *
1028
     * @param string $context     ticket quantity is somewhat subjective depending on the exact information sought
1029
     *                            therefore $context can be one of three values: '', 'reg_limit', or 'saleable'
1030
     *                            '' (default) quantity is the actual db value for TKT_qty, unaffected by other objects
1031
     *                            REG LIMIT: caps qty based on DTT_reg_limit for ALL related datetimes
1032
     *                            SALEABLE: also considers datetime sold and returns zero if ANY DTT is sold out, and
1033
     *                            is therefore the truest measure of tickets that can be purchased at the moment
1034
     * @return int
1035
     * @throws \EE_Error
1036
     */
1037
    public function qty($context = '')
1038
    {
1039
        switch ($context) {
1040
            case 'reg_limit':
1041
                return $this->real_quantity_on_ticket();
1042
            case 'saleable':
1043
                return $this->real_quantity_on_ticket('saleable');
1044
            default:
1045
                return $this->get_raw('TKT_qty');
1046
        }
1047
    }
1048
1049
1050
    /**
1051
     * Gets ticket quantity
1052
     *
1053
     * @param string $context     ticket quantity is somewhat subjective depending on the exact information sought
1054
     *                            therefore $context can be one of two values: 'reg_limit', or 'saleable'
1055
     *                            REG LIMIT: caps qty based on DTT_reg_limit for ALL related datetimes
1056
     *                            SALEABLE: also considers datetime sold and returns zero if ANY DTT is sold out, and
1057
     *                            is therefore the truest measure of tickets that can be purchased at the moment
1058
     * @param  int   $DTT_ID      the primary key for a particular datetime.
1059
     *                            set to 0 for all related datetimes
1060
     * @return int
1061
     * @throws \EE_Error
1062
     */
1063
    public function real_quantity_on_ticket($context = 'reg_limit', $DTT_ID = 0)
1064
    {
1065
        $raw = $this->get_raw('TKT_qty');
1066
        // return immediately if it's zero
1067
        if ($raw === 0) {
1068
            return $raw;
1069
        }
1070
        // echo "\n\n<br />Ticket: " . $this->name() . '<br />';
1071
        // ensure qty doesn't exceed raw value for THIS ticket
1072
        $qty = min(EE_INF, $raw);
1073
        // echo "\n . qty: " . $qty . '<br />';
1074
        // calculate this ticket's total sales and reservations
1075
        $sold_and_reserved_for_this_ticket = $this->sold() + $this->reserved();
1076
        // echo "\n . sold: " . $this->sold() . '<br />';
1077
        // echo "\n . reserved: " . $this->reserved() . '<br />';
1078
        // echo "\n . sold_and_reserved_for_this_ticket: " . $sold_and_reserved_for_this_ticket . '<br />';
1079
        // first we need to calculate the maximum number of tickets available for the datetime
1080
        // do we want data for one datetime or all of them ?
1081
        $query_params = $DTT_ID ? array(array('DTT_ID' => $DTT_ID)) : array();
1082
        $datetimes = $this->datetimes($query_params);
1083
        if (is_array($datetimes) && ! empty($datetimes)) {
1084
            foreach ($datetimes as $datetime) {
1085
                if ($datetime instanceof EE_Datetime) {
1086
                    $datetime->refresh_from_db();
1087
                    // echo "\n . . datetime name: " . $datetime->name() . '<br />';
1088
                    // echo "\n . . datetime ID: " . $datetime->ID() . '<br />';
1089
                    // initialize with no restrictions for each datetime
1090
                    // but adjust datetime qty based on datetime reg limit
1091
                    $datetime_qty = min(EE_INF, $datetime->reg_limit());
1092
                    // echo "\n . . . datetime reg_limit: " . $datetime->reg_limit() . '<br />';
1093
                    // echo "\n . . . datetime_qty: " . $datetime_qty . '<br />';
1094
                    // if we want the actual saleable amount, then we need to consider OTHER ticket sales
1095
                    // and reservations for this datetime, that do NOT include sales and reservations
1096
                    // for this ticket (so we add $this->sold() and $this->reserved() back in)
1097
                    if ($context === 'saleable') {
1098
                        $datetime_qty = max(
1099
                            $datetime_qty - $datetime->sold_and_reserved() + $sold_and_reserved_for_this_ticket,
1100
                            0
1101
                        );
1102
                        // echo "\n . . . datetime sold: " . $datetime->sold() . '<br />';
1103
                        // echo "\n . . . datetime reserved: " . $datetime->reserved() . '<br />';
1104
                        // echo "\n . . . datetime sold_and_reserved: " . $datetime->sold_and_reserved() . '<br />';
1105
                        // echo "\n . . . datetime_qty: " . $datetime_qty . '<br />';
1106
                        $datetime_qty = ! $datetime->sold_out() ? $datetime_qty : 0;
1107
                        // echo "\n . . . datetime_qty: " . $datetime_qty . '<br />';
1108
                    }
1109
                    $qty = min($datetime_qty, $qty);
1110
                    // echo "\n . . qty: " . $qty . '<br />';
1111
                }
1112
            }
1113
        }
1114
        // NOW that we know the  maximum number of tickets available for the datetime
1115
        // we can finally factor in the details for this specific ticket
1116
        if ($qty > 0 && $context === 'saleable') {
1117
            // and subtract the sales for THIS ticket
1118
            $qty = max($qty - $sold_and_reserved_for_this_ticket, 0);
1119
            // echo "\n . qty: " . $qty . '<br />';
1120
        }
1121
        // echo "\nFINAL QTY: " . $qty . "<br /><br />";
1122
        return $qty;
1123
    }
1124
1125
1126
    /**
1127
     * Sets qty - IMPORTANT!!! Does NOT allow QTY to be set higher than the lowest reg limit of any related datetimes
1128
     *
1129
     * @param int $qty
1130
     * @return void
1131
     * @throws \EE_Error
1132
     */
1133 View Code Duplication
    public function set_qty($qty)
1134
    {
1135
        $datetimes = $this->datetimes();
1136
        foreach ($datetimes as $datetime) {
1137
            if ($datetime instanceof EE_Datetime) {
1138
                $qty = min($qty, $datetime->reg_limit());
1139
            }
1140
        }
1141
        $this->set('TKT_qty', $qty);
1142
    }
1143
1144
1145
    /**
1146
     * Gets uses
1147
     *
1148
     * @return int
1149
     * @throws \EE_Error
1150
     */
1151
    public function uses()
1152
    {
1153
        return $this->get('TKT_uses');
1154
    }
1155
1156
1157
    /**
1158
     * Sets uses
1159
     *
1160
     * @param int $uses
1161
     * @return void
1162
     * @throws \EE_Error
1163
     */
1164
    public function set_uses($uses)
1165
    {
1166
        $this->set('TKT_uses', $uses);
1167
    }
1168
1169
1170
    /**
1171
     * returns whether ticket is required or not.
1172
     *
1173
     * @return boolean
1174
     * @throws \EE_Error
1175
     */
1176
    public function required()
1177
    {
1178
        return $this->get('TKT_required');
1179
    }
1180
1181
1182
    /**
1183
     * sets the TKT_required property
1184
     *
1185
     * @param boolean $required
1186
     * @return void
1187
     * @throws \EE_Error
1188
     */
1189
    public function set_required($required)
1190
    {
1191
        $this->set('TKT_required', $required);
1192
    }
1193
1194
1195
    /**
1196
     * Gets taxable
1197
     *
1198
     * @return boolean
1199
     * @throws \EE_Error
1200
     */
1201
    public function taxable()
1202
    {
1203
        return $this->get('TKT_taxable');
1204
    }
1205
1206
1207
    /**
1208
     * Sets taxable
1209
     *
1210
     * @param boolean $taxable
1211
     * @return void
1212
     * @throws \EE_Error
1213
     */
1214
    public function set_taxable($taxable)
1215
    {
1216
        $this->set('TKT_taxable', $taxable);
1217
    }
1218
1219
1220
    /**
1221
     * Gets is_default
1222
     *
1223
     * @return boolean
1224
     * @throws \EE_Error
1225
     */
1226
    public function is_default()
1227
    {
1228
        return $this->get('TKT_is_default');
1229
    }
1230
1231
1232
    /**
1233
     * Sets is_default
1234
     *
1235
     * @param boolean $is_default
1236
     * @return void
1237
     * @throws \EE_Error
1238
     */
1239
    public function set_is_default($is_default)
1240
    {
1241
        $this->set('TKT_is_default', $is_default);
1242
    }
1243
1244
1245
    /**
1246
     * Gets order
1247
     *
1248
     * @return int
1249
     * @throws \EE_Error
1250
     */
1251
    public function order()
1252
    {
1253
        return $this->get('TKT_order');
1254
    }
1255
1256
1257
    /**
1258
     * Sets order
1259
     *
1260
     * @param int $order
1261
     * @return void
1262
     * @throws \EE_Error
1263
     */
1264
    public function set_order($order)
1265
    {
1266
        $this->set('TKT_order', $order);
1267
    }
1268
1269
1270
    /**
1271
     * Gets row
1272
     *
1273
     * @return int
1274
     * @throws \EE_Error
1275
     */
1276
    public function row()
1277
    {
1278
        return $this->get('TKT_row');
1279
    }
1280
1281
1282
    /**
1283
     * Sets row
1284
     *
1285
     * @param int $row
1286
     * @return void
1287
     * @throws \EE_Error
1288
     */
1289
    public function set_row($row)
1290
    {
1291
        $this->set('TKT_row', $row);
1292
    }
1293
1294
1295
    /**
1296
     * Gets deleted
1297
     *
1298
     * @return boolean
1299
     * @throws \EE_Error
1300
     */
1301
    public function deleted()
1302
    {
1303
        return $this->get('TKT_deleted');
1304
    }
1305
1306
1307
    /**
1308
     * Sets deleted
1309
     *
1310
     * @param boolean $deleted
1311
     * @return void
1312
     * @throws \EE_Error
1313
     */
1314
    public function set_deleted($deleted)
1315
    {
1316
        $this->set('TKT_deleted', $deleted);
1317
    }
1318
1319
1320
    /**
1321
     * Gets parent
1322
     *
1323
     * @return int
1324
     * @throws \EE_Error
1325
     */
1326
    public function parent_ID()
1327
    {
1328
        return $this->get('TKT_parent');
1329
    }
1330
1331
1332
    /**
1333
     * Sets parent
1334
     *
1335
     * @param int $parent
1336
     * @return void
1337
     * @throws \EE_Error
1338
     */
1339
    public function set_parent_ID($parent)
1340
    {
1341
        $this->set('TKT_parent', $parent);
1342
    }
1343
1344
1345
    /**
1346
     * Gets a string which is handy for showing in gateways etc that describes the ticket.
1347
     *
1348
     * @return string
1349
     * @throws \EE_Error
1350
     */
1351
    public function name_and_info()
1352
    {
1353
        $times = array();
1354
        foreach ($this->datetimes() as $datetime) {
1355
            $times[] = $datetime->start_date_and_time();
1356
        }
1357
        return $this->name() . ' @ ' . implode(', ', $times) . ' for ' . $this->pretty_price();
1358
    }
1359
1360
1361
    /**
1362
     * Gets name
1363
     *
1364
     * @return string
1365
     * @throws \EE_Error
1366
     */
1367
    public function name()
1368
    {
1369
        return $this->get('TKT_name');
1370
    }
1371
1372
1373
    /**
1374
     * Gets price
1375
     *
1376
     * @return float
1377
     * @throws \EE_Error
1378
     */
1379
    public function price()
1380
    {
1381
        return $this->get('TKT_price');
1382
    }
1383
1384
1385
    /**
1386
     * Gets all the registrations for this ticket
1387
     *
1388
     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1389
     * @return EE_Registration[]|EE_Base_Class[]
1390
     * @throws \EE_Error
1391
     */
1392
    public function registrations($query_params = array())
1393
    {
1394
        return $this->get_many_related('Registration', $query_params);
1395
    }
1396
1397
1398
    /**
1399
     * Updates the TKT_sold attribute (and saves) based on the number of APPROVED registrations for this ticket.
1400
     * into account
1401
     *
1402
     * @return int
1403
     * @throws \EE_Error
1404
     */
1405 View Code Duplication
    public function update_tickets_sold()
1406
    {
1407
        $count_regs_for_this_ticket = $this->count_registrations(
1408
            array(
1409
                array(
1410
                    'STS_ID'      => EEM_Registration::status_id_approved,
1411
                    'REG_deleted' => 0,
1412
                ),
1413
            )
1414
        );
1415
        $sold = $this->sold();
1416
        if ($count_regs_for_this_ticket > $sold) {
1417
            $this->increase_sold($count_regs_for_this_ticket - $sold);
1418
            $this->save();
1419
        } elseif ($count_regs_for_this_ticket < $sold) {
1420
            $this->decrease_sold($count_regs_for_this_ticket - $sold);
1421
            $this->save();
1422
        }
1423
        return $count_regs_for_this_ticket;
1424
    }
1425
1426
1427
    /**
1428
     * Counts the registrations for this ticket
1429
     *
1430
     * @param array $query_params @see https://github.com/eventespresso/event-espresso-core/tree/master/docs/G--Model-System/model-query-params.md
1431
     * @return int
1432
     */
1433
    public function count_registrations($query_params = array())
1434
    {
1435
        return $this->count_related('Registration', $query_params);
1436
    }
1437
1438
1439
    /**
1440
     * Implementation for EEI_Has_Icon interface method.
1441
     *
1442
     * @see EEI_Visual_Representation for comments
1443
     * @return string
1444
     */
1445
    public function get_icon()
1446
    {
1447
        return '<span class="dashicons dashicons-tickets-alt"></span>';
1448
    }
1449
1450
1451
    /**
1452
     * Implementation of the EEI_Event_Relation interface method
1453
     *
1454
     * @see EEI_Event_Relation for comments
1455
     * @return EE_Event
1456
     * @throws \EE_Error
1457
     * @throws UnexpectedEntityException
1458
     */
1459
    public function get_related_event()
1460
    {
1461
        // get one datetime to use for getting the event
1462
        $datetime = $this->first_datetime();
1463
        if (! $datetime instanceof \EE_Datetime) {
1464
            throw new UnexpectedEntityException(
1465
                $datetime,
1466
                'EE_Datetime',
1467
                sprintf(
1468
                    __('The ticket (%s) is not associated with any valid datetimes.', 'event_espresso'),
1469
                    $this->name()
1470
                )
1471
            );
1472
        }
1473
        $event = $datetime->event();
1474
        if (! $event instanceof \EE_Event) {
1475
            throw new UnexpectedEntityException(
1476
                $event,
1477
                'EE_Event',
1478
                sprintf(
1479
                    __('The ticket (%s) is not associated with a valid event.', 'event_espresso'),
1480
                    $this->name()
1481
                )
1482
            );
1483
        }
1484
        return $event;
1485
    }
1486
1487
1488
    /**
1489
     * Implementation of the EEI_Event_Relation interface method
1490
     *
1491
     * @see EEI_Event_Relation for comments
1492
     * @return string
1493
     * @throws UnexpectedEntityException
1494
     * @throws \EE_Error
1495
     */
1496
    public function get_event_name()
1497
    {
1498
        $event = $this->get_related_event();
1499
        return $event instanceof EE_Event ? $event->name() : '';
1500
    }
1501
1502
1503
    /**
1504
     * Implementation of the EEI_Event_Relation interface method
1505
     *
1506
     * @see EEI_Event_Relation for comments
1507
     * @return int
1508
     * @throws UnexpectedEntityException
1509
     * @throws \EE_Error
1510
     */
1511
    public function get_event_ID()
1512
    {
1513
        $event = $this->get_related_event();
1514
        return $event instanceof EE_Event ? $event->ID() : 0;
1515
    }
1516
1517
1518
    /**
1519
     * This simply returns whether a ticket can be permanently deleted or not.
1520
     * The criteria for determining this is whether the ticket has any related registrations.
1521
     * If there are none then it can be permanently deleted.
1522
     *
1523
     * @return bool
1524
     */
1525
    public function is_permanently_deleteable()
1526
    {
1527
        return $this->count_registrations() === 0;
1528
    }
1529
}
1530