# SPEC.md - Payments Service

> **This is the example specification referenced in the *Compiling Intent* keynote.**
> Demo 1 (Symphony) used this exact spec as the source of truth: a Symphony run
> against a GitHub issue regenerated the implementation, the tests and the API
> docs from this file. The mid-build "dual approval over $10,000" change in
> slide 11 corresponds to the diff at the bottom.

---

## 1. Intent

Settle invoices against the customer ledger, exactly once per logical
transaction, with a full audit trail acceptable to a financial regulator.

The payments service is the **only** path to a settled ledger row.

## 2. Guarantees

These are the load-bearing promises the service makes. Every guarantee in this
list MUST be expressible as one or more automated checks. If a line below
cannot become a check, it is decoration and must be removed.

- **Idempotent** - A retried `POST /payments` with the same `Idempotency-Key`
  settles the payment exactly once. Concurrent retries (50 in flight) produce
  exactly one ledger row.
- **Audited** - Every state transition (received → authorised → settled →
  failed → reversed) appends an immutable event to the payment audit log.
- **Traceable** - Every request carries a `traceparent` header; the trace is
  attached to the ledger row and to every audit event.
- **Reversible** - Any settled payment can be reversed by reference within
  90 days. The reversal is itself a payment event and is fully audited.
- **PII-safe** - No card number, CVV or full bank account number is ever
  written to logs, traces or the audit table.

## 3. Interface

### POST /payments

```http
POST /payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: 8b3c1f70-2c7a-4d8b-9c6e-7c5a1d2e3f40
traceparent: 00-...-...-01

{
  "invoice_id":  "INV-2026-0042",
  "amount":      { "currency": "AUD", "minor_units": 12500 },
  "method":      { "type": "card", "token": "tok_..." }
}
```

**Responses**

| Status | Meaning |
|--------|---------|
| `201 Created`       | First settlement for this idempotency key. Returns `payment_id`, `ledger_row_id`, `status`. |
| `200 OK`            | Replay of a previously settled key. Body identical to the original `201`. |
| `409 Conflict`      | Same key, different request body. No state change. |
| `422 Unprocessable` | Validation failure (invoice not found, currency mismatch, etc.). |
| `500 Internal`      | Reserved for unhandled failures. Always retriable. |

### POST /payments/{id}/reverse

Reverses a settled payment. Idempotent on `Idempotency-Key`. Refuses
reversals older than 90 days with `409 Conflict`.

## 4. Acceptance Criteria

These are the checks that prove the guarantees hold. They are the agent's
contract.

1. **Idempotent settle** - Fire 50 concurrent `POST /payments` requests with
   the same body and the same `Idempotency-Key`. After all complete: exactly
   one ledger row exists with the expected amount; 49 responses are `200 OK`
   replays; 1 response is `201 Created`; the audit log contains exactly one
   `settled` event.
2. **Key reuse, different body** - POST with key `K`, then POST again with
   key `K` and a changed `amount`. Expect `409 Conflict` and no second ledger
   row.
3. **Reversal within window** - Settle, then reverse within 90 days. Ledger
   shows two rows (`+amount`, `-amount`); audit log shows `settled` then
   `reversed`.
4. **Reversal outside window** - Settle, advance the clock to day 91, attempt
   reversal. Expect `409 Conflict`, ledger unchanged.
5. **Trace propagation** - Send a request with `traceparent`. Assert the trace
   id appears on the ledger row and on every audit event for that payment.
6. **PII never logged** - Run the full suite with a log sink that fails the
   test on any pattern matching a 13–19 digit card-like number, a CVV, or a
   BSB/account pair.
7. **Rule: dual approval > $10,000** - POST a payment with
   `amount.minor_units > 1_000_000`. Expect `202 Accepted` with status
   `pending_approval`, no ledger row, and an `approval_requested` audit event.
   A subsequent approval call from a different user transitions it to
   `settled`. An approval from the **same** user that submitted the payment
   fails with `403 Forbidden`.

## 5. Rules

- **Idempotent** `POST /payments` (see acceptance criteria 1, 2).
- Payments **> $10,000 require dual approval** before settle. The submitter
  cannot also be the approver. (See acceptance criterion 7.)
- All amounts are integers in minor units. No floating point in the payment
  path, ever.
- Settlement is atomic with the audit event: a payment is `settled` iff its
  audit row exists. The implementation should use a single transaction or
  outbox pattern to guarantee this.

## 6. Non-Goals

- This spec does not cover payment **capture** flows (auth-then-capture).
- This spec does not cover **refunds initiated by the customer** - only
  operator-initiated reversals.
- This spec does not cover **subscriptions or scheduled billing**.

## 7. Open Questions

- Should the 90-day reversal window be configurable per merchant? (Defaulting
  to 90 unless the field is set.)
- Do we need a webhook for `approval_requested`, or is polling sufficient for
  v1? (Decision: polling for v1, webhook in v1.1.)

---

## Appendix A - The mid-build change (slide 11 of the keynote)

The keynote shows this exact diff being made mid-build. It is the **only**
human edit required to add a dual-approval rule across plan, code, tests and
docs.

```diff
  rules:
    - idempotent POST /payments
+   - payments > $10,000 require
+     dual approval before settle
  # plan, tasks, code, tests, docs → regenerated from here
```

Two lines added. Symphony then re-plans, generates the approval state
machine, adds the `same-submitter-cannot-approve` test, updates the OpenAPI
spec and the `/payments` reference page. All consistent - because they all
descend from this file.
