# 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.