gitea-saas-core/ARCHITECTURE.md

4.7 KiB

gitea-saas-core — Monetization Core

Two products, one billing core. The lane system (issue -> branch -> PR -> review -> merge) is the delivery machine; this service is the client-facing moat around it.

Roles

  • Billing core (shared): clients, ledger, holds, Stripe top-ups, credits. Pure Python + SQLite. No knowledge of lanes or Gitea.
  • Factory front-end (option 1): client repo provisioning, issue intake with acceptance criteria, funding gate at intake, delivery receipts on merge.
  • Eval farm front-end (option 2): batch task intake -> internal eval-task issues, results table, emitter (webhook or file drop).
  • Review gate (shared, THE quality moat): automated PR reviewer. Parses the linked issue's acceptance criteria, runs tests/build/diff checks, posts APPROVE/BOUNCE verdicts, auto-merges per client tier policy. Replaces the operator's manual review for client work.

Billing invariants (fail-closed)

  • balance = credits - debits - active holds (never negative)
  • every mutation is idempotent via an idempotency_key (Stripe event id, intake uuid, etc.)
  • holds expire (default 30 min) and release back automatically
  • debit happens exactly once per delivered issue (key = gitea repo + issue number + merge sha)

Gating model (intake-side, no dispatcher coupling)

A client's work NEVER becomes a Gitea issue while unfunded. submit_issue checks the client balance: funded -> issue created immediately; unfunded -> row in pending_issues queued locally. unlock(client_id) flushes the queue to Gitea with the tier's priority label. The dispatcher scans all repos but only ever sees funded work — zero changes to the lane/dispatcher scripts, no label-gating in the survey, no coupling between the monetization service and deployment infrastructure.

Review gate

reviewer.py polls PRs (or is webhook-driven) that carry in-review. For each:

  1. Fetch linked issue, parse the acceptance-criteria checklist.
  2. Checkout head in a worktree; run the repo's test command + build + git diff --check.
  3. Optional secret scan (reuse worker-ops patterns).
  4. LLM review pass against the acceptance criteria (files allowed per issue spec, scope-creep check, correctness, edge cases) — evidence-quoted verdict.
  5. Post verdict comment: APPROVE / BOUNCE (with findings) / NEEDS-GUIDANCE (cannot verify).
  6. Policy per client tier (config): auto-merge on APPROVE+green for gold tier; manual confirm for standard tier; BOUNCE routes back to the implementing lane.

The review gate applies to client repos; the operator's internal fleet can opt in per repo. Bounced PRs re-enter the normal lane flow (existing bounced label machinery).

Module ownership: gitea_saas/gitea_client.py

gitea_saas/gitea_client.py (the Gitea API client seam) is owned by issue #4 (factory intake) and lands once, from #4's PR. Every other front-end — review gate, eval farm, dashboard, delivery — imports that module; new Gitea API surface is added by extending it under #4's scope, never by writing another client. Open issues whose allowed-files set grants gitea_saas/gitea_client.py to any issue other than #4 are scoping errors (issue #21 owns this rule).

Layout (target)

gitea_saas/
  billing_core.py      # ledger schema + operations (pure, no HTTP)
  server.py            # Flask app (billing API + dashboards)
  stripe_client.py     # Stripe adapter (mockable seam, env keys)
  gitea_client.py      # Gitea API client (mockable seam, env token)
  intake_factory.py    # option 1: provision client repo + funded issue creation
  intake_eval.py       # option 2: eval batches -> issues + results + emitter
  reviewer.py          # review gate: acceptance parse, tests, verdicts, policy
  delivery.py          # merge event -> receipt debit + comment + lock-check
  config.py            # tier config, rates (loaded from env/config, never hardcoded)
  schema.sql / db.py   # SQLite migration
tests/                 # pytest, hermetic (mock Stripe + Gitea, tmp SQLite)
dashboard/             # html/static if needed (mirror botmanager pattern)

Environment (provided by operator at deploy time, mocked in CI)

  • STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET (optional: absent -> mock mode)
  • GITEA_BASE_URL / GITEA_TOKEN (factory/eval/reviewer)
  • SAAS_DB_PATH, SAAS_API_KEY (X-API-Key auth), SAAS_PORT

Tier/rate config (operator sets later; centralize in config.py)

Gold: priority p1, auto-merge on reviewer APPROVE+green. Standard: p2, manual merge confirm. Rates per issue, config not code. First client onboarding is manual; self-serve (Stripe checkout -> auto-provision) is the stated next step after core + both front-ends land.