auto: 일일 백업 2026-08-04 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,6 @@ from __future__ import annotations
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
@@ -22,6 +21,7 @@ from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import parsers as sms_parsers
|
||||
import imsg_cli
|
||||
import notify
|
||||
import whooing_balance
|
||||
import gahee_reminder
|
||||
@@ -209,7 +209,11 @@ MONEY_AMOUNT_RE = re.compile(r"(?:\d[\d,]*\s*원|(?:입금|출금)\s+\d[\d,]*)")
|
||||
|
||||
# 주유소 카드 결제는 보통 150,000원 선승인 → 실제 주유금액 승인 → 150,000원 승인취소 순서로 온다.
|
||||
# 선승인/취소는 후잉에 기록하지 않고, 실제 결제만 차량유지비/주유비로 남긴다.
|
||||
FUEL_KEYWORDS = ("석유", "주유소", "주유")
|
||||
# "현대가스"·"한경에너"는 상호에 주유/석유가 안 들어가는 충전소라 직접 등록 (2026-08-03 관리자님 확인).
|
||||
# 새 주유소를 만나면 여기와 whooing_merchant_map.json contains **양쪽에** 상호를 추가한다 —
|
||||
# 이 상수는 선승인/취소 스킵, merchant_map 은 실제 주유 결제 분류로 한 쌍이다.
|
||||
# ⚠️ 한경에너는 선승인이 149,900원이라 FUEL_PREAUTH_AMOUNT 정확일치엔 안 걸린다(분류만 적용).
|
||||
FUEL_KEYWORDS = ("석유", "주유소", "주유", "현대가스", "한경에너")
|
||||
FUEL_PREAUTH_AMOUNT = 150_000
|
||||
|
||||
|
||||
@@ -220,7 +224,7 @@ def _is_fuel_text(text: str) -> bool:
|
||||
def _should_skip_fuel_preauth(parsed: dict | None, text: str) -> bool:
|
||||
"""주유소 15만원 선승인/취소 스킵.
|
||||
|
||||
parsed 가 있으면 구조화 필드 기준으로 판단한다. 현대카드 취소처럼 아직 파서가 없는 형식은
|
||||
parsed 가 있으면 구조화 필드 기준으로 판단한다. 파서가 못 읽은 형식은 폴백으로
|
||||
raw 텍스트에서 '취소' + '150,000원' + 주유 키워드 조합만 보수적으로 스킵한다.
|
||||
"""
|
||||
if parsed:
|
||||
@@ -281,39 +285,33 @@ def resolve_chat_ids(carriers):
|
||||
needed = set(carriers.keys())
|
||||
if needed.issubset(cached_keys):
|
||||
return {k: cached[k] for k in needed}
|
||||
try:
|
||||
chats_raw = subprocess.run(
|
||||
["imsg", "chats", "--json"],
|
||||
capture_output=True, text=True, timeout=IMSG_TIMEOUT,
|
||||
)
|
||||
chats = [json.loads(l) for l in chats_raw.stdout.splitlines() if l.strip()]
|
||||
except FileNotFoundError:
|
||||
print("❌ imsg CLI가 설치되어 있지 않습니다.", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
except Exception as e:
|
||||
print(f"❌ imsg chats 실행 실패: {e}", file=sys.stderr)
|
||||
chats, err = imsg_cli.chats(IMSG_TIMEOUT)
|
||||
if err:
|
||||
print(f"❌ imsg chats 실행 실패: {err}", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
result = {}
|
||||
for sender in carriers.keys():
|
||||
ids = [c["id"] for c in chats if c.get("identifier") == sender and c.get("id") is not None]
|
||||
result[sender] = ids
|
||||
# 전 carrier 가 빈 리스트면 조회 자체가 실패한 것으로 본다. 이 상태를 캐시에 저장하면
|
||||
# 이후 모든 실행(launchd 포함)이 캐시에 적중해 조용히 0건 수집을 영구 반복한다.
|
||||
# 일부만 비는 건 정상 — 메시지 이력이 없는 carrier 가 있을 수 있다.
|
||||
if not any(result.values()):
|
||||
print("❌ imsg chats 결과에 매핑된 발신번호가 하나도 없습니다 — 캐시 저장 안 함", file=sys.stderr)
|
||||
sys.exit(3)
|
||||
save_json(CHAT_IDS_CACHE, result)
|
||||
return result
|
||||
|
||||
|
||||
def fetch_messages(chat_id, since):
|
||||
cmd = ["imsg", "history", "--chat-id", str(chat_id), "--limit", str(HISTORY_LIMIT), "--json"]
|
||||
args = ["history", "--chat-id", str(chat_id), "--limit", str(HISTORY_LIMIT), "--json"]
|
||||
if since:
|
||||
cmd += ["--start", since]
|
||||
try:
|
||||
raw = subprocess.run(cmd, capture_output=True, text=True, timeout=IMSG_TIMEOUT)
|
||||
return [json.loads(l) for l in raw.stdout.splitlines() if l.strip()]
|
||||
except subprocess.TimeoutExpired:
|
||||
print(f"⚠️ chat-id {chat_id} history 타임아웃", file=sys.stderr)
|
||||
return []
|
||||
except Exception as e:
|
||||
print(f"⚠️ chat-id {chat_id} history 실패: {e}", file=sys.stderr)
|
||||
args += ["--start", since]
|
||||
rows, err = imsg_cli.run_json(args, IMSG_TIMEOUT)
|
||||
if err:
|
||||
print(f"⚠️ chat-id {chat_id} history 실패: {err}", file=sys.stderr)
|
||||
return []
|
||||
return rows
|
||||
|
||||
|
||||
# --- 룰 엔진 (whooing_overrides.json) -----------------------------------------
|
||||
@@ -518,7 +516,19 @@ def build_structured(parsed, sender_info, accounts, merchant_map):
|
||||
return {**base, "left": "기타비용", "right": carrier_acct}
|
||||
|
||||
if kind == "card_cancel":
|
||||
return None # raw 폴백 (후잉이 원거래 찾아 상쇄)
|
||||
# 승인의 역분개. 승인이 {left: 비용, right: 카드} 이므로 좌우를 뒤집는다.
|
||||
# 부분취소(취소액 != 승인액)도 취소 문자 금액 그대로 상쇄되어 맞는다.
|
||||
if rule:
|
||||
left = rule.get("left")
|
||||
if left:
|
||||
out = {**base, "left": rule.get("right") or carrier_acct, "right": left}
|
||||
if rule.get("item"):
|
||||
out["item"] = rule["item"]
|
||||
return out
|
||||
return None
|
||||
if cat:
|
||||
return {**base, "left": carrier_acct, "right": cat}
|
||||
return {**base, "left": carrier_acct, "right": "기타비용"}
|
||||
|
||||
if kind == "withdrawal":
|
||||
if rule:
|
||||
|
||||
Reference in New Issue
Block a user