Completed
Branch BUG-10878-event-spaces-remaini... (07bd4d)
by
unknown
42:38 queued 31:47
created

calculateAvailableSpacesForTicket()   D

Complexity

Conditions 9
Paths 36

Size

Total Lines 45
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

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