Passed
Push — master ( ea3e19...ac1336 )
by Adrien
08:46
created

Bvr::getType()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

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