Failed Conditions
Push — master ( 76f788...13b360 )
by Sylvain
08:55
created

DatatransHandler::createTransactions()   B

Complexity

Conditions 6
Paths 6

Size

Total Lines 60
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 6.0006

Importance

Changes 0
Metric Value
eloc 37
c 0
b 0
f 0
dl 0
loc 60
ccs 37
cts 38
cp 0.9737
rs 8.7057
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 Psr\Http\Server\RequestHandlerInterface;
24
use Throwable;
25
26
class DatatransHandler extends AbstractHandler
27
{
28
    /**
29
     * @var TemplateRendererInterface
30
     */
31
    private $template;
32
33
    /**
34
     * @var EntityManager
35
     */
36
    private $entityManager;
37
38
    /**
39
     * @var array
40
     */
41
    private $config;
42
43
    /**
44
     * DatatransAction constructor.
45
     */
46 12
    public function __construct(EntityManager $entityManager, TemplateRendererInterface $template, array $config)
47
    {
48 12
        $this->entityManager = $entityManager;
49 12
        $this->template = $template;
50 12
        $this->config = $config;
51 12
    }
52
53
    /**
54
     * Webhook called by datatrans when a payment was made
55
     *
56
     * See documentation: https://api-reference.datatrans.ch/#failed-unsuccessful-authorization-response
57
     */
58 12
    public function handle(ServerRequestInterface $request): ResponseInterface
59
    {
60 12
        $body = $request->getParsedBody();
61 12
        $extraToLog = is_array($body) ? $body : ['rawBody' => $request->getBody()->getContents()];
62
63 12
        _log()->info(LogRepository::DATATRANS_WEBHOOK_BEGIN, $extraToLog);
64
65
        try {
66 12
            if (!is_array($body)) {
67 1
                throw new Exception('Parsed body is expected to be an array but got: ' . gettype($body));
68
            }
69
70 11
            if (isset($this->config['datatrans'], $this->config['datatrans']['key'])) {
71 11
                $this->checkSignature($body, $this->config['datatrans']['key']);
72
            }
73
74 9
            $status = $body['status'] ?? '';
75
76 9
            $message = $this->dispatch($status, $body);
77 7
        } catch (Throwable $exception) {
78 7
            $message = $this->createMessage('error', $exception->getMessage(), is_array($body) ? $body : []);
79
        }
80
81
        $viewModel = [
82 12
            'message' => $message,
83
        ];
84
85 12
        _log()->info(LogRepository::DATATRANS_WEBHOOK_END, $message);
86
87 12
        return new HtmlResponse($this->template->render('app::datatrans', $viewModel));
88
    }
89
90
    /**
91
     * Make sure the signature protecting important body fields is valid
92
     *
93
     * @param string $key HMAC-SHA256 signing key in hexadecimal format
94
     */
95 11
    private function checkSignature(array $body, string $key): void
96
    {
97 11
        if (!isset($body['sign'])) {
98 1
            throw new Exception('Missing HMAC signature');
99
        }
100 10
        $aliasCC = $body['aliasCC'] ?? '';
101 10
        $valueToSign = $aliasCC . @$body['merchantId'] . @$body['amount'] . @$body['currency'] . @$body['refno'];
102 10
        $expectedSign = hash_hmac('sha256', trim($valueToSign), hex2bin(trim($key)));
103 10
        if ($expectedSign !== $body['sign']) {
104 1
            throw new Exception('Invalid HMAC signature');
105
        }
106 9
    }
107
108
    /**
109
     * Create a message in a coherent way
110
     */
111 12
    private function createMessage(string $status, string $message, array $detail): array
112
    {
113
        return [
114 12
            'status' => $status,
115 12
            'message' => $message,
116 12
            'detail' => $detail,
117
        ];
118
    }
119
120
    /**
121
     * Dispatch the data received from Datatrans to take appropriate actions
122
     */
123 9
    private function dispatch(string $status, array $body): array
124
    {
125
        switch ($status) {
126 9
            case 'success':
127 6
                $this->createTransactions($body);
128 3
                $message = $this->createMessage($status, $body['responseMessage'], $body);
129
130 3
                break;
131 3
            case 'error':
132 1
                $message = $this->createMessage($status, $body['errorMessage'], $body);
133
134 1
                break;
135 2
            case 'cancel':
136 1
                $message = $this->createMessage($status, 'Cancelled', $body);
137
138 1
                break;
139
            default:
140 1
                throw new Exception('Unsupported status in Datatrans data: ' . $status);
141
        }
142
143 5
        return $message;
144
    }
145
146 6
    private function createTransactions(array $body): void
147
    {
148
        // Create only if a transaction with the same Datatrans reference doesn't already exist
149 6
        $datatransRef = $body['uppTransactionId'];
150 6
        $transactionRepository = $this->entityManager->getRepository(Transaction::class);
151 6
        $existing = $transactionRepository->count(['datatransRef' => $datatransRef]);
152
153 6
        if ($existing) {
154 3
            return;
155
        }
156
157 6
        $userId = $body['refno'] ?? null;
158
159
        /** @var UserRepository $userRepository */
160 6
        $userRepository = $this->entityManager->getRepository(User::class);
161 6
        $user = $userRepository->getOneById((int) $userId);
162 6
        if (!$user) {
163 1
            throw new Exception('Cannot create transactions without a user');
164
        }
165
166
        /** @var AccountRepository $accountRepository */
167 5
        $accountRepository = $this->entityManager->getRepository(Account::class);
168 5
        $userAccount = $accountRepository->getOrCreate($user);
169 5
        if (!isset($this->config['accounting'], $this->config['accounting']['bankAccountCode'])) {
170
            throw new Exception('Missing config accounting/bankAccountCode');
171
        }
172 5
        $bankAccount = $accountRepository->findOneByCode($this->config['accounting']['bankAccountCode']);
0 ignored issues
show
Bug introduced by
The method findOneByCode() does not exist on Application\Repository\AccountRepository. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

172
        /** @scrutinizer ignore-call */ 
173
        $bankAccount = $accountRepository->findOneByCode($this->config['accounting']['bankAccountCode']);
Loading history...
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