Commit Graph

4 Commits

Author SHA1 Message Date
hatiyildiz
7edf63ca7e docs(franchise),test(billing): voucher CRD propagation invariant
#118 verifies that the voucher shape on a franchised Sovereign is
identical to Catalyst-Zero. Two artefacts:

1. New §"Voucher shape propagates automatically" in
   docs/FRANCHISE-MODEL.md explaining WHY there is no propagation
   problem to solve: vouchers are not a CRD. They are rows in the
   per-Sovereign billing service's Postgres database, and every
   Sovereign runs the same SHA-pinned core/services/billing image.
   Same image → same migration → same schema → same handlers → same
   shape. The doc lists which file owns each part of the shape and
   includes a 4-step curl smoke test to run on any Sovereign at
   first-provisioning to confirm the invariant holds.

2. New core/services/billing/handlers/vouchers_test.go covering the
   public POST /billing/vouchers/redeem-preview endpoint added in
   #117. Four cases:
   - 404 on unknown / soft-deleted code (no tombstone leak)
   - 200 on a valid live code, asserting the public shape excludes
     times_redeemed and max_redemptions (defence-in-depth against
     enumeration)
   - 410 Gone on a code that exists but has hit its cap, with the
     credit/description still in the response so the landing page can
     show "campaign ended"
   - 400 on whitespace-only input

The tests run on every CI build of the billing service, on every
Sovereign that builds from this repo. If a future change drifts the
preview endpoint's shape, the tests fail before the regression can
ship.

Also tidies vouchers.go imports (removed two unused stdlib imports
that were placeholder).

Closes #118.
2026-04-28 13:59:31 +02:00
hatiyildiz
12387a4a74 feat(billing): /billing/vouchers/{issue,list,revoke,redeem-preview} surface
#117 adds a franchise-aligned URL surface for the existing PromoCode
voucher implementation, plus one new endpoint (redeem-preview) for the
public landing flow described in docs/FRANCHISE-MODEL.md §3.

The orchestrator's hint was right — the issue/list/revoke handlers
already exist (AdminUpsertPromo / AdminListPromos / AdminDeletePromo
on the legacy /billing/admin/promos surface). This commit:

1. Adds new endpoint handlers in core/services/billing/handlers/vouchers.go:
   - POST   /billing/vouchers/issue          (superadmin or sovereign-admin)
   - GET    /billing/vouchers/list           (superadmin or sovereign-admin)
   - DELETE /billing/vouchers/revoke/{code}  (superadmin or sovereign-admin)
   - POST   /billing/vouchers/redeem-preview (unauthenticated; public)

   The first three reuse the existing store-layer methods. The last is
   new — it validates a code without consuming it, returning a safe
   shape (no times_redeemed, no max_redemptions exposure) so an
   attacker scraping the public endpoint cannot enumerate cap status.

2. Distinguishes 404 (code never existed or soft-deleted — same
   tombstone-leak protection as #91) from 410 Gone (code exists but is
   inactive or capped). The 410 body still includes the credit and
   description so the landing page can show "this campaign has ended".

3. Keeps the legacy /billing/admin/promos endpoints in place — the
   existing admin UI continues to work without any breaking change.
   New code should target /billing/vouchers/...

4. Updates docs/FRANCHISE-MODEL.md to point to the new URL surface.

The actual REDEMPTION still happens transactionally inside POST
/billing/checkout via the `promo_code` field — that path locks the
promo row, inserts the promo_redemptions edge, increments
times_redeemed, and adds the credit_ledger entry in one transaction.
Splitting it into a separate /redeem endpoint would break that
atomicity, so we deliberately do not add one. The public redeem flow
is preview → signup → checkout-with-promo_code.

Closes #117.
2026-04-28 13:54:19 +02:00
hatiyildiz
6d539b906b docs(franchise): align FRANCHISE-MODEL.md with actual implementation
#114 verification of FRANCHISE-MODEL.md (committed at 9dfa4c8) against the
real code in core/admin and core/services/billing. Two drifts found and
fixed:

1. API endpoint paths were aspirational (/v1/admin/promos, /v1/redeem) but
   the implementation has /billing/admin/promos and a customer-side
   /billing/checkout with a promo_code field. Doc now matches code.
2. Auth flow described as "Keycloak signup" but marketplace today uses
   magic-link + Google OAuth (Keycloak is the documented design target,
   not the current implementation). Doc now reflects current auth.

Also expanded the PromoCode schema table to include the soft-delete
(deleted_at) column from #91 and the times_redeemed counter, plus a
note that the term "Voucher" in this document is the user-facing label
for the same row the code calls PromoCode.

Closes the #114 verification scope: doc reflects the code as of this commit.
2026-04-28 13:47:43 +02:00
hatiyildiz
9dfa4c8680 docs(franchise): canonical FRANCHISE-MODEL.md sourced from existing admin impl + plan status update
Per docs/PROVISIONING-PLAN.md ticket [H] franchise. Documents the franchise + voucher model exactly as it exists today (PromoCode CRUD in core/admin, BHD credit-based vouchers, public /v1/redeem endpoint that triggers Organization auto-creation). No new CRD designed — this captures what's already deployed.

docs/FRANCHISE-MODEL.md:
- Chain of responsibility: OpenOva → Catalyst → Catalyst-Zero (Contabo) → omantel.omani.works (franchised) → omantel-issued vouchers → tenant Orgs
- Voucher = PromoCode CRUD: code, credit_omr, description, active, max_redemptions
- API endpoints: GET/POST/PUT/DELETE /v1/admin/promos (org-admin or sovereign-admin), POST /v1/redeem (public, rate-limited)
- 5-step redemption flow: issuance → distribution → signup → install drawdown → revenue split
- What franchisees CAN/CANNOT do (Kyverno admission policies enforce signed-Blueprint constraints)
- Cross-Sovereign tenancy + Org migration between Sovereigns
- Deferred items (voucher CRD lift, cross-Sovereign voucher, percentage-discount tiers)

docs/PROVISIONING-PLAN.md:
- Adds "Execution status (live)" table tracking groups A-M
- 6 groups now in 🚧 active status with commit references
- 1 group (F charts) flipped to 
- 1 group (A consolidation) flipped to 
- DoD (group M) gated on operator-provided Hetzner credentials + first blueprint-release CI runs landing the 11 OCI artifacts at ghcr.io/openova-io/bp-*

Closes [H] tickets: docs/FRANCHISE-MODEL.md authored, voucher CRD shape documented (lift to CRD deferred), what-franchisees-can/cannot rules enumerated.
2026-04-28 12:54:10 +02:00