mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 00:10:00 +00:00
181 lines
5.7 KiB
PHP
181 lines
5.7 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use Illuminate\Support\Str;
|
||
use Illuminate\Support\Facades\Log;
|
||
use Carbon\Carbon;
|
||
|
||
class GmailParserService
|
||
{
|
||
|
||
protected array $parserMatrix = [
|
||
[
|
||
'keywords' => ['國泰世華銀行消費彙整通知'],
|
||
'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*<td[^>]*>\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');
|
||
}
|
||
}
|