auto: 일일 백업 2026-09-11 02:00
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import imsg_cli
|
||||
import notify
|
||||
import whooing_balance
|
||||
import gahee_reminder
|
||||
import securities_match
|
||||
|
||||
# 후잉 웹훅은 HTTP 200을 반환하면서 본문에 "fail"/"Error : ..." 를 줄 수 있어 본문 검증 필수.
|
||||
SUCCESS_BODY = "done"
|
||||
@@ -753,6 +754,32 @@ def detect_pairs(candidates, accounts):
|
||||
return pairs, used
|
||||
|
||||
|
||||
def try_securities_transfer(parsed, cand, accounts):
|
||||
"""자본 분개로 갈 뻔한 은행 SMS 가 실은 증권 이체인지 확인.
|
||||
|
||||
은행 SMS 에는 입금자명(본인 이름)만 찍혀 목적지를 알 수 없지만, 반대편은
|
||||
키움 kt00015 에 남는다. 짝을 찾으면 자본이 아니라 자산↔자산 이체로 분개한다.
|
||||
|
||||
반환: {payload, key, label} | None (짝 없음·애매·조회 실패)
|
||||
"""
|
||||
if not parsed or parsed.get("kind") not in ("withdrawal", "deposit"):
|
||||
return None
|
||||
bank_asset = accounts.get("carrier_to_account", {}).get(cand["info"].get("carrier"))
|
||||
if not bank_asset:
|
||||
return None # 은행 자산 계정을 모르면 이체를 구성할 수 없다
|
||||
entry_date = parsed.get("entry_date")
|
||||
amount = parsed.get("amount")
|
||||
if not entry_date or not amount:
|
||||
return None
|
||||
match = securities_match.find_match(
|
||||
entry_date, amount, parsed["kind"], cand.get("created_at"))
|
||||
if match is None:
|
||||
return None
|
||||
payload = securities_match.build_payload(
|
||||
match, bank_asset, entry_date, amount, parsed["kind"], parsed.get("balance"))
|
||||
return {"payload": payload, "key": match["key"], "label": match["label"]}
|
||||
|
||||
|
||||
def post_to_whooing(webhook_url, payload, dry_run=False):
|
||||
"""payload는 dict. 반환: (ok: bool, status: int, body: str).
|
||||
HTTP 2xx + body가 'done' 으로 시작해야 성공. 그 외(HTTP 200 + 'fail' 포함) 모두 실패."""
|
||||
@@ -879,6 +906,7 @@ def main():
|
||||
sent_transfer = 0
|
||||
sent_structured = 0
|
||||
sent_raw = 0
|
||||
sent_securities = 0
|
||||
sent_cancel = 0
|
||||
failed = 0
|
||||
new_failures = []
|
||||
@@ -994,24 +1022,45 @@ def main():
|
||||
# 처제 대납금 상환이었는데 입금자명이 "방효원"이라 자체이체 룰에 걸렸다).
|
||||
# ⚠️ 커서는 latest_skip 으로 진행시킨다 — 재시도해도 같은 룰이 같은 결과를 내므로
|
||||
# 여기서 커서를 막으면 이 메시지 뒤의 모든 거래가 영구히 멈춘다.
|
||||
sec_key = None
|
||||
if mode == "structured" and (
|
||||
structured.get("left") in capital_accounts
|
||||
or structured.get("right") in capital_accounts
|
||||
):
|
||||
capital_blocked.append({
|
||||
"label": c["info"]["label"],
|
||||
"created_at": c["created_at"],
|
||||
"payload": payload,
|
||||
"blocked_at": datetime.now(KST).isoformat(),
|
||||
})
|
||||
if c["created_at"] > (latest_skip or ""):
|
||||
latest_skip = c["created_at"]
|
||||
print(f" ⛔ [capital-block] {c['info']['label']} {c['created_at']} | {display} — 미반영, 알림만")
|
||||
continue
|
||||
# 자본으로 갈 뻔한 건이 실은 증권 이체일 수 있다. 은행 SMS 는 입금자명만
|
||||
# 주지만 반대편이 키움 kt00015 에 남으므로, 짝을 찾으면 자본이 아니라
|
||||
# 자산↔자산 이체로 분개한다. 짝이 없으면 기존대로 미반영+알림.
|
||||
sec = try_securities_transfer(parsed, c, accounts)
|
||||
if sec:
|
||||
payload = {k: str(v) for k, v in sec["payload"].items()}
|
||||
mode = "securities"
|
||||
sec_key = sec["key"]
|
||||
display = (f"{sec['payload']['left']} ← {sec['payload']['right']} "
|
||||
f"{sec['payload']['money']:,}원 (증권 {sec['label']} 이체)")
|
||||
else:
|
||||
capital_blocked.append({
|
||||
"label": c["info"]["label"],
|
||||
"created_at": c["created_at"],
|
||||
"payload": payload,
|
||||
"blocked_at": datetime.now(KST).isoformat(),
|
||||
})
|
||||
if c["created_at"] > (latest_skip or ""):
|
||||
latest_skip = c["created_at"]
|
||||
print(f" ⛔ [capital-block] {c['info']['label']} {c['created_at']} | {display} — 미반영, 알림만")
|
||||
continue
|
||||
|
||||
ok, status, body = post_to_whooing(webhook_url, payload, dry_run=args.dry_run)
|
||||
if ok:
|
||||
if mode == "structured":
|
||||
if mode == "securities":
|
||||
sent_securities += 1
|
||||
# POST 성공 후에만 기록한다 — 실패하면 다음에 다시 짝으로 잡혀야 한다.
|
||||
if not args.dry_run:
|
||||
securities_match.mark_matched(sec_key, {
|
||||
"sms_at": c["created_at"],
|
||||
"money": int(payload["money"]),
|
||||
"entry": display,
|
||||
})
|
||||
elif mode == "structured":
|
||||
sent_structured += 1
|
||||
carrier_key = c["info"].get("carrier")
|
||||
if carrier_key:
|
||||
@@ -1353,9 +1402,9 @@ def main():
|
||||
save_json(SYNC_STATE_FILE, sync_state)
|
||||
|
||||
if blocked_at:
|
||||
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
|
||||
print(f"⚠️ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 — {blocked_at} 에서 중단(다음 cron 재시도)")
|
||||
else:
|
||||
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 (last={final_latest or latest})")
|
||||
print(f"✅ 후잉 동기화: transfer {sent_transfer}건, structured {sent_structured}건, 증권이체 {sent_securities}건, raw {sent_raw}건, 취소감액 {sent_cancel}건, 실패 {failed}건 (last={final_latest or latest})")
|
||||
|
||||
# 가희 잔액 리마인더 & 답신 자동 분개 (격리 — 실패해도 결제 sync 결과는 위에서 이미 출력됨)
|
||||
run_gahee_reminder()
|
||||
|
||||
Reference in New Issue
Block a user