Lines 100.00% 65 / 65
Functions and Methods 100.00% 5 / 5
Classes and Traits 100.00% 1 / 1
Name Lines Functions and Methods CRAP Classes and Traits
SkCsobMailParser 100.00% 65 / 65 100.00% 5 / 5 22 100.00% 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% 20 / 20 100.00% 1 / 1 6
 parseAll 100.00% 4 / 4 100.00% 1 / 1 2
 parse 100.00% 37 / 37 100.00% 1 / 1 12
1<?php
2
3declare(strict_types=1);
4
5namespace Tomaj\BankMailsParser\Parser\Csob;
6
7use Tomaj\BankMailsParser\Exception\InvalidEmailFormatException;
8use Tomaj\BankMailsParser\MailContent;
9use Tomaj\BankMailsParser\Parser\BankDate;
10use Tomaj\BankMailsParser\Parser\ParserInterface;
11use Tomaj\BankMailsParser\Parser\ParsesBankMail;
12
13final readonly class SkCsobMailParser implements ParserInterface
14{
15    use ParsesBankMail;
16
17    private const string TRANSACTION = '/(?:^[ \t\x{00A0}\x{2009}\x{202F}]*|že[ \t\x{00A0}\x{2009}\x{202F}]+)dňa[ \t\x{00A0}\x{2009}\x{202F}]+(\d{1,2}\.\d{1,2}\.\d{4})[ \t\x{00A0}\x{2009}\x{202F}]+bola na účte[ \t\x{00A0}\x{2009}\x{202F}]+(SK[0-9][0-9 \x{00A0}\x{2009}\x{202F}\t]*).*?zaúčtovaná suma SEPA platobného príkazu/mu';
18
19    private const string HEADER = '/(?:^[ \t\x{00A0}\x{2009}\x{202F}]*|že[ \t\x{00A0}\x{2009}\x{202F}]+)dňa[ \t\x{00A0}\x{2009}\x{202F}]+\d{1,2}\.\d{1,2}\.\d{4}[ \t\x{00A0}\x{2009}\x{202F}]+bola na účte/mu';
20
21    private const string BALANCE = '/Zostatok na účte po zaúčtovaní sumy platobnej operácie:[ \t\x{00A0}\x{2009}\x{202F}]*([+-])[ \t\x{00A0}\x{2009}\x{202F}]*((?:\d{1,3}(?:[ \t\x{00A0}\x{2009}\x{202F}]\d{3})*|\d+),\d{2})[ \t\x{00A0}\x{2009}\x{202F}]+([A-Z]{3})/u';
22
23    /**
24     * @return list<string>
25     */
26    #[\Override]
27    public function bankDomains(): array
28    {
29        return ['csob.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(self::HEADER, $this->withoutFreeText($this->normalizeHtmlLineBreaks($this->withoutByteOrderMark($content)))),
37        );
38    }
39
40    /**
41     * @return list<MailContent>
42     */
43    public function parseMulti(string $content): array
44    {
45        $content = $this->normalizeHtmlLineBreaks($this->requireUniformLineBreaks($content));
46        $chunks = $this->splitOutsideFreeText($content, self::HEADER);
47        $parsed = [];
48        $balances = [];
49        $errors = [];
50        $first = null;
51        $index = 0;
52        foreach ($chunks as $chunk) {
53            if (!$this->matched(self::TRANSACTION, $this->withoutFreeText($chunk))) {
54                continue;
55            }
56            ++$index;
57            try {
58                $parsed[] = $this->parse($chunk);
59                $balances[] = $this->balanceAmount($chunk, self::BALANCE);
60            } catch (InvalidEmailFormatException $exception) {
61                $first ??= $exception;
62                $errors[] = sprintf('transaction %d: %s', $index, $exception->getMessage());
63            }
64        }
65
66        $parsed = $this->transactionsOrFail($parsed, $errors, $first);
67        if (count($parsed) > 1 && $this->matched(self::BALANCE, $content)) {
68            $this->assertBalanceChain($balances, $parsed);
69        }
70
71        return $parsed;
72    }
73
74    #[\Override]
75    public function parseAll(string $content): array
76    {
77        $parsed = $this->parseMulti($content);
78        if ($parsed === []) {
79            throw InvalidEmailFormatException::malformedContent(self::class, 'Could not find transaction pattern');
80        }
81
82        return $parsed;
83    }
84
85    public function parse(string $content): MailContent
86    {
87        $original = $this->normalizeHtmlLineBreaks($this->requireUniformLineBreaks($content));
88        $receiverMessage = $this->lineValue($original, 'informácia pre príjemcu');
89        $content = $this->withoutFreeText($original);
90        if (!$this->matched(self::TRANSACTION, $content, $result)) {
91            throw InvalidEmailFormatException::malformedContent(self::class, 'Could not find transaction pattern');
92        }
93
94        $transactionDate = BankDate::parse($this->group($result, 1), '!j.n.Y', BankDate::bratislava());
95        $ownAccountNumber = $this->normalizeAccount($this->group($result, 2));
96
97        $money = null;
98        if ($this->matched($this->field('suma:', '([+-])[ \t\x{00A0}\x{2009}\x{202F}]*(.*?)[ \t\x{00A0}\x{2009}\x{202F}]+([A-Z]{3})\b'), $content, $amount)) {
99            $signed = ($this->group($amount, 1) === '-' ? '-' : '') . $this->group($amount, 2);
100            $money = $this->moneyOrNull($signed, $this->group($amount, 3), ',');
101        }
102
103        $vs = null;
104        $ss = null;
105        $ks = null;
106        if ($this->matched($this->field('referencia platiteľa:', '\/?VS(\d*)(?:\/SS(\d*))?(?:\/KS(\d*))?'), $original, $symbol)) {
107            $vs = $this->paymentSymbol($this->group($symbol, 1), null);
108            $ssRaw = $this->optionalGroup($symbol, 2);
109            $ksRaw = $this->optionalGroup($symbol, 3);
110            $ss = $ssRaw === null ? null : $this->paymentSymbol($ssRaw, null);
111            $ks = $ksRaw === null ? null : $this->paymentSymbol($ksRaw, null);
112        }
113        // 4.1.0 reads the first VS token in the mail. That also matched the payer name (ADVS2).
114        // The owner kept the 4.x value when it comes from this line only. Length is not capped.
115        if ($vs === null && $this->matched($this->field('informácia pre príjemcu:', '.*?(?<![A-Za-z])VS([0-9]+)(?!\d)'), $original, $fromMessage)) {
116            $vs = $this->group($fromMessage, 1);
117        }
118        if ($ks === null && $this->matched($this->field('informácia pre príjemcu:', '.*?(?<![A-Za-z])KS([0-9]+)(?!\d)'), $original, $fromMessage)) {
119            $ks = $this->group($fromMessage, 1);
120        }
121
122        $accountNumber = null;
123        if ($this->matched($this->field('z účtu:', '([A-Z0-9 \x{00A0}\x{2009}\x{202F}\t]+)'), $content, $source)) {
124            $accountNumber = $this->normalizeAccount($this->group($source, 1));
125        }
126
127        return new MailContent(
128            money: $money,
129            transactionDate: $transactionDate,
130            accountNumber: $accountNumber,
131            ownAccountNumber: $ownAccountNumber,
132            vs: $vs,
133            ss: $ss,
134            ks: $ks,
135            receiverMessage: $receiverMessage,
136        );
137    }
138}