# -*- coding: utf-8 -*- """支付子应用仓储:充值订单行级操作(不掺 HTTP 逻辑)。""" from __future__ import annotations from sqlalchemy import select from ..infrastructure.repositories import new_id, utcnow_iso from .models import ComputeRechargeOrder, EventOrder, PaymentBinding, SettlementLog def _to_dict(o: ComputeRechargeOrder) -> dict: return { "id": o.id, "order_no": o.order_no, "user_id": o.user_id, "username": o.username, "engine_user_id": o.engine_user_id, "package_id": o.package_id, "amount_fen": o.amount_fen, "quota_micro": o.quota_micro, "bonus_quota_micro": o.bonus_quota_micro, "client_type": o.client_type, "appid": o.appid, "status": o.status, "prepay_id": o.prepay_id, "transaction_id": o.transaction_id, "code_url": o.code_url, "expires_at": o.expires_at, "paid_at": o.paid_at, "credited_at": o.credited_at, "notify_payload": o.notify_payload, "created_at": o.created_at, "updated_at": o.updated_at, } class ComputeRechargeOrderRepository: def __init__(self, session): self.session = session async def create(self, fields: dict) -> dict: now = utcnow_iso() row = ComputeRechargeOrder( id=new_id("cr"), created_at=now, updated_at=now, **{k: v for k, v in fields.items() if hasattr(ComputeRechargeOrder, k) and k != "id"}, ) self.session.add(row) await self.session.commit() return _to_dict(row) async def get_by_order_no(self, order_no: str) -> dict | None: row = await self.session.scalar( select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no) ) return _to_dict(row) if row else None async def find_pending_same_amount(self, user_id: str, amount_fen: int, now_iso: str) -> dict | None: """同用户同金额未过期 pending 单(下单防重复扫码复用)。""" row = await self.session.scalar( select(ComputeRechargeOrder) .where( ComputeRechargeOrder.user_id == user_id, ComputeRechargeOrder.amount_fen == amount_fen, ComputeRechargeOrder.status == "pending", ComputeRechargeOrder.expires_at > now_iso, ) .order_by(ComputeRechargeOrder.created_at.desc()) .limit(1) ) return _to_dict(row) if row else None async def list_by_user(self, user_id: str, limit: int = 50) -> list[dict]: rows = (await self.session.scalars( select(ComputeRechargeOrder) .where(ComputeRechargeOrder.user_id == user_id) .order_by(ComputeRechargeOrder.created_at.desc()) .limit(limit) )).all() return [_to_dict(r) for r in rows] async def _row(self, order_no: str) -> ComputeRechargeOrder | None: return await self.session.scalar( select(ComputeRechargeOrder).where(ComputeRechargeOrder.order_no == order_no) ) async def mark_paid(self, order_no: str, transaction_id: str, payload_json: str) -> bool: """pending → paid(幂等排他):仅当仍是 pending 才置 paid;False=已被处理(回调重放/并发)。""" row = await self._row(order_no) if row is None or row.status != "pending": return False row.status = "paid" row.transaction_id = transaction_id or row.transaction_id row.paid_at = utcnow_iso() row.notify_payload = payload_json row.updated_at = utcnow_iso() await self.session.commit() return True async def mark_credited(self, order_no: str) -> None: """paid → credited:引擎到账完成。""" row = await self._row(order_no) if row is None: return row.status = "credited" row.credited_at = utcnow_iso() row.updated_at = utcnow_iso() await self.session.commit() async def set_prepay(self, order_no: str, appid: str, prepay_id: str) -> None: """记录 JSAPI 预支付信息(pending 态,小程序扫码拉起支付时回填)。""" row = await self._row(order_no) if row is None: return row.appid = appid or row.appid row.prepay_id = prepay_id or row.prepay_id row.updated_at = utcnow_iso() await self.session.commit() async def mark_status(self, order_no: str, status: str, payload_json: str = "") -> None: """置 closed / failed 等非终态迁移。""" row = await self._row(order_no) if row is None: return row.status = status if payload_json: row.notify_payload = payload_json row.updated_at = utcnow_iso() await self.session.commit() # --------------------------------------------------------------------------- # 活动报名订单(课程/沙龙定价支付) # --------------------------------------------------------------------------- class EventOrderRepository: def __init__(self, session): self.session = session async def create(self, fields: dict) -> dict: now = utcnow_iso() row = EventOrder( id=new_id("ev"), created_at=now, updated_at=now, **{k: v for k, v in fields.items() if hasattr(EventOrder, k) and k != "id"}, ) self.session.add(row) await self.session.commit() return _to_event_dict(row) async def get_by_order_no(self, order_no: str) -> dict | None: row = await self.session.scalar( select(EventOrder).where(EventOrder.order_no == order_no) ) return _to_event_dict(row) if row else None async def find_pending_by_booking(self, booking_id: str, now_iso: str) -> dict | None: """同报名未过期 pending 单(继续支付复用,避免重复下单)。""" row = await self.session.scalar( select(EventOrder) .where( EventOrder.booking_id == booking_id, EventOrder.status == "pending", EventOrder.expires_at > now_iso, ) .order_by(EventOrder.created_at.desc()) .limit(1) ) return _to_event_dict(row) if row else None async def _row(self, order_no: str) -> EventOrder | None: return await self.session.scalar( select(EventOrder).where(EventOrder.order_no == order_no) ) async def mark_paid(self, order_no: str, transaction_id: str, payload_json: str) -> bool: """pending → paid(幂等排他):仅当仍是 pending 才置 paid;False=已被处理。""" row = await self._row(order_no) if row is None or row.status != "pending": return False row.status = "paid" row.transaction_id = transaction_id or row.transaction_id row.paid_at = utcnow_iso() row.notify_payload = payload_json row.updated_at = utcnow_iso() await self.session.commit() return True async def mark_credited(self, order_no: str) -> None: """paid → credited:报名确认完成。""" row = await self._row(order_no) if row is None: return row.status = "credited" row.credited_at = utcnow_iso() row.updated_at = utcnow_iso() await self.session.commit() async def set_prepay(self, order_no: str, appid: str, prepay_id: str) -> None: row = await self._row(order_no) if row is None: return row.appid = appid or row.appid row.prepay_id = prepay_id or row.prepay_id row.updated_at = utcnow_iso() await self.session.commit() async def mark_status(self, order_no: str, status: str, payload_json: str = "") -> None: row = await self._row(order_no) if row is None: return row.status = status if payload_json: row.notify_payload = payload_json row.updated_at = utcnow_iso() await self.session.commit() def _to_event_dict(o: EventOrder) -> dict: return { "id": o.id, "order_no": o.order_no, "booking_id": o.booking_id, "event_id": o.event_id, "event_title": o.event_title, "user_id": o.user_id, "username": o.username, "openid": o.openid, "amount_fen": o.amount_fen, "client_type": o.client_type, "appid": o.appid, "status": o.status, "prepay_id": o.prepay_id, "transaction_id": o.transaction_id, "expires_at": o.expires_at, "paid_at": o.paid_at, "credited_at": o.credited_at, "notify_payload": o.notify_payload, "created_at": o.created_at, "updated_at": o.updated_at, } # --------------------------------------------------------------------------- # OPC 收款绑定(微信服务商分账接收方) # --------------------------------------------------------------------------- def _to_binding_dict(b: PaymentBinding) -> dict: return { "id": b.id, "user_id": b.user_id, "bind_type": b.bind_type, "openid": b.openid, "real_name": b.real_name, "sub_mchid": b.sub_mchid, "applyment_id": b.applyment_id, "status": b.status, "detail": b.detail or "", "applyment_state": b.applyment_state or "", "sign_url": b.sign_url or "", "account_validation_json": b.account_validation_json or "{}", "audit_detail_json": b.audit_detail_json or "{}", "split_allowed": b.split_allowed or "", "split_max_ratio": b.split_max_ratio or 0, "created_at": b.created_at, "updated_at": b.updated_at, } class PaymentBindingRepository: """OPC 收款绑定仓储(个人 openid / 商户子商户号双路径)。""" def __init__(self, session): self.session = session async def create(self, fields: dict) -> dict: now = utcnow_iso() row = PaymentBinding( id=new_id("pb"), created_at=now, updated_at=now, **{k: v for k, v in fields.items() if hasattr(PaymentBinding, k) and k != "id"}, ) self.session.add(row) await self.session.commit() return _to_binding_dict(row) async def get(self, binding_id: str) -> dict | None: row = await self.session.get(PaymentBinding, binding_id) return _to_binding_dict(row) if row else None async def list_by_user(self, user_id: str) -> list[dict]: rows = (await self.session.scalars( select(PaymentBinding).where(PaymentBinding.user_id == user_id) .order_by(PaymentBinding.created_at.desc()) )).all() return [_to_binding_dict(r) for r in rows] async def list_all(self, bind_type: str | None = None, status: str | None = None, limit: int = 200) -> list[dict]: """管理端:列出全部 OPC 收款绑定(可筛 bind_type/status),按更新时间倒序。""" stmt = select(PaymentBinding) if bind_type: stmt = stmt.where(PaymentBinding.bind_type == bind_type) if status: stmt = stmt.where(PaymentBinding.status == status) stmt = stmt.order_by(PaymentBinding.updated_at.desc()).limit(limit) rows = (await self.session.scalars(stmt)).all() return [_to_binding_dict(r) for r in rows] async def get_active(self, user_id: str, bind_type: str | None = None) -> dict | None: """取该用户可接收分账的绑定(优先商户绑定→个人绑定;bind_type 指定时仅取该类型)。""" order = ("merchant", "personal") if bind_type is None else (bind_type,) for bt in order: row = await self.session.scalar( select(PaymentBinding) .where( PaymentBinding.user_id == user_id, PaymentBinding.status == "active", PaymentBinding.bind_type == bt, ) .order_by(PaymentBinding.created_at.desc()) .limit(1) ) if row is not None: return _to_binding_dict(row) return None async def update(self, binding_id: str, fields: dict) -> dict | None: row = await self.session.get(PaymentBinding, binding_id) if row is None: return None for k, v in fields.items(): if hasattr(PaymentBinding, k): setattr(row, k, v) row.updated_at = utcnow_iso() await self.session.commit() return _to_binding_dict(row) async def set_status(self, binding_id: str, status: str, **extra) -> dict | None: return await self.update(binding_id, {"status": status, **extra}) # --------------------------------------------------------------------------- # 分账流水镜像(对账) # --------------------------------------------------------------------------- def _to_log_dict(l: SettlementLog) -> dict: return { "id": l.id, "escrow_id": l.escrow_id, "task_id": l.task_id, "share_order_no": l.share_order_no, "channel": l.channel, "amount": l.amount, "commission": l.commission, "receivers_json": l.receivers_json, "status": l.status, "detail": l.detail, "created_at": l.created_at, "updated_at": l.updated_at, } class SettlementLogRepository: """分账流水镜像仓储(业务侧记录真金去向,供对账/审计)。""" def __init__(self, session): self.session = session async def create(self, fields: dict) -> dict: now = utcnow_iso() row = SettlementLog( id=new_id("sl"), created_at=now, updated_at=now, **{k: v for k, v in fields.items() if hasattr(SettlementLog, k) and k != "id"}, ) self.session.add(row) await self.session.commit() return _to_log_dict(row) async def get_by_share_order_no(self, share_order_no: str) -> dict | None: row = await self.session.scalar( select(SettlementLog).where(SettlementLog.share_order_no == share_order_no) ) return _to_log_dict(row) if row else None async def set_status(self, log_id: str, status: str, detail: str = "") -> dict | None: row = await self.session.get(SettlementLog, log_id) if row is None: return None row.status = status if detail: row.detail = detail row.updated_at = utcnow_iso() await self.session.commit() return _to_log_dict(row)