Lines
96.29%
52 / 54
Functions and Methods
87.50%
7 / 8
Classes and Traits
0.00%
0 / 1
| Name | Lines | Functions and Methods | CRAP | Classes and Traits | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| TatraBankaStatementMailParser | 96.29% | 52 / 54 | 87.50% | 7 / 8 | 24 | 0.00% | 0 / 1 | |||
| __construct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| bankDomains | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| supports | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | |||||
| parseMulti | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| parse | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | |||||
| parseAll | 100.00% | 4 / 4 | 100.00% | 1 / 1 | 2 | |||||
| decryptRows | 93.33% | 28 / 30 | 0.00% | 0 / 1 | 11.04 | |||||
| parseRow | 100.00% | 13 / 13 | 100.00% | 1 / 1 | 6 | |||||
| 1 | <?php | |
| 2 | ||
| 3 | declare(strict_types=1); | |
| 4 | ||
| 5 | namespace Tomaj\BankMailsParser\Parser\TatraBanka; | |
| 6 | ||
| 7 | use Tomaj\BankMailsParser\Exception\ConfigurationException; | |
| 8 | use Tomaj\BankMailsParser\Exception\DecryptionException; | |
| 9 | use Tomaj\BankMailsParser\Exception\InvalidEmailFormatException; | |
| 10 | use Tomaj\BankMailsParser\MailContent; | |
| 11 | use Tomaj\BankMailsParser\Parser\BankDate; | |
| 12 | use Tomaj\BankMailsParser\Parser\ParserInterface; | |
| 13 | use Tomaj\BankMailsParser\Parser\ParsesBankMail; | |
| 14 | ||
| 15 | final readonly class TatraBankaStatementMailParser implements ParserInterface | |
| 16 | { | |
| 17 | use ParsesBankMail; | |
| 18 | ||
| 19 | public function __construct( | |
| 20 | private ?TatraBankaMailDecryptor $decryptor = null, | |
| 21 | ) {} | |
| 22 | ||
| 23 | /** | |
| 24 | * @return list<string> | |
| 25 | */ | |
| 26 | #[\Override] | |
| 27 | public function bankDomains(): array | |
| 28 | { | |
| 29 | return ['tatrabanka.sk']; | |
| 30 | } | |
| 31 | ||
| 32 | #[\Override] | |
| 33 | public function supports(string $content, ?string $sender = null, ?string $subject = null): bool | |
| 34 | { | |
| 35 | return $this->supportsQuietly( | |
| 36 | fn(): bool => $this->matched('/^-----BEGIN PGP MESSAGE-----/m', $this->withoutFreeText($this->withoutByteOrderMark($content))), | |
| 37 | ); | |
| 38 | } | |
| 39 | ||
| 40 | /** | |
| 41 | * @return list<MailContent> | |
| 42 | */ | |
| 43 | public function parseMulti(string $content): array | |
| 44 | { | |
| 45 | return $this->parseAll($content); | |
| 46 | } | |
| 47 | ||
| 48 | public function parse(string $content): MailContent | |
| 49 | { | |
| 50 | return $this->parseAll($content)[0]; | |
| 51 | } | |
| 52 | ||
| 53 | #[\Override] | |
| 54 | public function parseAll(string $content): array | |
| 55 | { | |
| 56 | $content = $this->scrubUtf8($this->requireUniformLineBreaks($content)); | |
| 57 | if (str_contains($content, 'BEGIN PGP MESSAGE')) { | |
| 58 | return $this->decryptRows($content); | |
| 59 | } | |
| 60 | ||
| 61 | return [$this->parseRow($content)]; | |
| 62 | } | |
| 63 | ||
| 64 | /** | |
| 65 | * @return list<MailContent> | |
| 66 | */ | |
| 67 | private function decryptRows(string $content): array | |
| 68 | { | |
| 69 | if (!$this->decryptor instanceof TatraBankaMailDecryptor) { | |
| 70 | throw new ConfigurationException('PGP decryptor is not configured.'); | |
| 71 | } | |
| 72 | ||
| 73 | if (!$this->matched('/(-{5}BEGIN[A-Za-z0-9 \-\r\n+\/=]+END PGP MESSAGE-{5})/m', $content, $results)) { | |
| 74 | throw InvalidEmailFormatException::malformedContent(self::class, 'Encrypted statement could not be read'); | |
| 75 | } | |
| 76 | ||
| 77 | try { | |
| 78 | $decrypted = $this->decryptor->decrypt($this->group($results, 0)); | |
| 79 | } catch (ConfigurationException $exception) { | |
| 80 | throw $exception; | |
| 81 | } catch (DecryptionException $exception) { | |
| 82 | throw InvalidEmailFormatException::malformedContent(self::class, 'Encrypted statement could not be read', $exception); | |
| 83 | } | |
| 84 | ||
| 85 | $split = preg_split("/\r\n|\n|\r/", $decrypted); | |
| 86 | if ($split === false) { | |
| 87 | throw InvalidEmailFormatException::malformedContent(self::class, 'Encrypted statement could not be read'); | |
| 88 | } | |
| 89 | ||
| 90 | $parsed = []; | |
| 91 | $errors = []; | |
| 92 | $rowNumber = 0; | |
| 93 | foreach (array_slice($split, 1) as $transaction) { | |
| 94 | if (trim($transaction) === '') { | |
| 95 | continue; | |
| 96 | } | |
| 97 | ++$rowNumber; | |
| 98 | try { | |
| 99 | $parsed[] = $this->parseRow($transaction); | |
| 100 | } catch (InvalidEmailFormatException $exception) { | |
| 101 | $errors[] = sprintf('row %d: %s', $rowNumber, $exception->getMessage()); | |
| 102 | } | |
| 103 | } | |
| 104 | ||
| 105 | if ($errors !== []) { | |
| 106 | throw InvalidEmailFormatException::malformedContent( | |
| 107 | self::class, | |
| 108 | 'Could not parse every transaction: ' . implode('; ', $errors), | |
| 109 | ); | |
| 110 | } | |
| 111 | ||
| 112 | if ($parsed === []) { | |
| 113 | throw InvalidEmailFormatException::malformedContent(self::class, 'Encrypted statement could not be read'); | |
| 114 | } | |
| 115 | ||
| 116 | return $parsed; | |
| 117 | } | |
| 118 | ||
| 119 | private function parseRow(string $content): MailContent | |
| 120 | { | |
| 121 | $cols = array_filter(explode('|', $content), static fn(string $value): bool => $value !== '' && $value !== '0'); | |
| 122 | if ($cols === []) { | |
| 123 | throw InvalidEmailFormatException::malformedContent(self::class, 'No valid columns found'); | |
| 124 | } | |
| 125 | ||
| 126 | if (!isset($cols[9], $cols[10])) { | |
| 127 | throw InvalidEmailFormatException::missingRequiredField(self::class, 'amount/currency'); | |
| 128 | } | |
| 129 | ||
| 130 | $money = $this->money($cols[9], trim($cols[10]), '.'); | |
| 131 | $transactionDate = isset($cols[0]) ? BankDate::parse($cols[0], '!Ymd', BankDate::bratislava()) : null; | |
| 132 | ||
| 133 | return new MailContent( | |
| 134 | money: $money, | |
| 135 | transactionDate: $transactionDate, | |
| 136 | vs: $cols[18] ?? null, | |
| 137 | // Column 19 is the statement header "Číslo účtu": the merchant account | |
| 138 | // credited for this card sale. It can differ on every row. | |
| 139 | accountNumber: isset($cols[19]) ? $this->normalizeAccount($cols[19]) : null, | |
| 140 | ); | |
| 141 | } | |
| 142 | } |