# Understanding the YSP card work (a personal guide, not committed)

Written for: you, to understand and explain what was built on `task/ysp-card-postman-flow` (Sprint 1 and the 40 endpoints) and `task/ysp-card-support-errors` (Sprint 2, support-friendly errors).
This file is listed in `.git/info/exclude`, so git ignores it. It is not part of any PR.

## 1. The big picture

```
Bruno / Postman  ──►  middle layer (Phoenix, port 4004)  ──►  YSP card API
                      /api/card/<name>                        (here: the YSP simulator, port 4180)
        ◄──  { ysp_response: YSP's answer unchanged,  ◄──────────────
               human_readable_output: cause + action + correlation id }
```

- The **middle layer** is our app. It takes a simple request, adds what YSP needs, sends **one** request to YSP and hands back YSP's answer as it came, **wrapped** with a plain-language explanation for support (Sprint 2).
- The **simulator** ([ysp-simulator/](ysp-simulator/)) is a fake YSP so we can test without the real one. It is a separate program on purpose.
- The long-term idea: app → MMS Portal → this middle layer → real YSP prepaid card endpoints. So far the path is only proven with the simulator.

## 2. What happens to one request (read this first)

Example: `POST /api/card/load` with `{"proxyNumber": "...", "productInfo": {...}}`.

1. **Router** ([router.ex](lib/da_product_app_web/router.ex#L223), `scope "/api/card"`) runs the `:ysp_card` pipeline first ([L31](lib/da_product_app_web/router.ex#L31)), then `:api`, then sends the request to the controller.
2. **Correlation id plug** ([correlation_id.ex](lib/da_product_app_web/plugs/correlation_id.ex)) makes a fresh UUID for this request. It goes in `conn.assigns`, the logger metadata and the `x-correlation-id` response header. A header the caller sends is ignored, so ids are always ours.
3. **Controller** ([ysp_card_controller.ex](lib/da_product_app_web/controllers/ysp_card_controller.ex#L44), `proxy/2`) checks the required fields. Missing field → 400 and **nothing is sent**.
4. **Request builder** ([request.ex](lib/da_product_app/ysp_card/request.ex)) adds the headers YSP wants (`apikey`, `clientid`, `channeltype`, `netid`, `timestamp`), a `msgHdr`, and a `txnRefNo` if you did not give one.
5. **Client** ([ysp_card.ex](lib/da_product_app/ysp_card.ex#L37), `call/2`) makes **one** HTTP request with a timeout. **No retry.**
6. **Answer.** In [forward/3](lib/da_product_app_web/controllers/ysp_card_controller.ex#L67):
   - YSP answered with JSON → [respond_ysp/3](lib/da_product_app_web/controllers/ysp_card_controller.ex#L97): same HTTP status, the body untouched in `ysp_response`, and `human_readable_output` looked up from its `responseCode` in `ErrorMessages`. Code `00` gets the summary "Transaction Success". YSP error codes (`INVCRD`, `TIDEXT`, ...) are still HTTP 200, as YSP sends them.
   - YSP could not be reached → 502 `ysp_unreachable`; timeout → 504 `ysp_timeout`; answer not JSON → 502 `ysp_bad_response`. These go through [respond_error/5](lib/da_product_app_web/controllers/ysp_card_controller.ex#L115) with `ysp_response: null`. Validation errors (400) and unknown endpoints (404) use the same path.

The shape of every response:

```json
{
  "ysp_response": { ...YSP's body, or null },
  "human_readable_output": {
    "code": "CRDBLO", "summary": "...", "cause": "...", "action": "...",
    "correlation_id": "..."
  }
}
```

For a timeout or unreadable answer on a **critical** call, the `action` text says the call "may or may not have been applied" and to check the balance or status before trying again. That is text only: nothing records or checks the outcome yet.

## 3. The one table everything reads: `endpoints.ex`

[endpoints.ex](lib/da_product_app/ysp_card/endpoints.ex) lists all 40 endpoints. For each: its name in our URL, the YSP method and path, its `apikey`, whether it is `:critical` or `:standard`, and the **required fields**.

The request builder, the controller, the tests, the Postman collection and the README table all read from it. That is why adding or fixing an endpoint is a one-line change, and why the tests can check everything stays consistent.

The second table is [error_messages.ex](lib/da_product_app/ysp_card/error_messages.ex): a cause and an action for every YSP response code (copied from the simulator's `ResponseCodes`, because it is a separate project) and for our own errors. A test fails if the simulator has a code the table does not. Unknown codes get a generic text that quotes the code. The README's code table is generated from it.

## 4. Words you will see

| Word | Meaning |
|---|---|
| `proxyNumber` | YSP's 12-digit ID for a card (seed card `000000012845`). We send it instead of the full 16-digit card number, so the card number does not travel in requests. |
| `txnRefNo` | A one-time transaction ID. YSP refuses a repeat with `TIDEXT`. If you leave it out, we generate a new one, so each call is a new transaction. To test replay, send the same one twice. |
| `TIDEXT` | "Transaction Reference Already Existed": the repeat was refused, so nothing was applied twice. |
| critical | Money, card state, PINs, passwords. Never retried blindly or applied twice. A timeout message says "may or may not have been applied". |
| standard | Reads and low-risk settings. |
| `msgHdr` | A small header object YSP wants in every body. We add it; a caller cannot forge it. |
| passthrough | We do not rewrite YSP's answer. It sits untouched in `ysp_response`; our explanation is next to it in `human_readable_output`. |
| correlation id | A UUID per request, in the header, the response body and the log lines. Support quotes it to find one call. It is not the `txnRefNo`: that identifies the business transaction for YSP, the correlation id identifies one HTTP attempt on our side. |

## 5. Where things are

| What | Where |
|---|---|
| Plan and results of every step | [YSP_CARD_STEPS.md](YSP_CARD_STEPS.md) |
| The 40-endpoint table | [endpoints.ex](lib/da_product_app/ysp_card/endpoints.ex) |
| Error messages (cause and action per code) | [error_messages.ex](lib/da_product_app/ysp_card/error_messages.ex) |
| Correlation id plug | [correlation_id.ex](lib/da_product_app_web/plugs/correlation_id.ex) |
| Controller (validation, response envelope) | [ysp_card_controller.ex](lib/da_product_app_web/controllers/ysp_card_controller.ex) |
| Request builder / client / config | [request.ex](lib/da_product_app/ysp_card/request.ex), [ysp_card.ex](lib/da_product_app/ysp_card.ex), [config.ex](lib/da_product_app/ysp_card/config.ex) |
| Unit tests (no network) | [test/da_product_app/ysp_card/](test/da_product_app/ysp_card/), [ysp_card_controller_test.exs](test/da_product_app_web/controllers/ysp_card_controller_test.exs), [ysp_card_proxy_test.exs](test/da_product_app_web/controllers/ysp_card_proxy_test.exs), [envelope](test/da_product_app_web/controllers/ysp_card_envelope_test.exs), [correlation id](test/da_product_app_web/controllers/ysp_card_correlation_id_test.exs) |
| Integration tests (live simulator) | [batch A](test/da_product_app/ysp_card_batch_a_integration_test.exs), [B](test/da_product_app/ysp_card_batch_b_integration_test.exs), [C](test/da_product_app/ysp_card_batch_c_integration_test.exs), [D](test/da_product_app/ysp_card_batch_d_integration_test.exs), [sweep](test/da_product_app/ysp_card_sweep_integration_test.exs), [basic](test/da_product_app/ysp_card_integration_test.exs), [envelope](test/da_product_app/ysp_card_envelope_integration_test.exs) |
| Postman collection | [bank-bridge-ysp-card-sprint2.postman_collection.json](docs/postman/bank-bridge-ysp-card-sprint2.postman_collection.json) (generated); the Sprint 1 file [bank-bridge-ysp-card.postman_collection.json](docs/postman/bank-bridge-ysp-card.postman_collection.json) is kept as delivered |
| Collection generator | [builder.exs](docs/postman/builder.exs), [generate.exs](docs/postman/generate.exs) |
| The simulator | [ysp-simulator/](ysp-simulator/) |

## 6. How to read the code (about 30 minutes)

1. [README.md](README.md#L5), section "YSP card API".
2. [endpoints.ex](lib/da_product_app/ysp_card/endpoints.ex) (skim the list, read the moduledoc).
3. The controller: [proxy/2](lib/da_product_app_web/controllers/ysp_card_controller.ex#L44), [forward/3](lib/da_product_app_web/controllers/ysp_card_controller.ex#L67), then the two response helpers [respond_ysp/3](lib/da_product_app_web/controllers/ysp_card_controller.ex#L97) and [respond_error/5](lib/da_product_app_web/controllers/ysp_card_controller.ex#L115).
4. [error_messages.ex](lib/da_product_app/ysp_card/error_messages.ex): skim the table, read `for_error/2`.
5. [ysp_card.ex](lib/da_product_app/ysp_card.ex#L37): `call/2`.
6. One integration test, for example [ysp_card_batch_c_integration_test.exs](test/da_product_app/ysp_card_batch_c_integration_test.exs): see how a test checks balances before and after.
7. [YSP_CARD_STEPS.md](YSP_CARD_STEPS.md#L140), the "results" sections and the Sprint 2 section at the end: the decisions and findings.

## 7. How to run and test

```bash
cd ysp-simulator && mix run --no-halt                      # simulator on 4180
YSP_CARD_BASE_URL=http://localhost:4180 mix phx.server     # middle layer on 4004 (check 4004 is free first; this machine is shared)
```

Then run the Postman collection in Bruno, top to bottom (request 0 resets the simulator). The collection checks both `ysp_response` and `human_readable_output`.

Starting the full app also starts MQTT with a fixed `client_id`, which knocks any other running instance off the broker. The Newman runs used private ports (simulator 4181 via `YSP_SIM_PORT`, middle layer 4014) with only the web children started, no MQTT.

Tests: `mix test` is blocked by a duplicate migration (three files share version 20250707), so the YSP tests were run with
`MIX_ENV=test mix run --no-start -e 'Mix.Tasks.Test.run(["test/da_product_app/ysp_card", ...])'`. The last full YSP run: 261 tests, 0 failures, 105 integration tests excluded.
Integration tests need a live simulator and `--include integration`.

## 8. What is NOT built (be upfront about it)

- **No authentication** on `/api/card/*`. Fine against the simulator, must be fixed before real YSP.
- **No database log** of calls (Sprint 3). The correlation id is only in the logs for now.
- **Critical calls have no unknown-outcome handling or reconciliation** (Sprint 4). A timeout's action text says the outcome is unknown, but nothing stores or checks it.
- **The simulator cannot yet time out, delay or drop a response on demand** (Sprint 4), so a live timeout is only tested with a stub.
- Support-friendly errors exist (Sprint 2), but the wording was written from the simulator's codes, not reviewed by support or real YSP.
- Only tested against the simulator, never real YSP.

## 9. Questions to test yourself (answers are in the files above)

1. Why does request 2 (load) succeed every time you click it, but 3b (same `txnRefNo`) is refused?
2. Why is a failed critical call never retried?
3. What happens if you POST to `/api/card/nope`? And with a required field missing?
4. Why does the endpoint table live in one place, and why is the error table a copy of the simulator's codes?
5. Why does the 504 message say "may or may not have been applied" for a load but not for a balance?
6. What is the difference between `L` and `BL` in the block request?
7. A card is blocked and someone tries to load it. What does Postman show, which part is YSP's and which is ours, and what HTTP status?
8. Why does `ysp_response` become `null` on a timeout, and how does the `action` text differ for a load versus a balance?
9. What is the correlation id for, and why is it not the same as `txnRefNo`?
