Skip to content

Commit 05c15e7

Browse files
heehoclaude
andcommitted
feat(daemon): P1-01 봉인 제거 + broker create skeleton
`_run_prepare_preflight` 끝의 dict return 을 9필드(account_no, prepare_payload, prepared_order_info 등)로 확장하여 broker create 호출에 필요한 정보를 호출자에 전달. `place_order` 호출자에 `_final_submit_enabled` 가드 분기를 추가하여 OFF 시 기존 동작(submit_blocked + capability_not_ready raise) 유지, ON 시 신규 `_run_broker_create` 호출. broker create skeleton 은 P0-02 capture 로 확정한 `POST /api/v2/wts/trading/order/create` 를 prepare_payload − {"withOrderKey"} body 로 호출, 응답 `result.orderId` 존재 시 submitted, 부재 시 broker_rejected (P1-02 에서 reject taxonomy 정밀 매핑 예정) 분기. baseline 단위 테스트 12건 PASS (1건 expected message regex 만 갱신). broker create POST 는 `TOSS_BRIDGE_ENABLE_FINAL_SUBMIT` 명시 enable 시점 (Phase 6 supervised) 까지 발생하지 않음. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent edb0a34 commit 05c15e7

2 files changed

Lines changed: 155 additions & 22 deletions

File tree

src/toss_browser_bridge/daemon.py

Lines changed: 146 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,30 +1220,60 @@ def place_order(self, params: dict[str, Any]) -> dict[str, Any]:
12201220
)
12211221

12221222
preflight = self._run_prepare_preflight(normalized)
1223+
1224+
if not self._final_submit_enabled:
1225+
self._append_place_order_journal(
1226+
mutation_id=mutation_id,
1227+
normalized=normalized,
1228+
submit_state="submit_blocked",
1229+
verification_state="pending",
1230+
broker_ack={
1231+
"status": "prepared",
1232+
"code": "PREPARED",
1233+
"message": preflight["message"],
1234+
"guard_reason": self._final_submit_guard_reason,
1235+
"market": normalized["preview_receipt"]["inputs"]["market"],
1236+
"symbol": normalized["preview_receipt"]["inputs"]["symbol"],
1237+
"side": normalized["preview_receipt"]["inputs"]["side"],
1238+
"quantity": normalized["preview_receipt"]["inputs"]["quantity"],
1239+
"order_type": normalized["preview_receipt"]["inputs"]["order_type"],
1240+
},
1241+
)
1242+
raise self._mutation_error(
1243+
"place_order",
1244+
"order_submit_ready",
1245+
"capability_not_ready",
1246+
f"final submit is disabled ({self._final_submit_guard_reason}); prepare preflight succeeded",
1247+
preflight["context"],
1248+
extra_diagnostics={"mutation_id": mutation_id},
1249+
)
1250+
1251+
broker_ack, broker_context = self._run_broker_create(normalized, preflight)
1252+
submit_state = "submitted" if broker_ack.get("status") == "submitted" else "broker_rejected"
12231253
self._append_place_order_journal(
12241254
mutation_id=mutation_id,
12251255
normalized=normalized,
1226-
submit_state="submit_blocked",
1256+
submit_state=submit_state,
12271257
verification_state="pending",
1228-
broker_ack={
1229-
"status": "prepared",
1230-
"code": "PREPARED",
1231-
"message": preflight["message"],
1232-
"market": normalized["preview_receipt"]["inputs"]["market"],
1233-
"symbol": normalized["preview_receipt"]["inputs"]["symbol"],
1234-
"side": normalized["preview_receipt"]["inputs"]["side"],
1235-
"quantity": normalized["preview_receipt"]["inputs"]["quantity"],
1236-
"order_type": normalized["preview_receipt"]["inputs"]["order_type"],
1237-
},
1238-
)
1239-
raise self._mutation_error(
1240-
"place_order",
1241-
"order_submit_ready",
1242-
"capability_not_ready",
1243-
preflight["message"],
1244-
preflight["context"],
1245-
extra_diagnostics={"mutation_id": mutation_id},
1258+
broker_ack=broker_ack,
12461259
)
1260+
return {
1261+
"ok": True,
1262+
"kind": "place_order",
1263+
"source": SOURCE,
1264+
"checked_at": broker_context.checked_at,
1265+
"capability": "order_submit_ready",
1266+
"data": {
1267+
"mutation_id": mutation_id,
1268+
"submit_state": submit_state,
1269+
"verification_state": "pending",
1270+
"broker_ack": broker_ack,
1271+
},
1272+
"diagnostics": {
1273+
"endpoint_matrix": broker_context.endpoint_matrix,
1274+
"last_errors": broker_context.last_errors,
1275+
},
1276+
}
12471277
finally:
12481278
self._mutation_inflight = False
12491279

@@ -2019,10 +2049,106 @@ def _run_prepare_preflight(self, normalized: dict[str, Any]) -> dict[str, Any]:
20192049
)
20202050

20212051
return {
2022-
"message": "prepare preflight succeeded; final create remains blocked until post-submit verify path is implemented",
2052+
"message": "prepare preflight succeeded",
20232053
"context": context,
2054+
"account_no": account_no,
2055+
"submit_market": submit_market,
2056+
"currency_mode": currency_mode,
2057+
"allow_auto_exchange": allow_auto_exchange,
2058+
"prepare_payload": prepare_payload,
2059+
"prepare_body": prepare_body,
2060+
"prepared_order_info": prepared_order_info,
2061+
}
2062+
2063+
def _run_broker_create(
2064+
self,
2065+
normalized: dict[str, Any],
2066+
preflight: dict[str, Any],
2067+
) -> tuple[dict[str, Any], QueryContext]:
2068+
"""Call POST /api/v2/wts/trading/order/create after prepare preflight succeeded.
2069+
2070+
Endpoint URL/body schema captured in Phase 0 P0-02 (HAR diff against
2071+
simulation baseline). Body = prepare_payload minus the ``withOrderKey``
2072+
key; ``orderKey`` is tracked server-side via session cookie.
2073+
"""
2074+
prepare_payload = preflight["prepare_payload"]
2075+
create_payload = {key: value for key, value in prepare_payload.items() if key != "withOrderKey"}
2076+
account_no = preflight["account_no"]
2077+
inputs = normalized["preview_receipt"]["inputs"]
2078+
market_label = str(inputs.get("market") or "")
2079+
symbol_label = str(inputs.get("symbol") or "")
2080+
side_label = str(inputs.get("side") or "")
2081+
quantity_label = inputs.get("quantity")
2082+
order_type_label = str(inputs.get("order_type") or "")
2083+
2084+
create_request = {
2085+
"name": "order_create",
2086+
"method": "POST",
2087+
"url": "https://wts-cert-api.tossinvest.com/api/v2/wts/trading/order/create",
2088+
"path": "/api/v2/wts/trading/order/create",
2089+
"headers": {"X-Tossinvest-Account": account_no},
2090+
"include_app_version": True,
2091+
"body": create_payload,
2092+
}
2093+
results = self._fetch_many([create_request])
2094+
context = self._make_context(results)
2095+
result = results[0]
2096+
2097+
ordered_at = now_kst()
2098+
base_ack = {
2099+
"market": market_label,
2100+
"symbol": symbol_label,
2101+
"side": side_label,
2102+
"quantity": quantity_label,
2103+
"order_type": order_type_label,
20242104
}
20252105

2106+
if not result.get("ok"):
2107+
error = result.get("error") or {}
2108+
return (
2109+
{
2110+
**base_ack,
2111+
"status": "broker_rejected",
2112+
"code": "BROKER_REJECTED_UNKNOWN",
2113+
"message": str(error.get("message") or "broker create request failed"),
2114+
"ordered_at": ordered_at,
2115+
"http_status": result.get("status"),
2116+
},
2117+
context,
2118+
)
2119+
2120+
body = result.get("json") or {}
2121+
broker_result = body.get("result") or {}
2122+
order_id = str(broker_result.get("orderId") or "").strip()
2123+
if not order_id:
2124+
return (
2125+
{
2126+
**base_ack,
2127+
"status": "broker_rejected",
2128+
"code": "BROKER_REJECTED_UNKNOWN",
2129+
"message": str(broker_result.get("message") or body.get("message") or "broker create response missing orderId"),
2130+
"ordered_at": ordered_at,
2131+
"http_status": result.get("status"),
2132+
},
2133+
context,
2134+
)
2135+
2136+
return (
2137+
{
2138+
**base_ack,
2139+
"status": "submitted",
2140+
"code": "OK",
2141+
"message": str(broker_result.get("message") or ""),
2142+
"broker_order_id": order_id,
2143+
"order_no": broker_result.get("orderNo"),
2144+
"order_date": broker_result.get("orderDate"),
2145+
"is_reserved": bool(broker_result.get("isReserved") or False),
2146+
"ordered_at": ordered_at,
2147+
"http_status": result.get("status"),
2148+
},
2149+
context,
2150+
)
2151+
20262152
@staticmethod
20272153
def _resolve_prepare_market(receipt: dict[str, Any]) -> str:
20282154
market_code = str(((receipt.get("derived") or {}).get("market_code")) or "").upper()

tests/test_submit_runtime.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,15 @@ def test_place_order_returns_capability_not_ready_until_verify_is_implemented(tm
110110
monkeypatch.setattr(daemon_module, "MUTATION_JOURNAL_FILE", tmp_path / "mutation-journal.jsonl")
111111
runtime._run_prepare_preflight = MethodType(
112112
lambda self, normalized: {
113-
"message": "prepare preflight succeeded; final create remains blocked until post-submit verify path is implemented",
113+
"message": "prepare preflight succeeded",
114114
"context": None,
115+
"account_no": "44258118-01",
116+
"submit_market": "NSQ",
117+
"currency_mode": "USD",
118+
"allow_auto_exchange": False,
119+
"prepare_payload": {},
120+
"prepare_body": {},
121+
"prepared_order_info": {},
115122
},
116123
runtime,
117124
)
@@ -128,7 +135,7 @@ def test_place_order_returns_capability_not_ready_until_verify_is_implemented(tm
128135

129136
with pytest.raises(
130137
MutationDomainError,
131-
match="prepare preflight succeeded; final create remains blocked until post-submit verify path is implemented",
138+
match=r"final submit is disabled \(disabled_by_default\); prepare preflight succeeded",
132139
) as excinfo:
133140
runtime.place_order(
134141
{

0 commit comments

Comments
 (0)