['國泰世華銀行消費彙整通知'], 'method' => 'parseCathaySummary' ], [ 'keywords' => ['永豐銀行信用卡消費通知'], 'method' => 'parseSinoPacNotification' ], [ 'keywords' => ['電子發票', '載具', '發票開立通知'], 'method' => 'parseElectronicInvoice' ], [ 'keywords' => ['消費通知', '刷卡', '卡片消費'], 'method' => 'parseCreditCardNotification' ], ]; public function parse(string $subject, string $body): ?array { // 🔄 遍歷矩陣,尋找第一個命中的規則 foreach ($this->parserMatrix as $rule) { if (Str::contains($subject, $rule['keywords'])) { $method = $rule['method']; // 動態呼叫對應的方法 (例如 $this->parseCathaySummary($body) ) return $this->{$method}($body); } } Log::info("【Parser】忽略信件,主旨未命中任何對照規則: [{$subject}]"); return null; } /** * 🟢 國泰世華消費彙整信件專屬解析 */ private function parseCathaySummary(string $body): ?array { $cleanBody = preg_replace('/\s+/', ' ', $body); if (preg_match('/(\d{4}\/\d{2}\/\d{2})/u', $cleanBody, $dateMatches)) { $date = Carbon::createFromFormat('Y/m/d', $dateMatches[1])->format('Y-m-d'); } else { $date = now()->format('Y-m-d'); } $pattern = '/NT\$\s*([0-9,]+)\s*<\/td>\s*]*>\s*([^<\s&]+)\s*<\/td>/u'; if (!preg_match_all($pattern, $cleanBody, $matches, PREG_SET_ORDER)) { Log::error("【Parser】國泰信件未命中消費明細。"); return null; } $results = []; foreach ($matches as $match) { $sellerName = $this->convertFullWidthToHalfWidth(trim($match[2])); $results[] = [ 'amount' => floatval(str_replace(',', '', $match[1])), 'item_name' => "信用卡消費: {$sellerName}", 'seller_name' => $sellerName, 'date' => $date, 'category_id' => null, ]; } return $results; } /** * 永豐銀行信用卡消費通知(單筆制) */ private function parseSinoPacNotification(string $body): ?array { $cleanBody = preg_replace('/\s+/', ' ', $body); $amount = 0; $sellerName = '永豐刷卡消費'; if (preg_match('/(?:新臺幣|TWD)\s*([0-9,]+)/u', $cleanBody, $amountMatches)) { $amount = floatval(str_replace(',', '', $amountMatches[1])); } if (preg_match('/特約商店[::]\s*([^<\s]+)/u', $cleanBody, $merchantMatches)) { $sellerName = trim($merchantMatches[1]); } if ($amount === 0) return null; if (preg_match('/(\d{4}\/\d{2}\/\d{2})/u', $cleanBody, $dateMatches)) { $date = Carbon::createFromFormat('Y/m/d', $dateMatches[1])->format('Y-m-d'); } else { $date = now()->format('Y-m-d'); } $sellerName = $this->convertFullWidthToHalfWidth($sellerName); return [ 'amount' => $amount, 'item_name' => "信用卡消費: {$sellerName}", 'seller_name' => $sellerName, 'date' => $date, 'category_id' => null, ]; } /** * 解析電子發票信件 */ private function parseElectronicInvoice(string $body): ?array { $amount = 0; $sellerName = '未知商家'; if (preg_match('/(總計|金額|總金額)[::]\s*?\$?([0-9,]+)/u', $body, $matches)) { $amount = floatval(str_replace(',', '', $matches[2])); } if (preg_match('/(賣方|開立店家|商店名稱)[::]\s*?([^\n<\s]+)/u', $body, $matches)) { $sellerName = trim($matches[2]); } if ($amount === 0) return null; return [ 'amount' => $amount, 'item_name' => '電子發票消費', 'seller_name' => $sellerName, 'date' => now()->format('Y-m-d'), 'category_id' => null, ]; } /** * 解析信用卡消費通知(泛用型備援) */ private function parseCreditCardNotification(string $body): ?array { $amount = 0; $sellerName = '一般刷卡消費'; if (preg_match('/(NT\$|元|TWD)\s*?([0-9,]+)/u', $body, $matches)) { $amount = floatval(str_replace(',', '', $matches[2])); } elseif (preg_match('/([0-9,]+)\s*?(元)/u', $body, $matches)) { $amount = floatval(str_replace(',', '', $matches[1])); } if (preg_match('/於\s*?([^\n\]((]+?)\s*?消費/u', $body, $matches)) { $sellerName = trim($matches[1]); } elseif (preg_match('/(特約商店|消費商店|商店)[::]\s*?([^\n<\s]+)/u', $body, $matches)) { $sellerName = trim($matches[2]); } if ($amount === 0) return null; return [ 'amount' => $amount, 'item_name' => "刷卡消費: {$sellerName}", 'seller_name' => $sellerName, 'date' => now()->format('Y-m-d'), 'category_id' => null, ]; } private function convertFullWidthToHalfWidth(string $str): string { $str = str_replace(' ', '', $str); return mb_convert_kana($str, 'asKV', 'UTF-8'); } }