# Calling the SWDA REST API

This app already has a working SWDA (SSG/SkillsFuture) HTTP client. If you need to
send a request to SWDA, **you do not need to build any encryption, mTLS, or transport
code** — it exists. All you have to provide is:

1. the request path
2. the plain (unencrypted) payload as a PHP array

Everything else — AES-256-CBC encryption, the fixed IV, mTLS client certificates, base
URL, timeouts, and decrypting the response — is handled for you by
[`App\Services\Swda\SwdaClient`](app/Services/Swda/SwdaClient.php).

## How to make a call

```php
use App\Services\Swda\SwdaClient;

$client = app(SwdaClient::class); // or inject SwdaClient via the constructor

$result = $client->post('course-runs', [
    // plain array, e.g. a course run + its sessions
    'courseRunId' => 'RUN-1',
    'sessions' => [
        // ...
    ],
]);

// $result is already decrypted -- a plain PHP array, ready to use.
```

That's the entire contract:

- `post(string $path, array $payload): array`
- `$path` is relative to the configured base URL (e.g. `'course-runs'`, not a full URL).
- `$payload` is a plain, unencrypted array. `SwdaClient` JSON-encodes it, encrypts it
  with AES-256-CBC, and POSTs it with the mTLS client certificate attached.
- The return value is the SWDA response, already decrypted and JSON-decoded back into
  a plain array.
- On a non-2xx HTTP response, it throws (`Illuminate\Http\Client\RequestException` via
  `->throw()`).
- On any setup problem (SWDA disabled, missing cert, malformed AES key/IV), it throws
  `App\Services\Swda\SwdaConfigurationException` — this is a local misconfiguration,
  not a SWDA-side error.

**Do not** re-implement encryption, re-read the cert files, or call `Http::` directly
for SWDA endpoints. Route every SWDA request through `SwdaClient::post()`.

## Where the credentials live

These are already configured — you should never need to touch them just to make a
call, only to set them up initially or rotate them:

| What | Where |
|---|---|
| Base URL | `SWDA_BASE_URL` in `.env` |
| Kill switch | `SWDA_ENABLED` in `.env` (must be `true` to make real calls) |
| mTLS client certificate | `storage/app/private/swda/uat-client.pem` (path configurable via `SWDA_CLIENT_CERT_PATH`) |
| mTLS client key | `storage/app/private/swda/uat-client.key` (path configurable via `SWDA_CLIENT_KEY_PATH`) |
| AES-256 key (base64, 32 bytes decoded) | `SWDA_AES_KEY` in `.env` |
| AES IV (base64, 16 bytes decoded, fixed — not regenerated per call) | `SWDA_AES_IV` in `.env` |

Full config resolution: [`config/swda.php`](config/swda.php).

## Verifying the setup without sending a real request

```bash
php artisan swda:doctor
```

Checks (no network call): SWDA is enabled, base URL is set, both cert/key files exist
and are readable, the cert isn't expired, the cert and key actually match each other,
and the AES key/IV decode to the correct byte lengths. Run this first if a real call
fails — it will usually pinpoint whether the problem is local config vs. the SWDA side.

## Implementation reference

- [`app/Services/Swda/SwdaClient.php`](app/Services/Swda/SwdaClient.php) — the client;
  `post()` is the only method you need.
- [`app/Services/Swda/SwdaCrypto.php`](app/Services/Swda/SwdaCrypto.php) — AES-256-CBC
  encrypt/decrypt, used internally by `SwdaClient`.
- [`app/Console/Commands/SwdaDoctor.php`](app/Console/Commands/SwdaDoctor.php) — the
  `swda:doctor` preflight command.
- [`tests/Feature/SwdaConfigTest.php`](tests/Feature/SwdaConfigTest.php) — reference
  tests showing the encrypt/decrypt round trip and a faked `post()` call.

# Testing-Only Features

These features exist only to make manual/QA testing easier and must never be
reachable in production. Each is gated behind an env var named
`*_TESTING_FEATURES_ENABLED`/`ENABLE_TESTING_FEATURES` (the literal name
differs per app — see the table). **When adding a new testing-only feature
anywhere in the system, add a row here.**

| Feature | Where it lives | Gated in | Env var |
|---|---|---|---|
| Create Test Course Run | `cqtms-admin`, `CourseRunFormModal.vue` (`fillTestData`) | Frontend only — pure client-side form filler, submits through the normal create-course-run endpoint | `VITE_ENABLE_TESTING_FEATURES` (cqtms-admin) |
| Create Random Test Sessions | `cqtms-admin`, `CourseRunFormModal.vue` (`createRandomTestSessions`) | Frontend only | `VITE_ENABLE_TESTING_FEATURES` (cqtms-admin) |
| Generate Spanning Session | `cqtms-admin`, `CourseRunFormModal.vue` (`generateSpanningSession`) | Frontend only | `VITE_ENABLE_TESTING_FEATURES` (cqtms-admin) |
| Create Test Payment | `cqtms-learner`, `MakePayment.vue` (`makeTestPayment`) → `POST /me/course-runs/{course_run}/payments` ([`MePaymentController::store`](app/Http/Controllers/Api/MePaymentController.php)) | Frontend **and** backend — this endpoint creates a `Completed` payment with no gateway check, so it's also blocked server-side | `VITE_ENABLE_TESTING_FEATURES` (cqtms-learner) + `TESTING_FEATURES_ENABLED` (cqtms-api) |

**Why some rows are frontend-only:** the admin course-run helpers don't call
any dedicated backend endpoint — they fill out the same form a real course
run uses, then submit it the normal way. There's nothing backend-specific to
block. Only gate the backend when a testing feature has its own reachable
endpoint or side effect, like the payments one.

**Backend gate implementation:** [`config('app.testing_features_enabled')`](config/app.php),
read from `TESTING_FEATURES_ENABLED` in `.env`, enforced by the
[`testing`](bootstrap/app.php) route middleware alias →
[`EnsureTestingFeaturesEnabled`](app/Http/Middleware/EnsureTestingFeaturesEnabled.php)
(404s when the flag is off).
