Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-server/src/signup.rs

//! **The engine half of the paid-signup seam**: the store that makes a webhook
//! idempotent, the checks that turn a stranger's assertions into a named
//! refusal, and the open default sink.
//!
//! ```text
//!   POST /callback/<vendor>          the vendor's wire (callback.rs)
//!        │ parse_callback VERIFIED it and said Signup
//!        ▼
//!   record_pending   (vendor, session) ─── already there? ──▶ answer what it says, call nothing
//!        │ fresh, or a PENDING one being retried
//!        ▼
//!   check            username · plan · amount        ──▶ Refused { reason }
//!        │ all three agree with the config
//!        ▼
//!   SignupSink::paid                                 ──▶ Created { namespace, username }
//!        │                                           ──▶ Unavailable: stays PENDING, tried again
//!        ▼
//!   store.set        one functional_status line per state change
//! ```
//!
//! # Why the store and not the ledger
//!
//! The ledger's key is a REFERENCE — `<product>/<namespace>/<period>` — and it
//! is keyed that way because every row in it is about a namespace that exists.
//! A signup has no namespace until the sink has run, which is the event being
//! recorded. Writing one as a ledger attempt would mean inventing the tenant id
//! before the thing that mints it has answered, and then living with whatever
//! the sink actually called them.
//!
//! So: a second redb file, `signups.redb`, keyed the only way the two sides of
//! the redirect agree on — `(vendor, session)`. That key is the vendor's, not
//! ours, which is exactly what makes the return page able to ask before we have
//! heard anything at all.
//!
//! # The three answers a webhook can produce, and the one status code
//!
//! **Every verified signup is answered 200**, refusals included. A 5xx makes
//! Stripe retry for days and then disable the endpoint (measured 2026-09-07,
//! `4cafd1d`), and a refusal is not a delivery failure: the delivery worked
//! perfectly and the answer is "no". What became of it is read over
//! `Signup.Status`, by the party that cares — the customer's return page and
//! the operator's console.

use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::{Arc, Mutex};

use monetize_pay::signup::{NameVerdict, PaidSignup, SignupError, SignupOutcome, SignupSink};
use redb::{Database, ReadableTable, ReadableTableMetadata, TableDefinition};

use crate::config::SignupPlan;
use crate::store::StoreError;

/// `(vendor, session) → SignupRecord` as JSON. One table: the store answers two
/// questions ("what became of this session" and "show me all of them") and both
/// are this table read two ways.
const SIGNUPS: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("signups");

/// `(vendor, session) → SignupRefund` as JSON: **what an operator did about a
/// row's money** (`SignupAdmin.Refund` / `Settle`). A second table and not a
/// field on the record, because the record is what the VENDOR's callback said
/// and the checks made of it, and a refund is a later act by somebody else —
/// the two are written by different verbs, and a restore of one must not
/// rewrite the other. Same file, same key as [`SIGNUPS`].
const REFUNDS: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("refunds");
/// **The checkout sessions THIS box minted** (STINKY ST-7): `(vendor, session)` → the estate
/// the signup began on (empty: none named). A verified webhook — or a session the reconciler
/// finds paid at the vendor — for a session not in here is another system's on the same
/// merchant account, and is never settled here.
const MINTED: TableDefinition<(&str, &str), &str> = TableDefinition::new("minted");
/// ★ **The card checks this box minted for a free account** (owner 2026-10-15), keyed
/// `(vendor instance, session)`: what the account will be once the card is checked
/// ([`CardCheckStarted`], JSON). Not a payment and never one: nothing is charged, so a
/// card check is never a [`SignupRecord`] until it has made the account.
const CARD_CHECKS: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("card_checks");
/// ★ **The welcome page a paid signup this box minted returns to** (A11 fix 7), keyed
/// `(vendor, session)` → the page with `{session}` in it: the sign-in link mailed once the
/// account is made.
const WELCOMES: TableDefinition<(&str, &str), &str> = TableDefinition::new("welcomes");
/// ★ **The names held while their vendor page runs** (owner 2026-10-15, B), keyed
/// `("<product>\u{0}<site>", username)` → [`NameHold`] (JSON): a name being paid for (or
/// card-checked) is nobody else's until the page is done, closed, or [`NAME_HOLD_MS`] is up.
const NAME_HOLDS: TableDefinition<(&str, &str), &[u8]> = TableDefinition::new("name_holds");

/// ★ How long a name is held for a vendor page: the page's own life (the Stripe guest
/// sets `expires_at` = 30 min on every signup and card-check page) and a margin.
pub const NAME_HOLD_MS: i64 = 35 * 60 * 1000;

/// ★ A name held for one vendor page ([`NAME_HOLDS`]).
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct NameHold {
    pub vendor: String,
    pub session: String,
    /// The address the page was opened for: the same buyer starting again is not shut out by
    /// their own page.
    pub email: String,
    pub until_unix_ms: i64,
}

/// **How long a PENDING signup may stay pending before it becomes a refusal.**
///
/// 24 hours. A sink that has answered `Unavailable` for a day is not having a
/// blip; something is broken that nobody is watching, and a row that says
/// PENDING forever is a customer whose card was charged sitting on a spinner
/// with no words. Turning it into a REFUSED with a named reason at least puts
/// it in front of an operator and gives the return page something to say.
pub const PENDING_CEILING_MS: i64 = 24 * 60 * 60 * 1000;

/// The reason a signup that timed out carries. A constant because the console
/// and the test both quote it, and two spellings of one refusal is two bugs.
/// OPERATOR words: the customer reads [`words::COULD_NOT_CREATE`] instead.
pub const NEVER_ANSWERED: &str = "sink never answered";

/// **What a paying customer is told**, one constant per situation.
///
/// Kept apart from every `reason` in this file because the two have different
/// readers. A reason is for the operator and says what the product said —
/// which, for gunnar, can be `Unauthenticated: the credential is 412000 ms away
/// from this server's clock`. A customer who has just paid must never read
/// that, and must always read whether they need to pay again (they do not).
/// The operator's copy goes to the log, to `functional_status` and to
/// `SignupAdmin.List`; these go to `Signup.Status` and nowhere else.
pub mod words {
    /// ★ PENDING, settling (PAID-WORDS, owner's real-card test 2026-10-26): the first seconds,
    /// an account queued for the product's main, or any "not now" that is not the product
    /// saying it is in maintenance (a timeout, a box that does not answer, a clock that will
    /// agree again). Within [`super::SETTLE_BOUND_MS`].
    pub const BEING_CREATED: &str = "Payment received. Setting up your account; this takes a few seconds.";
    /// ★ PENDING past [`super::SETTLE_BOUND_MS`], where the account's sign-in link is mailed
    /// once it is made (a welcome page was minted for the payment).
    pub const TAKING_LONGER_MAILED: &str =
        "Payment received. This is taking longer than usual; your payment is safe, and we will e-mail you the sign-in link as soon as your account is ready — you do not need to pay again.";
    /// ★ PENDING past [`super::SETTLE_BOUND_MS`], with no mail promised (none is sent here).
    pub const TAKING_LONGER: &str =
        "Payment received. This is taking longer than usual; your payment is safe, and this page finishes on its own when your account is ready — you do not need to pay again.";
    /// ★ A rename after paying that the product could not answer just now.
    pub const NOT_ANSWERING_PAID: &str = "Payment received. That name could not be checked just now; try again in a moment — you do not need to pay again.";
    /// PENDING because the PRODUCT said, by name, that it is in maintenance
    /// ([`monetize_pay::signup::SignupError::Maintenance`]). Never for a generic "not now".
    pub const MAINTENANCE: &str =
        "Payment received. The shop is temporarily down for maintenance; your account will be ready as soon as it is back — you do not need to pay again.";
    /// CREATED.
    pub const READY: &str = "Payment received. Your account is ready.";
    /// REFUSED by the product, or pending past the ceiling: the words that
    /// replace a reason nobody outside the operation should read.
    pub const COULD_NOT_CREATE: &str =
        "Payment received, but your account could not be created automatically. Please contact us and we will set it right — you do not need to pay again.";
    /// A name check or a checkout that could not be made because the product
    /// or the vendor is not answering. No money has moved.
    /// Not "maintenance" (PAID-WORDS): a generic "not now" is never called that.
    pub const SHOP_DOWN: &str = "The shop is not answering right now. Nothing has been charged; please try again in a little while.";
    /// A signup for an address that already has an account here (PLAN §3c,
    /// OM-19 (4)): refused, offering sign-in — never naming the account, and
    /// never merged into it. No money has moved.
    pub const EMAIL_HAS_ACCOUNT: &str = "This email address already has an account here. Sign in to it instead — nothing has been charged.";
    /// ★ **One free account per card per site** (owner 2026-10-15): the card checked at the
    /// vendor already has a free account on this site. Never names the account.
    pub const CARD_HAS_ACCOUNT: &str = "This card already has a free account here — sign in with it, or choose a plan. Nothing has been charged.";
    /// ★ The card-check page closed with no card checked.
    pub const CARD_NOT_CHECKED: &str = "The card check was not finished, so no account was made. Nothing has been charged — start again to make your free account.";
    /// ★ Back from the vendor, the card not checked yet (3-D Secure still at the bank).
    pub const CARD_BEING_CHECKED: &str = "Your card is being checked. Nothing is charged.";
    /// ★ A site whose vendor cannot check a card makes no free account.
    pub const CARD_CHECK_NOT_HERE: &str = "A free account cannot be made here right now. Nothing has been charged.";
    /// ★ The name is held for somebody else's open payment page (owner 2026-10-15, B).
    pub const NAME_BEING_BOUGHT: &str = "Someone is signing up with this account name right now. Choose another name, or try again in half an hour.";
    /// ★ Back from the vendor, the payment not there yet.
    pub const PAYMENT_WAITING: &str = "Waiting for your payment to be confirmed. Nothing more is needed from you.";
    /// ★ The payment page closed unpaid: no account, nothing charged.
    pub const PAYMENT_NOT_FINISHED: &str = "The payment was not finished, so no account was made. Nothing has been charged — start again to sign up.";
    /// ★ **THE SECOND MAINTENANCE LIE, and the reason it outlived the first.**
    ///
    /// This constant used to read "…right now while we carry out maintenance",
    /// and its own doc justified that by pointing at
    /// `monetize::transaction::words` — which said the same thing about the
    /// same condition. That sibling stopped saying it on 2026-09-20: the
    /// capacity refusals there are now COMPOSED from what was measured, and the
    /// word "maintenance" is held out of them by a test. So the justification
    /// this constant rested on no longer exists, and the sentence outlived the
    /// argument for it by one repository.
    ///
    /// It is the worse of the two, because a buyer told "maintenance" waits.
    /// Maintenance ends by itself; a shop with no room left does not. A
    /// customer who is never told that somebody has to act will wait for a
    /// state that will never arrive, and will not ask — which is precisely the
    /// message that would fix it.
    ///
    /// # Why this one is not composed from numbers
    ///
    /// Its sibling can name what is left because a raise measures it. Signup
    /// reaches here from two doors — `can_absorb` answering no, and the
    /// oversell ceiling — and one of them is a POLICY, not a measurement, so a
    /// figure would be true of one caller and invented for the other. It says
    /// what is true of both instead, and says who acts next.
    ///
    /// [`NEVER_SHOWN`] still holds: no meter, no device, no word from the
    /// machine room.
    ///
    /// **Round 5.3** (owner 2026-10-08: "the customer must not know about a
    /// cap!"): on an estate whose set grows, a signup past the room is SOLD
    /// and QUEUED for the resize and never reaches this sentence; it is left
    /// for a box that is never resized, and names no room left and no cap.
    pub const NO_ROOM: &str =
        "We cannot create an account of this size here right now. Nothing has been charged. Please ask us and we \
         will set it right, or choose a smaller size.";

    /// Every infrastructure word a customer sentence must not contain. The
    /// tests hold every constant above, and every sentence `Signup.Status`
    /// builds, to it.
    pub const NEVER_SHOWN: &[&str] =
        &["clock", "skew", "credential", "unauthenticated", "ms away", "grpc", "sink", "disk", "filesystem", "data set", "capacity"];
}

/// Where a signup got to. The wire's `SignupStatusResponse.State`, minus
/// `UNKNOWN` — which is not a state a record can be in, it is the absence of a
/// record, and giving it a variant here would make "we never heard of this" a
/// thing that could be STORED.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum SignupState {
    /// Recorded, not yet decided: the sink has not answered, or answered
    /// [`SignupError::Unavailable`] and will be asked again.
    Pending,
    /// The customer exists.
    Created { namespace: String, username: String, at_unix_ms: i64 },
    /// The customer will not be created, and this is why, in words a human is
    /// meant to read.
    Refused {
        /// The OPERATOR's reason: what the checks or the product said.
        reason: String,
        at_unix_ms: i64,
        /// **What the customer reads instead of `reason`**, when `reason` came
        /// from somewhere whose words are not for a customer — the product's
        /// refusal, or the pending ceiling. `None` means `reason` was written
        /// for them: a name refusal, or an order refusal the checks above made.
        ///
        /// `default` and skipped when empty, so rows written before the field
        /// read back and a backup of them is byte-identical to before.
        #[serde(default, skip_serializing_if = "Option::is_none")]
        words: Option<String>,
        /// **Whether another name would fix it**, as a value and not as a
        /// substring of `reason`. See [`Refusal`].
        ///
        /// `#[serde(default)]` because this table already holds rows written
        /// before the field existed, and a store that would not parse its own
        /// history would lose the record of a payment. Those rows read as
        /// [`Refusal::Unknown`] and are not renameable — guessing that an old
        /// refusal was about the name would re-run a sink for a customer whose
        /// money may have gone somewhere else entirely.
        #[serde(default)]
        refusal: Refusal,
        /// **What a human must now do about the money.** See [`Owed`]. Written
        /// at the moment of the refusal, because only the code that made it
        /// knows whether the sink ever ran; `#[serde(default)]` reads every row
        /// written before 2026-09-18 as [`Owed::Unrecorded`], which is the safe
        /// wrong answer — it sends somebody to look.
        #[serde(default)]
        owed: Owed,
    },
}

/// **Money taken, account not created: what a human must do about it.**
///
/// Every row in the signup store is a payment that has already landed — the
/// webhook is delivered by the vendor *after* the card cleared — so a REFUSED
/// row is, without exception, a customer who has been charged and has no
/// account. Until 2026-09-18 that fact lived nowhere but in the prose of
/// `reason`: [`tax_sales`] declared only the CREATED rows and its comment said
/// the rest was "owed back by hand", and nothing anywhere counted the hands.
///
/// The arms are not four spellings of "refund". They differ in what is left on
/// the appliance, and an operator's next step differs with them:
///
/// * the checks refuse BEFORE the sink runs (`crate::signup::refusal`), so
///   nothing was created and the money is the whole of it;
/// * a sink that refuses has already created the principal, the namespace and
///   the username at its step (b) — `monetize-signup-impl` pushes the
///   entitlement at step (c) — so the NAME is spent: nothing here purges it
///   (nonpay purges only a customer who stopped paying), and a second
///   purchase of the same name is refused `NameTaken`;
/// * a name refusal is the one the CUSTOMER can still finish, with
///   `Signup.Rename`, against the money that has already moved.
///
/// This type records the fact and names the job. The money goes back through
/// [`refund`] (`SignupAdmin.Refund`, `monetize-server --refund`, 2026-09-28
/// g-refund-verb), which asks the row's own vendor and records it in
/// [`SignupRefund`]; a row refunded by hand at the vendor is closed with
/// [`settle_by_hand`]. Either takes the row off [`SignupStore::owed_back`].
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum Owed {
    /// **A row written before this field existed** — money moved, no account,
    /// and which of the arms below it is was never written down. Not folded
    /// into any of them: guessing that an old row's sink never ran would tell
    /// an operator that a name is free when the appliance is holding it.
    #[default]
    Unrecorded,
    /// **Refund, and nothing else.** The checks refused before the sink was
    /// called: no principal, no namespace, no name spent.
    Refund,
    /// **Refund, and the name may be spent.** The sink ran and did not answer
    /// `Created` — it refused, or it stopped answering altogether — and it
    /// creates the principal before it pushes the entitlement. Nothing here
    /// knows how far it got, so the appliance has to be asked.
    RefundAndTheNameMayBeSpent,
    /// **The customer can still finish this one**, with another name, against
    /// the money already taken (`Signup.Rename`). It is owed back only if they
    /// never do — which is why it is counted with the rest rather than treated
    /// as settled.
    RenameOrRefund,
}

impl Owed {
    /// `unrecorded` | `refund` | `refund_and_the_name_may_be_spent` |
    /// `rename_or_refund` — the wire and log spelling, one writer.
    pub fn name(self) -> &'static str {
        match self {
            Owed::Unrecorded => "unrecorded",
            Owed::Refund => "refund",
            Owed::RefundAndTheNameMayBeSpent => "refund_and_the_name_may_be_spent",
            Owed::RenameOrRefund => "rename_or_refund",
        }
    }

    /// **The job, in words, for the operator who reads the queue.** One writer,
    /// because a console and a log line that described this differently would
    /// be two different instructions for one piece of somebody's money.
    pub fn remedy(self) -> &'static str {
        match self {
            Owed::Unrecorded => {
                "Refund this payment (SignupAdmin.Refund; `monetize-server --refund <vendor>:<session>`), and ASK THE APPLIANCE whether the username exists: this row \
                 was written before monetize recorded how far the signup got, so whether a principal was created is unknown."
            }
            Owed::Refund => {
                "Refund this payment (SignupAdmin.Refund; `monetize-server --refund <vendor>:<session>`). Nothing was created for it: \
                 the checks refused before the account was made."
            }
            Owed::RefundAndTheNameMayBeSpent => {
                "Refund this payment (SignupAdmin.Refund; `monetize-server --refund <vendor>:<session>`), AND deal with the name: the account creation had already begun, so the \
                 principal, the namespace and the username may be on the appliance for good. Nothing purges them automatically, and \
                 a second purchase of the same name will be refused as taken."
            }
            Owed::RenameOrRefund => {
                "Ask the customer for another username (Signup.Rename finishes the signup against the money already taken); \
                 refund this payment (SignupAdmin.Refund; `monetize-server --refund <vendor>:<session>`) if they will not take one."
            }
        }
    }
}

/// **What an operator has done about a signup's money** — the [`REFUNDS`] row.
///
/// Written in two steps around the vendor call, and that order is the whole
/// point: [`SignupRefund::Requested`] is committed BEFORE the vendor is asked,
/// [`SignupRefund::Done`] after it said yes. A crash (or a lost reply) between
/// the two leaves `Requested`, and the next [`refund`] asks the vendor AGAIN
/// with the RECORDED amount — the same idempotency key
/// ([`refund_key`], Stripe's `<handle>:refund:<amount>`) — so the vendor
/// answers the first refund and gives nothing back twice.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub enum SignupRefund {
    /// Recorded, the vendor about to be (or already) asked; its yes not heard.
    /// **Still owed back**: nobody here knows the money moved.
    Requested {
        amount: monetize_pay::Money,
        reason: String,
        at_unix_ms: i64,
        /// A CREATED signup refunded with `force`.
        #[serde(default)]
        forced: bool,
    },
    /// The vendor refunded it.
    Done {
        /// [`refund_key`]: the key the vendor holds this refund under.
        refund_id: String,
        amount: monetize_pay::Money,
        reason: String,
        requested_at_unix_ms: i64,
        at_unix_ms: i64,
        #[serde(default)]
        forced: bool,
    },
    /// An operator refunded it BY HAND at the vendor and said so, with a note.
    /// No money moved here.
    SettledByHand { note: String, at_unix_ms: i64 },
}

impl SignupRefund {
    /// **Is the money back?** `Done` or settled by hand; a `Requested` one is
    /// still owed.
    pub fn settles(&self) -> bool {
        !matches!(self, SignupRefund::Requested { .. })
    }
}

/// **An account at the shop, without room here** (one shop, owner 2026-10-07):
/// a signup plan whose `amount_minor` is 0. It sells no room and asks no
/// vendor — the person gets the account they see and grow what they bought
/// under — so `Signup.Start` makes it at once ([`ACCOUNT_VENDOR`], a session
/// of its own, [`ACCOUNT_SESSION_PREFIX`]). It is never offered as room
/// (`Signup.Plans`), is no sale, and its fact never lapses and holds no room
/// ([`NO_ROOM_CAP`]).
pub const ACCOUNT_VENDOR: &str = "account";

/// **The "vendor" of an account fact the poller GAVE** (2026-10-08): an account that held no
/// fact at a selling shop (an invite's, an operator's, a failed sign-up's) — its source reads
/// `account:given:<reference>`, so its attestation line says it was given, not signed up.
pub const GIVEN_VENDOR: &str = "given";

/// How an account-at-the-shop session id begins: 128 random bits follow, hex.
pub const ACCOUNT_SESSION_PREFIX: &str = "acct_";

/// **The cap an account without room holds**: one byte. Not zero — gunnar
/// reads a zero cap as NO cap (a typo in a plan must never close every write,
/// gunnar-server `entitlement`), and an account here is not unlimited room.
pub const NO_ROOM_CAP: u64 = 1;

/// **A session of the shop's own** for an account at the shop:
/// [`ACCOUNT_SESSION_PREFIX`] + 128 bits from the system's randomness, hex.
/// It is the credential the welcome page and the passkey registration carry,
/// as a vendor's session is for a paid signup — so never guessable.
pub fn account_session() -> Result<String, String> {
    use std::io::Read as _;
    let mut b = [0u8; 16];
    std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut b)).map_err(|e| format!("/dev/urandom: {e}"))?;
    Ok(format!("{ACCOUNT_SESSION_PREFIX}{}", b.iter().map(|x| format!("{x:02x}")).collect::<String>()))
}

/// **Whose session it is**: an account-at-the-shop session is
/// [`ACCOUNT_VENDOR`]'s, whatever vendor a return page names (a console asks
/// with the one it always names); any other is the named vendor's.
pub fn vendor_of<'a>(named: &'a str, session: &str) -> &'a str {
    if session.starts_with(ACCOUNT_SESSION_PREFIX) {
        ACCOUNT_VENDOR
    } else {
        named
    }
}

/// True for an account at the shop: a plan with no price (and no room).
pub fn is_account_plan(plan: &SignupPlan) -> bool {
    plan.amount_minor == 0
}

/// **The idempotency key a refund of `session` for `amount_minor` is sent
/// under** — the one monetize-impl's Stripe builds (`stripe/mod.rs`,
/// `Stripe::refund`: `<handle.id>:refund:<amount>`), and the id
/// `SignupAdmin.Refund` answers. The handle of a signup's payment is its
/// session. Same amount, same key: that is what makes a retry safe, and why a
/// retry re-sends the RECORDED amount and never a newly asked one.
pub fn refund_key(session: &str, amount_minor: u64) -> String {
    format!("{session}:refund:{amount_minor}")
}

/// **What kind of refusal it was** — the value a page branches on, so that no
/// page has to match on prose.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)]
pub enum Refusal {
    /// A row from before this field existed. Not renameable — see
    /// [`SignupState::Refused`].
    #[default]
    Unknown,
    /// **The username was the problem and only the username**: taken, reserved,
    /// or not a name. `Signup.Rename` takes a different one against the money
    /// that has already moved.
    Name,
    /// Final. A plan nobody sells, an amount that is not the plan's price, a
    /// product that refused the entitlement. Nothing the customer can type
    /// changes any of it.
    Final,
}

impl Refusal {
    /// **May a different username finish this signup?** Answered in one place so
    /// the store, the service and the page cannot disagree about it.
    pub fn renameable(self) -> bool {
        self == Refusal::Name
    }
}

impl SignupState {
    pub fn is_pending(&self) -> bool {
        matches!(self, SignupState::Pending)
    }
    /// **What is owed back for this record**, or `None` when nothing is: a
    /// signup that is still pending may yet become an account, and a created one
    /// IS the account. Only a refusal is money taken for nothing — see [`Owed`].
    pub fn owed(&self) -> Option<Owed> {
        match self {
            SignupState::Refused { owed, .. } => Some(*owed),
            SignupState::Pending | SignupState::Created { .. } => None,
        }
    }
    /// When it stopped being pending; `0` while it still is.
    pub fn decided_at_unix_ms(&self) -> i64 {
        match self {
            SignupState::Pending => 0,
            SignupState::Created { at_unix_ms, .. } | SignupState::Refused { at_unix_ms, .. } => *at_unix_ms,
        }
    }
}

/// One row: what the vendor said, and what became of it.
///
/// It keeps the whole [`PaidSignup`] — **including the e-mail** — for one
/// reason only: a sink that answered [`SignupError::Unavailable`] is asked
/// again later, and it cannot be asked again without the thing it was asked
/// about. Nothing reads the address except a retry; `Signup.Status` and
/// `SignupAdmin.List` both leave it behind, because a return page is
/// unauthenticated and a console is not a mailing list.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct SignupRecord {
    pub signup: PaidSignup,
    pub state: SignupState,
    /// When the webhook landed HERE. Not [`PaidSignup::paid_at_unix_ms`], which
    /// is the vendor's clock on the money; the difference between the two is
    /// how long the vendor sat on the event and is worth being able to see.
    pub recorded_at_unix_ms: i64,
    /// **The estate the signup began on** (ONE monetize, several sites of one product): the
    /// caller's own bearer named it at `Signup.Start` ([`crate::auth::CallerEstate`]). Kept so
    /// a later settle makes the account on that estate too. `None`: the product's home, or a
    /// paid signup, whose account names its estate ([`crate::registry::Registry::estate_of_vendor`]).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub estate: Option<String>,
    /// ★ **The card checked for a free account** (owner 2026-10-15: one free account per
    /// card per site): the vendor's FINGERPRINT of it, never the number. `None`: an account
    /// made without a card check (before 2026-10-15, or a site with no vendor at all), and
    /// every paid signup. Skipped when empty, so older rows and their backups read the same.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub card: Option<CardOnFile>,
}

/// ★ **The card a free account was checked with** ([`SignupRecord::card`]).
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct CardOnFile {
    /// The vendor instance that checked it (`stripe/gunnar-test2`): a fingerprint is only
    /// comparable on the vendor account that made it.
    pub vendor: String,
    /// The vendor's fingerprint of the card number.
    pub fingerprint: String,
    /// The vendor's customer and the saved payment method (a later paid plan may reuse them).
    #[serde(default)]
    pub customer: String,
    #[serde(default)]
    pub payment_method: String,
    /// The vendor's reference for the check (Stripe: the SetupIntent).
    #[serde(default)]
    pub reference: String,
    pub checked_at_unix_ms: i64,
}

/// ★ **A card check minted for a free account, before the card is checked** (owner
/// 2026-10-15). Everything the account needs once the vendor says the card is checked —
/// kept HERE, never read back off the vendor's page.
#[derive(Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
pub struct CardCheckStarted {
    pub product: String,
    pub plan: String,
    pub username: String,
    pub email: String,
    /// The plan's currency (the account's [`PaidSignup::amount`] is zero in it).
    pub currency: monetize_pay::Currency,
    /// The estate the signup began on (the caller's bearer), and its site.
    #[serde(default)]
    pub estate: Option<String>,
    #[serde(default)]
    pub site: Option<String>,
    /// The console's welcome page, `{session}` in it: where the sign-in link leads.
    pub success_url: String,
    pub started_at_unix_ms: i64,
    /// **Refused, and the sentence the buyer reads** — set once, final for this session
    /// (the card already has a free account here, or the page closed unchecked).
    #[serde(default)]
    pub refused: Option<String>,
}

/// What [`SignupStore::record_pending`] found.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum Recorded {
    /// Nobody had seen this `(vendor, session)`; it is PENDING now.
    Fresh,
    /// It was already there. **The second webhook for a session does nothing** —
    /// this carries what the first one made of it, so the caller can answer 200
    /// with the truth instead of running the sink again.
    Already(Box<SignupRecord>),
}

/// **The signup store: idempotency and status, keyed `(vendor, session)`.**
pub struct SignupStore {
    db: Database,
    /// **The sessions a sink call is running for, right now, in this process.**
    ///
    /// `record_pending` makes a webhook's claim race-free, but a PENDING row is
    /// retried from three places — a vendor re-delivery, the return page's
    /// `Signup.Status` poll, and the poller's background re-settle — and two of
    /// them can reach the sink for the same session at once. With gunnar's sink
    /// that is not harmless: the second caller can find the principal the first
    /// has just made and be told the name is TAKEN, and whichever `set` lands
    /// last decides whether a paying customer has an account. So one call per
    /// session at a time, and the loser answers PENDING without calling
    /// anything. In memory, because one process serves this store: redb holds
    /// the file lock.
    in_flight: Mutex<HashSet<(String, String)>>,
    /// **Why each still-PENDING session is still pending**: the sink's last
    /// `Unavailable` words, for the operator's console and for choosing the
    /// customer's sentence. In memory and not in the row, because it changes on
    /// every retry and is only true of this process's last attempt; after a
    /// restart it is empty until the next retry, at most one poll tick away.
    waiting: Mutex<HashMap<(String, String), NotNow>>,
    /// **The vendor has confirmed this session paid**, by its own ledger:
    /// `paid_at` as the reconciler ([`crate::reconcile`]) read it back. Noted
    /// beside a row the store already holds — a webhook's or the reconciler's
    /// own — so a console can tell "the vendor says this money is real" from
    /// "a callback said so". In memory like `waiting`, and for the same
    /// reason: it is re-established by the next pass, at most one tick away,
    /// and it is a fact ABOUT the row, not part of the payment record.
    confirmed: Mutex<HashMap<(String, String), i64>>,
}

/// One session's sink call, held by [`SignupStore::fly`]; dropping it frees the
/// session for the next caller.
pub struct Flight<'a> {
    store: &'a SignupStore,
    key: (String, String),
}

impl Drop for Flight<'_> {
    fn drop(&mut self) {
        self.store.in_flight.lock().unwrap_or_else(|p| p.into_inner()).remove(&self.key);
    }
}

fn parse_signup(vendor: &str, session: &str, bytes: &[u8]) -> Result<SignupRecord, StoreError> {
    serde_json::from_slice(bytes).map_err(|e| StoreError::corrupted(format!("signup row {vendor}:{session} will not parse: {e}")))
}

impl SignupStore {
    /// The most rows [`SignupStore::list`] returns when the caller names no
    /// limit. A console page, not an export.
    pub const LIST_CEILING: usize = 500;

    pub fn open(path: &Path) -> Result<Self, StoreError> {
        Self::init(Database::create(path)?)
    }
    pub fn in_memory() -> Result<Self, StoreError> {
        Self::init(Database::builder().create_with_backend(redb::backends::InMemoryBackend::new())?)
    }
    fn init(db: Database) -> Result<Self, StoreError> {
        let txn = db.begin_write()?;
        txn.open_table(SIGNUPS)?;
        txn.open_table(REFUNDS)?;
        txn.open_table(MINTED)?;
        txn.open_table(CARD_CHECKS)?;
        txn.open_table(NAME_HOLDS)?;
        txn.open_table(WELCOMES)?;
        txn.commit()?;
        Ok(Self { db, in_flight: Mutex::new(HashSet::new()), waiting: Mutex::new(HashMap::new()), confirmed: Mutex::new(HashMap::new()) })
    }

    /// **This box minted `(vendor, session)`** for a signup on `estate` (ST-7).
    pub fn record_minted(&self, vendor: &str, session: &str, estate: Option<&str>) -> Result<(), StoreError> {
        let txn = self.db.begin_write()?;
        txn.open_table(MINTED)?.insert((vendor, session), estate.unwrap_or(""))?;
        txn.commit()?;
        Ok(())
    }

    /// ★ **The welcome page of a paid signup this box minted** (A11 fix 7), `{session}` in it.
    pub fn record_welcome(&self, vendor: &str, session: &str, welcome_url: &str) -> Result<(), StoreError> {
        let txn = self.db.begin_write()?;
        txn.open_table(WELCOMES)?.insert((vendor, session), welcome_url)?;
        txn.commit()?;
        Ok(())
    }

    /// The welcome page kept for `(vendor, session)`, if any.
    pub fn welcome_of(&self, vendor: &str, session: &str) -> Result<Option<String>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(WELCOMES)?;
        Ok(t.get((vendor, session))?.map(|v| v.value().to_owned()))
    }

    /// ★ **A card check minted for a free account** (owner 2026-10-15), kept until the card is
    /// checked. A second write for the same `(vendor, session)` replaces the first.
    pub fn record_card_check(&self, vendor: &str, session: &str, started: &CardCheckStarted) -> Result<(), StoreError> {
        let bytes = StoreError::encode("a card check", started)?;
        let txn = self.db.begin_write()?;
        txn.open_table(CARD_CHECKS)?.insert((vendor, session), bytes.as_slice())?;
        txn.commit()?;
        Ok(())
    }

    /// ★ **Hold `username` on `site` for a vendor page** (replaces any hold of it).
    pub fn hold_name(&self, product: &str, site: &str, username: &str, hold: &NameHold) -> Result<(), StoreError> {
        let key = format!("{product}\u{0}{site}");
        let bytes = StoreError::encode("a name hold", hold)?;
        let txn = self.db.begin_write()?;
        txn.open_table(NAME_HOLDS)?.insert((key.as_str(), username), bytes.as_slice())?;
        txn.commit()?;
        Ok(())
    }

    /// ★ **The live hold on `username` on `site`**, or `None` (none, or its time is up).
    pub fn name_hold(&self, product: &str, site: &str, username: &str, now_unix_ms: i64) -> Result<Option<NameHold>, StoreError> {
        let key = format!("{product}\u{0}{site}");
        let txn = self.db.begin_read()?;
        let t = txn.open_table(NAME_HOLDS)?;
        let Some(v) = t.get((key.as_str(), username))? else { return Ok(None) };
        let hold: NameHold = serde_json::from_slice(v.value()).map_err(|e| StoreError::corrupted(format!("name hold {username} will not parse: {e}")))?;
        Ok((hold.until_unix_ms > now_unix_ms).then_some(hold))
    }

    /// ★ **Release every name held for `(vendor, session)`**: its page closed, refused, or
    /// made the account. Answers how many.
    pub fn release_names_of(&self, vendor: &str, session: &str) -> Result<usize, StoreError> {
        let txn = self.db.begin_write()?;
        let n = {
            let mut t = txn.open_table(NAME_HOLDS)?;
            let mut gone: Vec<(String, String)> = Vec::new();
            for row in t.iter()? {
                let (k, v) = row?;
                if let Ok(h) = serde_json::from_slice::<NameHold>(v.value()) {
                    if h.vendor == vendor && h.session == session {
                        gone.push((k.value().0.to_owned(), k.value().1.to_owned()));
                    }
                }
            }
            for (a, b) in &gone {
                t.remove((a.as_str(), b.as_str()))?;
            }
            gone.len()
        };
        txn.commit()?;
        Ok(n)
    }

    /// The card check this box minted as `(vendor, session)`, or `None`: not one of ours.
    pub fn card_check(&self, vendor: &str, session: &str) -> Result<Option<CardCheckStarted>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(CARD_CHECKS)?;
        let Some(v) = t.get((vendor, session))? else { return Ok(None) };
        serde_json::from_slice(v.value()).map(Some).map_err(|e| StoreError::corrupted(format!("card check {vendor}:{session} will not parse: {e}")))
    }

    /// The card check `(vendor, session)` is refused, final: `words` is what the buyer reads.
    pub fn refuse_card_check(&self, vendor: &str, session: &str, words: &str) -> Result<(), StoreError> {
        if let Some(mut started) = self.card_check(vendor, session)? {
            started.refused = Some(words.to_owned());
            self.record_card_check(vendor, session, &started)?;
        }
        Ok(())
    }

    /// ★ **Put the checked card on a recorded signup** ([`SignupRecord::card`]). `Ok(false)`:
    /// no such row.
    pub fn set_card(&self, vendor: &str, session: &str, card: &CardOnFile) -> Result<bool, StoreError> {
        let txn = self.db.begin_write()?;
        let wrote = {
            let mut t = txn.open_table(SIGNUPS)?;
            let held: Option<Vec<u8>> = t.get((vendor, session))?.map(|v| v.value().to_vec());
            match held.map(|b| parse_signup(vendor, session, &b)).transpose()? {
                Some(mut rec) => {
                    rec.card = Some(card.clone());
                    let bytes = StoreError::encode("a signup record", &rec)?;
                    t.insert((vendor, session), bytes.as_slice())?;
                    true
                }
                None => false,
            }
        };
        txn.commit()?;
        Ok(wrote)
    }

    /// ★ **The free account this card already has on this site**, as `(session, username)`,
    /// or `None` (owner 2026-10-15: at most ONE free account per card).
    ///
    /// **Per site, and on the vendor account that checked it**: each site sells through its
    /// own Stripe account (`stripe/gunnar`, `stripe/gunnar-test2`), and a card's fingerprint
    /// is the same only on ONE vendor account — the same card checked on another site's
    /// account reads as another card, so a per-vendor key across sites could not be kept
    /// anyway. The site is the record's estate, else `home` (as
    /// [`Self::has_account_with_email_on`] reads it). An account still being made (PENDING:
    /// a pulled product's main has not acked yet) counts — it will be one; a refused row
    /// never made one.
    pub fn free_account_with_card_on(&self, product: &str, site: Option<&str>, home: Option<&str>, vendor: &str, fingerprint: &str) -> Result<Option<(String, String)>, StoreError> {
        let fingerprint = fingerprint.trim();
        if fingerprint.is_empty() {
            return Ok(None);
        }
        let named = |e: Option<&str>| e.map(str::trim).filter(|e| !e.is_empty()).map(str::to_owned);
        let mine = named(site).or_else(|| named(home));
        Ok(self.export_all()?.into_iter().find_map(|rec| {
            let card = rec.card.as_ref()?;
            let theirs = named(rec.estate.as_deref()).or_else(|| named(home));
            let held = rec.signup.product == product && theirs == mine && card.vendor == vendor && card.fingerprint == fingerprint && !matches!(rec.state, SignupState::Refused { .. });
            held.then(|| (rec.signup.session.clone(), rec.signup.username.clone()))
        }))
    }

    /// **Whether this box minted `(vendor, session)`** — `Some(estate)` (`None` inside: no
    /// estate named) — or `None`: another system's session (ST-7).
    pub fn minted(&self, vendor: &str, session: &str) -> Result<Option<Option<String>>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(MINTED)?;
        Ok(t.get((vendor, session))?.map(|v| Some(v.value().to_owned()).filter(|e| !e.is_empty())))
    }

    /// The reconciler read this session back from the vendor as paid at
    /// `paid_at_unix_ms`. Noted, not stored — see `confirmed`.
    pub fn note_vendor_confirmed(&self, vendor: &str, session: &str, paid_at_unix_ms: i64) {
        self.confirmed.lock().unwrap_or_else(|p| p.into_inner()).insert((vendor.to_owned(), session.to_owned()), paid_at_unix_ms);
    }

    /// When the vendor's own ledger last confirmed this session paid, if the
    /// reconciler has read it back in this process.
    pub fn vendor_confirmed(&self, vendor: &str, session: &str) -> Option<i64> {
        self.confirmed.lock().unwrap_or_else(|p| p.into_inner()).get(&(vendor.to_owned(), session.to_owned())).copied()
    }

    /// **Take the one sink call for `(vendor, session)`**, or `None` when
    /// another thread holds it. See `in_flight`.
    pub fn fly(&self, vendor: &str, session: &str) -> Option<Flight<'_>> {
        let key = (vendor.to_owned(), session.to_owned());
        let fresh = self.in_flight.lock().unwrap_or_else(|p| p.into_inner()).insert(key.clone());
        fresh.then_some(Flight { store: self, key })
    }

    /// The sink's last "not now" for a pending session, if this process has had one.
    pub fn waiting_on(&self, vendor: &str, session: &str) -> Option<String> {
        self.not_now(vendor, session).map(|n| n.why)
    }

    /// **The last "not now" the product gave for this signup**, its words and whether it was
    /// its maintenance, by name (RA4-20). In memory, like the rest of `waiting`.
    pub fn not_now(&self, vendor: &str, session: &str) -> Option<NotNow> {
        self.waiting.lock().unwrap_or_else(|p| p.into_inner()).get(&(vendor.to_owned(), session.to_owned())).cloned()
    }

    /// **Whether the "stays pending" line is said** — once per CHANGE, never
    /// every tick: `previous` is the reason last noted for this signup (what
    /// [`SignupStore::waiting_on`] keeps), `why` the one this pass got.
    pub fn says_pending(previous: Option<&str>, why: &str) -> bool {
        previous != Some(why)
    }

    fn note_waiting(&self, vendor: &str, session: &str, why: Option<NotNow>) {
        let mut w = self.waiting.lock().unwrap_or_else(|p| p.into_inner());
        let key = (vendor.to_owned(), session.to_owned());
        match why {
            Some(why) => {
                w.insert(key, why);
            }
            None => {
                w.remove(&key);
            }
        }
    }

    /// **Every PENDING row, oldest first** — what the background re-settle
    /// walks. A full scan like [`SignupStore::list`], and not capped by its
    /// ceiling: a paid signup the 501st row back is still owed an account.
    pub fn pending(&self) -> Result<Vec<SignupRecord>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(SIGNUPS)?;
        let mut out = Vec::new();
        for entry in t.iter()? {
            let (_, v) = entry?;
            if let Ok(rec) = serde_json::from_slice::<SignupRecord>(v.value()) {
                if rec.state.is_pending() {
                    out.push(rec);
                }
            }
        }
        out.sort_by_key(|r| r.recorded_at_unix_ms);
        Ok(out)
    }

    /// **THE OWED-BACK QUEUE: every payment that was taken for an account that
    /// was never created**, oldest first.
    ///
    /// A full scan like [`SignupStore::pending`] and, like it, NOT capped by
    /// [`SignupStore::LIST_CEILING`]: the ceiling is a console page's, and the
    /// 501st row back is still somebody's hundred kronor. It is the store read a
    /// third way rather than a second table, for the reason the module header
    /// gives — this table is the record of the money, and a queue kept beside it
    /// could disagree with it after a restore.
    ///
    /// It survives a restart because the rows do: redb, `signups.redb`, the same
    /// file the idempotency key lives in. A row LEAVES it when its money is
    /// back: a [`SignupRefund::Done`] ([`refund`]) or a
    /// [`SignupRefund::SettledByHand`] ([`settle_by_hand`]). A refund still
    /// `Requested` stays — the vendor's yes was never heard.
    pub fn owed_back(&self) -> Result<Vec<SignupRecord>, StoreError> {
        let settled: HashSet<(String, String)> =
            self.export_refunds()?.into_iter().filter(|(_, _, r)| r.settles()).map(|(v, s, _)| (v, s)).collect();
        let mut out: Vec<SignupRecord> = self
            .export_all()?
            .into_iter()
            .filter(|r| r.state.owed().is_some() && !settled.contains(&(r.signup.vendor.clone(), r.signup.session.clone())))
            .collect();
        out.sort_by_key(|r| r.recorded_at_unix_ms);
        Ok(out)
    }

    /// **How many payments are owed back**, for the health line. `Err` is
    /// propagated and never folded to 0: a store that would not answer is not a
    /// deployment that owes nobody anything.
    pub fn owed_back_count(&self) -> Result<usize, StoreError> {
        Ok(self.owed_back()?.len())
    }

    /// **Claim `(vendor, session)`, or report who claimed it first.**
    ///
    /// The read and the write are ONE redb transaction on purpose: two
    /// deliveries of the same webhook can arrive on two threads, and a
    /// check-then-write with a gap between them creates two accounts for one
    /// payment. redb serialises writers, so the loser of the race sees the
    /// winner's row and answers [`Recorded::Already`].
    pub fn record_pending(&self, signup: &PaidSignup, now_unix_ms: i64) -> Result<Recorded, StoreError> {
        self.record_pending_on(signup, now_unix_ms, None)
    }

    /// [`SignupStore::record_pending`], keeping the estate the signup began on
    /// ([`SignupRecord::estate`]).
    pub fn record_pending_on(&self, signup: &PaidSignup, now_unix_ms: i64, estate: Option<&str>) -> Result<Recorded, StoreError> {
        let txn = self.db.begin_write()?;
        let found = {
            let mut t = txn.open_table(SIGNUPS)?;
            let held: Option<Vec<u8>> = t.get((signup.vendor.as_str(), signup.session.as_str()))?.map(|v| v.value().to_vec());
            match held {
                Some(bytes) => serde_json::from_slice::<SignupRecord>(&bytes).ok().map(Box::new).map(Recorded::Already),
                None => {
                    let rec = SignupRecord { signup: signup.clone(), state: SignupState::Pending, recorded_at_unix_ms: now_unix_ms, estate: estate.map(str::to_owned), card: None };
                    let bytes = StoreError::encode("a signup record", &rec)?;
                    t.insert((signup.vendor.as_str(), signup.session.as_str()), bytes.as_slice())?;
                    Some(Recorded::Fresh)
                }
            }
        };
        txn.commit()?;
        // A row that will not deserialize is a row from a future or a broken
        // format. It is NOT treated as absent — overwriting it would destroy
        // the only record of a payment — so the claim fails loudly instead.
        found.ok_or_else(|| StoreError::corrupted(format!("signup row {}:{} will not parse", signup.vendor, signup.session)))
    }

    /// Move a recorded signup to its outcome. A no-op with `Ok(false)` when
    /// nobody claimed the key first — a `set` without a `record_pending` is a
    /// caller bug, not a row to invent.
    pub fn set(&self, vendor: &str, session: &str, state: SignupState) -> Result<bool, StoreError> {
        let txn = self.db.begin_write()?;
        let wrote = {
            let mut t = txn.open_table(SIGNUPS)?;
            let held: Option<Vec<u8>> = t.get((vendor, session))?.map(|v| v.value().to_vec());
            match held.map(|b| parse_signup(vendor, session, &b)).transpose()? {
                Some(mut rec) => {
                    rec.state = state;
                    let bytes = StoreError::encode("a signup record", &rec)?;
                    t.insert((vendor, session), bytes.as_slice())?;
                    true
                }
                None => false,
            }
        };
        txn.commit()?;
        Ok(wrote)
    }

    /// **Replace what the row says the customer asked for, and what became of
    /// it, in one write.**
    ///
    /// [`SignupStore::set`] moves the outcome and leaves the [`PaidSignup`]
    /// alone, which is right for every path except a rename: there the NAME
    /// changed, and a row whose stored signup still said the old one would make
    /// `SignupAdmin.List` show an operator a name nobody has and a retry ask the
    /// sink for it again. `recorded_at_unix_ms` is kept, because when the
    /// webhook landed here did not change.
    pub fn rewrite(&self, vendor: &str, session: &str, signup: &PaidSignup, state: SignupState) -> Result<bool, StoreError> {
        let txn = self.db.begin_write()?;
        let wrote = {
            let mut t = txn.open_table(SIGNUPS)?;
            let held: Option<Vec<u8>> = t.get((vendor, session))?.map(|v| v.value().to_vec());
            match held.map(|b| parse_signup(vendor, session, &b)).transpose()? {
                Some(rec) => {
                    let next = SignupRecord { signup: signup.clone(), state, recorded_at_unix_ms: rec.recorded_at_unix_ms, estate: rec.estate.clone(), card: rec.card.clone() };
                    let bytes = StoreError::encode("a signup record", &next)?;
                    t.insert((vendor, session), bytes.as_slice())?;
                    true
                }
                None => false,
            }
        };
        txn.commit()?;
        Ok(wrote)
    }

    /// A row that will not parse is an ERROR, never "no such signup": read as
    /// absent, a settle would ask the sink again and a reversal would be
    /// answered "not ours".
    /// Tests: overwrite a row with bytes that will not parse.
    #[cfg(test)]
    pub(crate) fn break_row(&self, vendor: &str, session: &str) {
        let txn = self.db.begin_write().unwrap();
        txn.open_table(SIGNUPS).unwrap().insert((vendor, session), b"{not json".as_slice()).unwrap();
        txn.commit().unwrap();
    }

    pub fn get(&self, vendor: &str, session: &str) -> Result<Option<SignupRecord>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(SIGNUPS)?;
        t.get((vendor, session))?.map(|v| parse_signup(vendor, session, v.value())).transpose()
    }

    /// What [`refund`] / [`settle_by_hand`] recorded for this session. A row
    /// that will not parse is an ERROR, never "no refund": reading it as absent
    /// would let a second refund be requested over the first.
    pub fn refund_of(&self, vendor: &str, session: &str) -> Result<Option<SignupRefund>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(REFUNDS)?;
        match t.get((vendor, session))? {
            None => Ok(None),
            Some(v) => serde_json::from_slice(v.value())
                .map(Some)
                .map_err(|e| StoreError::corrupted(format!("refund row {vendor}:{session} will not parse: {e}"))),
        }
    }

    /// **Write the refund row, only if there is none** — and answer what is
    /// there now. One write transaction, so two callers cannot both think they
    /// wrote the first `Requested` (in-process they are also serialised by the
    /// flight, [`SignupStore::fly`]).
    pub fn claim_refund(&self, vendor: &str, session: &str, first: &SignupRefund) -> Result<SignupRefund, StoreError> {
        let txn = self.db.begin_write()?;
        let now = {
            let mut t = txn.open_table(REFUNDS)?;
            let held: Option<Vec<u8>> = t.get((vendor, session))?.map(|v| v.value().to_vec());
            match held {
                Some(bytes) => serde_json::from_slice(&bytes)
                    .map_err(|e| StoreError::corrupted(format!("refund row {vendor}:{session} will not parse: {e}")))?,
                None => {
                    let bytes = StoreError::encode("a refund row", first)?;
                    t.insert((vendor, session), bytes.as_slice())?;
                    first.clone()
                }
            }
        };
        txn.commit()?;
        Ok(now)
    }

    /// Overwrite the refund row: `Requested` → `Done`, or → settled by hand.
    pub fn put_refund(&self, vendor: &str, session: &str, r: &SignupRefund) -> Result<(), StoreError> {
        let txn = self.db.begin_write()?;
        {
            let mut t = txn.open_table(REFUNDS)?;
            let bytes = StoreError::encode("a refund row", r)?;
            t.insert((vendor, session), bytes.as_slice())?;
        }
        txn.commit()?;
        Ok(())
    }

    /// **Every refund row, in key order** — what the owed-back filter, the tax
    /// fold and a backup read. A row that will not parse fails the read, for
    /// the reason [`SignupStore::refund_of`] gives.
    pub fn export_refunds(&self) -> Result<Vec<(String, String, SignupRefund)>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(REFUNDS)?;
        let mut out = Vec::new();
        for entry in t.iter()? {
            let (k, v) = entry?;
            let (vendor, session) = k.value();
            let r = serde_json::from_slice(v.value())
                .map_err(|e| StoreError::corrupted(format!("refund row {vendor}:{session} will not parse: {e}")))?;
            out.push((vendor.to_owned(), session.to_owned(), r));
        }
        Ok(out)
    }

    /// **The e-mail a tenant signed up with**, for `product`: the address of
    /// the newest signup that CREATED `namespace` (the same fold as the
    /// signup's entitlement, `created_signups`), or `None` (an account an
    /// operator made by hand, or a signup still pending).
    ///
    /// Why (owner 2026-09-28, test2.gunnar.rs): a tenant logged in to the
    /// console ordered 10 GiB and Stripe's page asked for "Kontaktinformation /
    /// E-postadress" again. monetize already holds the address the buyer typed
    /// and confirmed at the signup; the purchase hands it to the vendor as the
    /// payer, and the Stripe vendor opens the page with `customer_email` set.
    /// A full scan, like [`SignupStore::list`], and for the same reason.
    pub fn email_of(&self, product: &str, namespace: &str) -> Result<Option<String>, StoreError> {
        Ok(created_signups(self, product)?
            .into_iter()
            .find(|(_, tenant, _)| tenant == namespace)
            .map(|(rec, _, _)| rec.signup.email.trim().to_owned())
            .filter(|e| !e.is_empty()))
    }

    /// **Whether an account of `product` already answers to `email`** (PLAN
    /// §3c, OM-19 (4), the owner 2026-10-10 23:49Z): the address of a signup
    /// that CREATED an account, compared trimmed and case-blind. A signup
    /// still pending is no account yet, so a buyer who left a checkout unpaid
    /// may start again. A full scan, like [`SignupStore::email_of`].
    pub fn has_account_with_email(&self, product: &str, email: &str) -> Result<bool, StoreError> {
        let wanted = email.trim().to_lowercase();
        if wanted.is_empty() {
            return Ok(false);
        }
        Ok(created_signups(self, product)?.iter().any(|(rec, _, _)| rec.signup.email.trim().to_lowercase() == wanted))
    }

    /// ★ ST-6: **[`Self::has_account_with_email`] on ONE SITE** (owner 2026-10-11: one
    /// address, one account per site — gunnar.rs and test2.gunnar.rs are two sites of one
    /// product on one box). A created account's site is the estate its signup began on
    /// ([`SignupRecord::estate`]), else its namespace's (`@<estate>/…`), else `home` (the
    /// product's own estate); `site` is the caller's, else `home`. Another site's account with
    /// this address is no account here.
    pub fn has_account_with_email_on(&self, product: &str, site: Option<&str>, home: Option<&str>, email: &str) -> Result<bool, StoreError> {
        let wanted = email.trim().to_lowercase();
        if wanted.is_empty() {
            return Ok(false);
        }
        let named = |e: Option<&str>| e.map(str::trim).filter(|e| !e.is_empty()).map(str::to_owned);
        let mine = named(site).or_else(|| named(home));
        Ok(created_signups(self, product)?.iter().any(|(rec, tenant, _)| {
            let theirs = named(rec.estate.as_deref())
                .or_else(|| monetize_product::TenantId(tenant.clone()).estate().map(|(e, _)| e.to_owned()))
                .or_else(|| named(home));
            theirs == mine && rec.signup.email.trim().to_lowercase() == wanted
        }))
    }

    /// **Every signup, newest first**, at most `limit` (0 ⇒
    /// [`SignupStore::LIST_CEILING`]).
    ///
    /// redb orders by the key, which is `(vendor, session)` — alphabetical, not
    /// chronological — so this reads the table and sorts by when the row landed
    /// here. That is a full scan, and it is the right trade for this table: a
    /// deployment that has taken a hundred thousand signups has a bigger
    /// problem than a console page, and a second time-keyed table (the ledger's
    /// `recent` trick) would be two copies of every signup to keep in step.
    pub fn list(&self, limit: usize) -> Result<Vec<SignupRecord>, StoreError> {
        let limit = if limit == 0 { Self::LIST_CEILING } else { limit.min(Self::LIST_CEILING) };
        let txn = self.db.begin_read()?;
        let t = txn.open_table(SIGNUPS)?;
        let mut out: Vec<SignupRecord> = Vec::new();
        for entry in t.iter()? {
            let (_, v) = entry?;
            if let Ok(rec) = serde_json::from_slice::<SignupRecord>(v.value()) {
                out.push(rec);
            }
        }
        out.sort_by(|a, b| b.recorded_at_unix_ms.cmp(&a.recorded_at_unix_ms).then_with(|| a.signup.session.cmp(&b.signup.session)));
        out.truncate(limit);
        Ok(out)
    }

    /// **Every record, in KEY order** — what a backup exports.
    ///
    /// Key order and not [`SignupStore::list`]'s newest-first: an export must be
    /// deterministic and total, so two exports of unchanged state are
    /// byte-identical and a git-backed backup has nothing to commit. Two rows
    /// recorded in the same millisecond sort stably here and would not there.
    pub fn export_all(&self) -> Result<Vec<SignupRecord>, StoreError> {
        let txn = self.db.begin_read()?;
        let t = txn.open_table(SIGNUPS)?;
        let mut out = Vec::new();
        for entry in t.iter()? {
            let (_, v) = entry?;
            if let Ok(rec) = serde_json::from_slice::<SignupRecord>(v.value()) {
                out.push(rec);
            }
        }
        Ok(out)
    }

    /// **Read a backup back, into an EMPTY store.**
    ///
    /// It refuses a store that already holds rows, the way `Ledger::restore`
    /// does and for a sharper reason: this table IS the idempotency key. A
    /// restore interleaved with live history would leave one `(vendor, session)`
    /// holding whichever of the two rows was written last, and the loser's
    /// customer — already created, already charged — would be created a second
    /// time by the vendor's next retry.
    pub fn restore(&self, records: &[SignupRecord]) -> Result<usize, StoreError> {
        {
            let txn = self.db.begin_read()?;
            if txn.open_table(SIGNUPS)?.len()? > 0 {
                return Err(StoreError::corrupted("restore into a signup store that already holds rows"));
            }
        }
        let txn = self.db.begin_write()?;
        {
            let mut t = txn.open_table(SIGNUPS)?;
            for rec in records {
                let bytes = StoreError::encode("a signup record", rec)?;
                t.insert((rec.signup.vendor.as_str(), rec.signup.session.as_str()), bytes.as_slice())?;
            }
        }
        txn.commit()?;
        Ok(records.len())
    }

    /// **Another machine's signups INTO this live store** (G10, the ledger
    /// merge): one write transaction, refused whole if ONE `(vendor, session)`
    /// is already here or comes twice — this table is the idempotency key, and
    /// two rows for one key would leave the loser's customer created twice.
    pub fn merge(&self, records: &[SignupRecord]) -> Result<usize, StoreError> {
        let txn = self.db.begin_write()?;
        {
            let mut t = txn.open_table(SIGNUPS)?;
            let mut seen = std::collections::BTreeSet::new();
            for rec in records {
                let key = (rec.signup.vendor.as_str(), rec.signup.session.as_str());
                if !seen.insert(key) || t.get(key)?.is_some() {
                    return Err(StoreError::corrupted(format!("merge of a signup this store already holds: {}:{}", key.0, key.1)));
                }
            }
            for rec in records {
                let bytes = StoreError::encode("a signup record", rec)?;
                t.insert((rec.signup.vendor.as_str(), rec.signup.session.as_str()), bytes.as_slice())?;
            }
        }
        txn.commit()?;
        Ok(records.len())
    }
}

/// **Everything about the signup that this deployment can check on its own** —
/// `None` when it all agrees, the refusal reason otherwise.
///
/// It runs BEFORE the sink, and that ordering is the point: a refusal here
/// costs nothing and reaches the console with the customer's own words in it,
/// while a sink that has already created a principal cannot un-create one.
///
/// The three checks are the three things a stranger asserted:
///
/// 1. **the username** — a hosted page will happily take `Bob!` and there is
///    no product on earth that can be told to make that a namespace;
/// 2. **the plan** — a page that sells something `[signups.<product>.plans]` does not name
///    is stale or forged, and either way the deployment does not know what to
///    grant;
/// 3. **the amount, NET of tax** — the one check that catches a page whose price
///    drifted from the config's, which is how a 10 GiB plan quietly becomes
///    free. It is the NET that is compared: a checkout that collects VAT hands
///    up a total larger than the plan price by exactly the tax, and comparing
///    the gross would refuse every one of those as a drifted price.
///
/// Each answer carries its [`Refusal`] beside its words, because the first of
/// the three is about the NAME — the one a customer can fix by typing another —
/// and the other two are about the ORDER and are final.
/// ★ **The plan a paid sign-up named**: this deployment's `[signups.<product>.plans]`
/// entry, or — round 5.3.1, room by the rule — the terms a PRICED id stands for
/// (`crate::plans::priced_signup_plan`: the declared size and period, the price
/// the checkout charged).
pub fn plan_named(state: &crate::state::AppState, id: &str) -> Option<SignupPlan> {
    state.signup_plans.get(id).or_else(|| crate::plans::priced_signup_plan(state, id))
}

/// [`plan_named`] for the checks: the table, with the priced plan `id` names
/// beside it when it is one.
pub fn plans_with(state: &crate::state::AppState, id: &str) -> std::collections::BTreeMap<String, SignupPlan> {
    let mut plans = state.signup_plans.snapshot();
    if !plans.contains_key(id) {
        if let Some(p) = crate::plans::priced_signup_plan(state, id) {
            plans.insert(id.to_owned(), p);
        }
    }
    plans
}

pub fn refusal(plans: &std::collections::BTreeMap<String, SignupPlan>, s: &PaidSignup) -> Option<(Refusal, String)> {
    if let Some(why) = monetize_pay::signup::username_refusal(&s.username) {
        return Some((Refusal::Name, why));
    }
    let Some(plan) = plans.get(&s.plan) else {
        let known: Vec<&str> = plans.keys().map(String::as_str).collect();
        return Some((Refusal::Final, format!("plan {:?} is not sold here; [signups.<product>.plans] names {:?}", s.plan, known)));
    };
    let want = plan.price();
    let net_minor = s.amount.minor.saturating_sub(s.tax.minor);
    if want.currency != s.amount.currency || want.minor != net_minor {
        // Every figure in major units with its currency (owner, live
        // 2026-10-12: "costs 11000 SEK … took 12500 SEK" was öre read as kronor).
        let money = |minor| monetize_pay::Money { minor, currency: s.amount.currency }.in_words();
        return Some((Refusal::Final, format!(
            "paid {} ({} of it tax, {} net), but plan {:?} costs {} here",
            money(s.amount.minor),
            money(s.tax.minor),
            money(net_minor),
            s.plan,
            want.in_words()
        )));
    }
    None
}

/// **A price refusal stored before 2026-10-12, in today's words.** Those rows
/// say `plan "p" costs 11000 SEK here and the vendor took 12500 SEK (2500 of it
/// tax), a net of 10000 SEK` — öre printed as kronor. The row is kept as it was
/// written; the console is handed it re-said in major units. `None` for any
/// other reason, which is carried as written.
pub fn legacy_price_refusal(reason: &str) -> Option<String> {
    let rest = reason.strip_prefix("plan ")?;
    let (plan, rest) = rest.split_once(" costs ")?;
    let (want, rest) = rest.split_once(" here and the vendor took ")?;
    let (took, rest) = rest.split_once(" (")?;
    let (tax, rest) = rest.split_once(" of it tax), a net of ")?;
    let words = |pair: &str| -> Option<String> {
        let (n, code) = pair.trim().split_once(' ')?;
        Some(monetize_pay::major_words(n.parse().ok()?, code))
    };
    let code = took.trim().split_once(' ')?.1;
    let tax = monetize_pay::major_words(tax.trim().parse().ok()?, code);
    Some(format!("paid {} ({tax} of it tax, {} net), but plan {plan} costs {} here", words(took)?, words(rest)?, words(want)?))
}

/// **What the CHECKS' refusal leaves owed.** They run before the sink, so
/// nothing was created whatever they said — the only question is whether the
/// customer can finish it themselves, which is the same question [`Refusal`]
/// already answers. One writer, so the two cannot drift apart.
fn owed_for_checks(refusal: Refusal) -> Owed {
    if refusal.renameable() {
        Owed::RenameOrRefund
    } else {
        Owed::Refund
    }
}

/// **The open build's sink: it says what it WOULD do, and answers Created.**
///
/// It is not a stub and not a `todo!()`. The open half of monetize is a real
/// deployment for people sharing their servers for free, and the whole seam —
/// the webhook, the store, the idempotency,
/// the refusals, the poll from the return page — has to be exercisable end to
/// end by somebody who has no private crate at all. So it logs the line an
/// operator would need and reports the namespace it would have made.
///
/// It answers **Created** rather than `Refused("not implemented")` for the same
/// reason [`crate::registry::Linked::open`] links a `server` cloud rather than
/// none: the open path must be a working path, or nobody can tell a broken
/// deployment from an unconfigured one. What it does NOT do is create anything,
/// and it says so in every line it writes.
/// **The open server's sink when none is loaded** (owner 2026-09-26): it
/// creates nothing and SAYS it creates nothing — `Signup.Start` is refused
/// before any checkout, and a paid webhook that reached it anyway is refused
/// by name (stored, for a refund by hand), never answered `Created`.
pub struct NoSink;

impl SignupSink for NoSink {
    fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
        Err(SignupError::Refused(format!(
            "no signup sink is loaded on this server, so no account was created for {} ({}:{}); the money is at the vendor and needs a refund by hand",
            s.username, s.vendor, s.session
        )))
    }
    fn name_verdict(&self, _username: &str) -> Result<NameVerdict, SignupError> {
        Err(SignupError::Unavailable("no signup sink is loaded on this server".into()))
    }
    fn creates_accounts(&self) -> bool {
        false
    }
}

pub struct LoudSink;

impl SignupSink for LoudSink {
    fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
        let line = format!("would create {} for {} ({} at {})", s.username, s.plan, s.amount.major(), s.vendor);
        skidbladnir::functional_status("monetize-server/signup", &s.vendor, true, &line);
        eprintln!("monetize-server: signup {}:{} — {line}; NO principal was created (the open build's LoudSink)", s.vendor, s.session);
        Ok(SignupOutcome::Created { namespace: s.username.clone(), username: s.username.clone() })
    }

    /// **The shape rule and nothing else, because this sink holds nothing.**
    ///
    /// It creates no principal, so no name it was ever asked about is taken —
    /// answering anything but `Free` for a well-formed name would be inventing
    /// an occupant. The deployment that DOES hold names is the one with a real
    /// sink, and its reserved list and its product's own answer live there,
    /// behind this same method, so nothing outside a sink ever needs a second
    /// copy of either.
    fn name_verdict(&self, username: &str) -> Result<NameVerdict, SignupError> {
        Ok(match monetize_pay::signup::username_refusal(username) {
            Some(why) => NameVerdict::Taken(why),
            None => NameVerdict::Free,
        })
    }
}

/// The sink slot: shared, because the gRPC `Status` poll retries through the
/// same one the webhook used.
pub type Sink = Arc<dyn SignupSink>;

/// **Every product's `[signups.<product>.plans]` and the meter each product's
/// caps land on, as the server holds them LIVE** — shared (a clone reads the
/// same terms), replaced whole by the guest watcher
/// ([`crate::guests::apply_signup`]) when the config moves. A plan id names its
/// product (`holger/base-se`), so one map holds them all and
/// [`Plans::snapshot_of`] is one product's.
#[derive(Clone, Default)]
pub struct Plans(Arc<std::sync::RwLock<PlanTerms>>);

type PlanMap = std::collections::BTreeMap<String, SignupPlan>;
type MeterMap = std::collections::BTreeMap<String, String>;

#[derive(Clone, Default, PartialEq)]
struct PlanTerms {
    plans: PlanMap,
    cap_meters: MeterMap,
}

impl std::fmt::Debug for Plans {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Plans").field("plans", &self.snapshot()).field("cap_meters", &self.read().cap_meters).finish()
    }
}

impl From<PlanMap> for Plans {
    fn from(plans: PlanMap) -> Plans {
        Plans::of(plans, MeterMap::new())
    }
}

impl<const N: usize> From<[(String, SignupPlan); N]> for Plans {
    fn from(plans: [(String, SignupPlan); N]) -> Plans {
        Plans::of(plans.into_iter().collect(), MeterMap::new())
    }
}

/// The product a plan id names: `holger/base-se` → `holger`.
pub fn plan_product(plan_id: &str) -> &str {
    plan_id.split('/').next().unwrap_or_default()
}

impl Plans {
    /// Every product's plans, with the meter each product's caps land on
    /// (`product → meter`; a product not named is [`SIGNUP_CAP_METER`]).
    pub fn of(plans: PlanMap, cap_meters: MeterMap) -> Plans {
        Plans(Arc::new(std::sync::RwLock::new(PlanTerms { plans, cap_meters })))
    }
    fn read(&self) -> std::sync::RwLockReadGuard<'_, PlanTerms> {
        self.0.read().unwrap_or_else(|p| p.into_inner())
    }
    /// The plan `id` names, or none: a plan this deployment does not list is never sold.
    pub fn get(&self, id: &str) -> Option<SignupPlan> {
        self.read().plans.get(id).copied()
    }
    pub fn is_empty(&self) -> bool {
        self.read().plans.is_empty()
    }
    /// Every product's plans now, as a map (they are few and small).
    pub fn snapshot(&self) -> PlanMap {
        self.read().plans.clone()
    }
    /// **The meter `product`'s signup `cap_bytes` land on** —
    /// `[signups.<product>] cap_meter` (holger: `disk_bytes`), else
    /// [`SIGNUP_CAP_METER`] (gunnar's `pack_bytes`).
    pub fn cap_meter_of(&self, product: &str) -> String {
        self.read().cap_meters.get(product).filter(|m| !m.trim().is_empty()).cloned().unwrap_or_else(|| SIGNUP_CAP_METER.to_owned())
    }
    /// `product`'s plans only (a plan id names its product: `holger/…`).
    pub fn snapshot_of(&self, product: &str) -> PlanMap {
        self.read().plans.iter().filter(|(id, _)| plan_product(id) == product).map(|(id, p)| (id.clone(), *p)).collect()
    }
    /// Replace the terms whole; `true` when they changed.
    pub fn set(&self, plans: PlanMap, cap_meters: MeterMap) -> bool {
        let next = PlanTerms { plans, cap_meters };
        let mut cur = self.0.write().unwrap_or_else(|p| p.into_inner());
        if *cur == next {
            return false;
        }
        *cur = next;
        true
    }
}

/// **Each product's signup sink, each replaceable while the server runs** (one
/// monetize, several products, 2026-10-10). A call goes to the sink of the
/// product it is FOR (`PaidSignup::product`, the notice's product, the name
/// check's product) as it stands at that moment; [`SignupReload`] swaps one
/// product's sink when its `[signups.<product>]` names a new guest. A swap
/// never interrupts a call in flight, and never touches another product's.
pub struct HotSink {
    cur: std::sync::RwLock<std::collections::BTreeMap<String, Sink>>,
    /// **Where the host's relay is read from** — the secrets dir the installer
    /// feeds `secret.smtp_url` / `secret.smtp_from` (and `secret.smtp_ca`) into.
    /// `None`: the environment names only. See [`HotSink::notify`].
    mail: std::sync::RwLock<Option<std::path::PathBuf>>,
}

impl HotSink {
    /// One sink per product (`[signups.<product>]`), each replaceable live.
    pub fn per_product(sinks: std::collections::BTreeMap<String, Sink>) -> Arc<HotSink> {
        Arc::new(HotSink { cur: std::sync::RwLock::new(sinks), mail: std::sync::RwLock::new(None) })
    }
    /// Read the host's relay from `secrets_dir` (the served process's).
    pub fn mail_from(&self, secrets_dir: &std::path::Path) {
        *self.mail.write().unwrap_or_else(|p| p.into_inner()) = Some(secrets_dir.to_owned());
    }
    /// **The host's relay, as it stands NOW** — re-read at every notice, so a
    /// relay fed (or rotated) while the server runs is the one used. `Ok(None)`:
    /// this host has none.
    pub fn host_mailer(&self) -> Result<Option<monetize_mail::Mailer>, String> {
        self.host_mailer_of(None)
    }
    /// [`Self::host_mailer`] for `product`'s mail (A16): its own sender where it is fed.
    pub fn host_mailer_of(&self, product: Option<&str>) -> Result<Option<monetize_mail::Mailer>, String> {
        let dir = self.mail.read().unwrap_or_else(|p| p.into_inner()).clone();
        host_mailer_for(dir.as_deref(), product)
    }
    /// `product`'s sink now, or the refusal that names the block it would come from.
    fn of(&self, product: &str) -> Result<Sink, SignupError> {
        self.cur.read().unwrap_or_else(|p| p.into_inner()).get(product).cloned().ok_or_else(|| {
            SignupError::Refused(format!("no signup sink serves product {product:?}: this server has no [signups.{product}] that names one"))
        })
    }
    /// The products a sink stands for.
    pub fn products(&self) -> Vec<String> {
        self.cur.read().unwrap_or_else(|p| p.into_inner()).keys().cloned().collect()
    }
    /// Replace `product`'s sink alone.
    pub fn replace(&self, product: &str, next: Sink) {
        self.cur.write().unwrap_or_else(|p| p.into_inner()).insert(product.to_owned(), next);
    }
}

/// **The host's relay as fed in `dir` NOW** (`secret.smtp_url` / `secret.smtp_from` /
/// `secret.smtp_ca`), else the environment's names. `Ok(None)`: none. Read again by every
/// caller (a relay fed or rotated while the box runs is the one used): the customer notices
/// ([`HotSink::host_mailer`]) and the ONE box's operator alerts (monetize-admin `alerts`).
pub fn host_mailer_in(dir: Option<&std::path::Path>) -> Result<Option<monetize_mail::Mailer>, String> {
    host_mailer_for(dir, None)
}

/// Pure: **the relay and sender for `product`'s mail** (A16): its own
/// (`smtp_url.<product>` + `smtp_from.<product>`, both or neither) where fed, else the host's.
pub fn relay_names(fed: &dyn Fn(&str) -> Option<String>, product: Option<&str>) -> (Option<String>, Option<String>) {
    if let Some(p) = product.map(str::trim).filter(|p| !p.is_empty()) {
        if let (Some(url), Some(from)) = (fed(&format!("smtp_url.{p}")), fed(&format!("smtp_from.{p}"))) {
            return (Some(url), Some(from));
        }
    }
    (fed("smtp_url"), fed("smtp_from"))
}

/// [`host_mailer_in`] for `product`'s mail: its own relay and sender where fed.
pub fn host_mailer_for(dir: Option<&std::path::Path>, product: Option<&str>) -> Result<Option<monetize_mail::Mailer>, String> {
    let fed_bytes = |name: &str| dir.and_then(|d| monetize_product_wasm::secrets::read(d, name).ok()).map(|s| s.bytes().to_vec());
    let env = |name: &str| std::env::var(name).ok().filter(|v| !v.trim().is_empty());
    let text = |b: Vec<u8>| String::from_utf8(b).ok().map(|s| s.trim().to_owned()).filter(|s| !s.is_empty());
    let fed = |name: &str| fed_bytes(name).and_then(text);
    let (url, from) = relay_names(&fed, product);
    let url = url.or_else(|| env("MONETIZE_SMTP_URL"));
    let from = from.or_else(|| env("MONETIZE_SMTP_FROM"));
    let fed = fed_bytes;
    let ca = fed("smtp_ca").or_else(|| env("MONETIZE_SMTP_CA").and_then(|p| std::fs::read(p).ok()));
    match (url, from) {
        (None, None) => Ok(None),
        (Some(url), Some(from)) => monetize_mail::Mailer::with_root(&url, &from, ca.as_deref()).map(Some),
        (Some(_), None) => Err("the host's relay is named (smtp_url) and its sender is not (smtp_from); a relay with no sender sends nothing".into()),
        (None, Some(_)) => Err("the host's sender is named (smtp_from) and its relay is not (smtp_url)".into()),
    }
}

impl SignupSink for HotSink {
    fn paid(&self, s: &PaidSignup) -> Result<SignupOutcome, SignupError> {
        self.of(&s.product)?.paid(s)
    }
    /// A name is free or taken FOR a product; with none named there is no answer.
    fn name_verdict(&self, username: &str) -> Result<NameVerdict, SignupError> {
        Err(SignupError::Refused(format!("the name {username:?} is checked for a product, and none was named")))
    }
    fn name_verdict_for(&self, product: &str, username: &str) -> Result<NameVerdict, SignupError> {
        self.of(product)?.name_verdict(username)
    }
    /// The product's sink, told the estate the account would be made on.
    fn name_verdict_on(&self, product: &str, username: &str, estate: Option<&str>) -> Result<NameVerdict, SignupError> {
        self.of(product)?.name_verdict_on(product, username, estate)
    }
    fn paid_on(&self, s: &PaidSignup, estate: Option<&str>) -> Result<SignupOutcome, SignupError> {
        self.of(&s.product)?.paid_on(s, estate)
    }
    fn resend_welcome(&self, s: &PaidSignup, force: bool) -> Result<String, SignupError> {
        self.of(&s.product)?.resend_welcome(s, force)
    }
    /// Some product's sink creates accounts.
    fn creates_accounts(&self) -> bool {
        self.cur.read().unwrap_or_else(|p| p.into_inner()).values().any(|s| s.creates_accounts())
    }
    fn creates_accounts_for(&self, product: &str) -> bool {
        self.of(product).is_ok_and(|s| s.creates_accounts())
    }
    /// **The product's sink's own mail first; the HOST's relay when it sends
    /// none.** A linked sink with a mailer (monetize-impl's) answers itself. A
    /// signup GUEST, `NoSink` and a product with no sink send nothing, and a
    /// notice to a customer the host must deliver (g-nonpay, g-renewal) goes
    /// through the host's relay, the same `monetize_mail` the welcome mail uses.
    fn notify(&self, notice: &monetize_pay::CustomerNotice) -> Result<String, SignupError> {
        match self.of(&notice.product).and_then(|s| s.notify(notice)) {
            Err(SignupError::Refused(sink)) => match self.host_mailer_of(Some(&notice.product)) {
                Ok(Some(m)) => {
                    let to = notice.to.trim();
                    if to.is_empty() {
                        return Err(SignupError::Refused(format!("{}/{} has no e-mail address; the {} notice was not sent", notice.product, notice.tenant, notice.kind)));
                    }
                    m.notice(to, &notice.kind, &notice.subject, &notice.body)
                        .map(|reply| format!("{} mail sent to {} through the host's relay {}; the relay accepted it: {reply}", notice.kind, monetize_mail::masked(to), m.relay().describe()))
                        .map_err(SignupError::Unavailable)
                }
                Ok(None) => Err(SignupError::Refused(format!("{sink}, and this host names no relay (secret.smtp_url + secret.smtp_from, or MONETIZE_SMTP_URL + MONETIZE_SMTP_FROM)"))),
                Err(e) => Err(SignupError::Refused(format!("{sink}, and the host's relay is misconfigured: {e}"))),
            },
            other => other,
        }
    }
}

/// What builds a sink from a `[signups.<product>]` block that names a guest
/// (the server's: the plans, the policy and the signing key are its). The
/// block arrives with `product` set from its name.
pub type BuildSink = Arc<dyn Fn(&serde_json::Value) -> Result<Sink, String> + Send + Sync>;

/// **Each `[signups.<product>] guest = …` taken in live** (owner 2026-09-28,
/// holger's base plan; one per product 2026-10-10): when a block — or the
/// module file it names — changes, that product's guest is built through every
/// gate a start passes and swapped into the [`HotSink`]; a block that does not
/// build is REFUSED by name and the sink that stood keeps serving. A block that
/// names no guest changes nothing here (a linked sink is the start's); a guest
/// block that VANISHES leaves its product with no sink — nothing sold, no
/// account made — never with the last guest's.
pub struct SignupReload {
    hot: Arc<HotSink>,
    build: BuildSink,
    last: Mutex<std::collections::BTreeMap<String, String>>,
}

impl SignupReload {
    pub fn new(hot: Arc<HotSink>, build: BuildSink) -> SignupReload {
        SignupReload { hot, build, last: Mutex::new(Default::default()) }
    }

    /// What each product's guest was built from at start, so the first look
    /// does not build it twice.
    pub fn started_from(self, blocks: &std::collections::BTreeMap<String, serde_json::Value>) -> SignupReload {
        *self.last.lock().unwrap_or_else(|p| p.into_inner()) =
            blocks.iter().filter(|(_, b)| monetize_product_wasm::signup::is_guest_block(b)).map(|(p, b)| (p.clone(), Self::key(b))).collect();
        self
    }

    fn key(settings: &serde_json::Value) -> String {
        let of = |p: &str| std::fs::metadata(p).ok().map(|m| (m.len(), m.modified().ok()));
        let module = settings.get("guest").and_then(|g| g.as_str()).unwrap_or_default();
        format!("{settings}\u{0}{:?}\u{0}{:?}", of(module), of(&format!("{module}.p7s")))
    }

    /// One look at every `[signups.<product>]`; answers the lines to log.
    pub fn apply(&self, blocks: &std::collections::BTreeMap<String, serde_json::Value>) -> Vec<String> {
        let mut lines = Vec::new();
        let mut last = self.last.lock().unwrap_or_else(|p| p.into_inner());
        let gone: Vec<String> = last.keys().filter(|p| !blocks.get(*p).is_some_and(monetize_product_wasm::signup::is_guest_block)).cloned().collect();
        for product in gone {
            last.remove(&product);
            self.hot.replace(&product, Arc::new(NoSink));
            lines.push(format!("signup guest REMOVED live [signups.{product}] names no guest any more — product {product} sells no signup"));
        }
        for (product, settings) in blocks.iter().filter(|(_, b)| monetize_product_wasm::signup::is_guest_block(b)) {
            let key = Self::key(settings);
            if last.get(product) == Some(&key) {
                continue;
            }
            last.insert(product.clone(), key);
            match (self.build)(settings) {
                Ok(sink) => {
                    self.hot.replace(product, sink);
                    lines.push(format!("signup guest LOADED live [signups.{product}] — product {product}'s signup sink is replaced, no restart"));
                }
                Err(e) => lines.push(format!("signup guest REFUSED [signup-guest-refused-live] [signups.{product}] {e} — the sink that stood keeps serving")),
            }
        }
        lines
    }
}

/// **The estate `signup`'s account is made on**: `kept` (the estate its `Signup.Start` came
/// from, [`SignupRecord::estate`]), else the declared site whose own account its money went
/// through ([`crate::registry::Registry::estate_of_vendor`]); `None` is the product's home.
pub fn sited(state: &crate::state::AppState, signup: &PaidSignup, kept: Option<String>) -> Option<String> {
    kept.or_else(|| state.registry.estate_of_vendor(&signup.product, &signup.vendor))
}

/// **Record a verified signup and settle it. The whole engine, in one place.**
///
/// One writer (one implementation): the webhook calls it and so does the `Status` poll's
/// retry, so a signup that was created by a retry is created by the same code
/// that would have created it the first time — including the log line and the
/// checks.
///
/// It never fails: every path ends in a [`SignupState`] the caller stores and
/// answers 200 with. A store that cannot be written is the one exception and it
/// leaves the signup PENDING in memory, which is honest — nothing was recorded,
/// so the vendor's next retry will find nothing and try again.
pub fn settle(state: &crate::state::AppState, signup: &PaidSignup) -> SignupState {
    let now = state.now() as i64;
    match state.signups.record_pending(signup, now) {
        // Already decided: **the second webhook does nothing at all**. Not the
        // checks, not the sink, not a log line claiming a state change that did
        // not happen — just the answer the first one arrived at.
        Ok(Recorded::Already(rec)) if !rec.state.is_pending() => return rec.state,
        // Already there and still pending: this is a retry (a vendor
        // re-delivery, or a Status poll). Fall through and ask again.
        Ok(Recorded::Already(_)) => {}
        Ok(Recorded::Fresh) => {}
        Err(e) => {
            skidbladnir::functional_status("monetize-server/signup", &signup.vendor, false, &format!("signup store refused the record: {e}"));
            eprintln!("monetize-server: signup {}:{} could not be recorded: {e}", signup.vendor, signup.session);
            return SignupState::Pending;
        }
    }
    // **One sink call per session at a time.** A background re-settle and the
    // customer's poll can now reach this line together; the loser answers what
    // the row says — PENDING — and calls nothing. See `SignupStore::in_flight`.
    let Some(_flight) = state.signups.fly(&signup.vendor, &signup.session) else {
        return SignupState::Pending;
    };
    // Read again UNDER the flight: the holder before us may have decided this
    // session between `record_pending` above and the flight being free, and a
    // second sink call for a decided signup is exactly what the flight is for.
    let kept = match state.signups.get(&signup.vendor, &signup.session) {
        Ok(Some(rec)) if !rec.state.is_pending() => return rec.state,
        Ok(Some(rec)) => rec.estate,
        _ => None,
    };
    // The estate the account is made on: where the signup began (kept with its record), else
    // the site whose own account took the money; neither is the product's home.
    let estate = sited(state, signup, kept);
    let plans = plans_with(state, &signup.plan);
    let next = match refusal(&plans, signup) {
        Some((refusal, reason)) => SignupState::Refused { reason, at_unix_ms: now, refusal, words: None, owed: owed_for_checks(refusal) },
        None => match state.signup_sink.paid_on(signup, estate.as_deref()) {
            Ok(SignupOutcome::Created { namespace, username }) => SignupState::Created { namespace, username, at_unix_ms: now },
            // The product's own words are the operator's; the customer reads a
            // sentence that tells them what to do.
            Err(SignupError::Refused(reason)) => SignupState::Refused {
                reason,
                at_unix_ms: now,
                refusal: Refusal::Final,
                words: Some(words::COULD_NOT_CREATE.into()),
                // **The sink had already begun**: it creates the principal and
                // then pushes the entitlement, so a refusal here leaves a name
                // spent and money taken. See [`Owed`].
                owed: Owed::RefundAndTheNameMayBeSpent,
            },
            // **The one refusal a customer can get out of.** Recorded as such so
            // `Signup.Rename` can be offered without any page reading prose.
            Err(SignupError::NameTaken(reason)) => SignupState::Refused { reason, at_unix_ms: now, refusal: Refusal::Name, words: None, owed: Owed::RenameOrRefund },
            // **Ours, not theirs.** Stay PENDING and let the next poll ask
            // again — unless it has been pending so long that "again" has
            // stopped meaning anything (see [`PENDING_CEILING_MS`]).
            Err(not_now @ (SignupError::Unavailable(_) | SignupError::Maintenance(_))) => {
                // RA4-20: the product's maintenance by NAME, never by a prefix of its words.
                let (why, maintenance) = match not_now {
                    SignupError::Maintenance(why) => (why, true),
                    SignupError::Unavailable(why) | SignupError::Refused(why) | SignupError::NameTaken(why) => (why, false),
                };
                let recorded = state.signups.get(&signup.vendor, &signup.session).ok().flatten().map(|r| r.recorded_at_unix_ms).unwrap_or(now);
                if now.saturating_sub(recorded) >= PENDING_CEILING_MS {
                    // Not renameable: nothing was ever decided about the name.
                    // The sink stopped answering, and a customer typing a second
                    // name would be sent at the same silence.
                    SignupState::Refused {
                        reason: format!("{NEVER_ANSWERED}: {why}"),
                        at_unix_ms: now,
                        refusal: Refusal::Final,
                        words: Some(words::COULD_NOT_CREATE.into()),
                        // The sink RAN — for a day — and never said what it did.
                        // Whether it got as far as the principal is exactly what
                        // nobody here can know, so an operator is sent to look.
                        owed: Owed::RefundAndTheNameMayBeSpent,
                    }
                } else {
                    // Said once per change of reason, not on every tick.
                    let said = state.signups.waiting_on(&signup.vendor, &signup.session);
                    if SignupStore::says_pending(said.as_deref(), &why) {
                        skidbladnir::functional_status("monetize-server/signup", &signup.vendor, true, &format!("{}:{} stays pending: {why}", signup.vendor, signup.session));
                        eprintln!("monetize-server: signup {}:{} stays pending: {why}", signup.vendor, signup.session);
                    }
                    state.signups.note_waiting(&signup.vendor, &signup.session, Some(NotNow { why, maintenance }));
                    return SignupState::Pending;
                }
            }
        },
    };
    state.signups.note_waiting(&signup.vendor, &signup.session, None);
    if let Err(e) = state.signups.set(&signup.vendor, &signup.session, next.clone()) {
        // The sink already ran. Saying so is all that is left, and it must be
        // loud: this is the shape in which a created account loses its record.
        skidbladnir::functional_status("monetize-server/signup", &signup.vendor, false, &format!("{}:{} decided but not stored: {e}", signup.vendor, signup.session));
        eprintln!("monetize-server: signup {}:{} was decided {next:?} and the store refused it: {e}", signup.vendor, signup.session);
        return next;
    }
    let ok = matches!(next, SignupState::Created { .. });
    if ok {
        // ★ The account's sign-in link, by mail (A11 fix 7): once, here, where the signup is
        // decided; only for a payment this box minted with a welcome page.
        if let SignupState::Created { username, .. } = &next {
            match state.signups.welcome_of(&signup.vendor, &signup.session) {
                Ok(Some(url)) => {
                    let site = estate.as_deref().and_then(|e| state.registry.site_of(e));
                    crate::services::signup::mail_sign_in_link(state, &signup.product, username, &signup.email, &url.replace("{session}", &signup.session), site.as_deref());
                }
                Ok(None) => {}
                Err(e) => eprintln!("monetize-server: signup {}:{} has no welcome page to mail ({e})", signup.vendor, signup.session),
            }
        }
    }
    let line = match &next {
        SignupState::Created { namespace, username, .. } => format!("{}:{} CREATED {username} in {namespace}", signup.vendor, signup.session),
        SignupState::Refused { reason, .. } => format!("{}:{} REFUSED {reason}", signup.vendor, signup.session),
        SignupState::Pending => unreachable!("pending returns before the store write"),
    };
    skidbladnir::functional_status("monetize-server/signup", &signup.vendor, ok, &line);
    next
}

/// **The poll's half of the retry.** `Signup.Status` is read-only about the
/// STORE and still pushes a stuck signup forward, because a customer watching
/// the page should not wait for a poller tick. It is no longer the only thing
/// that does: [`settle_pending`] runs on every poller tick, for the customer
/// who closed the tab. The two meet in [`settle`], one sink call per session.
///
/// Idempotent in the sense the return page needs: a poll of a decided signup
/// reads one row and calls nothing.
pub fn status_with_retry(state: &crate::state::AppState, vendor: &str, session: &str) -> Result<Option<SignupRecord>, StoreError> {
    let Some(rec) = state.signups.get(vendor, session)? else { return Ok(None) };
    if !rec.state.is_pending() {
        return Ok(Some(rec));
    }
    let next = settle(state, &rec.signup);
    Ok(Some(SignupRecord { state: next, ..rec }))
}

/// **The background half of the retry: every PENDING signup, settled again.**
///
/// Until 2026-09-14 a pending signup moved only when the customer's return page
/// polled `Signup.Status`. A customer who paid and closed the tab during a
/// gunnar outage — or a clock-skew refusal, which gunnar clears by re-syncing —
/// left a paid signup stuck until somebody opened that exact URL. The poller
/// calls this on each tick, beside `resume_reference` for purchases, so the
/// interval is `poll_minutes` (5 on the front): slow enough that a gunnar which
/// is down costs one timed-out round per tick rather than a loop of them, fast
/// enough that a customer whose account could be made waits minutes and not a
/// day. The customer on the page still gets the 2 s path.
///
/// Through [`settle`], so the checks, the flight, the ceiling and the log lines
/// are the webhook's own. Returns how many signups it DECIDED this pass.
pub fn settle_pending(state: &crate::state::AppState) -> usize {
    let pending = match state.signups.pending() {
        Ok(v) => v,
        Err(e) => {
            eprintln!("monetize-server: signup store pending scan: {e}");
            return 0;
        }
    };
    pending.iter().filter(|rec| !settle(state, &rec.signup).is_pending()).count()
}

/// ★ **How long a paid account may settle before the buyer is told it is taking longer**
/// (PAID-WORDS): two minutes. A pulled product's main makes the account on its next call,
/// seconds; past this something is slow, and the buyer reads so, with the payment safe.
pub const SETTLE_BOUND_MS: i64 = 2 * 60 * 1000;

/// **A product's "not now" for a paid signup**: its words (the operator's) and whether it said,
/// by name, that it is in maintenance ([`SignupError::Maintenance`], RA4-20).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotNow {
    /// The product's words, for the operator; never the buyer's.
    pub why: String,
    /// The product said [`SignupError::Maintenance`].
    pub maintenance: bool,
}

/// Pure: **the sentence a PENDING signup shows**: `maintenance` whether the product's last "not
/// now" was its maintenance, by name ([`SignupError::Maintenance`]); `waited_ms` since the
/// payment was recorded; `mailed` whether its sign-in link is mailed once the account is made.
pub fn pending_words(maintenance: bool, waited_ms: i64, mailed: bool) -> &'static str {
    if maintenance {
        words::MAINTENANCE
    } else if waited_ms >= SETTLE_BOUND_MS {
        if mailed {
            words::TAKING_LONGER_MAILED
        } else {
            words::TAKING_LONGER
        }
    } else {
        words::BEING_CREATED
    }
}

/// ★ RA4-19: **the sentence a PENDING record sends AHEAD for the shop's silence**, and in how many
/// ms it is due: what the return page shows once that long has passed with no answer — the
/// settle bound's "taking longer" ([`SETTLE_BOUND_MS`], from the payment), or the product's
/// maintenance. `("", 0)` for any other state: the page keeps no bound and no words of its own.
pub fn words_ahead(store: &SignupStore, rec: &SignupRecord, now_ms: i64) -> (String, i64) {
    let SignupState::Pending = &rec.state else { return (String::new(), 0) };
    let (vendor, session) = (&rec.signup.vendor, &rec.signup.session);
    let mailed = matches!(store.welcome_of(vendor, session), Ok(Some(_)));
    let maintenance = store.not_now(vendor, session).is_some_and(|n| n.maintenance);
    let waited = now_ms.saturating_sub(rec.recorded_at_unix_ms);
    (pending_words(maintenance, waited.max(SETTLE_BOUND_MS), mailed).to_owned(), SETTLE_BOUND_MS.saturating_sub(waited).max(0))
}

/// **The sentence `Signup.Status` shows the customer for a record** at `now_ms`, and the
/// `reason` it may carry — see [`words`] for why these are not the operator's.
pub fn customer_words(store: &SignupStore, rec: &SignupRecord, now_ms: i64) -> (String, String) {
    match &rec.state {
        SignupState::Pending => {
            let (vendor, session) = (&rec.signup.vendor, &rec.signup.session);
            let mailed = matches!(store.welcome_of(vendor, session), Ok(Some(_)));
            let w = pending_words(store.not_now(vendor, session).is_some_and(|n| n.maintenance), now_ms.saturating_sub(rec.recorded_at_unix_ms), mailed);
            (w.to_owned(), String::new())
        }
        SignupState::Created { .. } => (words::READY.to_owned(), String::new()),
        SignupState::Refused { words: Some(w), .. } => (w.clone(), w.clone()),
        SignupState::Refused { reason, words: None, .. } => (reason.clone(), reason.clone()),
    }
}

/// Why a rename did not happen. Every arm is a different sentence to a customer
/// who has already paid, which is why none of them is a `String`.
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum RenameRefusal {
    /// Nobody has heard of this `(vendor, session)`.
    Unknown,
    /// The record is not one a different name can finish: it is still pending,
    /// or it was refused over the ORDER — a plan nobody sells, an amount that is
    /// not the plan's price. The string says which.
    NotRenameable(String),
    /// The NEW name will not be accepted either. The customer types another.
    Name(String),
    /// The sink could not be asked. Ours, not theirs; the record is untouched
    /// and the same request works when it comes back.
    Unavailable(String),
}

/// **Finish a signup that was refused over its name, with a different name.**
///
/// The money is already at the vendor and the alternative is a refund by hand,
/// so this exists for exactly one shape of row: `Refused` with
/// [`Refusal::Name`]. Everything else is refused here rather than re-run —
/// re-running a FINAL refusal would charge the same customer's already-charged
/// card against an order that is still wrong.
///
/// It goes through the same [`refusal`] checks and the same sink as [`settle`],
/// in the same order, for the reason `settle` gives about having one writer: a
/// customer created by a rename must be created by the code that would have
/// created them the first time, including the log line.
///
/// **Idempotent per session.** A rename of a signup that is already CREATED
/// answers what it became and runs nothing — a return page that posts twice
/// (a double tap, a retry over a dropped connection) must not push a second
/// name at a sink that has already made an account.
pub fn rename(state: &crate::state::AppState, vendor: &str, session: &str, username: &str) -> Result<SignupRecord, RenameRefusal> {
    let now = state.now() as i64;
    // The same one-call-per-session rule as `settle`, taken BEFORE the row is
    // read: a double-tapped rename is two sink calls for one payment otherwise,
    // and the second can find the first one's principal and call it taken.
    let Some(_flight) = state.signups.fly(vendor, session) else {
        return Err(RenameRefusal::Unavailable(format!("{vendor}:{session} is already being settled by another call")));
    };
    let rec = match state.signups.get(vendor, session) {
        Ok(Some(rec)) => rec,
        Ok(None) => return Err(RenameRefusal::Unknown),
        Err(e) => return Err(RenameRefusal::Unavailable(format!("signup store: {e}"))),
    };
    // **A refunded payment finishes no signup** (g-refund-verb): an operator
    // who refunded a RenameOrRefund row gave the money back, and an account
    // created against it afterwards would be an account nobody paid for. A
    // refund merely REQUESTED counts too — the vendor may already have paid it.
    match state.signups.refund_of(vendor, session) {
        Ok(None) => {}
        Ok(Some(_)) => {
            return Err(RenameRefusal::NotRenameable(
                "This payment has been refunded, so it cannot finish a signup any more. A new signup is a new payment.".into(),
            ))
        }
        Err(e) => return Err(RenameRefusal::Unavailable(format!("signup store: {e}"))),
    }
    match &rec.state {
        // Already done. Answer what it became; touch nothing.
        SignupState::Created { .. } => return Ok(rec),
        SignupState::Pending => {
            return Err(RenameRefusal::NotRenameable(
                "this signup has not been decided yet; nothing has been refused and there is nothing to rename".into(),
            ))
        }
        // The CUSTOMER's words for the refusal, because the page shows this
        // message: a product's refusal must not reach them as the product said it.
        SignupState::Refused { refusal, .. } if !refusal.renameable() => {
            return Err(RenameRefusal::NotRenameable(customer_words(&state.signups, &rec, state.now() as i64).1))
        }
        SignupState::Refused { .. } => {}
    }

    // The shape rule first, because it names the offending character, and then
    // the sink — which is the only thing that knows what this deployment has
    // already spoken for.
    if let Some(why) = monetize_pay::signup::username_refusal(username) {
        return Err(RenameRefusal::Name(why));
    }
    let estate = sited(state, &rec.signup, rec.estate.clone());
    match state.signup_sink.name_verdict_on(&rec.signup.product, username, estate.as_deref()) {
        Ok(NameVerdict::Free) => {}
        Ok(NameVerdict::Taken(why)) => return Err(RenameRefusal::Name(why)),
        Err(SignupError::Unavailable(why) | SignupError::Maintenance(why)) => return Err(RenameRefusal::Unavailable(why)),
        // A sink that answers a REFUSAL to "is this name free" is answering a
        // question nobody asked; take it as the refusal it is rather than
        // pretending the name was free.
        Err(other) => return Err(RenameRefusal::Name(other.to_string())),
    }

    let renamed = PaidSignup { username: username.to_owned(), ..rec.signup.clone() };
    let next = match refusal(&plans_with(state, &renamed.plan), &renamed) {
        Some((refusal, reason)) => SignupState::Refused { reason, at_unix_ms: now, refusal, words: None, owed: owed_for_checks(refusal) },
        None => match state.signup_sink.paid_on(&renamed, estate.as_deref()) {
            Ok(SignupOutcome::Created { namespace, username }) => SignupState::Created { namespace, username, at_unix_ms: now },
            Err(SignupError::Refused(reason)) => SignupState::Refused {
                reason,
                at_unix_ms: now,
                refusal: Refusal::Final,
                words: Some(words::COULD_NOT_CREATE.into()),
                // **The sink had already begun**: it creates the principal and
                // then pushes the entitlement, so a refusal here leaves a name
                // spent and money taken. See [`Owed`].
                owed: Owed::RefundAndTheNameMayBeSpent,
            },
            Err(SignupError::NameTaken(reason)) => SignupState::Refused { reason, at_unix_ms: now, refusal: Refusal::Name, words: None, owed: Owed::RenameOrRefund },
            // The record still says what it said; the customer tries again.
            Err(SignupError::Unavailable(why) | SignupError::Maintenance(why)) => return Err(RenameRefusal::Unavailable(why)),
        },
    };

    // **The row carries the new name whatever the outcome was**, because the
    // name is what the customer asked for and the console shows it beside the
    // refusal. A row left holding the old one would show an operator a name
    // nobody now wants.
    if let Err(e) = state.signups.rewrite(vendor, session, &renamed, next.clone()) {
        skidbladnir::functional_status("monetize-server/signup", vendor, false, &format!("{vendor}:{session} renamed but not stored: {e}"));
        eprintln!("monetize-server: signup {vendor}:{session} was renamed to {username} and the store refused it: {e}");
        return Err(RenameRefusal::Unavailable(format!("signup store: {e}")));
    }
    let ok = matches!(next, SignupState::Created { .. });
    let line = match &next {
        SignupState::Created { namespace, username, .. } => {
            format!("{vendor}:{session} RENAMED {:?} -> {username} CREATED in {namespace}", rec.signup.username)
        }
        SignupState::Refused { reason, .. } => format!("{vendor}:{session} RENAMED {:?} -> {username} REFUSED {reason}", rec.signup.username),
        SignupState::Pending => unreachable!("a rename never leaves a signup pending"),
    };
    skidbladnir::functional_status("monetize-server/signup", vendor, ok, &line);
    Ok(SignupRecord { signup: renamed, state: next, recorded_at_unix_ms: rec.recorded_at_unix_ms, estate: rec.estate.clone(), card: rec.card.clone() })
}

/// **Every CREATED paid signup, as the sale a tax return has to count.**
///
/// Until 2026-09-15 the tax report folded the ledger only, and a signup writes
/// no ledger row (it has no reference and no tenant until the sink answers —
/// see this module's header). So every paid signup was money taken and never
/// declared: the momsdeklaration read 0.00 SEK in every box after a real
/// 125.00 SEK signup.
///
/// The store IS the record of that money, keyed `(vendor, session)`, so the fold
/// reads it rather than inventing a ledger row: one sale per session, whatever
/// number of webhooks, polls or reconciler passes handed it up — a replay cannot
/// add a second. The reference is `signup:<vendor>:<session>` (traceable to the
/// vendor's dashboard), the date is when the money moved, the net is the
/// vendor's total minus the tax it collected, and the rate is the one that
/// charge actually carried. A REFUSED signup is not a sale: its money is owed
/// back by hand, which the refund makes true — and since 2026-09-18 that hand is
/// counted rather than assumed. See [`SignupStore::owed_back`] and [`Owed`].
///
/// **A CREATED signup refunded with `force`** ([`refund`], g-refund-verb, or
/// by the vendor) stays declared as charged, in the period it was paid in — a
/// return that may already be filed — and what went back is a correction
/// ([`monetize::tax::TaxSale::credit`]) dated when the refund was DONE, in
/// that period (stinky row 17, 2026-10-10): the share of the gross, its tax
/// the same fraction. A refused signup was never declared, so its refund
/// changes nothing.
pub fn tax_sales(store: &SignupStore, books: &monetize::Books) -> Result<Vec<monetize::tax::TaxSale>, StoreError> {
    let refunded: HashMap<(String, String), (u64, i64)> = store
        .export_refunds()?
        .into_iter()
        .filter_map(|(v, s, r)| match r {
            SignupRefund::Done { amount, at_unix_ms, .. } => Some(((v, s), (amount.minor, at_unix_ms))),
            _ => None,
        })
        .collect();
    Ok(store
        .export_all()?
        .into_iter()
        .filter(|rec| matches!(rec.state, SignupState::Created { .. }))
        // An account at the shop sold nothing: no tax line.
        .filter(|rec| rec.signup.vendor != ACCOUNT_VENDOR)
        .flat_map(|rec| {
            let s = rec.signup;
            let net = monetize_pay::Money { minor: s.amount.minor.saturating_sub(s.tax.minor), currency: s.amount.currency };
            let basis_points = if net.minor == 0 { 0 } else { ((u128::from(s.tax.minor) * 10_000 + u128::from(net.minor) / 2) / u128::from(net.minor)) as u32 };
            let charged = monetize::tax::TaxSale {
                reference: format!("signup:{}:{}", s.vendor, s.session),
                at_unix_ms: s.paid_at_unix_ms.max(0) as u64,
                net,
                basis_points,
                tax: s.tax,
                // **The buyer's billing country** (stinky row 15, 2026-10-10):
                // where the VAT is owed. The currency's market is only the
                // fallback for a row stored before the vendor's country was
                // kept, or a vendor that does not say — EUR is FI's currency
                // and every other euro country's too.
                customer_country: s
                    .customer_country
                    .as_deref()
                    .map(str::trim)
                    .filter(|c| c.len() == 2 && c.is_ascii())
                    .map(str::to_ascii_uppercase)
                    .unwrap_or_else(|| s.amount.currency.market().country().to_owned()),
                customer_tax_id: None,
                // The vendor's fee on a signup is not read back yet; `None` is
                // "not reported", never zero.
                fee: None,
                fee_country: Some(books.country_of(&s.vendor)),
                cost: None,
                cost_country: None,
                credit: false,
            };
            let back = refunded.get(&(s.vendor.clone(), s.session.clone())).and_then(|(back, at)| charged.correction((*at).max(0) as u64, 0, *back));
            std::iter::once(charged).chain(back)
        })
        .collect())
}

/// Why [`refund`] or [`settle_by_hand`] did not do it. Each arm is its own
/// gRPC code (`services::signup`), because an operator does something
/// different about each.
#[derive(Debug, PartialEq, Eq)]
pub enum RefundRefusal {
    /// The store holds no such session.
    Unknown,
    /// Not a row this verb may act on — pending, created without `force`,
    /// already refunded or settled, its vendor not configured — or the vendor
    /// said no. The sentence says which, and what to do.
    NotRefundable(String),
    /// The request itself is wrong: more than was paid, no reason given.
    Invalid(String),
    /// The vendor or the store did not answer; the same call again is safe.
    Unavailable(String),
}

/// **Give a signup's money back, through the vendor that took it** —
/// `SignupAdmin.Refund` (g-refund-verb, 2026-09-28).
///
/// Until this existed a customer charged for a signup that was then refused
/// (name taken, grant failed) could only be refunded by hand in the vendor's
/// dashboard, and the owed-back alarm could never go green.
///
/// # What it acts on, and what `force` means
///
/// A REFUSED signup — every row [`SignupStore::owed_back`] lists. A PENDING
/// one never: it may still become an account, and refunding it would make an
/// account nobody paid for. A CREATED one only with `force` — the customer's
/// right of withdrawal, a goodwill refund: the money goes back and the
/// ACCOUNT STAYS (taking it away is `Entitlement.Set`, by hand, or nonpay's purge);
/// [`tax_sales`] stops declaring what was refunded.
///
/// # Order, and why
///
/// 1. Every refusal first — the row, the amount, the vendor — so a refusal
///    writes nothing and calls nobody.
/// 2. [`SignupRefund::Requested`] committed. From here the store says "we may
///    have asked", which is true.
/// 3. The vendor asked, with [`refund_key`]'s key.
/// 4. [`SignupRefund::Done`] committed.
///
/// A `Requested` row found at step 2 is a refund whose answer was never
/// written down (a lost reply, a crash): it is asked AGAIN with its RECORDED
/// amount, which is the same key, and the vendor answers the first refund. A
/// `Done` one is answered and nobody is called. One call per session at a
/// time: it takes the same flight as the sink ([`SignupStore::fly`]).
///
/// A vendor that says NO (a decline, no record of the payment) leaves the row
/// `Requested` — nothing here can know it did not half-happen — and the
/// sentence sends the operator to the dashboard and [`settle_by_hand`].
pub fn refund(
    state: &crate::state::AppState,
    vendor: &str,
    session: &str,
    amount_minor: u64,
    reason: &str,
    force: bool,
) -> Result<SignupRefund, RefundRefusal> {
    let reason = reason.trim();
    if reason.is_empty() {
        return Err(RefundRefusal::Invalid("a refund names its reason (the operator's words, recorded with it)".into()));
    }
    let Some(_flight) = state.signups.fly(vendor, session) else {
        return Err(RefundRefusal::Unavailable(format!("{vendor}:{session} is being settled by another call; ask again")));
    };
    let store = |e: StoreError| RefundRefusal::Unavailable(format!("signup store: {e}"));
    let rec = state.signups.get(vendor, session).map_err(store)?.ok_or(RefundRefusal::Unknown)?;
    let paid = rec.signup.amount;
    let (amount, requested_at, forced) = match state.signups.refund_of(vendor, session).map_err(store)? {
        Some(done @ SignupRefund::Done { .. }) => {
            let SignupRefund::Done { amount, .. } = &done else { unreachable!() };
            if amount_minor != 0 && amount_minor != amount.minor {
                return Err(RefundRefusal::NotRefundable(format!(
                    "{vendor}:{session} was already refunded {}; one refund per payment through this verb (ask for 0 or {} to read it back)",
                    amount.major(),
                    amount.minor
                )));
            }
            return Ok(done);
        }
        Some(SignupRefund::SettledByHand { note, .. }) => {
            return Err(RefundRefusal::NotRefundable(format!("{vendor}:{session} was settled by hand ({note}); nothing is owed through this verb")))
        }
        // **The retry.** The recorded amount, whatever was asked now: another
        // amount would be another key, and a second refund.
        Some(SignupRefund::Requested { amount, at_unix_ms, forced, .. }) => {
            if amount_minor != 0 && amount_minor != amount.minor {
                return Err(RefundRefusal::NotRefundable(format!(
                    "a refund of {} is already REQUESTED for {vendor}:{session} and its answer was never recorded; finish it first \
                     (ask for 0 or {}), or check the vendor's dashboard and Settle",
                    amount.major(),
                    amount.minor
                )));
            }
            (amount, at_unix_ms, forced)
        }
        None => {
            match &rec.state {
                SignupState::Pending => {
                    return Err(RefundRefusal::NotRefundable(format!(
                        "{vendor}:{session} is PENDING: it may still become an account, so it is not owed back; nothing was refunded"
                    )))
                }
                SignupState::Created { namespace, .. } if !force => {
                    return Err(RefundRefusal::NotRefundable(format!(
                        "{vendor}:{session} is CREATED (namespace {namespace:?}): the customer has the account they paid for. \
                         To refund it anyway pass force — the account stays; remove it with Entitlement by hand"
                    )))
                }
                SignupState::Created { .. } | SignupState::Refused { .. } => {}
            }
            let amount = if amount_minor == 0 { paid } else { monetize_pay::Money { minor: amount_minor, currency: paid.currency } };
            if amount.minor > paid.minor {
                return Err(RefundRefusal::Invalid(format!("{} is more than the {} {vendor}:{session} paid", amount.major(), paid.major())));
            }
            if amount.minor == 0 {
                return Err(RefundRefusal::NotRefundable(format!("{vendor}:{session} paid nothing; there is nothing to refund")));
            }
            (amount, state.now() as i64, matches!(rec.state, SignupState::Created { .. }))
        }
    };
    // The vendor BEFORE the record: a vendor this server does not have is a
    // refusal that must leave nothing behind.
    let Some(pay) = state.registry.find_vendor(vendor) else {
        return Err(RefundRefusal::NotRefundable(format!(
            "vendor {vendor:?} is not configured on this server (linked: {:?}); refund {vendor}:{session} at that vendor by hand, then Settle",
            state.registry.vendor_names()
        )));
    };
    state
        .signups
        .claim_refund(vendor, session, &SignupRefund::Requested { amount, reason: reason.to_owned(), at_unix_ms: requested_at, forced })
        .map_err(store)?;
    let key = refund_key(session, amount.minor);
    let handle = monetize_pay::ChargeHandle { vendor: vendor.to_owned(), id: session.to_owned() };
    let answer = pay.refund(&handle, amount);
    let line = |what: &str| format!("{vendor}:{session} REFUND {} ({key}) {what}; reason: {reason}", amount.major());
    match answer {
        Ok(_) => {
            let done = SignupRefund::Done {
                refund_id: key.clone(),
                amount,
                reason: reason.to_owned(),
                requested_at_unix_ms: requested_at,
                at_unix_ms: state.now() as i64,
                forced,
            };
            if let Err(e) = state.signups.put_refund(vendor, session, &done) {
                // The vendor refunded; the record says Requested. The next call
                // re-asks under the same key and completes it.
                let l = line(&format!("DONE at the vendor and NOT recorded ({e}); the same call again records it"));
                skidbladnir::functional_status("monetize-server/signup", vendor, false, &l);
                eprintln!("monetize-server: signup {l}");
                return Err(RefundRefusal::Unavailable(format!("the vendor refunded and the store refused the record: {e}; ask again")));
            }
            let l = line("DONE");
            skidbladnir::functional_status("monetize-server/signup", vendor, true, &l);
            eprintln!("monetize-server: signup {l}");
            Ok(done)
        }
        Err(e) => {
            let l = line(&format!("NOT CONFIRMED, stays REQUESTED: {e}"));
            skidbladnir::functional_status("monetize-server/signup", vendor, false, &l);
            eprintln!("monetize-server: signup {l}");
            Err(match e {
                monetize_pay::PayError::Unavailable(why) => RefundRefusal::Unavailable(format!(
                    "the vendor did not answer ({why}); the refund of {} stays REQUESTED and the same call again finishes it without refunding twice",
                    amount.major()
                )),
                other => RefundRefusal::NotRefundable(format!(
                    "the vendor did not refund {vendor}:{session} ({other}); it stays REQUESTED. Check the vendor's dashboard: refund it there if \
                     it is not, then Settle with a note"
                )),
            })
        }
    }
}

/// **Close out a refused signup refunded BY HAND** at the vendor's dashboard
/// — `SignupAdmin.Settle`. It moves no money; it records the operator's
/// `note` and takes the row off the owed-back queue. Only a REFUSED row, and
/// one with no refund recorded or one still `Requested` (the vendor never
/// confirmed it, and the operator has looked); never over a `Done` one.
pub fn settle_by_hand(state: &crate::state::AppState, vendor: &str, session: &str, note: &str) -> Result<SignupRefund, RefundRefusal> {
    let note = note.trim();
    if note.is_empty() {
        return Err(RefundRefusal::Invalid("a settle says what was done (the vendor's refund id, who did it)".into()));
    }
    let Some(_flight) = state.signups.fly(vendor, session) else {
        return Err(RefundRefusal::Unavailable(format!("{vendor}:{session} is being settled by another call; ask again")));
    };
    let store = |e: StoreError| RefundRefusal::Unavailable(format!("signup store: {e}"));
    let rec = state.signups.get(vendor, session).map_err(store)?.ok_or(RefundRefusal::Unknown)?;
    if rec.state.owed().is_none() {
        return Err(RefundRefusal::NotRefundable(format!("{vendor}:{session} is not owed back (it is not REFUSED); there is nothing to settle")));
    }
    match state.signups.refund_of(vendor, session).map_err(store)? {
        None | Some(SignupRefund::Requested { .. }) => {}
        Some(other) => return Err(RefundRefusal::NotRefundable(format!("{vendor}:{session} is already settled: {other:?}"))),
    }
    let settled = SignupRefund::SettledByHand { note: note.to_owned(), at_unix_ms: state.now() as i64 };
    state.signups.put_refund(vendor, session, &settled).map_err(store)?;
    let l = format!("{vendor}:{session} SETTLED BY HAND by the operator: {note}");
    skidbladnir::functional_status("monetize-server/signup", vendor, true, &l);
    eprintln!("monetize-server: signup {l}");
    Ok(settled)
}

/// Why [`resend_welcome`] sent nothing.
#[derive(Debug, PartialEq, Eq)]
pub enum ResendRefusal {
    /// The store holds no such session.
    Unknown,
    /// Not CREATED, or the sink cannot send a welcome (no relay, no address).
    NotSendable(String),
    /// The store or the relay did not answer; asking again is fine.
    Unavailable(String),
}

/// **The operator's door: send a CREATED signup's welcome mail again.**
///
/// Measured 2026-09-15: a paid signup was created and its welcome mail was
/// refused by the relay (a transport that never negotiated STARTTLS). The
/// account existed; the mail was the one thing missing, and nothing could send
/// it again short of a second payment. This is that door.
///
/// Only a CREATED record, and with the username the product SETTLED on (a
/// renamed signup's welcome names the account it has, not the one it asked
/// for). Idempotence and the per-attempt audit record are the sink's — it is
/// the one that knows whether its mail went out — and this adds the operator's
/// line to the log and to `functional_status`.
pub fn resend_welcome(state: &crate::state::AppState, vendor: &str, session: &str, force: bool) -> Result<String, ResendRefusal> {
    let rec = match state.signups.get(vendor, session) {
        Ok(Some(rec)) => rec,
        Ok(None) => return Err(ResendRefusal::Unknown),
        Err(e) => return Err(ResendRefusal::Unavailable(format!("signup store: {e}"))),
    };
    let SignupState::Created { username, .. } = &rec.state else {
        return Err(ResendRefusal::NotSendable(format!("{vendor}:{session} is not CREATED ({:?}); there is no account to welcome anybody to", rec.state)));
    };
    let as_created = PaidSignup { username: username.clone(), ..rec.signup.clone() };
    let out = state.signup_sink.resend_welcome(&as_created, force);
    let (ok, line) = match &out {
        Ok(what) => (true, format!("{vendor}:{session} WELCOME RESENT by the operator (force={force}): {what}")),
        Err(e) => (false, format!("{vendor}:{session} WELCOME NOT RESENT by the operator (force={force}): {e}")),
    };
    skidbladnir::functional_status("monetize-server/signup", vendor, ok, &line);
    eprintln!("monetize-server: signup {line}");
    out.map_err(|e| match e {
        SignupError::Unavailable(why) | SignupError::Maintenance(why) => ResendRefusal::Unavailable(why),
        SignupError::Refused(why) | SignupError::NameTaken(why) => ResendRefusal::NotSendable(why),
    })
}

#[cfg(test)]
pub(crate) mod tests;

/// **A paid sign-up's line on the vendor's page** (A16, owner 2026-10-23: never a plan id):
/// `Holger — 5.0 GiB storage, 1 month — 100.00 SEK`.
pub fn signup_checkout_words(product: &str, plan: &crate::config::SignupPlan) -> String {
    let mut name = product.trim().to_owned();
    if let Some(first) = name.get_mut(0..1) {
        first.make_ascii_uppercase();
    }
    let period = match plan.period_days {
        0 => String::new(),
        d if d % 30 == 0 && d / 30 == 1 => ", 1 month".to_owned(),
        d if d % 30 == 0 && d <= 360 => format!(", {} months", d / 30),
        365 => ", 12 months".to_owned(),
        d => format!(", {d} days"),
    };
    format!("{name} — {} storage{period} — {}", monetize_product::bytes(plan.cap_bytes), plan.price().major())
}

/// **The meter a signup's `cap_bytes` lands on** — the private sink's
/// `DEFAULT_CAP_METER` (`monetize-signup-impl`), gunnar's pack quota.
pub const SIGNUP_CAP_METER: &str = "pack_bytes";

/// The newest CREATED signup per `(product, tenant)`, the tenant being the
/// namespace the sink made. One row per vendor session in the store, so a
/// replayed webhook, a poll or a reconciler pass cannot add a second.
fn created_signups(store: &SignupStore, product: &str) -> Result<Vec<(SignupRecord, String, i64)>, StoreError> {
    let mut newest: std::collections::BTreeMap<String, (SignupRecord, String, i64)> = std::collections::BTreeMap::new();
    for rec in store.export_all()? {
        if !product.is_empty() && rec.signup.product != product {
            continue;
        }
        let SignupState::Created { namespace, at_unix_ms, .. } = &rec.state else { continue };
        let (tenant, at) = (namespace.clone(), *at_unix_ms);
        let key = format!("{}\u{0}{tenant}", rec.signup.product);
        match newest.get(&key) {
            Some((old, _, _)) if old.signup.paid_at_unix_ms >= rec.signup.paid_at_unix_ms => {}
            _ => {
                newest.insert(key, (rec, tenant, at));
            }
        }
    }
    Ok(newest.into_values().collect())
}

/// **The entitlement a CREATED paid signup gave its tenant**, as monetize's own
/// signed fact — the shape the sink pushed to the product (tenant, the plan's
/// `pack_bytes` cap, paid-until = paid_at + period, `payment:<vendor>:<ref>`).
///
/// Until 2026-09-15 a signup wrote no entitlement row (it writes no ledger row
/// at all — see this module's header), so `Entitlement.List` did not know the
/// cap the appliance was enforcing and the console listed a paying tenant as
/// "no cap". Folded from the store at read time rather than written at settle,
/// so a signup created before this fix reads correctly too. A plan the config
/// no longer names contributes nothing: a cap is never invented.
pub fn signup_facts(state: &crate::state::AppState, product: &str) -> Result<Vec<monetize_product::EntitlementFact>, StoreError> {
    let now = state.now();
    facts_at(state, product, Some(now))
}

/// **The fact the SINK PUSHED, exactly as it pushed it** — the same fold, with
/// the state pinned to `Paid` instead of re-derived at `now`.
///
/// `monetize_signup_impl::GunnarSignup::fact_for` says why that pin is the
/// truth and not a shortcut: *"at signup `paid_until` is always in the future,
/// and `derive_state` answers Paid for every policy there is"*. So this is
/// what the appliance was told, and it is what [`crate::poller`] seeds the
/// ledger with when it adopts a signup-granted tenant — the ladder can only
/// MOVE a tenant off a state it knows they were last put in, and re-deriving
/// it at `now` would hand the ladder the answer it is supposed to compute and
/// leave the appliance holding a fact nobody ever replaced.
pub fn granted_facts(state: &crate::state::AppState, product: &str) -> Result<Vec<monetize_product::EntitlementFact>, StoreError> {
    facts_at(state, product, None)
}

/// The shared fold. `now = None` pins the state to `Paid` — see [`granted_facts`].
fn facts_at(state: &crate::state::AppState, product: &str, now: Option<u64>) -> Result<Vec<monetize_product::EntitlementFact>, StoreError> {
    Ok(created_signups(&state.signups, product)?
        .into_iter()
        .filter_map(|(rec, tenant, _)| {
            let plan = plan_named(state, &rec.signup.plan)?;
            let meter = state.signup_plans.cap_meter_of(product);
            let s = &rec.signup;
            let tenant = monetize_product::TenantId(tenant);
            let paid_at = s.paid_at_unix_ms.max(0) as u64;
            if is_account_plan(&plan) {
                // An account at the shop: Free, no end, no room.
                return Some(account_fact(state.signer_for(&s.product), &s.product, tenant, paid_at, meter, &s.vendor));
            }
            let paid_until = paid_at + u64::from(plan.period_days) * 24 * 60 * 60 * 1000;
            let caps = std::collections::BTreeMap::from([(meter, plan.cap_bytes)]);
            let reference = monetize::transaction::reference_of(&s.product, &tenant, paid_at, &caps);
            Some(state.signer_for(&s.product).sign_fact(monetize_product::EntitlementFact {
                tenant,
                plan: reference.clone(),
                state: match now {
                    Some(now) => monetize::derive_state(&state.terms.read().unwrap_or_else(|e| e.into_inner()).policy, Some(paid_until), now),
                    None => monetize_product::State::Paid,
                },
                paid_until_unix_ms: Some(paid_until),
                caps,
                source: format!("payment:{}:{reference}", s.vendor),
                signature: Vec::new(),
                issued_unix_ms: None,
                issued_signature: Vec::new(),
            }))
        })
        .collect())
}

/// **The ledger's facts, with every signup-granted tenant the ledger does not
/// hold added.** The ledger wins for a tenant it has a fact for: that is a
/// later purchase or an operator's hand, and either replaced the signup's.
pub fn with_signup_facts(
    state: &crate::state::AppState,
    product: &str,
    mut facts: Vec<monetize_product::EntitlementFact>,
) -> Result<Vec<monetize_product::EntitlementFact>, StoreError> {
    for f in signup_facts(state, product)? {
        if !facts.iter().any(|held| held.tenant == f.tenant) {
            facts.push(f);
        }
    }
    Ok(facts)
}

/// **Every CREATED paid signup as a "Recently bought" line** — one per vendor
/// session, `signup:<vendor>:<session>` (the reference [`tax_sales`] declares),
/// price = the vendor's total minus the tax it collected, the plan's cap as both
/// the caps and the delta, `granted`, drawn. A REFUSED signup is not a sale.
/// `product` empty = every product.
pub fn signup_sales(state: &crate::state::AppState, product: &str) -> Result<Vec<monetize_proto::Sale>, StoreError> {
    Ok(created_signups(&state.signups, product)?
        .into_iter()
        // An account at the shop sold nothing: no money, no line.
        .filter(|(rec, _, _)| rec.signup.vendor != ACCOUNT_VENDOR)
        .map(|(rec, tenant, at)| {
            let s = rec.signup;
            let caps: std::collections::BTreeMap<String, u64> = plan_named(state, &s.plan)
                .map(|p| std::collections::BTreeMap::from([(state.signup_plans.cap_meter_of(&s.product), p.cap_bytes)]))
                .unwrap_or_default();
            let net = monetize_pay::Money { minor: s.amount.minor.saturating_sub(s.tax.minor), currency: s.amount.currency };
            monetize_proto::Sale {
                reference: format!("signup:{}:{}", s.vendor, s.session),
                product: s.product.clone(),
                tenant,
                price: Some(crate::convert::money_to_pb(net)),
                // Nothing was bought at a cloud for a signup: the room was on hand.
                cost: Some(crate::convert::money_to_pb(monetize_pay::Money { minor: 0, currency: s.amount.currency })),
                // The vendor's fee on a signup is not read back yet: absent, never zero.
                fee: None,
                // The tax the VENDOR collected on it (Stripe's amount_tax), per
                // order; the rate is what that figure is of the net.
                tax: Some(crate::convert::money_to_pb(s.tax)),
                tax_from_vendor: true,
                vat_basis_points: if net.minor > 0 { ((s.tax.minor as u128 * 10_000 + net.minor as u128 / 2) / net.minor as u128) as u32 } else { 0 },
                delta: caps.clone(),
                caps,
                started_unix_ms: s.paid_at_unix_ms.max(0) as u64,
                at_unix_ms: at.max(0) as u64,
                steps: 0,
                drawn: true,
                outcome: "granted".into(),
                reason: String::new(),
                needs_you: false,
                // The session's own mode marker (Stripe's `cs_live_`/`cs_test_`).
                livemode: monetize_pay::livemode_of(&s.vendor, &s.session),
                // The buyer's country as the vendor returned it (Stripe Tax).
                customer_country: s.customer_country.clone().unwrap_or_default(),
            }
        })
        .collect())
}

/// **The entitlement a paid signup grants**, SIGNED — what a signup guest
/// delivers to the product (`monetize_product_wasm::signup`). The same shape
/// [`granted_facts`] folds for a created signup: the plan's cap on
/// [`SIGNUP_CAP_METER`], paid until `paid_at + period_days`, the state derived
/// by the deployment's policy at `now`. `Err` when the plan is not one this
/// deployment sells.
pub fn signup_fact(
    plans: &std::collections::BTreeMap<String, crate::config::SignupPlan>,
    policy: &monetize::Policy,
    signer: &monetize::Signer,
    s: &PaidSignup,
    now_ms: u64,
    cap_meter: &str,
) -> Result<monetize_product::EntitlementFact, String> {
    let plan = plans.get(&s.plan).ok_or_else(|| format!("plan {:?} is not in this deployment's [signups.<product>.plans]", s.plan))?;
    let tenant = monetize_product::TenantId(s.username.clone());
    let paid_at = s.paid_at_unix_ms.max(0) as u64;
    if is_account_plan(plan) {
        let _ = (policy, now_ms);
        return Ok(account_fact(signer, &s.product, tenant, paid_at, cap_meter.to_owned(), &s.vendor));
    }
    let paid_until = paid_at + u64::from(plan.period_days) * 24 * 60 * 60 * 1000;
    let caps = std::collections::BTreeMap::from([(cap_meter.to_owned(), plan.cap_bytes)]);
    let reference = monetize::transaction::reference_of(&s.product, &tenant, paid_at, &caps);
    Ok(signer.sign_fact(monetize_product::EntitlementFact {
        tenant,
        plan: reference.clone(),
        state: monetize::derive_state(policy, Some(paid_until), now_ms),
        paid_until_unix_ms: Some(paid_until),
        caps,
        source: format!("payment:{}:{reference}", s.vendor),
        signature: Vec::new(),
        issued_unix_ms: None,
        issued_signature: Vec::new(),
    }))
}

/// **The fact an account at the shop holds** ([`ACCOUNT_VENDOR`]): Free, with
/// no end — nothing was bought, so nothing lapses and nothing is deleted for
/// lack of payment — and [`NO_ROOM_CAP`] on the cap meter: no room here.
pub(crate) fn account_fact(signer: &monetize::Signer, product: &str, tenant: monetize_product::TenantId, made_at: u64, meter: String, vendor: &str) -> monetize_product::EntitlementFact {
    let caps = std::collections::BTreeMap::from([(meter, NO_ROOM_CAP)]);
    let reference = monetize::transaction::reference_of(product, &tenant, made_at, &caps);
    signer.sign_fact(monetize_product::EntitlementFact {
        tenant,
        plan: reference.clone(),
        state: monetize_product::State::Free,
        paid_until_unix_ms: None,
        caps,
        source: format!("account:{vendor}:{reference}"),
        signature: Vec::new(),
        issued_unix_ms: None,
        issued_signature: Vec::new(),
    })
}

#[cfg(test)]
mod a16_checkout_words {
    /// ★ A16 (owner 2026-10-23: a buyer never sees monetize's words): a paid sign-up's line on the
    /// vendor's page names the product, its room and its period, and the price — never a plan id.
    #[test]
    fn a_sign_ups_checkout_line_names_the_product_and_the_plan() {
        let plan = crate::config::SignupPlan { amount_minor: 10_000, currency: monetize_pay::Currency::Sek, cap_bytes: 5 << 30, period_days: 30 };
        assert_eq!(super::signup_checkout_words("holger", &plan), "Holger — 5.0 GiB storage, 1 month — 100.00 SEK");
        let six = crate::config::SignupPlan { amount_minor: 59_400, cap_bytes: 10 << 30, period_days: 180, ..plan };
        assert_eq!(super::signup_checkout_words("gunnar", &six), "Gunnar — 10.0 GiB storage, 6 months — 594.00 SEK");
    }
}

#[cfg(test)]
mod a16_product_mail {
    /// ★ A16 (owner 2026-10-23: a buyer never sees monetize): **a product's own relay and sender
    /// carry its mail** where they are fed (`secret.smtp_url.<product>` + `secret.smtp_from.<product>`,
    /// pmo `[products.<p>.mail]`); else the host's shared one.
    #[test]
    fn a_products_mail_goes_from_its_own_sender() {
        let fed = |name: &str| match name {
            "smtp_url" => Some("smtp://relay.mock.invalid".to_owned()),
            "smtp_from" => Some("shop@mock.invalid".to_owned()),
            "smtp_url.holger" => Some("smtp://holger-relay.mock.invalid".to_owned()),
            "smtp_from.holger" => Some("Holger <holger@mock.invalid>".to_owned()),
            _ => None,
        };
        assert_eq!(super::relay_names(&fed, Some("holger")), (Some("smtp://holger-relay.mock.invalid".to_owned()), Some("Holger <holger@mock.invalid>".to_owned())));
        assert_eq!(super::relay_names(&fed, Some("gunnar")), (Some("smtp://relay.mock.invalid".to_owned()), Some("shop@mock.invalid".to_owned())));
        assert_eq!(super::relay_names(&fed, None).1, Some("shop@mock.invalid".to_owned()));
    }
}