Passed
Push — master ( a1cd93...1851db )
by Sylvain
11:56
created

DatatransAction::createTransactions()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 55
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 34
nc 5
nop 1
dl 0
loc 55
ccs 14
cts 14
cp 1
crap 5
rs 9.0648
c 2
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Application\Action;
6
7
use Application\Model\Account;
8
use Application\Model\Transaction;
9
use Application\Model\TransactionLine;
10
use Application\Model\User;
11
use Application\Repository\AccountRepository;
12
use Cake\Chronos\Chronos;
13
use Doctrine\ORM\EntityManager;
14
use Money\Money;
15
use Psr\Http\Message\ResponseInterface;
16
use Psr\Http\Message\ServerRequestInterface;
17
use Psr\Http\Server\RequestHandlerInterface;
18
use Zend\Diactoros\Response\HtmlResponse;
19
use Zend\Expressive\Template\TemplateRendererInterface;
20
21
class DatatransAction extends AbstractAction
22
{
23
    /** @var TemplateRendererInterface */
24
    private $template;
25
26
    /**
27
     * @var EntityManager
28
     */
29
    private $entityManager;
30
31 10
    /**
32
     * @var array
33 10
     */
34 10
    private $config;
35 10
36
    /**
37
     * DatatransAction constructor.
38
     *
39
     * @param EntityManager $entityManager
40
     * @param TemplateRendererInterface $template
41
     * @param array $config
42
     */
43
    public function __construct(EntityManager $entityManager, TemplateRendererInterface $template, array $config)
44
    {
45
        $this->entityManager = $entityManager;
46
        $this->template = $template;
47 10
        $this->config = $config;
48
    }
49 10
50 10
    /**
51
     * Webhook called by datatrans when a payment was made
52
     *
53 10
     * See documentation: https://api-reference.datatrans.ch/#failed-unsuccessful-authorization-response
54 1
     *
55
     * @param ServerRequestInterface $request
56
     * @param RequestHandlerInterface $handler
57 9
     *
58
     * @return ResponseInterface
59 9
     */
60 5
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
61 5
    {
62
        $request->getMethod();
63
        $body = $request->getParsedBody();
64
65 10
        try {
66
            if (!is_array($body)) {
67
                throw new \Exception('Parsed body is expected to be an array but got: ' . gettype($body));
68 10
            }
69
70
            if (isset($this->config['key'])) {
71
                $this->checkSignature($body, $this->config['key']);
72
            }
73
74
            $status = $body['status'] ?? '';
75
76
            $message = $this->dispatch($status, $body);
77
        } catch (\Throwable $exception) {
78
            $message = $this->createMessage('error', $exception->getMessage(), is_array($body) ? $body : []);
79
        }
80 10
81
        $viewModel = [
82
            'message' => $message,
83 10
        ];
84 10
85 10
        return new HtmlResponse($this->template->render('app::datatrans', $viewModel));
86
    }
87
88
    /**
89
     * Make sure the signature protecting important body fields is valid
90
     *
91
     * @param array $body
92
     * @param string $key HMAC-SHA256 signing key in hexadecimal format
93
     *
94
     * @throws \Exception
95
     */
96
    private function checkSignature(array $body, string $key): void
97 9
    {
98
        if (!isset($body['sign'])) {
99 9
            throw new \Exception('Missing HMAC signature');
100 9
        }
101 6
        $aliasCC = $body['aliasCC'] ?? '';
102 3
        $valueToSign = $aliasCC . @$body['merchantId'] . @$body['amount'] . @$body['currency'] . @$body['refno'];
103
        $expectedSign = hash_hmac('sha256', trim($valueToSign), hex2bin(trim($key)));
104 3
        if ($expectedSign !== $body['sign']) {
105 3
            throw new \Exception('Invalid HMAC signature');
106 1
        }
107
    }
108 1
109 2
    /**
110 1
     * Create a message in a coherent way
111
     *
112 1
     * @param string $status
113
     * @param string $message
114 1
     * @param array $detail
115
     *
116
     * @return array
117 5
     */
118
    private function createMessage(string $status, string $message, array $detail): array
119
    {
120 6
        return [
121
            'status' => $status,
122
            'message' => $message,
123 6
            'detail' => $detail,
124 6
        ];
125 6
    }
126
127 6
    /**
128 3
     * Dispatch the data received from Datatrans to take appropriate actions
129
     *
130
     * @param string $status
131 6
     * @param $body
132
     *
133
     * @return array
134 6
     */
135 6
    private function dispatch(string $status, array $body): array
136 1
    {
137
        switch ($status) {
138
            case 'success':
139 5
                $this->createTransactions($body);
140 5
                $message = $this->createMessage($status, $body['responseMessage'], $body);
141 5
142
                break;
143 5
            case 'error':
144
                $message = $this->createMessage($status, $body['errorMessage'], $body);
145 1
146
                break;
147
            case 'cancel':
148 4
                $message = $this->createMessage($status, 'Cancelled', $body);
149 4
150 1
                break;
151
            default:
152
                throw new \Exception('Unsupported status in Datatrans data: ' . $status);
153 3
        }
154 3
155
        return $message;
156 3
    }
157 3
158 3
    private function createTransactions(array $body): void
159 3
    {
160 3
        // Create only if a transaction with the same Datatrans reference doesn't already exist
161
        $datatransRef = $body['uppTransactionId'];
162
        $transactionRepository = $this->entityManager->getRepository(Transaction::class);
163 3
        $existing = $transactionRepository->count(['datatransRef' => $datatransRef]);
164
165 3
        if ($existing) {
166 3
            return;
167 3
        }
168 3
169 3
        $userId = $body['refno'] ?? null;
170 3
171 3
        /** @var User $user */
172 3
        $user = $this->entityManager->getRepository(User::class)->getOneById((int) $userId);
173
        if (!$user) {
174 3
            throw new \Exception('Cannot create transactions without a user');
175 3
        }
176
177
        $accountRepository = $this->entityManager->getRepository(Account::class);
178
        $userAccount = $accountRepository->getOrCreate($user);
179
        $bankAccount = $accountRepository->getOneById(AccountRepository::ACCOUNT_ID_FOR_BANK);
180
181
        if (!array_key_exists('amount', $body)) {
182
            // Do not support "registrations"
183
            throw new \Exception('Cannot create transactions without an amount');
184
        }
185
186
        $currency = $body['currency'] ?? '';
187
        if ($currency !== 'CHF') {
188
            throw new \Exception('Can only create transactions for CHF, but got: ' . $currency);
189
        }
190
191
        $now = Chronos::now();
192
        $name = 'Versement en ligne';
193
194
        $transaction = new Transaction();
195
        $this->entityManager->persist($transaction);
196
        $transaction->setName($name);
197
        $transaction->setTransactionDate($now);
198
        $transaction->setDatatransRef($datatransRef);
199
200
        // This could be removed later on. For now it's mostly for debugging
201
        $transaction->setInternalRemarks(json_encode($body, JSON_PRETTY_PRINT));
202
203
        $line = new TransactionLine();
204
        $this->entityManager->persist($line);
205
        $line->setName($name);
206
        $line->setTransactionDate($now);
207
        $line->setBalance(Money::CHF($body['amount']));
208
        $line->setTransaction($transaction);
209
        $line->setCredit($userAccount);
210
        $line->setDebit($bankAccount);
211
212
        $this->entityManager->flush();
213
    }
214
}
215