# backend/app/connectors/finance/normalize.py — provider → finance_tx 정규화 + upsert import uuid from sqlmodel import Session, select from ...models import ExternalLink, FinanceTx from ..base import NormalizedRecord, RawRecord # 가맹점/내용 키워드 → life-data.js 카테고리 6종 _CATS = [ ("식비", ["식당", "김밥", "백반", "마트", "푸드", "배달", "음식", "고기"]), ("카페·간식", ["카페", "커피", "스타벅스", "베이커리", "디저트", "투썸", "이디야"]), ("교통", ["택시", "버스", "지하철", "주유", "기차", "ktx", "교통", "카카오t"]), ("쇼핑", ["쿠팡", "올리브영", "백화점", "쇼핑", "무신사", "29cm", "패션"]), ("구독", ["넷플릭스", "유튜브", "스포티파이", "구독", "멤버십", "프라임"]), ("문화·여가", ["영화", "cgv", "공연", "전시", "서점", "교보", "여가", "헬스"]), ] def categorize(merchant: str) -> str: low = (merchant or "").lower() for cat, kws in _CATS: if any(kw in low for kw in kws): return cat return "기타" def normalize_tx(account, raw: RawRecord) -> NormalizedRecord: p = raw.payload merchant = str(p.get("merchant", p.get("내용", p.get("가맹점", "")))).strip() raw_amt = p.get("amount", p.get("금액", 0)) try: amount = int(str(raw_amt).replace(",", "").replace("원", "").strip() or 0) except ValueError: amount = 0 category = p.get("category") or p.get("분류") or categorize(merchant) fields = { "account_id": account.id, "date": str(p.get("date", p.get("거래일", p.get("날짜", "")))).strip(), "merchant": merchant, "amount": amount, "category": category, "account_label": account.name, } return NormalizedRecord( entity_type="finance_tx", external_id=raw.external_id, fields=fields, etag=raw.etag, external_updated_at=raw.external_updated_at, ) def upsert_tx(session: Session, norm: NormalizedRecord) -> tuple[str, bool]: link = session.exec( select(ExternalLink).where( ExternalLink.external_id == norm.external_id, ExternalLink.entity_type == "finance_tx", ) ).first() if link: tx = session.get(FinanceTx, link.entity_id) if tx: for k, v in norm.fields.items(): if v is not None and hasattr(tx, k): setattr(tx, k, v) session.add(tx) return tx.id, False tid = "ftx-" + uuid.uuid4().hex[:8] session.add(FinanceTx(id=tid, **{k: v for k, v in norm.fields.items() if v is not None})) return tid, True