Completed
Branch BUG-10878-event-spaces-remaini... (49b4f1)
by
unknown
53:16 queued 42:45
created

EventSpacesCalculator::spacesRemaining()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace EventEspresso\core\domain\services\event;
4
5
use DomainException;
6
use EE_Datetime;
7
use EE_Error;
8
use EE_Event;
9
use EE_Ticket;
10
use EEM_Ticket;
11
use EventEspresso\core\exceptions\InvalidDataTypeException;
12
use EventEspresso\core\exceptions\InvalidInterfaceException;
13
use EventEspresso\core\exceptions\UnexpectedEntityException;
14
use InvalidArgumentException;
15
16
defined('EVENT_ESPRESSO_VERSION') || exit;
17
18
19
20
/**
21
 * Class EventSpacesCalculator
22
 * Calculates total available spaces for an event with no regard for sold tickets,
23
 * or spaces remaining based on "saleable" tickets.
24
 * This is done by looping through all of the tickets and datetimes for the event
25
 * and simulating the sale of available tickets until each datetime reaches its maximum capacity.
26
 *
27
 * @package EventEspresso\core\domain\services\event
28
 * @author  Brent Christensen
29
 * @since   4.9.45
30
 */
31
class EventSpacesCalculator
32
{
33
34
    /**
35
     * @var EE_Event $event
36
     */
37
    private $event;
38
39
    /**
40
     * @var array $datetime_query_params
41
     */
42
    private $datetime_query_params;
43
44
    /**
45
     * @var EE_Ticket[] $active_tickets
46
     */
47
    private $active_tickets = array();
48
49
    /**
50
     * @var EE_Datetime[] $datetimes
51
     */
52
    private $datetimes = array();
53
54
    /**
55
     * Array of Ticket IDs grouped by Datetime
56
     *
57
     * @var array $datetimes
58
     */
59
    private $datetime_tickets = array();
60
61
    /**
62
     * Max spaces for each Datetime (reg limit - previous sold)
63
     *
64
     * @var array $datetime_spaces
65
     */
66
    private $datetime_spaces = array();
67
68
    /**
69
     * Array of Datetime IDs grouped by Ticket
70
     *
71
     * @var array $ticket_datetimes
72
     */
73
    private $ticket_datetimes = array();
74
75
    /**
76
     * maximum ticket quantities for each ticket (adjusted for reg limit)
77
     *
78
     * @var array $ticket_quantities
79
     */
80
    private $ticket_quantities = array();
81
82
    /**
83
     * total quantity of sold and reserved for each ticket
84
     *
85
     * @var array $tickets_sold
86
     */
87
    private $tickets_sold = array();
88
89
    /**
90
     * total spaces available across all datetimes
91
     *
92
     * @var array $total_spaces
93
     */
94
    private $total_spaces = array();
95
96
    /**
97
     * @var boolean $debug
98
     */
99
    private $debug = false;
100
101
102
103
    /**
104
     * EventSpacesCalculator constructor.
105
     *
106
     * @param EE_Event $event
107
     * @param array    $datetime_query_params
108
     * @throws EE_Error
109
     */
110
    public function __construct(EE_Event $event, array $datetime_query_params = array())
111
    {
112
        $this->event                 = $event;
113
        $this->datetime_query_params = $datetime_query_params + array('order_by' => array('DTT_reg_limit' => 'ASC'));
114
    }
115
116
117
118
    /**
119
     * @return EE_Ticket[]
120
     * @throws EE_Error
121
     * @throws InvalidDataTypeException
122
     * @throws InvalidInterfaceException
123
     * @throws InvalidArgumentException
124
     */
125
    public function getActiveTickets()
126
    {
127
        if (empty($this->active_tickets)) {
128
            $this->active_tickets = $this->event->tickets(
129
                array(
130
                    array(
131
                        'TKT_end_date' => array('>=', EEM_Ticket::instance()->current_time_for_query('TKT_end_date')),
132
                        'TKT_deleted'  => false,
133
                    ),
134
                    'order_by' => array('TKT_qty' => 'ASC'),
135
                )
136
            );
137
        }
138
        return $this->active_tickets;
139
    }
140
141
142
143
    /**
144
     * @param EE_Ticket[] $active_tickets
145
     * @throws EE_Error
146
     * @throws DomainException
147
     * @throws UnexpectedEntityException
148
     */
149
    public function setActiveTickets(array $active_tickets = array())
150
    {
151
        if ( ! empty($active_tickets)) {
152
            foreach ($active_tickets as $active_ticket) {
153
                $this->validateTicket($active_ticket);
154
            }
155
            // sort incoming array by ticket quantity (asc)
156
            usort(
157
                $active_tickets,
158
                function (EE_Ticket $a, EE_Ticket $b) {
159
                    if ($a->qty() === $b->qty()) {
160
                        return 0;
161
                    }
162
                    return ($a->qty() < $b->qty())
163
                        ? -1
164
                        : 1;
165
                }
166
            );
167
        }
168
        $this->active_tickets = $active_tickets;
169
    }
170
171
172
173
    /**
174
     * @param $ticket
175
     * @throws DomainException
176
     * @throws EE_Error
177
     * @throws UnexpectedEntityException
178
     */
179
    private function validateTicket($ticket)
180
    {
181
        if ( ! $ticket instanceof EE_Ticket) {
182
            throw new DomainException(
183
                esc_html__(
184
                    'Invalid Ticket. Only EE_Ticket objects can be used to calculate event space availability.',
185
                    'event_espresso'
186
                )
187
            );
188
        }
189 View Code Duplication
        if ($ticket->get_event_ID() !== $this->event->ID()) {
190
            throw new DomainException(
191
                sprintf(
192
                    esc_html__(
193
                        'An EE_Ticket for Event %1$d was supplied while calculating event space availability for Event %2$d.',
194
                        'event_espresso'
195
                    ),
196
                    $ticket->get_event_ID(),
197
                    $this->event->ID()
198
                )
199
            );
200
        }
201
    }
202
203
204
205
    /**
206
     * @return EE_Datetime[]
207
     */
208
    public function getDatetimes()
209
    {
210
        return $this->datetimes;
211
    }
212
213
214
215
    /**
216
     * @param EE_Datetime $datetime
217
     * @throws EE_Error
218
     * @throws DomainException
219
     */
220
    public function setDatetime(EE_Datetime $datetime)
221
    {
222 View Code Duplication
        if ($datetime->event()->ID() !== $this->event->ID()) {
223
            throw new DomainException(
224
                sprintf(
225
                    esc_html__(
226
                        'An EE_Datetime for Event %1$d was supplied while calculating event space availability for Event %2$d.',
227
                        'event_espresso'
228
                    ),
229
                    $datetime->event()->ID(),
230
                    $this->event->ID()
231
                )
232
            );
233
        }
234
        $this->datetimes[ $datetime->ID() ] = $datetime;
235
    }
236
237
238
239
    /**
240
     * calculate spaces remaining based on "saleable" tickets
241
     *
242
     * @return float|int
243
     * @throws EE_Error
244
     * @throws DomainException
245
     * @throws UnexpectedEntityException
246
     * @throws InvalidDataTypeException
247
     * @throws InvalidInterfaceException
248
     * @throws InvalidArgumentException
249
     */
250
    public function spacesRemaining()
251
    {
252
        $this->initialize();
253
        return $this->calculate();
254
    }
255
256
257
258
    /**
259
     * calculates total available spaces for an event with no regard for sold tickets
260
     *
261
     * @return int|float
262
     * @throws EE_Error
263
     * @throws DomainException
264
     * @throws UnexpectedEntityException
265
     * @throws InvalidDataTypeException
266
     * @throws InvalidInterfaceException
267
     * @throws InvalidArgumentException
268
     */
269
    public function totalSpacesAvailable()
270
    {
271
        $this->initialize();
272
        return $this->calculate(false);
273
    }
274
275
276
277
    /**
278
     * Loops through the active tickets for the event
279
     * and builds a series of data arrays that will be used for calculating
280
     * the total maximum available spaces, as well as the spaces remaining.
281
     * Because ticket quantities affect datetime spaces and vice versa,
282
     * we need to be constantly updating these data arrays as things change,
283
     * which is the entire reason for their existence.
284
     *
285
     * @throws EE_Error
286
     * @throws DomainException
287
     * @throws UnexpectedEntityException
288
     * @throws InvalidDataTypeException
289
     * @throws InvalidInterfaceException
290
     * @throws InvalidArgumentException
291
     */
292
    private function initialize()
293
    {
294
        if ($this->debug) {
295
            \EEH_Debug_Tools::printr(__FUNCTION__, __CLASS__, __FILE__, __LINE__, 2);
296
        }
297
        $this->datetime_tickets  = array();
298
        $this->datetime_spaces   = array();
299
        $this->ticket_datetimes  = array();
300
        $this->ticket_quantities = array();
301
        $this->tickets_sold      = array();
302
        $this->total_spaces      = array();
303
        $active_tickets          = $this->getActiveTickets();
304
        if ( ! empty($active_tickets)) {
305
            foreach ($active_tickets as $ticket) {
306
                $this->validateTicket($ticket);
307
                // we need to index our data arrays using strings for the purpose of sorting,
308
                // but we also need them to be unique, so  we'll just prepend a letter T to the ID
309
                $ticket_identifier = "T{$ticket->ID()}";
310
                // to start, we'll just consider the raw qty to be the maximum availability for this ticket
311
                $max_tickets = $ticket->qty();
312
                // but we'll adjust that after looping over each datetime for the ticket and checking reg limits
313
                $ticket_datetimes = $ticket->datetimes($this->datetime_query_params);
314
                foreach ($ticket_datetimes as $datetime) {
315
                    // save all datetimes
316
                    $this->setDatetime($datetime);
317
                    $datetime_identifier = "D{$datetime->ID()}";
318
                    $reg_limit           = $datetime->reg_limit();
319
                    // ticket quantity can not exceed datetime reg limit
320
                    $max_tickets = min($max_tickets, $reg_limit);
321
                    // as described earlier, because we need to be able to constantly adjust numbers for things,
322
                    // we are going to move all of our data into the following arrays:
323
                    // datetime spaces initially represents the reg limit for each datetime,
324
                    // but this will get adjusted as tickets are accounted for
325
                    $this->datetime_spaces[ $datetime_identifier ] = $reg_limit;
326
                    // just an array of ticket IDs grouped by datetime
327
                    $this->datetime_tickets[ $datetime_identifier ][] = $ticket_identifier;
328
                    // and an array of datetime IDs grouped by ticket
329
                    $this->ticket_datetimes[ $ticket_identifier ][] = $datetime_identifier;
330
                }
331
                // total quantity of sold and reserved for each ticket
332
                $this->tickets_sold[ $ticket_identifier ] = $ticket->sold() + $ticket->reserved();
333
                // and the maximum ticket quantities for each ticket (adjusted for reg limit)
334
                $this->ticket_quantities[ $ticket_identifier ] = $max_tickets;
335
            }
336
        }
337
        // sort datetime spaces by reg limit, but maintain our string indexes
338
        asort($this->datetime_spaces, SORT_NUMERIC);
339
        // datetime tickets need to be sorted in the SAME order as the above array...
340
        // so we'll just use array_merge() to take the structure of datetime_spaces
341
        // but overwrite all of the data with that from datetime_tickets
342
        $this->datetime_tickets = array_merge(
343
            $this->datetime_spaces,
344
            $this->datetime_tickets
345
        );
346
        if ($this->debug) {
347
            \EEH_Debug_Tools::printr($this->datetime_spaces, 'datetime_spaces', __FILE__, __LINE__);
348
            \EEH_Debug_Tools::printr($this->datetime_tickets, 'datetime_tickets', __FILE__, __LINE__);
349
            \EEH_Debug_Tools::printr($this->ticket_quantities, 'ticket_quantities', __FILE__, __LINE__);
350
        }
351
    }
352
353
354
355
    /**
356
     * performs calculations on initialized data
357
     *
358
     * @param bool $consider_sold
359
     * @return int|float
360
     */
361
    private function calculate($consider_sold = true)
362
    {
363
        if ($this->debug) {
364
            \EEH_Debug_Tools::printr(__FUNCTION__, __CLASS__, __FILE__, __LINE__, 2);
365
        }
366
        foreach ($this->datetime_tickets as $datetime_identifier => $tickets) {
367
            $this->trackAvailableSpacesForDatetimes($datetime_identifier, $tickets);
368
        }
369
        // total spaces available is just the sum of the spaces available for each datetime
370
        $spaces_remaining = array_sum($this->total_spaces);
371
        if ($consider_sold) {
372
            // less the sum of all tickets sold for these datetimes
373
            $spaces_remaining -= array_sum($this->tickets_sold);
374
        }
375 View Code Duplication
        if ($this->debug) {
376
            \EEH_Debug_Tools::printr($this->total_spaces, '$this->total_spaces', __FILE__, __LINE__);
377
            \EEH_Debug_Tools::printr($this->tickets_sold, '$this->tickets_sold', __FILE__, __LINE__);
378
            \EEH_Debug_Tools::printr($spaces_remaining, '$spaces_remaining', __FILE__, __LINE__);
379
        }
380
        return $spaces_remaining;
381
    }
382
383
384
385
    /**
386
     * @param string $datetime_identifier
387
     * @param array  $tickets
388
     */
389
    private function trackAvailableSpacesForDatetimes($datetime_identifier, array $tickets)
390
    {
391
        // make sure a reg limit is set for the datetime
392
        $reg_limit = isset($this->datetime_spaces[ $datetime_identifier ])
393
            ? $this->datetime_spaces[ $datetime_identifier ]
394
            : 0;
395
        // and bail if it is not
396
        if ( ! $reg_limit) {
397
            if ($this->debug) {
398
                \EEH_Debug_Tools::printr('AT CAPACITY', " . {$datetime_identifier}", __FILE__, __LINE__);
399
            }
400
            return;
401
        }
402
        if ($this->debug) {
403
            \EEH_Debug_Tools::printr($datetime_identifier, '$datetime_identifier', __FILE__, __LINE__, 1);
404
            \EEH_Debug_Tools::printr((string)$reg_limit, 'REG LIMIT', __FILE__, __LINE__);
405
        }
406
        // set default number of available spaces
407
        $available_spaces                           = 0;
408
        $this->total_spaces[ $datetime_identifier ] = 0;
409
        foreach ($tickets as $ticket_identifier) {
410
            $available_spaces = $this->calculateAvailableSpacesForTicket(
411
                $datetime_identifier,
412
                $reg_limit,
413
                $ticket_identifier,
414
                $available_spaces
415
            );
416
        }
417
        // spaces can't be negative
418
        $available_spaces = max($available_spaces, 0);
419
        if ($available_spaces) {
420
            // track any non-zero values
421
            $this->total_spaces[ $datetime_identifier ] += $available_spaces;
422
            if ($this->debug) {
423
                \EEH_Debug_Tools::printr((string)$available_spaces, ' . spaces: ', __FILE__, __LINE__);
424
            }
425
        } else {
426
            if ($this->debug) {
427
                \EEH_Debug_Tools::printr(' ', ' . NO TICKETS AVAILABLE FOR DATETIME', __FILE__, __LINE__);
428
            }
429
        }
430 View Code Duplication
        if ($this->debug) {
431
            \EEH_Debug_Tools::printr($this->total_spaces[ $datetime_identifier ], '$spaces_remaining', __FILE__,
432
                __LINE__);
433
            \EEH_Debug_Tools::printr($this->ticket_quantities, '$ticket_quantities', __FILE__, __LINE__);
434
            \EEH_Debug_Tools::printr($this->datetime_spaces, 'datetime_spaces', __FILE__, __LINE__);
435
        }
436
    }
437
438
439
440
    /**
441
     * @param string $datetime_identifier
442
     * @param int    $reg_limit
443
     * @param string $ticket_identifier
444
     * @param int    $available_spaces
445
     * @return int
446
     */
447
    private function calculateAvailableSpacesForTicket(
448
        $datetime_identifier,
449
        $reg_limit,
450
        $ticket_identifier,
451
        $available_spaces
452
    ) {
453
        // make sure ticket quantity is set
454
        $ticket_quantity = isset($this->ticket_quantities[ $ticket_identifier ])
455
            ? $this->ticket_quantities[ $ticket_identifier ]
456
            : 0;
457
        if ($this->debug) {
458
            \EEH_Debug_Tools::printr("{$ticket_quantity}", "ticket $ticket_identifier quantity: ",
459
                __FILE__, __LINE__, 2);
460
        }
461
        if ($ticket_quantity) {
462
            if ($this->debug) {
463
                \EEH_Debug_Tools::printr(
464
                    ($available_spaces <= $reg_limit)
465
                        ? 'true'
466
                        : 'false',
467
                    ' . available_spaces({$available_spaces}) <= reg_limit ({$reg_limit}) = ',
468
                    __FILE__, __LINE__
469
                );
470
            }
471
            // if the datetime is NOT at full capacity yet
472
            if ($available_spaces <= $reg_limit) {
473
                // then the maximum ticket quantity we can allocate is the lowest value of either:
474
                //  the number of remaining spaces for the datetime, which is the limit - spaces already taken
475
                //  or the maximum ticket quantity
476
                $ticket_quantity = min($reg_limit - $available_spaces, $ticket_quantity);
477
                // adjust the available quantity in our tracking array
478
                $this->ticket_quantities[ $ticket_identifier ] -= $ticket_quantity;
479
                // and increment spaces allocated for this datetime
480
                $available_spaces += $ticket_quantity;
481
                if ($this->debug) {
482
                    \EEH_Debug_Tools::printr("{$ticket_quantity} tickets ({$ticket_identifier})", ' . . allocate ',
483
                        __FILE__, __LINE__);
484
                    if ($available_spaces >= $reg_limit) {
485
                        \EEH_Debug_Tools::printr('AT CAPACITY', " . {$datetime_identifier}", __FILE__, __LINE__, 3);
486
                    }
487
                }
488
                // now adjust all other datetimes that allow access to this ticket
489
                $this->adjustDatetimes(
490
                    $datetime_identifier,
491
                    $available_spaces,
492
                    $reg_limit,
493
                    $ticket_identifier,
494
                    $ticket_quantity
495
                );
496
            }
497
        }
498
        return $available_spaces;
499
    }
500
501
502
503
    /**
504
     * subtracts ticket amounts from all datetime reg limits
505
     * that allow access to the ticket specified,
506
     * because that ticket could be used
507
     * to attend any of the datetimes it has access to
508
     *
509
     * @param string $datetime_identifier
510
     * @param int    $available_spaces
511
     * @param int    $reg_limit
512
     * @param string $ticket_identifier
513
     * @param int    $ticket_quantity
514
     */
515
    private function adjustDatetimes(
516
        $datetime_identifier,
517
        $available_spaces,
518
        $reg_limit,
519
        $ticket_identifier,
520
        $ticket_quantity
521
    ) {
522
        foreach ($this->datetime_tickets as $datetime_ID => $datetime_tickets) {
523
            // if the supplied ticket has access to this datetime
524
            if (in_array($ticket_identifier, $datetime_tickets, true)) {
525
                $datetime_at_capacity = $datetime_ID === $datetime_identifier && $available_spaces >= $reg_limit;
526
                // and datetime has spaces available
527
                $this->adjustDatetimeSpaces(
528
                    $datetime_identifier,
529
                    $ticket_quantity,
530
                    $ticket_identifier
531
                );
532
                // if this datetime is at full capacity
533
                if ($datetime_at_capacity) {
534
                    // then all of it's tickets are now unavailable
535
                    foreach ($datetime_tickets as $datetime_ticket) {
536
                        if ($this->debug) {
537
                            \EEH_Debug_Tools::printr($datetime_ticket, ' . . . $datetime_ticket', __FILE__, __LINE__);
538
                        }
539
                        // so  set any tracked available quantities to zero
540
                        if (isset($this->ticket_quantities[ $datetime_ticket ])) {
541
                            $this->ticket_quantities[ $datetime_ticket ] = 0;
542
                        }
543
                        // but we also need to adjust spaces for any other datetimes this ticket has access to
544
                        if ($datetime_ticket !== $ticket_identifier) {
545
                            if (isset($this->ticket_datetimes[ $datetime_ticket ])
546
                                && is_array($this->ticket_datetimes[ $datetime_ticket ])
547
                            ) {
548
                                foreach ($this->ticket_datetimes[ $datetime_ticket ] as $datetime) {
549
                                    // don't adjust the current datetime twice
550
                                    if ($datetime !== $datetime_identifier) {
551
                                        $this->adjustDatetimeSpaces(
552
                                            $datetime,
553
                                            $ticket_quantity,
554
                                            $datetime_ticket
555
                                        );
556
                                    }
557
                                }
558
                            }
559
                        }
560
                        if ($this->debug) {
561
                            \EEH_Debug_Tools::printr("quantity set to 0 because Datetime {$datetime_identifier} is at capacity",
562
                                " . . . . datetime ticket {$datetime_ticket}", __FILE__, __LINE__);
563
                        }
564
                    }
565
                }
566
            }
567
        }
568
    }
569
570
    private function adjustDatetimeSpaces($datetime_identifier, $ticket_quantity = 0, $ticket_identifier = '')
571
    {
572
        if (isset($this->datetime_spaces[ $datetime_identifier ])) {
573
            if ($this->debug) {
574
                \EEH_Debug_Tools::printr($datetime_identifier, ' . . adjust Datetime Spaces for', __FILE__, __LINE__);
575
            }
576
            // then decrement the available spaces for the datetime
577
            $this->datetime_spaces[ $datetime_identifier ] -= $ticket_quantity;
578
            // but don't let quantities go below zero
579
            $this->datetime_spaces[ $datetime_identifier ] = max(
580
                $this->datetime_spaces[ $datetime_identifier ],
581
                0
582
            );
583
            if ($this->debug) {
584
                \EEH_Debug_Tools::printr($ticket_quantity . " because it allows access to {$ticket_identifier}",
585
                    " . . . {$datetime_identifier} capacity reduced by", __FILE__, __LINE__);
586
            }
587
        }
588
    }
589
590
}
591
// Location: EventSpacesCalculator.php
592