# APIs and Request Flows

## Jube

### HTTP API

**Primary scoring endpoint** (`Jube.App/Controllers/Invoke/InvokeController.cs:228-356`):

```
POST /api/invoke/EntityAnalysisModel/{guid}          (synchronous)
POST /api/invoke/EntityAnalysisModel/{guid}/Async     (asynchronous, returns a callback token)
GET  /api/invoke/EntityAnalysisModel/Callback/{guid}  (poll for the async result)
```

- **Model routing**: the model is selected purely by the **GUID in the URL path**
  (`Request.RouteValues["guid"]`). The controller linear-searches
  `engine.Context.Tasks.EntityAnalysisModelManager.Context.EntityAnalysisModels.ActiveEntityAnalysisModels`
  for a matching `Instance.Guid`; no match → `404`.
- **Request body**: free-form JSON. Fields are pulled out via per-model
  `EntityAnalysisModelRequestXPath` configuration (JSONPath expressions against the raw body),
  not a fixed DTO. `EntityInstanceEntryId` and `ReferenceDate` are the two fields every model
  configuration must be able to resolve.
- **Response body** (`BuildJsonResponses.cs:332-354`) — always includes:
  `CreatedDate`, `EntityAnalysisModelInstanceEntryGuid`, `EntityInstanceEntryId`, `ReferenceDate`,
  `ResponseElevation` (`{Value, Redirect, ForeColor, BackColor, Content, CreatedDate}`).
  Conditionally includes (gated by per-row "ResponsePayload" flags in each rule's config):
  `Payload`, `Dictionary`, `TtlCounter`, `Sanction`, `Abstraction`, `AbstractionCalculation`,
  `HttpAdaptation`, `ExhaustiveAdaptation`, `Activation`, `CreateCase`, `Tag`.
- **No top-level `decision`/`score` field.** `ResponseElevation.Value` (a numeric scale, typically
  0–100) *is* the risk signal; there's no separate categorical decision.
- **Authentication**: JWT bearer (HS256, 15-minute expiry, auto-refreshed within 10 minutes of
  expiry) via `POST /api/authentication/ByUserNamePassword`, or Windows/Kerberos "Negotiate" auth.
  The invoke controller can be made public via an `EnablePublicInvokeController` flag.
- **Max body size** is enforced via a configurable `MaxInvokeControllerRequestBytes` setting.

### Non-HTTP transport

RabbitMQ (AMQP) inbound (`jubeInbound` queue) and outbound (`jubeOutbound` queue) are supported as
a full alternative transport — the model GUID travels as a message header
(`EntityAnalysisModelGuid`), the payload is the same JSON shape as the HTTP body, and it's
processed through the identical `EntityAnalysisModelInvoke` path. No gRPC support.

### Dry-run / test-only mode

**There is none.** Both the sync and async HTTP paths, and the AMQP path, always run the full
pipeline including every side effect: TTL counter increments, case creation, DB archiving, and
outbound AMQP publication. The "async" variant only changes *when* the caller gets the response
(immediately with a callback token vs. blocking) — it does not skip any persistence.

## mw-core

### HTTP API

**Two structurally different paths exist, with incompatible payload schemas:**

1. **Admin verify endpoint** (`POST /admin/api/models/:model_guid/verify` →
   `GatewayWebWeb.ModelVerificationController.verify/2` → `MwRisk.Verifier.verify/2`):
   - Model routing: GUID **or** integer model ID in the URL path (`Verifier.resolve_model/1`
     tries both).
   - Request body: Jube-style nested `{"Payload": {...PascalCase fields...}}` JSON, matching the
     convention this whole rule engine was built to evaluate (`Payload.AccountId`-style dotted
     paths resolve against it directly).
   - Response body: `CreatedDate`, `EntityAnalysisModelInstanceEntryGuid`,
     `EntityInstanceEntryId`, `ReferenceDate`, `decision`, `score`, `model_id`, `model_guid`,
     `model_name`, `ResponseElevation`, `CreateCase`, `Payload`, `TtlCounter`, `Sanction`,
     `Abstraction`, `AbstractionCalculation`, `Activation` — deliberately modeled on Jube's shape,
     but **adds** explicit `decision`/`score`/`model_id`/`model_guid`/`model_name` fields Jube's
     response never has.
   - **Originally a true dry run** (`Pipeline.run(ctx, dispatch_side_effects: false)`) — no
     counters incremented, no cases opened, matching the moduledoc's stated contract: *"No side
     effects are dispatched."*
   - **Changed mid-engagement, by explicit user request**: `Verifier.run/2` now also calls
     `VelocityPipeline.update_sync/1` synchronously after computing the decision, so repeated
     `/verify` calls accumulate real TTL-counter and abstraction-journal history — matching how a
     real transaction would behave for *velocity/counter* purposes specifically. Cases,
     notifications, and response-elevation persistence remain dry (no side effects there). This is
     **the opposite of Jube's "always full side effects, no dry-run exists"** — mw-core now has a
     *hybrid* mode: full side effects for velocity, none for case/notification workflows.
   - AMQP equivalent: `AdapterFraud.AmqpVerifyConsumer` delegates to the same `Verifier.verify/2`
     — same dry-run-except-velocity semantics.

2. **Real transaction endpoint** (`POST /api/v1/transactions` →
   `GatewayApiWeb.TransactionController.create/2` → `MwTransform.Mapper.to_canonical/2` →
   `MwRouter.Pipeline.run/1` → `MwRouter.RiskScoringPlug`):
   - Request body: a **different, simpler canonical schema** — snake_case flat fields
     (`account_id`, `amount`, `currency`), produced by `Mapper.to_canonical("transaction.payment",
     params)`. This does **not** carry the PascalCase Jube-style fields
     (`AccountId`/`CurrencyAmount`/etc.) that the entire rule engine (gateway/activation/
     abstraction/TTL-counter `data_name` resolution) was built to read.
   - **Consequence**: a transaction submitted through this endpoint will not populate any of the
     PascalCase-keyed velocity/abstraction state this session's fixes target, because the field
     names simply don't match. This is a real, confirmed schema mismatch between the
     "Jube-parity" rule engine and the system's other real transaction-ingestion path — flagged
     to the user, who confirmed `/verify` is the intended path for this work and the mismatch is
     out of scope for now.
   - **O3 closed.** Velocity counter writes on this path were additionally gated by a feature flag
     (`Application.get_env(:mw_risk, :risk_velocity_pipeline, false)`) that was never set anywhere
     in config, alongside a separate `risk_scoring_enabled` flag gating this whole path (also
     unset) — so neither scoring nor `VelocityPipeline.update/1` ran from this path at all. Both
     are now set to `true` in `config/dev.exs`, explicitly accepting that real transactions on this
     path get scored against the schema described above until O2 is separately addressed — not
     carried into `prod.exs`.
   - Requires `Idempotency-Key` header (8–255 chars); a different auth scheme than the admin JWT.

### Authentication

JWT bearer, similar in shape to Jube's (`Bearer <token>`, claims include `tenant_id`, `roles`),
issued via `POST /admin/api/auth/login`. Mechanically parallel to Jube's
`/api/authentication/ByUserNamePassword`, though claim contents differ.

## Gap table

| Aspect | Jube | mw-core | Tag |
|---|---|---|---|
| Model routing | GUID in URL path only | GUID or integer ID in URL path | 🟢 Parity (mw-core is a superset) |
| Dry-run mode | Does not exist — always full side effects | `/verify` exists, with velocity side effects now enabled by request, cases/notifications still dry | 🟡 Intentional divergence |
| Async + callback pattern | First-class (`/Async` + `/Callback/{guid}`) | Not implemented | ⚠️ Open gap (not requested; note for completeness) |
| Non-HTTP transport | RabbitMQ AMQP, full parity with HTTP | AMQP exists but only as a dry-run `/verify` equivalent (`AmqpVerifyConsumer`); no AMQP path mirrors the *real* transaction endpoint | ⚠️ Open gap |
| Response decision signal | Single numeric `ResponseElevation.Value`, no categorical decision | Categorical `decision` (`approve`/`review`/`decline`) **plus** numeric `score`, both explicit top-level fields | 🟡 Intentional divergence — mw-core's response is more explicit than Jube's |
| Real-transaction payload schema | Same XPath-configurable schema as scoring endpoint — one schema for everything | **Two incompatible schemas**: Jube-style PascalCase for `/verify`, snake_case canonical for `/api/v1/transactions` | 🔴 Confirmed gap, scoping deferred by user decision |
| Velocity counter gating on real-transaction path | Always active | Feature-flag gated, flag never set — dormant by default | ⚠️ Open gap (separate from the schema mismatch above) |
