# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

A fake Yalamanchili (YSP) HTTP server (Elixir, Plug + Bandit, no Phoenix, no database) used to develop and test the sibling QRMiddleLayer app without the real YSP hosts. It simulates:

- The **Narada card API** (section 4 of "Narada-CORE-REST-API Specifications V2.3"; the PDF is in `../QRMiddleLayer/`, a Markdown copy in `../backup/`). QRMiddleLayer doesn't call this API yet.
- The **YSP calls QRMiddleLayer already makes** (payment notification, merchant onboarding). There is no spec for these; responses match what QRMiddleLayer parses, and onboarding follows Alipay+ conventions.

README.md is the user-facing entry point (setup and the endpoint table); the rest of the user-facing reference is in `docs/`, indexed at the bottom of the README. `docs/behaviour/` records every place the simulator made a choice the spec leaves open, grouped by endpoint (see its README). Keep it up to date when behaviour changes.

## Commands

```bash
mix deps.get
mix run --no-halt                          # serves on port 4180 (YSP_SIM_PORT overrides)
mix test
mix test test/ysp_simulator/load_card_test.exs        # one file
mix test test/ysp_simulator/load_card_test.exs:42     # one test by line
mix format
bash demo.sh                               # curl round trip against a running simulator
```

A running simulator keeps serving old code, so restart it after changes. `YSP_SIM_PUBLIC_KEY_PATH` (read in `config/runtime.exs`) turns on real RSA verification of onboarding signatures. Without it, the simulator only checks the format.

## Architecture

**Request pipeline** (`router.ex`): `RawBody` → `LogRequest` → match. `Plugs.RawBody` reads the full body into `conn.assigns.raw_body` instead of using `Plug.Parsers`. This is needed because the Narada endpoints include GETs with JSON bodies (Query Balance), and onboarding signatures are computed over the exact body bytes. Handlers decode `raw_body` themselves.

**Card API endpoints** all go through `CardApi.run(conn, apikey, &Handler.handle/1)`, which does the shared work in order and stops at the first failure:
1. `Validation.validate_headers/2`: the five headers, including the per-endpoint `apikey` (spec appendix 7.1).
2. JSON decode + `Validation.validate_body/1` (`msgHdr`, `txnRefNo`).
3. `Store.claim_txn_ref/1`: each `txnRefNo` can be used once, even if the call fails later (`TIDEXT`). The response code sent is saved against it for 4.20 Status Request, which is routed with `claim_txn_ref: false` because its `txnRefNo` is another request's.
4. The handler.

A handler takes the decoded body (and the conn, if it's a 2-arity function, as Change Password needs the `netid` header) and returns `{:ok, fields}` or `{:error, code, echo_fields}`. `CardApi` adds `txnRefNo`/`responseCode`/`responseText` and sends errors with HTTP 200 by default (`:error_http_status`). Response codes and texts come from spec appendix 7.3 and live in `ResponseCodes`, so new codes go there. The non-card handlers (`NotifyPayment`, `MerchantRegistration`) take the `conn` directly and don't use `CardApi`.

**State** is a single `Agent` (`Store`) holding cards (keyed by 12-digit proxy number), unassigned instant-card inventory, used txnRefNos, merchant registrations, queued forced failures and the last 200 requests. Seed cards, inventory and product codes come from `config/config.exs` and are reloaded on `Store.reset/0` / `POST /_sim/reset`. Card lookups accept either the proxy number or the 16-digit card number. Use `Store.update_card/2` for any check-then-change, so the check and the write happen atomically inside the Agent.

Card statuses use spec appendix 7.2 codes (`M` issued/inactive, `I` active, `L` locked, `HL`/`HS`/`HD` permanently blocked). `docs/how-it-works.md` has the allowed transitions.

`/_sim/*` routes are test controls. `fail_next` only works for endpoints listed in the router's `@failable_endpoints`. Card endpoints take a `mode` (`error_code`, `latency_ms`, `timeout`, `drop_response_after_apply`) and `CardApi.run/4` pops the queued spec itself, so card handlers need no code. `notify_payment` is not a `CardApi` handler and still calls `Store.pop_forced_failure/1` itself (spec `%{mode: :http_status, status: n}`). Behaviour is in `docs/behaviour/general-and-cards.md`.

## Adding a card API endpoint

1. Add `lib/ysp_simulator/handlers/<name>.ex` with `handle/1`. The `@moduledoc` names the spec section, path and apikey, as in the existing handlers.
2. Add one route in `router.ex` calling `CardApi.run(conn, "<apikey>", &Handlers.X.handle/1)`.
3. Add the endpoint to the README endpoint table and to `docs/endpoints.md` ("What each endpoint does"), and put any simulator choices in the matching file under `docs/behaviour/` (add a file and an index line in `docs/behaviour/README.md` if none fits).
4. Tests go in `test/ysp_simulator/<name>_test.exs`. They use `async: false` with `Store.reset()` in `setup` (shared global Agent), and the helpers in `test/support/card_api_helpers.ex` (`headers/1`, `card_body/1`, `card_request/4`, `purchase_body/1`, `purchase_card!/1`). `test/support` is compiled only in the test env.

The test env sets `start_server: false`, so tests call `Router` directly through `Plug.Test` and no port is opened.
