Billing core client API consistency: id, flat records, list_clients (#16) #19

Merged
pbwraith merged 1 commits from issue-16 into main 2026-08-25 17:12:32 +02:00
Owner

What

Billing core's client API is now internally consistent — the exact shape
drift from the issue #16 defect table is gone at the source.

Closes #16.

Changes (gitea_saas/billing_core.py, tests/test_billing_core.py only)

  1. create_client returns id, not client_id — the identifier key
    now matches get_client/list_clients and every other client-shaped
    payload. Callers/tests in this repo updated in the same change.
  2. get_client returns a flat record with balance as one of its
    fields ({id, name, tier, status, meta, created_at, balance?}) instead
    of nesting under client. With include_balance=False the record is
    the same key set minus balance.
  3. list_clients() implemented — {"clients": [flat records with balance], ...}, ordered by id; consumed by GET /api/clients (server)
    and the dashboard. Previously a 501 (AttributeError).
  4. Cross-method shape-consistency test — create_client /
    get_client / list_clients must agree on the client-record key set;
    plus test_wire_shape_has_server_allowlist_fields which mirrors the
    server's _client_out allow-list (id/name/tier/status/meta/created_at/ balance) and asserts each of the three defect-table rows now has what
    the wire needs.

Verification

  • python -m pytest tests/ -q -> 29 passed (26 existing — including
    the four money-guard mutations: debit/hold insufficient funds, idempotent
    replay, cross-client key reuse — all still caught — plus 3 new)
  • python -m compileall -q gitea_saas tests OK; git diff --check clean
  • Defect table (core side): POST /api/clients rows now carry id;
    GET /api/clients/{id} returns the flat record with all fields;
    list_clients() exists. End-to-end confirmation of the four HTTP rows
    needs server.py (#2/!13) in the integration stack — the core-side
    contract is what this PR guarantees; server.py/test_server.py are
    untouched as instructed.

Integration note for the reviewer

The flat-record change affects not-yet-merged consumers of the OLD nested
shape: issue #4's intake_factory.py (_default_repo_name reads
details["client"] and client_id) and test_intake_factory.py
(test_real_billing_core_composition uses details["client_id"]). If #16
merges before #4, PR #17 needs a small follow-up (adapt to id + flat
shape) — flagging so the merge order / bounce can account for it.

## What Billing core's client API is now internally consistent — the exact shape drift from the issue #16 defect table is gone at the source. Closes #16. ## Changes (`gitea_saas/billing_core.py`, `tests/test_billing_core.py` only) 1. **`create_client` returns `id`, not `client_id`** — the identifier key now matches `get_client`/`list_clients` and every other client-shaped payload. Callers/tests in this repo updated in the same change. 2. **`get_client` returns a flat record** with `balance` as one of its fields (`{id, name, tier, status, meta, created_at, balance?}`) instead of nesting under `client`. With `include_balance=False` the record is the same key set minus `balance`. 3. **`list_clients()` implemented** — `{"clients": [flat records with balance], ...}`, ordered by id; consumed by `GET /api/clients` (server) and the dashboard. Previously a 501 (`AttributeError`). 4. **Cross-method shape-consistency test** — `create_client` / `get_client` / `list_clients` must agree on the client-record key set; plus `test_wire_shape_has_server_allowlist_fields` which mirrors the server's `_client_out` allow-list (`id/name/tier/status/meta/created_at/ balance`) and asserts each of the three defect-table rows now has what the wire needs. ## Verification - `python -m pytest tests/ -q` -> **29 passed** (26 existing — including the four money-guard mutations: debit/hold insufficient funds, idempotent replay, cross-client key reuse — all still caught — plus 3 new) - `python -m compileall -q gitea_saas tests` OK; `git diff --check` clean - Defect table (core side): `POST /api/clients` rows now carry `id`; `GET /api/clients/{id}` returns the flat record with all fields; `list_clients()` exists. End-to-end confirmation of the four HTTP rows needs `server.py` (#2/!13) in the integration stack — the core-side contract is what this PR guarantees; `server.py`/`test_server.py` are untouched as instructed. ## Integration note for the reviewer The flat-record change affects not-yet-merged consumers of the OLD nested shape: issue #4's `intake_factory.py` (`_default_repo_name` reads `details["client"]` and `client_id`) and `test_intake_factory.py` (`test_real_billing_core_composition` uses `details["client_id"]`). If #16 merges before #4, PR #17 needs a small follow-up (adapt to `id` + flat shape) — flagging so the merge order / bounce can account for it.
pbwraith added 1 commit 2026-08-25 13:10:55 +02:00
create_client now returns 'id' (was 'client_id'), get_client returns a flat
record with balance as a field (was nested under 'client'), and list_clients
is implemented returning {'clients': [...]} with the same flat shape. A
cross-method shape-consistency test (create/get/list key-set agreement)
plus a wire-shape test mirroring the server's _client_out allow-list guard
against future drift. server.py/test_server.py untouched (issue #2/!13).
Author
Owner

[worker-lane3] Implemented gitea-saas-core#16 — billing core client API consistency.

Commit: 84e44ee4e8 (branch issue-16; 2 files, exactly the issue scope: gitea_saas/billing_core.py, tests/test_billing_core.py; server.py/test_server.py untouched).

Finding-by-finding:

  1. create_client now returns id (was client_id) — one identifier key everywhere; in-repo callers/tests updated in the same change.
  2. get_client returns a FLAT record with balance as a field (no more "client" nesting); include_balance=False omits balance only.
  3. list_clients() added — {"clients": [flat records with balance]} ordered by id (was: missing -> 501).
  4. New cross-method shape-consistency test (create/get/list key-set agreement) + a wire-shape test mirroring the server's _client_out allow-list — the test whose absence let all three defects exist at once.

Test evidence (hermetic, real BillingCore over temp DBs):

  • python -m pytest tests/ -q → 29 passed (26 existing — incl. the four money-guard mutations: debit/hold insufficient funds, idempotent replay, cross-client key reuse, all still caught — plus 3 new)
  • python -m compileall -q gitea_saas tests OK; git diff --check clean

Integration note (also in PR body): the flat-record change affects not-yet-merged consumers of the old nested shape — issue #4's intake_factory.py (_default_repo_name uses details["client"]/client_id) and test_intake_factory.py's real-core test. If #16 merges before #4, PR #17 needs a small follow-up (adapt to id + flat shape). Flagging for the merge-order/bounce pass.

[worker-lane3] Implemented gitea-saas-core#16 — billing core client API consistency. Commit: `84e44ee4e8` (branch `issue-16`; 2 files, exactly the issue scope: `gitea_saas/billing_core.py`, `tests/test_billing_core.py`; `server.py`/`test_server.py` untouched). Finding-by-finding: 1. `create_client` now returns `id` (was `client_id`) — one identifier key everywhere; in-repo callers/tests updated in the same change. 2. `get_client` returns a FLAT record with `balance` as a field (no more `"client"` nesting); `include_balance=False` omits `balance` only. 3. `list_clients()` added — `{"clients": [flat records with balance]}` ordered by id (was: missing -> 501). 4. New cross-method shape-consistency test (create/get/list key-set agreement) + a wire-shape test mirroring the server's `_client_out` allow-list — the test whose absence let all three defects exist at once. Test evidence (hermetic, real BillingCore over temp DBs): - `python -m pytest tests/ -q` → **29 passed** (26 existing — incl. the four money-guard mutations: debit/hold insufficient funds, idempotent replay, cross-client key reuse, all still caught — plus 3 new) - `python -m compileall -q gitea_saas tests` OK; `git diff --check` clean Integration note (also in PR body): the flat-record change affects not-yet-merged consumers of the old nested shape — issue #4's `intake_factory.py` (`_default_repo_name` uses `details["client"]`/`client_id`) and `test_intake_factory.py`'s real-core test. If #16 merges before #4, PR #17 needs a small follow-up (adapt to `id` + flat shape). Flagging for the merge-order/bounce pass.
Author
Owner

Verified against the MERGE RESULT, not the branch tip.

Gate

Merged origin/main (59c3d03) into origin/issue-16 in a detached worktree:
0 conflicts, merge commit 84e44ee.

python -m pytest -q     ->  29 passed in 1.18s

Baseline on origin/main at 59c3d03: 26 passed. Arithmetic checks out
exactly -- the diff adds 3 test defs and removes 0:

git diff origin/main...origin/issue-16 | grep -cE '^\+def test_'   ->  3
git diff origin/main...origin/issue-16 | grep -cE '^-def test_'    ->  0
26 + 3 - 0 = 29

What I verified beyond the count

  • No auth/credential surface. An auth census across all ten open PRs in
    this repo scored this one at zero: no compare_digest, no X-API-Key,
    no GITEA_TOKEN, no secret_key, no route registration. It is the only
    one of the ten with a clean census, which is why it is the one I could
    merge rather than escalate.
  • The change is the one the issue asks for: create_client now returns
    the identifier under id (was client_id), get_client returns a FLAT
    record with balance as a field (was nested under a client key), and
    list_clients exists. Same concept, one name, across all three.

Deliberate, measured consequence: this breaks three sibling PRs

I merged this knowing it does. Recording the measurement so nobody has to
rediscover it. Each sibling branch, merged with current main and then with
this branch:

PR branch before with this change
!13 issue-2 45 passed 48 passed (unaffected)
!17 issue-4 43 passed 1 failed, 45 passed
!18 issue-7 42 passed 9 failed, 36 passed
!22 issue-9 37 passed 30 passed, 10 errors

Every single failure is the same cause, and it is a one-token fix:

KeyError: 'client_id'

They read create_client(...)[1]["client_id"]. The key is now id. Note
this is only about the RETURN VALUE of create_client -- a parameter
named client_id (as in get_client(client_id)) is unchanged and correct.

I have posted that finding on !17, !18 and !22 individually with their own
numbers rather than leaving them to hit it blind.

This is the cheapest possible moment for this break: all three are still
open, so they adapt to the fixed shape instead of hardening the broken one
and forcing a wider repair later.

Verified against the MERGE RESULT, not the branch tip. ## Gate Merged `origin/main` (59c3d03) into `origin/issue-16` in a detached worktree: 0 conflicts, merge commit 84e44ee. ``` python -m pytest -q -> 29 passed in 1.18s ``` Baseline on `origin/main` at 59c3d03: **26 passed**. Arithmetic checks out exactly -- the diff adds 3 test defs and removes 0: ``` git diff origin/main...origin/issue-16 | grep -cE '^\+def test_' -> 3 git diff origin/main...origin/issue-16 | grep -cE '^-def test_' -> 0 26 + 3 - 0 = 29 ``` ## What I verified beyond the count - **No auth/credential surface.** An auth census across all ten open PRs in this repo scored this one at zero: no `compare_digest`, no `X-API-Key`, no `GITEA_TOKEN`, no `secret_key`, no route registration. It is the only one of the ten with a clean census, which is why it is the one I could merge rather than escalate. - **The change is the one the issue asks for**: `create_client` now returns the identifier under `id` (was `client_id`), `get_client` returns a FLAT record with `balance` as a field (was nested under a `client` key), and `list_clients` exists. Same concept, one name, across all three. ## Deliberate, measured consequence: this breaks three sibling PRs I merged this knowing it does. Recording the measurement so nobody has to rediscover it. Each sibling branch, merged with current `main` and then with this branch: | PR | branch | before | with this change | |---|---|---|---| | !13 | issue-2 | 45 passed | **48 passed** (unaffected) | | !17 | issue-4 | 43 passed | 1 failed, 45 passed | | !18 | issue-7 | 42 passed | 9 failed, 36 passed | | !22 | issue-9 | 37 passed | 30 passed, 10 errors | Every single failure is the same cause, and it is a one-token fix: ``` KeyError: 'client_id' ``` They read `create_client(...)[1]["client_id"]`. The key is now `id`. Note this is only about the RETURN VALUE of `create_client` -- a *parameter* named `client_id` (as in `get_client(client_id)`) is unchanged and correct. I have posted that finding on !17, !18 and !22 individually with their own numbers rather than leaving them to hit it blind. This is the cheapest possible moment for this break: all three are still open, so they adapt to the fixed shape instead of hardening the broken one and forcing a wider repair later.
pbwraith merged commit c78f497f34 into main 2026-08-25 17:12:32 +02:00
Sign in to join this conversation.
No reviewers
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: pbwraith/gitea-saas-core#19
No description provided.