"""份额批次读侧仓储(jinrong_core · 仅 SELECT)· T-3。 core_share_lot 的读侧封装:FIFO 选批 + 可用份额汇总。写侧(扣减 / 补建新批) 归 `convert_core_repository`(D2),本文件不写。 D18(单一副本):FIFO 排序规则(confirmed_at ASC, lot_id ASC)的唯一出处是 `CoreReadOnlyRepository.list_share_lots` 的 ORDER BY —— 本类复用它取批次, **不另写一份排序 SQL**,贪心分配只在 Python 端做,避免排序口径漂移。 """ from __future__ import annotations from decimal import Decimal from typing import Any from sqlalchemy.engine import Engine from app.config.settings import settings from app.repository.core_ro import CoreReadOnlyRepository from app.utils.db import get_engine class ShareLotRepository: """core_share_lot 读侧:FIFO 选批 + 汇总(D2:写侧在 convert_core_repository)。""" def __init__(self, engine: Engine | None = None) -> None: self._engine = engine or get_engine(settings.mysql_core_database, "ro") # 复用 core_ro 的 ORDER BY(D18 单一副本),避免排序规则漂移 self._core = CoreReadOnlyRepository(engine=self._engine) def select_for_convert( self, customer_id: str, product_id: str, qty: Decimal ) -> list[dict[str, Any]]: """FIFO 选批:按 confirmed_at ASC, lot_id ASC 贪心覆盖 `qty`。 返回被选中的批次(dict:lot_id / qty 本次可用 / nav / confirmed_at)。 若可用份额不足 `qty`,返回全部可用批次(差额由调用方校验,读侧不抛业务异常)。 零请求量返回空列表。 """ if qty <= 0: return [] remaining = Decimal(qty) selected: list[dict[str, Any]] = [] for lot in self._core.list_share_lots(customer_id, product_id): if remaining <= 0: break available = Decimal(lot["remain_qty"]) if available <= 0: continue take = min(available, remaining) selected.append( { "lot_id": lot["lot_id"], "qty": take, "nav": Decimal(lot["nav"]), "confirmed_at": lot["confirmed_at"], } ) remaining -= take return selected def available_qty(self, customer_id: str, product_id: str) -> Decimal: """可用份额合计(委托 core_ro.sum_remain_qty,仅统计 remain_qty > 0)。""" return self._core.sum_remain_qty(customer_id, product_id)