Failed Conditions
Push — master ( 398dce...01f535 )
by Adrien
02:21
created

Bvr::pad()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\Service;
6
7
use Exception;
8
use Money\Money;
9
10
/**
11
 * Class to generate BVR reference number and encoding lines.
12
 *
13
 * Typically usage would one of the following:
14
 *
15
 * ```php
16
 * <?php
17
 *
18
 * // Provided by your bank
19
 * $bankAccount = '800876';
20
 * $postAccount = '01-3456-0';
21
 *
22
 * // Your own custom ID to uniquely identify the payment
23
 * $myId = (string) $user->getId();
24
 *
25
 * $referenceNumberToCopyPasteInEBanking = Bvr::getReferenceNumber($bankAccount, $myId);
26
 *
27
 * // OR get encoding line
28
 * $amount = Money::CHF(1995);
29
 * $encodingLineToCopyPasteInEBanking = Bvr::getEncodingLine($bankAccount, $myId, $postAccount, $amount);
30
 * ```
31
 *
32
 * @see https://www.postfinance.ch/content/dam/pfch/doc/cust/download/inpayslip_isr_man_fr.pdf
33
 */
34
class Bvr
35
{
36
    private const TABLE = [
37
        [0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
38
        [9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
39
        [4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
40
        [6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
41
        [8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
42
        [2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
43
        [7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
44
        [1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
45
        [3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
46
        [5, 0, 9, 4, 6, 8, 2, 7, 1, 3],
47
    ];
48
49
    /**
50
     * Get the reference number, including the verification digit
51
     *
52
     * @param string $bankAccount
53
     * @param string $customId
54
     *
55
     * @return string
56
     */
57 12
    public static function getReferenceNumber(string $bankAccount, string $customId): string
58
    {
59 12
        if (!preg_match('~^\d{0,20}$~', $customId)) {
60 4
            throw new Exception('Invalid custom ID. It must be 20 or less digits, but got: `' . $customId . '`');
61
        }
62
63 8
        if (!preg_match('~^\d{6}$~', $bankAccount)) {
64 1
            throw new Exception('Invalid bank account. It must be exactly 6 digits, but got: `' . $bankAccount . '`');
65
        }
66 7
        $value = $bankAccount . self::pad($customId, 20);
67
68 7
        return $value . self::modulo10($value);
69
    }
70
71
    /**
72
     * Extract the custom ID as string from a valid reference number
73
     *
74
     * @param string $referenceNumber
75
     *
76
     * @return string
77
     */
78 5
    public static function extractCustomId(string $referenceNumber): string
79
    {
80 5
        if (!preg_match('~^\d{27}$~', $referenceNumber)) {
81 1
            throw new Exception('Invalid reference number. It must be exactly 27 digits, but got: `' . $referenceNumber . '`');
82
        }
83 4
        $value = mb_substr($referenceNumber, 0, 26);
84 4
        $expectedVerificationDigit = (int) mb_substr($referenceNumber, 26, 27);
85 4
        $actualVerificationDigit = self::modulo10($value);
86 4
        if ($expectedVerificationDigit !== $actualVerificationDigit) {
87 1
            throw new Exception('Invalid reference number. The verification digit does not match. Expected `' . $expectedVerificationDigit . '`, but got `' . $actualVerificationDigit . '`');
88
        }
89
90 3
        return mb_substr($referenceNumber, 6, 20);
91
    }
92
93
    /**
94
     * Get the full encoding line
95
     *
96
     * @param string $bankAccount
97
     * @param string $customId
98
     * @param string $postAccount
99
     * @param null|Money $amount
100
     *
101
     * @return string
102
     */
103 8
    public static function getEncodingLine(string $bankAccount, string $customId, string $postAccount, ?Money $amount = null): string
104
    {
105 8
        $type = self::getType($amount);
106 7
        $referenceNumber = self::getReferenceNumber($bankAccount, $customId);
107 5
        $formattedPostAccount = self::getPostAccount($postAccount);
108
109
        $result =
110 3
            $type . '>'
111 3
            . $referenceNumber . '+ '
112 3
            . $formattedPostAccount . '>';
113
114 3
        return $result;
115
    }
116
117 7
    private static function pad(string $string, int $length): string
118
    {
119 7
        return str_pad($string, $length, '0', STR_PAD_LEFT);
120
    }
121
122 29
    public static function modulo10(string $number): int
123
    {
124 29
        $report = 0;
125
126 29
        if ($number === '') {
127 1
            return $report;
128
        }
129
130 28
        $digits = mb_str_split($number);
131 28
        if ($digits === false) {
132
            throw new \Exception('Could not split number into digits');
133
        }
134
135 28
        foreach ($digits as $value) {
136 28
            $report = self::TABLE[$report][(int) $value];
137
        }
138
139 28
        return (10 - $report) % 10;
140
    }
141
142 5
    private static function getPostAccount(string $postAccount): string
143
    {
144 5
        if (!preg_match('~^(\d+)-(\d+)-(\d)$~', $postAccount, $m)) {
145 1
            throw new Exception('Invalid post account number, got `' . $postAccount . '`');
146
        }
147
148 4
        $participantNumber = self::pad($m[1], 2) . self::pad($m[2], 6) . $m[3];
149
150 4
        if (mb_strlen($participantNumber) !== 9) {
151 1
            throw new Exception('The post account number is too long, got `' . $postAccount . '`');
152
        }
153
154 3
        return $participantNumber;
155
    }
156
157
    /**
158
     * Get type of document and amount
159
     *
160
     * @param null|Money $amount
161
     *
162
     * @return string
163
     */
164 8
    private static function getType(?Money $amount): string
165
    {
166 8
        if ($amount === null) {
167 6
            $type = '04';
168 2
        } elseif ($amount->isNegative()) {
169 1
            throw new Exception('Invalid amount. Must be positive, but got: `' . $amount->getAmount() . '`');
170
        } else {
171 1
            $type = '01' . self::pad($amount->getAmount(), 10);
172
        }
173
174 7
        return $type . self::modulo10($type);
175
    }
176
}
177