Skip to content

D608: a manual credit grant or deduction is the Super Admin's, through the grant service (#1308) - #1321

Merged
guillaumelauzier merged 1 commit into
mainfrom
agent/claude-b/1308-manual-credit-adjust
Oct 6, 2026
Merged

guillaumelauzier merged 1 commit into
mainfrom
agent/claude-b/1308-manual-credit-adjust

Conversation

@guillaumelauzier

Copy link
Copy Markdown
Contributor

Objective

POST /api/perks/admin/credits wrote perk_credit_ledger itself, which broke D540's rule that every grant goes through one service. #1165's audit found it (area 7). The route:

  • accepted any admin;
  • asked for no reason and wrote no audit row;
  • sent the member no notice;
  • passed a reused source_ref through as a raw index error;
  • wrote rows the Super Admin's grants list never showed.

This PR routes the adjustment through services/creditGrants.ts.

Closes #1308

Implementation

manualAdjust in services/creditGrants.ts. The route checks requireSuperAdmin and hands the rest to the service.

  • delta: a non-zero whole number, at most 100,000 either way. That is the bound admin_credit_rules.ts uses. A string such as "10" is refused.
  • reason: at least 10 characters, the shared CREDIT_REASON_MIN floor. It is recorded in the audit row, not shown to the member.
  • note: what the member sees, at most 500 characters. A longer one is refused, not cut. With no note it reads "Credits added (or deducted) by the Axal team".
  • Idempotent on source_ref (default admin:<id>:<ISO>). A ref already on that member's ledger, of any kind, is a 409 already_recorded with the existing ledger_id. That includes a rule grant's ref, so a manual line can't double a reward.
  • No overdraw. A deduction below zero is a 400 would_overdraw with the balance.
  • Both checks are inside the conditional INSERT, so two adjustments racing can't both pass.
  • What is written:
    • one ledger line with rule_key = 'manual': a grant line for a grant, an admin_adjust line for a deduction;
    • one logAdminAction row, credit_manual_adjust;
    • one inbox notice, credit_manual_adjust.
  • Not a bounty. A manual line never counts toward the monthly cap.

The route returns refusals through refuse():

  • 400: invalid_user, invalid_delta, reason_too_short, note_too_long, invalid_source_ref, would_overdraw;
  • 404: user_not_found;
  • 409: already_recorded.

The grants list (GET /api/admin/credit-rules/grants):

  • manual lines appear beside rule grants, with manual: true;
  • a deduction shows as negative credits;
  • revoke lines (revoke:<id>) are left out, though revoking a manual grant also writes a manual admin_adjust line.

No migration. rule_key (migration 381) is the marker, so 414 is not taken.

Docs: new decisions/D608.md, and a one-line pointer in D540.

Files changed

  • cloudflare-worker/src/services/creditGrants.ts: manualAdjust, MANUAL_RULE_KEY, the bounds.
  • cloudflare-worker/src/routes/perks.ts: the /admin/credits handler only.
  • cloudflare-worker/src/routes/admin_credit_rules.ts: the grants list only.
  • cloudflare-worker/test/manual_credit_adjust_d608.test.ts (new, 9 tests).
  • frontend/test/perks_live.test.mjs: its source pin on the old route's message now points at the service's in-insert balance check.
  • documentation/architecture/decisions/D608.md (new), D540.md (pointer).

Testing

  • npm run build, then npm run test:drift > drift.log 2>&1; echo EXIT=$?, gives EXIT=0.
  • The new tests run on real SQLite, using the Perks harness's migrations read off disk, through the real routers with real JWTs. They fail on main. They cover:
    • a plain admin and a founder refused with 403;
    • a missing or short reason;
    • the bounds on delta, note and source;
    • a grant and a deduction, each writing one ledger line, one audit row and one inbox notice;
    • a repeated source_ref of either sign, and a rule grant's ref, each a 409 that writes nothing;
    • an overdraw;
    • two deductions racing;
    • the grants list, with and without ?user_id=.
  • Mutations: 36 of 36 caught, each restored from a sha256-checked copy. One moves the balance check to a read before the insert, and the race test catches it.

Risks

  • What ships: Worker only. No migration, no binding, no wrangler.toml change.
  • Behaviour change: a plain admin who could call the route gets 403. No screen calls it today: api.perkGrantCredits exists and nothing uses it.
  • Callers must now send reason.
  • Rollback: revert the PR.

Dependencies

D540 (#1103), and the grant service. #1165 area 7 item 2 (the screen's design) is separate.

Agent

S08 · Claude Code

Review requested

S1.

🤖 Generated with Claude Code

https://claude.ai/code/session_016Q3DcWsMTSxPxJa7tMeDAs


Generated by Claude Code

…h the grant service (#1308)

POST /perks/admin/credits wrote the ledger itself: any admin, no reason,
no audit row, no notice, a raw index error on a reused source, and rows the
grants list never showed. It now calls manualAdjust in creditGrants.ts:
Super Admin only, a reason of at least 10 characters in the audit row, a
note of at most 500 the member sees, idempotent on source_ref (409
already_recorded naming the row), no overdraw (400 would_overdraw with the
balance), both tests inside the insert, one logAdminAction row and one
inbox notice. Manual lines carry rule_key 'manual' and show in the Super
Admin's grants list, marked manual. No migration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016Q3DcWsMTSxPxJa7tMeDAs
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Preview: https://studioos-pr-1321.guillaumelauzier.workers.dev (built from e1735b5)

The pull request's SPA build on a Worker with no bindings: pages and deep links work, /api/* is a 404, and nothing here can reach production data. Redeployed on every push; deleted when the PR closes.

@guillaumelauzier
guillaumelauzier marked this pull request as ready for review October 6, 2026 23:19
@guillaumelauzier
guillaumelauzier merged commit 44b7b22 into main Oct 6, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Credits: an admin's manual grant or deduction goes through the grant service, Super Admin only, with a reason and an audit row (D608)

2 participants