Passed
Pull Request — master (#82)
by Chris
06:20 queued 02:55
created

TicketController::validateDiscountCode()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 1
dl 0
loc 22
rs 8.9197
c 0
b 0
f 0
1
<?php
2
3
namespace ConferenceTools\Tickets\Controller;
4
5
use Carnage\Cqrs\MessageBus\MessageBusInterface;
6
use ConferenceTools\Tickets\Domain\Service\Availability\DiscountCodeAvailability;
7
use Doctrine\ORM\EntityManager;
8
use ConferenceTools\Tickets\Domain\Command\Ticket\AssignToDelegate;
9
use ConferenceTools\Tickets\Domain\Command\Ticket\CompletePurchase;
10
use ConferenceTools\Tickets\Domain\Command\Ticket\ReserveTickets;
11
use ConferenceTools\Tickets\Domain\Event\Ticket\TicketPurchaseCreated;
12
use ConferenceTools\Tickets\Domain\ReadModel\TicketCounts\TicketCounter;
13
use ConferenceTools\Tickets\Domain\ReadModel\TicketRecord\PurchaseRecord;
14
use ConferenceTools\Tickets\Domain\Service\Configuration;
15
use ConferenceTools\Tickets\Domain\Service\Availability\TicketAvailability;
16
use ConferenceTools\Tickets\Domain\ValueObject\Delegate;
17
use ConferenceTools\Tickets\Domain\ValueObject\TicketReservationRequest;
18
use ConferenceTools\Tickets\Form\ManageTicket;
19
use ConferenceTools\Tickets\Form\PurchaseForm;
20
use Zend\Form\FormElementManager\FormElementManagerV2Polyfill;
21
use Zend\Stdlib\ArrayObject;
22
use Zend\View\Model\ViewModel;
23
use ZfrStripe\Client\StripeClient;
24
use ZfrStripe\Exception\CardErrorException;
25
26
class TicketController extends AbstractController
27
{
28
    private static $cardErrorMessages = [
29
        'invalid_number' => 'The card number is not a valid credit card number.',
30
        'invalid_expiry_month' => 'The card\'s expiration month is invalid.',
31
        'invalid_expiry_year' => 'The card\'s expiration year is invalid.',
32
        'invalid_cvc' => 'The card\'s security code/CVC is invalid.',
33
        'invalid_swipe_data' => 'The card\'s swipe data is invalid.',
34
        'incorrect_number' => 'The card number is incorrect.',
35
        'expired_card' => 'The card has expired.',
36
        'incorrect_cvc' => 'The card\'s security code/CVC is incorrect.',
37
        'incorrect_zip' => 'The address for your card did not match the card\'s billing address.',
38
        'card_declined' => 'The card was declined.',
39
        'missing' => 'There is no card on a customer that is being charged.',
40
        'processing_error' => 'An error occurred while processing the card.',
41
    ];
42
    /**
43
     * @var TicketAvailability
44
     */
45
    private $ticketAvailability;
46
47
    /**
48
     * @var DiscountCodeAvailability
49
     */
50
    private $discountCodeAvailability;
51
52
    /**
53
     * @var FormElementManagerV2Polyfill
54
     */
55
    private $formElementManager;
56
57
    public function __construct(
58
        MessageBusInterface $commandBus,
59
        EntityManager $entityManager,
60
        StripeClient $stripeClient,
61
        Configuration $configuration,
62
        TicketAvailability $ticketAvailability,
63
        DiscountCodeAvailability $discountCodeAvailability,
64
        FormElementManagerV2Polyfill $formElementManager
65
    ) {
66
        parent::__construct($commandBus, $entityManager, $stripeClient, $configuration);
67
        $this->ticketAvailability = $ticketAvailability;
68
        $this->formElementManager = $formElementManager;
69
        $this->discountCodeAvailability = $discountCodeAvailability;
70
    }
71
72
    public function indexAction()
73
    {
74
        return $this->redirect()->toRoute('tickets/select-tickets');
75
    }
76
77
    public function selectTicketsAction()
78
    {
79
        $tickets = $this->ticketAvailability->fetchAllAvailableTickets();
80
81
        if ($this->getRequest()->isPost()) {
82
            $data = $this->getRequest()->getPost();
83
            $failed = false;
84
            try {
85
                $purchases = $this->validateSelectedTickets($data, $tickets);
86
            } catch (\InvalidArgumentException $e) {
87
                $failed = true;
88
            }
89
90
            try {
91
                $discountCode = $this->validateDiscountCode($data);
92
                $discountCodeStr = $data['discount_code'];
93
            } catch (\InvalidArgumentException $e) {
94
                $failed = true;
95
                $discountCodeStr = '';
96
            }
97
98
            if (!$failed) {
99
                if ($discountCode !== null) {
100
                    $command = ReserveTickets::withDiscountCode($discountCode, ...$purchases);
101
                } else {
102
                    $command = new ReserveTickets(...$purchases);
103
                }
104
105
                $this->getCommandBus()->dispatch($command);
106
                /** @var TicketPurchaseCreated $event */
107
                $event = $this->events()->getEventsByType(TicketPurchaseCreated::class)[0];
108
                return $this->redirect()->toRoute('tickets/purchase', ['purchaseId' => $event->getId()]);
109
            }
110
        } else {
111
            try {
112
                $discountCodeStr = $this->params()->fromRoute('discount-code');
113
                $this->validateDiscountCode(['discount_code' => $discountCodeStr]);
114
            } catch (\InvalidArgumentException $e) {
115
                $discountCodeStr = '';
116
            }
117
        }
118
119
        return new ViewModel(['tickets' => $tickets, 'discountCode' => $discountCodeStr]);
120
    }
121
122
    /**
123
     * @param $data
124
     * @param TicketCounter[] $tickets
125
     * @return array
126
     */
127
    private function validateSelectedTickets($data, $tickets): array
128
    {
129
        $total = 0;
130
        $purchases = [];
131
        $errors = false;
132
        foreach ($data['quantity'] as $id => $quantity) {
133
            if (!is_numeric($quantity) || $quantity < 0) {
134
                $this->flashMessenger()->addErrorMessage('Quantity needs to be a number :)');
135
                $errors = true;
136
            } elseif (!$this->ticketAvailability->isAvailable($tickets[$id]->getTicketType(), $quantity)) {
137
                $this->flashMessenger()->addErrorMessage(
138
                    sprintf('Not enough %s remaining', $tickets[$id]->getTicketType()->getDisplayName())
139
                );
140
                $total++;
141
                $errors = true;
142
            } elseif ($quantity > 0) {
143
                $total += $quantity;
144
                $purchases[] = new TicketReservationRequest($tickets[$id]->getTicketType(), (int) $quantity);
145
            }
146
        }
147
148
        if ($total < 1) {
149
            $this->flashMessenger()->addErrorMessage('You must specify at least 1 ticket to purchase');
150
            $errors = true;
151
        }
152
153
        if ($errors) {
154
            throw new \InvalidArgumentException('input contained errors');
155
        }
156
157
        return $purchases;
158
    }
159
160
    /**
161
     * @param $data
162
     * @return ?DiscountCode
163
     */
0 ignored issues
show
Documentation Bug introduced by
The doc comment ?DiscountCode at position 0 could not be parsed: Unknown type name '?DiscountCode' at position 0 in ?DiscountCode.
Loading history...
164
    private function validateDiscountCode($data)
165
    {
166
        $discountCode = trim(strtolower($data['discount_code']));
167
        if ($discountCode === '') {
168
            return null;
169
        }
170
171
        $validCodes = $this->getConfiguration()->getDiscountCodes();
172
173
        if (!array_key_exists($discountCode, $validCodes)) {
174
            $this->flashMessenger()->addErrorMessage('Invalid discount code');
175
            throw new \InvalidArgumentException('input contained errors');
176
        }
177
178
        $discountCode = $validCodes[$discountCode];
179
180
        if (!$this->discountCodeAvailability->isAvailable($discountCode)) {
181
            $this->flashMessenger()->addErrorMessage('Discount code cannot be applied to your purchase');
182
            throw new \InvalidArgumentException('input contained errors');
183
        }
184
185
        return $discountCode;
186
    }
187
188
    public function purchaseAction()
189
    {
190
        $purchaseId = $this->params()->fromRoute('purchaseId');
191
        $noPayment = false;
192
        $purchase = $this->fetchPurchaseRecord($purchaseId);
193
194
        if ($purchase === null || $purchase->hasTimedout()) {
195
            $this->flashMessenger()->addErrorMessage('Purchase Id invalid or your purchase timed out');
196
            return $this->redirect()->toRoute('tickets/select-tickets');
197
        }
198
199
        if ($purchase->isPaid()) {
200
            $this->flashMessenger()->addInfoMessage('This purchase has already been paid for');
201
            return $this->redirect()->toRoute('tickets/complete', ['purchaseId' => $purchaseId]);
202
        }
203
204
        $form = new PurchaseForm($purchase->getTicketCount());
205
206
        if ($this->getRequest()->isPost()) {
207
            $noPayment = true;
208
            $data = $this->params()->fromPost();
209
            $form->setData($data);
210
            if ($form->isValid()) {
211
                try {
212
                    $this->getStripeClient()->createCharge([
213
                        "amount" => $purchase->getTotalCost()->getGross()->getAmount(),
214
                        "currency" => $purchase->getTotalCost()->getGross()->getCurrency(),
215
                        'source' => $data['stripe_token'],
216
                        'metadata' => [
217
                            'email' => $data['purchase_email'],
218
                            'purchaseId' => $purchaseId
219
                        ]
220
                    ]);
221
222
                    $delegateInfo = [];
223
224
                    for ($i = 0; $i < $purchase->getTicketCount(); $i++) {
225
                        $delegateInfo[] = Delegate::fromArray($data['delegates_' . $i]);
226
                    }
227
228
                    $command = new CompletePurchase($purchaseId, $data['purchase_email'], ...$delegateInfo);
229
                    $this->getCommandBus()->dispatch($command);
230
                    $this->flashMessenger()
231
                        ->addSuccessMessage(
232
                            'Your ticket purchase is completed. ' .
233
                            'You will receive an email shortly with your receipt. ' .
234
                            'Tickets will be sent to the delegates shortly before the event'
235
                        );
236
                    return $this->redirect()->toRoute('tickets/complete', ['purchaseId' => $purchaseId]);
237
                } catch (CardErrorException $e) {
238
                    $this->flashMessenger()->addErrorMessage(
239
                        sprintf(
240
                            'There was an issue with taking your payment: %s Please try again.',
241
                            $this->getDetailedErrorMessage($e)
242
                        )
243
                    );
244
                    $noPayment = false;
245
                }
246
            }
247
        }
248
249
        $this->flashMessenger()->addInfoMessage('Your tickets have been reserved for 30 mins, please complete payment before then');
250
        return new ViewModel(['purchase' => $purchase, 'form' => $form, 'noPayment' => $noPayment]);
251
    }
252
253
    /**
254
     * @param $purchaseId
255
     * @return PurchaseRecord|null
256
     */
257
    private function fetchPurchaseRecord($purchaseId)
258
    {
259
        /** @var PurchaseRecord $purchase */
260
        $purchase = $this->getEntityManager()->getRepository(PurchaseRecord::class)->findOneBy([
261
            'purchaseId' => $purchaseId
262
        ]);
263
        return $purchase;
264
    }
265
266
    private function getDetailedErrorMessage(CardErrorException $e)
267
    {
268
        $response = $e->getResponse();
269
        $errors = json_decode($response->getBody(true), true);
270
        $code = isset($errors['error']['code']) ? $errors['error']['code'] : 'processing_error';
271
        $code = isset(static::$cardErrorMessages[$code]) ? $code : 'processing_error';
0 ignored issues
show
Bug introduced by
Since $cardErrorMessages is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $cardErrorMessages to at least protected.
Loading history...
272
273
        return static::$cardErrorMessages[$code];
274
    }
275
276
    public function completeAction()
277
    {
278
        $purchaseId = $this->params()->fromRoute('purchaseId');
279
        $purchase = $this->fetchPurchaseRecord($purchaseId);
280
281
        if ($purchase === null) {
282
            $this->flashMessenger()->addErrorMessage('Purchase Id invalid');
283
            return $this->redirect()->toRoute('tickets/select-tickets');
284
        }
285
286
        return new ViewModel(['purchase' => $purchase]);
287
    }
288
289
    public function manageAction()
290
    {
291
        $purchaseId = $this->params()->fromRoute('purchaseId');
292
        $ticketId = $this->params()->fromRoute('ticketId');
293
294
        $purchase = $this->fetchPurchaseRecord($purchaseId);
295
        $ticketRecord = $purchase->getTicketRecord($ticketId);
296
        $delegate = $ticketRecord->getDelegate();
297
298
        $form = $this->formElementManager->get(ManageTicket::class);
299
        $data = [
300
            'delegate' => $delegate->toArray()
301
        ];
302
303
        $form->bind(new ArrayObject($data));
304
305
        if ($this->getRequest()->isPost()) {
306
            $form->setData($this->params()->fromPost());
307
            if ($form->isValid()) {
308
                $data = $form->getData();
309
                $newDelegateInfo = Delegate::fromArray($data['delegate']);
310
311
                $command = new AssignToDelegate($newDelegateInfo, $ticketId, $purchaseId);
312
                $this->getCommandBus()->dispatch($command);
313
                $this->flashMessenger()
314
                    ->addSuccessMessage(
315
                        'Details updated successfully'
316
                    );
317
                return $this->redirect()->refresh();
318
            }
319
        }
320
321
        return new ViewModel(['purchase' => $purchase, 'form' => $form]);
322
    }
323
}
324