176 lines
3.7 KiB
PHP
176 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Model\Faktura;
|
|
|
|
use Mpdf\QrCode\Output\Png;
|
|
use Mpdf\QrCode\QrCode;
|
|
|
|
/**
|
|
* Český QR platební kód (SPD / SPAYD).
|
|
*/
|
|
final class SpaydQr
|
|
{
|
|
/**
|
|
* Binární PNG QR kódu pro mPDF imageVars, nebo null pokud nelze sestavit.
|
|
*/
|
|
public static function png(
|
|
string $ucetNeboIban,
|
|
float $castka,
|
|
string $vs = '',
|
|
?\DateTimeInterface $splatnost = null,
|
|
string $zprava = '',
|
|
int $size = 240,
|
|
): ?string {
|
|
$spd = self::spd($ucetNeboIban, $castka, $vs, $splatnost, $zprava);
|
|
if ($spd === null) {
|
|
return null;
|
|
}
|
|
|
|
return (new Png())->output(new QrCode($spd), $size, [255, 255, 255], [0, 0, 0]);
|
|
}
|
|
|
|
public static function spd(
|
|
string $ucetNeboIban,
|
|
float $castka,
|
|
string $vs = '',
|
|
?\DateTimeInterface $splatnost = null,
|
|
string $zprava = '',
|
|
): ?string {
|
|
$iban = self::toIban($ucetNeboIban);
|
|
if ($iban === null) {
|
|
return null;
|
|
}
|
|
|
|
$parts = [
|
|
'SPD',
|
|
'1.0',
|
|
'ACC:' . $iban,
|
|
];
|
|
|
|
if ($castka > 0) {
|
|
$parts[] = 'AM:' . number_format($castka, 2, '.', '');
|
|
}
|
|
$parts[] = 'CC:CZK';
|
|
|
|
if ($splatnost !== null) {
|
|
$parts[] = 'DT:' . $splatnost->format('Ymd');
|
|
}
|
|
|
|
$msg = self::spdText($zprava, 60);
|
|
if ($msg !== '') {
|
|
$parts[] = 'MSG:' . $msg;
|
|
}
|
|
|
|
$vsDigits = self::variableSymbol($vs);
|
|
if ($vsDigits !== '') {
|
|
$parts[] = 'X-VS:' . $vsDigits;
|
|
}
|
|
|
|
return implode('*', $parts) . '*';
|
|
}
|
|
|
|
/**
|
|
* IBAN z IBAN řetězce nebo českého formátu „účet/kód banky“.
|
|
*/
|
|
public static function toIban(string $value): ?string
|
|
{
|
|
$value = strtoupper(preg_replace('/\s+/', '', trim($value)) ?? '');
|
|
if ($value === '') {
|
|
return null;
|
|
}
|
|
|
|
if (preg_match('/^CZ\d{22}$/', $value)) {
|
|
return self::isValidIban($value) ? $value : null;
|
|
}
|
|
|
|
if (!preg_match('/^(?:(\d{1,6})-)?(\d{2,10})\/(\d{4})$/', $value, $m)) {
|
|
return null;
|
|
}
|
|
|
|
$prefix = str_pad($m[1] !== '' ? $m[1] : '0', 6, '0', STR_PAD_LEFT);
|
|
$number = str_pad($m[2], 10, '0', STR_PAD_LEFT);
|
|
$bank = $m[3];
|
|
$bban = $bank . $prefix . $number;
|
|
|
|
// CZ = 12 35 → kontrolní číslo z BBAN + "123500"
|
|
$checkBase = $bban . '123500';
|
|
$mod = self::mod97($checkBase);
|
|
if ($mod === null) {
|
|
return null;
|
|
}
|
|
$check = 98 - $mod;
|
|
|
|
return 'CZ' . sprintf('%02d', $check) . $bban;
|
|
}
|
|
|
|
private static function variableSymbol(string $value): string
|
|
{
|
|
$digits = preg_replace('/\D+/', '', $value) ?? '';
|
|
if ($digits === '') {
|
|
return '';
|
|
}
|
|
if (strlen($digits) > 10) {
|
|
$digits = substr($digits, -10);
|
|
}
|
|
|
|
return ltrim($digits, '0') !== '' ? $digits : '';
|
|
}
|
|
|
|
private static function spdText(string $value, int $maxLen): string
|
|
{
|
|
$value = self::bezDiakritiky(trim($value));
|
|
$value = preg_replace('/[^A-Za-z0-9 \/?:().,+\-]/', '', $value) ?? '';
|
|
$value = trim(preg_replace('/\s+/', ' ', $value) ?? '');
|
|
|
|
return mb_substr($value, 0, $maxLen);
|
|
}
|
|
|
|
private static function bezDiakritiky(string $value): string
|
|
{
|
|
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
|
if ($ascii === false) {
|
|
return $value;
|
|
}
|
|
|
|
return $ascii;
|
|
}
|
|
|
|
private static function isValidIban(string $iban): bool
|
|
{
|
|
$rearranged = substr($iban, 4) . substr($iban, 0, 4);
|
|
$numeric = '';
|
|
$length = strlen($rearranged);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$ch = $rearranged[$i];
|
|
if ($ch >= 'A' && $ch <= 'Z') {
|
|
$numeric .= (string) (ord($ch) - 55);
|
|
} elseif ($ch >= '0' && $ch <= '9') {
|
|
$numeric .= $ch;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
$mod = self::mod97($numeric);
|
|
|
|
return $mod === 1;
|
|
}
|
|
|
|
private static function mod97(string $number): ?int
|
|
{
|
|
if ($number === '' || !ctype_digit($number)) {
|
|
return null;
|
|
}
|
|
|
|
$remainder = 0;
|
|
$length = strlen($number);
|
|
for ($i = 0; $i < $length; $i++) {
|
|
$remainder = ($remainder * 10 + (int) $number[$i]) % 97;
|
|
}
|
|
|
|
return $remainder;
|
|
}
|
|
}
|