openova/core/marketplace
hatiyildiz 9404632830 feat(marketplace): public /redeem?code=... voucher landing flow
#116 adds the public landing page that the franchise model relies on
to convert voucher distribution into Catalyst signups (per
docs/FRANCHISE-MODEL.md §3, "redemption flow end-to-end").

New page core/marketplace/src/pages/redeem.astro:

- Reads ?code=... from the URL (or accepts manual entry if absent).
- POSTs to /api/billing/vouchers/redeem-preview (added in #117) — does
  NOT consume the voucher, just validates it.
- Renders one of four states:
  * Valid (200): "X OMR credit" + description + "Sign up to redeem"
    CTA. The CTA stashes the code in localStorage under
    `sme-pending-voucher` and routes to /plans (the start of the
    existing signup wizard).
  * Campaign ended (410): inactive or capped — shows the credit that
    was offered + a path to sign up without a voucher.
  * Not valid (404): never existed or soft-deleted (#91 tombstone-leak
    protection — the two are indistinguishable on the public surface).
  * No code present: a manual input form so a redeemer who landed on
    /redeem without a query string can paste their code.

CheckoutStep wiring (core/marketplace/src/components/CheckoutStep.svelte):

- The `promoCode` $state now hydrates from `sme-pending-voucher` so a
  redeemer arriving via /redeem reaches /checkout with the field
  pre-filled. They can still edit or clear it.
- After submitting to /billing/checkout, we clear the localStorage
  stash. This prevents a second signup on the same browser from
  silently carrying over the previous voucher.

The actual redemption (insert into promo_redemptions, increment
times_redeemed, credit_ledger entry) still happens transactionally
inside POST /billing/checkout — splitting it out would risk a
partially-redeemed code with no Order to show for it (the same
class of bug #91 fixed).

Per docs/INVIOLABLE-PRINCIPLES.md §1: target-state shape, not MVP.
The page handles all four observable backend states; manual-entry
fallback is included; the "campaign ended" path keeps the user moving
into signup rather than dead-ending.

Closes #116.
2026-04-28 13:56:54 +02:00
..
public feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
src feat(marketplace): public /redeem?code=... voucher landing flow 2026-04-28 13:56:54 +02:00
astro.config.mjs feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
Containerfile feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
nginx.conf feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
package-lock.json feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
package.json feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00
tsconfig.json feat(consolidation): Phase 1 — move Catalyst-Zero apps + CI + manifests into public monorepo 2026-04-28 12:08:09 +02:00