initial: bootstrap from BukidBountyApp base

This commit is contained in:
Jonathan Sykes
2026-06-06 18:43:00 +08:00
commit eb4a5731fb
5674 changed files with 160857 additions and 0 deletions

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Helpers;
/**
* Decoder for QRPH / EMV QRCPS strings.
*
* QRPH follows the EMVCo Merchant-Presented QR Code Specification.
* Each field is encoded as: 2-char tag + 2-char length (decimal) + value.
* Nested (sub-field) structures use the same encoding inside a parent value.
*/
class QrphDecoder
{
/**
* Decode a QRPH string and return human-readable fields.
*/
public static function decode(string $qrString): array
{
$qrString = trim($qrString);
$fields = self::parseTlv($qrString);
return [
'raw' => $qrString,
'valid' => self::validateCrc($qrString, $fields),
'initiation_method'=> self::initiationLabel($fields['01'] ?? null),
'merchant_name' => $fields['59'] ?? null,
'merchant_city' => $fields['60'] ?? null,
'country_code' => $fields['58'] ?? null,
'currency' => $fields['53'] ?? null, // 608 = PHP
'amount' => isset($fields['54']) ? (float) $fields['54'] : null,
'merchant_account' => self::extractMerchantAccount($fields),
];
}
/**
* Parse a flat TLV string into [ tag => value ] pairs.
*/
public static function parseTlv(string $data): array
{
$result = [];
$pos = 0;
$len = strlen($data);
while ($pos + 4 <= $len) {
$tag = substr($data, $pos, 2);
$length = (int) substr($data, $pos + 2, 2);
$value = substr($data, $pos + 4, $length);
$result[$tag] = $value;
$pos += 4 + $length;
if ($tag === '63') break; // CRC is always last
}
return $result;
}
/**
* Extract primary account info from merchant account fields (tags 2651).
* Returns the first one found with a recognisable GUID.
*/
private static function extractMerchantAccount(array $fields): ?array
{
for ($i = 26; $i <= 51; $i++) {
$tag = str_pad((string) $i, 2, '0', STR_PAD_LEFT);
if (!isset($fields[$tag])) continue;
$sub = self::parseTlv($fields[$tag]);
$guid = $sub['00'] ?? '';
return [
'guid' => $guid,
'network' => self::networkFromGuid($guid),
'account' => $sub['01'] ?? null,
'name' => $sub['02'] ?? null,
];
}
return null;
}
private static function networkFromGuid(string $guid): string
{
$map = [
// Standard QRPH / InstaPay (BSP / PPMI)
'com.p2pqrph' => 'InstaPay (QRPH)',
'ph.ppmi' => 'InstaPay',
'ph.bsp.qrph' => 'InstaPay (BSP)',
// GoTyme Bank
'com.gotyme' => 'GoTyme',
'ph.gotyme' => 'GoTyme',
'gotyme' => 'GoTyme',
// GCash
'com.gcash' => 'GCash',
// Maya / PayMaya
'com.paymaya' => 'Maya',
'com.maya' => 'Maya',
// Major PH banks
'ph.bpi' => 'BPI',
'ph.bdo' => 'BDO',
'com.landbank' => 'Landbank',
'ph.landbank' => 'Landbank',
'ph.ubp' => 'UnionBank',
'ph.metrobank' => 'Metrobank',
'ph.rcbc' => 'RCBC',
'ph.pnb' => 'PNB',
'ph.securitybank' => 'Security Bank',
'ph.chinabank' => 'China Bank',
'ph.eastwest' => 'EastWest Bank',
'ph.psbank' => 'PSBank',
'ph.boc' => 'Bank of Commerce',
'com.seabank' => 'SeaBank',
'com.tonik' => 'Tonik',
'com.uno' => 'UNO Digital Bank',
'com.komo' => 'CIMB / Komo',
'com.ownbank' => 'OwnBank',
];
$lower = strtolower($guid);
foreach ($map as $key => $label) {
if (str_contains($lower, $key)) return $label;
}
// Fallback: return the raw GUID so it's still visible
return $guid ?: 'Unknown';
}
private static function initiationLabel(?string $code): string
{
return match ($code) {
'11' => 'Static',
'12' => 'Dynamic',
default => $code ?? 'Unknown',
};
}
/**
* Validate the CRC-16/CCITT-FALSE checksum embedded in the QR string.
*/
public static function validateCrc(string $qrString, array $parsed = []): bool
{
if (empty($parsed['63'])) return false;
// CRC covers everything up to and including the "6304" tag+length prefix.
$crcPos = strrpos($qrString, '6304');
if ($crcPos === false) return false;
$payload = substr($qrString, 0, $crcPos + 4);
$expected = strtoupper($parsed['63']);
$computed = self::crc16($payload);
return $computed === $expected;
}
/**
* CRC-16/CCITT-FALSE (polynomial 0x1021, seed 0xFFFF).
*/
public static function crc16(string $data): string
{
$crc = 0xFFFF;
for ($i = 0, $l = strlen($data); $i < $l; $i++) {
$crc ^= (ord($data[$i]) << 8);
for ($j = 0; $j < 8; $j++) {
$crc = ($crc & 0x8000)
? (($crc << 1) ^ 0x1021) & 0xFFFF
: ($crc << 1) & 0xFFFF;
}
}
return sprintf('%04X', $crc);
}
}