$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 26–51). * 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); } }