Gunnar

nordisk/monetize

DESIGN-PRIVATE-PAY-ONCE.md

<!-- nornir:class: design -->
# A private Gunnar is paid at Stripe ONCE

Owner order, 2026-10-22: *"ja! så måste det vara! dokumentera gärna"* — a visitor who buys
a PRIVATE Gunnar instance meets Stripe once.

## Before (to 2026-10-22)

A visitor on `/signup?plan=private` went to Stripe twice:

1. the free account's 0 kr card check (`/auth/signup/start`, plan `gunnar/account`, Stripe
   `mode=setup`) → welcome → passkey → signed in;
2. back on the private choice: `/auth/private/ladder` → `/auth/private/start`
   (`EstateOrder.Start` with the signed-in account's `estate` ticket) → Stripe payment →
   `/auth/private/status`.

## Now

```text
  visitor on the private choice: e-mail + account name + company (+ domain, size)
     │  POST /auth/private/start  (gunnar-ui: nobody signed in → new_account, welcome_url)
     ▼
  EstateOrder.Start{new_account}         monetize: the name is free here, the address has
     │                                   no account here, the product has an account plan;
     │                                   the name is held while the payment page runs
     ▼
  Stripe: the private Gunnar's payment   (the ONE vendor page)
     ▼
  tick: ORDERED → paid                   monetize makes the ACCOUNT now, as a paid signup
     │                                   does (signup sink; a pulled product: "<site>'s main
     │                                   creates <user> when it next calls monetize"), and
     │                                   mails its sign-in link (welcome_url + session)
     ▼
  HELD → laid → installed → ready mail   the estate order is that account's (run.account)
  sign-in link → /welcome → passkey → signed in → /signup?plan=private&company=<co>
```

* **The 0 kr card check stays ONLY for the free account.** A private buyer pays; the
  payment is the proof of a real person.
* **A signed-in account keeps today's path**: its `estate` ticket proves it, nothing new.
* **Where it lives**: monetize. `EstateOrder.Start` takes `new_account = true` with no
  ticket and a `welcome_url` (`https://<site>/welcome?session={session}…`, on the order's
  own site). The run keeps the account to make (`EstateRun.new_account`: name, address,
  plan, session `acct_…`); `paid` makes it once (idempotent by its session) through the
  same `make_account` the free account and the test button use — one record under
  `account`, the sign-in mail, a pending one settled by `settle_pending`.
* **Refusals before any money**: a taken or held name, an address that already has an
  account here ("sign in to it instead"), a welcome page off the site, a product with no
  account plan. A name taken anyway after payment parks the paid order with an ALERT for
  the operator; nothing is laid for a stranger's account.
* **gunnar-ui stays thin**: `/auth/private/start` sends what the visitor typed and
  monetize's answer comes back; the page no longer makes the free account first on the
  private choice. gunnar-server is not touched.

## The buy form (owner additions, 2026-10-22)

On `/signup?plan=private`:

1. **One heading**: the private choice's heading is "New private Gunnar". The choice row
   stays (signed in: "New instance" / "Grow <company>'s Gunnar" / "Buy a plan" or "Grow your
   account"; a visitor: "Account on <host>" / "Private Gunnar instance") — real paths.
2. **The button says what is paid**: "Pay <amount>", the chosen step's amount as monetize
   words it (`EstateLadderStep.pay_words`, e.g. "Pay 3.21 EUR"). The other buttons stay.
3. **The e-mail is not changed in the buy** ("man skall inte kunna byta email"): signed in,
   there is no e-mail field; monetize sends the order's mail to the address the account was
   made with (the signup record it holds), whatever a page sends. An account monetize holds no
   address for is refused by name before any money. A visitor types the e-mail once: it is the
   new account's address and the order's.
4. **The account line is short**: "x14 (you)".

Kept: the company and its address preview, the machines, the VPN line.