"""返利班次:领表 4 起草行 / 表 3 只录协议行 → runner 查号+总单+细单+送审 → 写表 4。""" from __future__ import annotations from collections import OrderedDict from dataclasses import dataclass from typing import Callable from persist import ( KIND_FAIL, KIND_NOT_FOUND, KIND_OK, AgreementOutcome, RebateGroup, agreement_ok, build_found, build_not_found, ) from repo import Claimed Runner = Callable[[RebateGroup], AgreementOutcome] Header = Callable[[str], str] Importer = Callable[[str], object] Submitter = Callable[[str], object] @dataclass class JobSummary: claimed: int = 0 groups: int = 0 ok: int = 0 not_found: int = 0 failed: int = 0 def recorder( contract_no: str, header: Header, importer: Importer, submitter: Submitter, ) -> AgreementOutcome: try: order_no = (header(contract_no) or "").strip() except Exception as exc: return AgreementOutcome(KIND_FAIL, None, None, str(exc) or "总单失败") if not order_no: return AgreementOutcome(KIND_FAIL, None, None, "总单未拿到号") try: imported = importer(contract_no) except Exception as exc: return AgreementOutcome(KIND_FAIL, None, None, str(exc) or "细单导入失败") if not _import_ok(imported): return AgreementOutcome(KIND_FAIL, None, None, _import_reason(imported)) try: submitted = submitter(contract_no) except Exception as exc: return AgreementOutcome(KIND_FAIL, None, None, str(exc) or "送审失败") if submitted is None or not bool(getattr(submitted, "submitted", False)): return AgreementOutcome(KIND_FAIL, None, None, "送审失败") agreement_id = (getattr(submitted, "agreement_id", None) or "").strip() if not agreement_id: return AgreementOutcome(KIND_FAIL, None, None, "送审未读到总单号") agreement_dtl_id = (getattr(submitted, "agreement_dtl_id", None) or "").strip() or None return AgreementOutcome(KIND_OK, agreement_id, agreement_dtl_id, None) def run_batch( repo, *, runner: Runner, limit: int | None = None, ) -> JobSummary: claimed = repo.claim_pending(limit=limit) groups = group_claimed(claimed) summary = JobSummary( claimed=len(claimed.drafts) + len(claimed.protocols), groups=len(groups), ) for group in groups: try: outcome = runner(group) except Exception as exc: outcome = AgreementOutcome(KIND_FAIL, None, None, str(exc)) if outcome is None: outcome = AgreementOutcome(KIND_FAIL, None, None, "协议录入失败") if outcome.kind == KIND_NOT_FOUND: repo.save_group(build_not_found(group)) summary.not_found += 1 continue repo.save_group(build_found(group, outcome)) if agreement_ok(outcome): summary.ok += 1 else: summary.failed += 1 return summary def group_claimed(claimed: Claimed) -> list[RebateGroup]: groups: OrderedDict[str, RebateGroup] = OrderedDict() def bucket(contract_no: str) -> RebateGroup: number = (contract_no or "").strip() if number not in groups: groups[number] = RebateGroup(contract_no=number) return groups[number] for row in claimed.drafts: bucket(row["contract_no"]).drafts.append(row) for row in claimed.protocols: bucket(row["contract_no"]).protocols.append(row) return list(groups.values()) def _import_ok(result: object) -> bool: if result is None: return False success = int(getattr(result, "success_count", 0) or 0) fail = int(getattr(result, "fail_count", 0) or 0) parsed = getattr(result, "parsed", True) return bool(parsed) and success > 0 and fail == 0 def _import_reason(result: object) -> str: if result is None: return "细单导入失败" reasons = getattr(result, "fail_reasons", ()) or () if reasons: return str(reasons[0]).strip() or "细单导入失败" return "细单导入失败"