# `DpExchange.Core.OrderRoundTripCheck`
[🔗](https://github.com/DistortionPoint/dp-exchange-core/blob/v0.3.120/lib/dp_exchange/core/order_round_trip_check.ex#L1)

Asks a venue's fake whether it accepts the order ids its own order-minting calls just
returned.

## The defect this exists for

**A fake that refuses its own ids.** `dp_exchange_gemini`'s fake answered `place_order/3`
with an order, and then answered `get_order/3` and `cancel_order/3` for that same order
`{:error, {:invalid_order_id, _}}` — the id it had minted was not one its own read and
cancel would take (gemini issue #5). Every assertion in `Core.AdapterContract` passed:
group 5 calls `get_order/3` with an id it makes up and accepts a refusal as an answer,
group 12 asks only that an active endpoint not answer `:not_supported`, and nothing placed
an order and then used what came back. A consumer's tier-1 test that places, reads back
and cancels — the first thing anyone writes against a trading fake — could not pass.

The same class is recorded in `dp_exchange_webull`'s fake, whose `place_order/3` echoed a
caller-chosen `client_order_id` that its own `get_order/3` and `cancel_order/3` then
refused as `:not_found` because they knew only one fixed id. Different mechanics, one
shape: the fake is a set of independently written functions, and nothing made them agree
about the one value that connects them.

## What it checks

Four calls mint order ids. Each is exercised **only when `capabilities/0` declares it
active**, and the ids it mints are collected:

  * `place_order/3` must answer `{:ok, %Types.Order{}}` with a non-empty binary `:id`.
  * `place_orders/3` must answer `{:ok, list}` with one entry per request. An entry that
    is a `%Types.Order{}` (bare, or as `{:ok, order}`) must carry a non-empty binary
    `:id`, which is collected. An entry that is a refusal (`{:error, _}` or
    `{:refused, _}`) is the venue refusing that one order — the callback documents the
    partial batch as the expected shape — and is **not** a round-trip failure. A bare map
    (venue JSON, which is all `[map()]` promises) names an id this check cannot read
    without guessing the key, so it is not asked about.
  * `replace_order/4` is exercised on an order the check places for the purpose (needs
    `place_order/3` active), and must answer `{:ok, %Types.Order{}}` with an id. The id
    the replace answered is collected; the order that was replaced is not, because a
    venue may legitimately have retired it.
  * `close_position/3` answers an order like `place_order/3` does. The suite opens no
    position, so a refusal (`{:error, _}` or `{:refused, _}`) is an answer
    `c:DpExchange.Core.Venue.close_position/3`'s `result/1` type allows, and the call is
    **not applicable** rather than failed. `{:error, :not_supported}` is not such an
    answer for an active endpoint, nor is any other shape.

Every distinct collected id is then offered to the fake, reads first, then cancels:

  1. If `{:get_order, 3}` is active, `get_order/3` must answer `{:ok, %Types.Order{}}`.
  2. If `{:cancel_order, 3}` is active, `cancel_order/3` must answer
     `{:ok, %Types.Order{}}` or `{:ok, :cancelled}` — the two shapes
     `c:DpExchange.Core.Venue.cancel_order/3` allows.

All reads run before any cancel, so a fake that forgets an order on cancel is not blamed
for a read.

## What it deliberately does not do

  * **It does not require the answered order to carry the placed id.** A venue may
    key an order by a client id on the way in and its own id on the way out, and the
    contract does not say which. The id is checked for being *accepted*, not for being
    *echoed*.
  * **It does not fail a venue that declares `get_order/3` or `cancel_order/3`
    `:unsupported`.** Those are not asked at all; their `{:error, :not_supported}` is
    group 12's.
  * **It does not run a minting call that is `:unsupported`.** There is no id to carry.
    `run/3` answers `:skipped` only when none of the four ran, and a caller reading that
    as a pass is right.
  * **It does not exercise `replace_order/4` without `place_order/3`.** The suite has no
    order it could honestly say is open to replace; `place_order/3` being `:unsupported`
    makes the replace not applicable here.
  * **It does not read ids out of venue JSON.** A `place_orders/3` entry that is a bare
    map is skipped, not guessed at.
  * **It does not prove the real venue behaves this way.** It drives a fake. A fake that
    accepts its own ids says the fake is self-consistent, nothing more.

## Fail closed

An active minting call that does not answer as above is not a skip. This check answers
`{:error, reason}` and the caller fails, because treating a refusal as "nothing to
round-trip" is how three packages once passed assertions that never ran on them (see
`docs/reference/core/assertion-coverage.md`, "Second axis"). The one exception is
`close_position/3`'s refusal, for the reason above. The remedy is in the message the suite
attaches: either the endpoint is not really active, or the fake needs an option the call
did not carry — declare it in `endpoint_opts:`.

A raise or an exit from the fake is captured and reported as the answer; it does not
escape and take the rest of the test with it.

# `calls`

```elixir
@type calls() :: %{
  :place_order =&gt; [term()],
  :get_order =&gt; (String.t() -&gt; [term()]),
  :cancel_order =&gt; (String.t() -&gt; [term()]),
  optional(:place_orders) =&gt; [term()],
  optional(:replace_order) =&gt; (String.t() -&gt; [term()]),
  optional(:close_position) =&gt; [term()]
}
```

The arguments for each call. `place_order`, `place_orders` and `close_position` are full
argument lists; `replace_order`, `get_order` and `cancel_order` are functions from an
order id to the full argument list, so the id lands in whichever position the caller's own
call shape puts it. Every key but `place_order` is optional; a missing key means that call
is not made.

# `violation`

```elixir
@type violation() :: %{
  step: :get_order | :cancel_order,
  id: String.t(),
  minted_by: :place_order | :place_orders | :replace_order | :close_position,
  answered: term()
}
```

A follow-up the fake refused: the id, the call that minted it, and what was answered.

# `format`

```elixir
@spec format([violation()]) :: String.t()
```

Renders `run/3` violations as one line per refused follow-up, for a failure message.

# `format_error`

```elixir
@spec format_error(term()) :: String.t()
```

Renders a `run/3` error, for a failure message.

# `run`

```elixir
@spec run(module(), DpExchange.Core.Capabilities.t(), calls()) ::
  :skipped | {:ok, [violation()]} | {:error, term()}
```

Mints order ids on `fake` and offers each to `get_order/3` and `cancel_order/3`.

`caps` is the venue's `Capabilities`, read to decide which calls are active.

Returns `:skipped` when none of `place_order/3`, `place_orders/3`, `replace_order/4` and
`close_position/3` could run, `{:ok, violations}` when at least one did (`[]` is a pass),
and `{:error, reason}` when an active minting call did not answer as the contract
requires.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
