Failed Conditions
Push — master ( 893dc8...0c1f2a )
by Adrien
06:01
created

DatatransHandler::createTransactions()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 61
Code Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 38
CRAP Score 6.0006

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 38
c 1
b 1
f 0
dl 0
loc 61
ccs 38
cts 39
cp 0.9744
rs 8.6897
cc 6
nc 6
nop 1
crap 6.0006

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