"""dcy-019 / BAT-025: buy admission reallocates closed slots (REQ-007). When no idle slot remains, _admit_buy must admit into a CLOSED slot via the new closed→pending_buy transition, ordered by entry_count (least-entered first). """ from __future__ import annotations from datetime import date from trex_core.adapters.persistence.in_memory_intent_store import InMemoryIntentStore from trex_core.application.intents import BuyIntent, IntentService from trex_core.domain.slot import SlotService, SlotState from trex_core.domain.slot.in_memory_repository import InMemorySlotRepository DAY = date(2026, 6, 9) def _cycle_to_closed(svc, slot_id): svc.transit_slot(slot_id, SlotState.IDLE, SlotState.PENDING_BUY, reason="r", actor="a", symbol="005930") svc.transit_slot(slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="r", actor="a", avg_price=70_000) svc.transit_slot(slot_id, SlotState.HOLDING, SlotState.PENDING_SELL, reason="r", actor="a") svc.transit_slot(slot_id, SlotState.PENDING_SELL, SlotState.CLOSED, reason="r", actor="a") def test_admits_into_closed_slot_when_no_idle_remains(): repo = InMemorySlotRepository() svc = SlotService(repo) intents = IntentService(svc, repo, InMemoryIntentStore()) svc.preallocate_daily_slots(DAY, 1) slot = repo.find_idle_slot(DAY) _cycle_to_closed(svc, slot.slot_id) assert repo.find_idle_slot(DAY) is None # nothing idle d = intents.submit_buy(BuyIntent( intent_id="buy-realloc", trading_date=DAY, symbol="000660", qty=3, portfolio_id="PF-1", )) assert d.status == "admitted" realloc = repo.fetch_slot(d.slot_id) assert realloc.status is SlotState.PENDING_BUY assert realloc.symbol == "000660" # second entry into the same physical slot assert realloc.entry_count == 2 def test_rejects_when_no_idle_and_no_closed(): repo = InMemorySlotRepository() svc = SlotService(repo) intents = IntentService(svc, repo, InMemoryIntentStore()) svc.preallocate_daily_slots(DAY, 1) slot = repo.find_idle_slot(DAY) # take it to holding (not closed, not idle → not allocatable) svc.transit_slot(slot.slot_id, SlotState.IDLE, SlotState.PENDING_BUY, reason="r", actor="a", symbol="005930") svc.transit_slot(slot.slot_id, SlotState.PENDING_BUY, SlotState.HOLDING, reason="r", actor="a", avg_price=70_000) d = intents.submit_buy(BuyIntent( intent_id="buy-none", trading_date=DAY, symbol="000660", portfolio_id="PF-1", )) assert d.status == "rejected" assert d.reason == "no_idle_slot" def test_prefers_idle_over_closed(): repo = InMemorySlotRepository() svc = SlotService(repo) intents = IntentService(svc, repo, InMemoryIntentStore()) svc.preallocate_daily_slots(DAY, 2) slots = repo.list_slots(DAY) _cycle_to_closed(svc, slots[0].slot_id) # one closed (entry_count=1) d = intents.submit_buy(BuyIntent( intent_id="buy-prefer-idle", trading_date=DAY, symbol="000660", portfolio_id="PF-1", )) assert d.status == "admitted" chosen = repo.fetch_slot(d.slot_id) # the fresh idle slot (entry_count 0→1) is preferred over the closed one assert chosen.entry_count == 1