Files
finance_app/app/Services/GmailParserService.php
henry yo 85972f950c feat: 前端分離+功能api化
1. 新增資產管理CRUD、googleOAuth登入
2. 更改原記帳controller
2026-06-25 14:19:07 +08:00

181 lines
5.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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('&nbsp;', '', $str);
return mb_convert_kana($str, 'asKV', 'UTF-8');
}
}