2.9 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,
unfundedgating, delivery receipts on merge. - Eval farm front-end (option 2): batch task intake -> internal
eval-taskissues, results table, emitter (webhook or file drop).
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)
- client with balance below the debit floor is unfunded: the dispatcher must never claim
its issues (gate =
unfundedlabel skipped by the lane survey)
Gating model (no dispatcher state)
The dispatcher scans all repos under the owner. Intake creates issues carrying the
unfunded label; payment removes it and applies the tier's priority label. The lane survey
already skips a known label set — unfunded joins it. Zero-balance re-locks by re-adding
unfunded on the next delivery check. No new dispatcher machinery, no shared DB reads.
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 + issues + unlock
intake_eval.py # option 2: eval batches -> issues + results + emitter
delivery.py # merge event -> receipt debit + comment + relock 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 front-ends)
- SAAS_DB_PATH, SAAS_API_KEY (X-API-Key auth), SAAS_PORT
Tier/rate config (operator sets later; centralize in config.py)
Enterprise default: priority p1, auto-merge on green (opt-in per client), rate per issue. Rates are 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.