Bvr::modulo10()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 2
b 0
f 0
nc 3
nop 1
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 3
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Ecodev\Felix\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
 * ```
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
 *
27
 * @see https://www.postfinance.ch/content/dam/pfch/doc/cust/download/inpayslip_isr_man_fr.pdf
28
 */
29
final class Bvr
30
{
31
    private const TABLE = [
32
        [0, 9, 4, 6, 8, 2, 7, 1, 3, 5],
33
        [9, 4, 6, 8, 2, 7, 1, 3, 5, 0],
34
        [4, 6, 8, 2, 7, 1, 3, 5, 0, 9],
35
        [6, 8, 2, 7, 1, 3, 5, 0, 9, 4],
36
        [8, 2, 7, 1, 3, 5, 0, 9, 4, 6],
37
        [2, 7, 1, 3, 5, 0, 9, 4, 6, 8],
38
        [7, 1, 3, 5, 0, 9, 4, 6, 8, 2],
39
        [1, 3, 5, 0, 9, 4, 6, 8, 2, 7],
40
        [3, 5, 0, 9, 4, 6, 8, 2, 7, 1],
41
        [5, 0, 9, 4, 6, 8, 2, 7, 1, 3],
42
    ];
43
44
    /**
45
     * Get the reference number, including the verification digit.
46
     */
47 5
    public static function getReferenceNumber(string $bankAccount, string $customId): string
48
    {
49 5
        if (!preg_match('~^\d{0,20}$~', $customId)) {
50 2
            throw new Exception('Invalid custom ID. It must be 20 or less digits, but got: `' . $customId . '`');
51
        }
52
53 3
        if (!preg_match('~^\d{6}$~', $bankAccount)) {
54 1
            throw new Exception('Invalid bank account. It must be exactly 6 digits, but got: `' . $bankAccount . '`');
55
        }
56 2
        $value = $bankAccount . self::pad($customId);
57
58 2
        return $value . self::modulo10($value);
59
    }
60
61
    /**
62
     * Extract the custom ID as string from a valid reference number.
63
     */
64 5
    public static function extractCustomId(string $referenceNumber): string
65
    {
66 5
        if (!preg_match('~^\d{27}$~', $referenceNumber)) {
67 1
            throw new Exception('Invalid reference number. It must be exactly 27 digits, but got: `' . $referenceNumber . '`');
68
        }
69 4
        $value = mb_substr($referenceNumber, 0, 26);
70 4
        $expectedVerificationDigit = (int) mb_substr($referenceNumber, 26, 27);
71 4
        $actualVerificationDigit = self::modulo10($value);
72 4
        if ($expectedVerificationDigit !== $actualVerificationDigit) {
73 1
            throw new Exception('Invalid reference number. The verification digit does not match. Expected `' . $expectedVerificationDigit . '`, but got `' . $actualVerificationDigit . '`');
74
        }
75
76 3
        return mb_substr($referenceNumber, 6, 20);
77
    }
78
79
    /**
80
     * Check if an IBAN is actually a valid Swiss QR-IBAN.
81
     */
82 4
    public static function isQrIban(string $iban): bool
83
    {
84 4
        if (!preg_match('/^CH[0-9]{2}([0-9]{5})[0-9A-Z]{12}$/', $iban, $m)) {
85 2
            return false;
86
        }
87
88 2
        $bankClearing = (int) $m[1];
89
90 2
        if ($bankClearing >= 30000 && $bankClearing <= 31199) {
91 1
            return true;
92
        }
93
94 1
        return false;
95
    }
96
97 2
    private static function pad(string $string): string
98
    {
99 2
        return str_pad($string, 20, '0', STR_PAD_LEFT);
100
    }
101
102
    /**
103
     * @internal
104
     */
105 22
    public static function modulo10(string $number): int
106
    {
107 22
        $report = 0;
108
109 22
        if ($number === '') {
110 1
            return $report;
111
        }
112
113 21
        $digits = mb_str_split($number);
114 21
        foreach ($digits as $value) {
115 21
            $report = self::TABLE[$report][(int) $value];
116
        }
117
118 21
        return (10 - $report) % 10;
119
    }
120
}
121