Kufrik/app/Model/Faktura/SpaydQr.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;
}
}