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/callback.rs

//! **`POST /callback/<kind>/<instance>`** — one vendor instance's webhook
//! (`/callback/stripe/gunnar`; each Stripe account has its own endpoint and
//! signing secret, and a bare `/callback/stripe` is a 404). HTTP, not gRPC, because
//! it is the vendor's wire; on the SAME listener as the gRPC services (tonic's
//! router is an axum router, `accept_http1` is on), so a reverse proxy forwards
//! only `/callback/*` from the public edge.
//!
//! The one rule (monetize-pay's header): a callback moves nothing until
//! `Payment::callback` has VERIFIED it. Then, only for a reference
//! the ledger holds open and whose handle is the one the vendor names, the
//! transaction is resumed through the same path `Purchase.Start` uses.
//!
//! | answer | when |
//! |---|---|
//! | 413 | a body larger than [`MAX_CALLBACK_BYTES`]: no vendor sends one, and it is refused before the parser is instantiated |
//! | 503 | every one of the [`CALLBACK_SLOTS`] is taken (with `Retry-After`), or `parse_callback` said `Unavailable` — our outage, so the vendor retries |
//! | 404 | no such vendor linked. **Not** "no such reference": that verified, so it is a 200 — see below |
//! | 401 | `parse_callback` said `Unverified` — logged, counted, never acted on |
//! | 200 | `parse_callback` said `Ignored`, or the reference names no open attempt — it verified and there is nothing to do: `{ignored, reason}` |
//! | 200 | `parse_callback` said `Signup` — a PAID SIGNUP: `{signup, vendor, session, state, reason}`, and the ledger is never touched (see [`crate::signup`]) |
//! | 409 | verified, but names a handle the ledger did not mint for that reference |
//! | 200 | `{reference, state: PENDING, reason, accepted: true}` — KEPT ([`crate::inbox`]) and granted behind the answer: the grant took longer than `inbox::ANSWER_WITHIN`, is in flight for an earlier delivery, or met a vendor/cloud/product that did not answer (it retries on its own; the vendor need not). Until 2026-10-01 the grant ran inside the request and a growth made it a 504 (money-webhook-504-in-growth) |
//! | 200 | `{…, duplicate: true}` — a redelivery of an event whose grant is done, answered from the kept row |
//! | 500 | the delivery could not be KEPT (inbox or signup store), or the ledger failed: the vendor's retry is the only copy. The body is `internal error` and nothing else; the cause goes to the journal and the status line |
//! | 200 | `{reference, state, reason}` — state after the resume (PAID, PENDING, RELEASED) |
//!
//! # What a refusal leaves behind — and what it deliberately does not
//!
//! **No status code above changes**, and none of the reasoning for them does
//! either. What changed 2026-09-14 is that three of these refusals now leave a
//! ROW an operator can find, in [`crate::refusals`]: a bounded in-memory ring,
//! read over `CallbackAdmin.ListRefusals` and published on the bus so a console
//! that is already open lights up at once.
//!
//! Before it, a refused callback wrote a `functional_status` and an `eprintln!`
//! and nothing else. The front's clock ran two hours fast, every genuine Stripe
//! delivery fell outside the signed 300 s window, and the owner watched a
//! customer pay while monetize recorded absolutely nothing. journald had it;
//! nobody was reading journald.
//!
//! **The three that are recorded** are the three that can leave money at the
//! vendor with no trace ANYWHERE else in monetize:
//!
//! | arm | why it is a row |
//! |---|---|
//! | 401 `Unverified` | the incident itself: a delivery we threw away, and the ledger will never hear of it |
//! | 400 not understood | same shape — a delivery this server could not act on, and no other store sees it |
//! | 409 handle mismatch | VERIFIED, and it says a charge exists against a handle the ledger did not mint |
//!
//! **The rest are deliberately NOT rows**, because each already has a truer
//! home and a panel of noise is a panel nobody reads:
//!
//! * **404, no such vendor** — internet background noise against a path a
//!   stranger chose. Recording it would let anyone write a string of their
//!   choosing into the operator's screen, which is the one thing this record
//!   must not offer.
//! * **200 `Ignored`** — it VERIFIED and there is nothing to do: another
//!   system's payment on the same merchant account. Listing those would bury
//!   the refusals under the vendor's ordinary traffic.
//! * **200 no open attempt** — a late or duplicate delivery. The poller and the
//!   webhook race by design; this is the race working.
//! * **`Signup`, refusals included** — already stored, durably, in
//!   [`crate::signup`], and already on the console's `Paid signups` panel. One
//!   failure on one screen twice is two failures as far as a reader is concerned.
//! * **a grant still running or retrying** — the attempt is still OPEN in the ledger, so
//!   it is already on the console under `Needs you`, with its checkout link.

use std::sync::Arc;

use axum::body::Bytes;
use axum::extract::{Path, State};
use axum::http::{HeaderMap, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::Json;
use monetize_pay::{PayError, RawCallback};

use crate::refusals::{Refusal, Refused};
use crate::state::AppState;

pub async fn handle(
    State(state): State<Arc<AppState>>,
    Path(vendor): Path<String>,
    headers: HeaderMap,
    body: Bytes,
) -> Response {
    // **Bounded before anything is instantiated.** Both checks are free and
    // neither reads the path segment into a record: it is the caller's.
    if body.len() > MAX_CALLBACK_BYTES {
        return (StatusCode::PAYLOAD_TOO_LARGE, "callback body too large").into_response();
    }
    let Some(slot) = state.callback_slots.take() else {
        eprintln!("monetize-server: every one of the {CALLBACK_SLOTS} callback slots is taken; a delivery was answered 503");
        return (
            [(axum::http::header::RETRY_AFTER, "10")],
            (StatusCode::SERVICE_UNAVAILABLE, "busy; retry"),
        )
            .into_response();
    };
    let headers: Vec<(String, String)> = headers
        .iter()
        .map(|(k, v)| {
            (
                k.as_str().to_string(),
                v.to_str().unwrap_or_default().to_string(),
            )
        })
        .collect();
    let v = vendor.clone();
    let result = tokio::task::spawn_blocking(move || {
        callback_blocking(&state, &vendor, &headers, &body, slot)
    })
    .await;
    match result {
        Ok(r) => r,
        Err(e) => internal(&v, "callback handler panicked", e),
    }
}

/// **500, and nothing about why.** The error is ours and goes to the journal
/// and the status line; whoever posted is told only that it failed here.
fn internal(vendor: &str, what: &str, e: impl std::fmt::Display) -> Response {
    eprintln!("monetize-server: {vendor} callback: {what}: {e}");
    skidbladnir::functional_status(
        "monetize-server/callback",
        vendor,
        false,
        &format!("{what}: {e}"),
    );
    (StatusCode::INTERNAL_SERVER_ERROR, "internal error").into_response()
}

/// **A verified event about nothing we hold, answered as a receipt.**
///
/// The vendor proved the event is its own; the ledger simply has no open attempt
/// for the reference it names — most often because the attempt already closed and
/// this is a late or duplicate delivery. Retrying that produces the same answer
/// forever, and every non-2xx is a failed delivery a vendor counts against the
/// endpoint (Stripe's delivery table treats 400, 404 and 409 alike, and their own
/// reference handler answers 200 to an event type it does not handle).
///
/// Same shape as the `Ignored` arm above, so one thing an operator greps for means
/// "nothing happened, and nothing was wrong".
/// **How many deliveries may be in this process at once** — parsing, settling
/// or granting. Every delivery instantiates the vendor's wasm BEFORE its
/// signature is known, so anyone on the internet can ask for one; past this
/// many the answer is 503 at once and the vendor retries.
pub const CALLBACK_SLOTS: usize = 32;

/// **The largest webhook body this server reads.** A vendor's event is a few
/// kilobytes; anything near this is not one, and it is refused before the
/// vendor's parser is instantiated for it.
pub const MAX_CALLBACK_BYTES: usize = 256 * 1024;

/// The bounded pool of [`CALLBACK_SLOTS`] a delivery must hold a [`Slot`] from.
#[derive(Clone)]
pub struct Slots(Arc<tokio::sync::Semaphore>);

/// One delivery's place in [`Slots`]; given back when dropped, which for a
/// grant or settle running behind the answer is when that work ends.
pub struct Slot {
    _permit: tokio::sync::OwnedSemaphorePermit,
}

impl Default for Slots {
    fn default() -> Self {
        Slots::with(CALLBACK_SLOTS)
    }
}

impl Slots {
    pub fn with(n: usize) -> Self {
        Slots(Arc::new(tokio::sync::Semaphore::new(n)))
    }

    /// A slot if one is free, at once; never waits.
    pub fn take(&self) -> Option<Slot> {
        Arc::clone(&self.0)
            .try_acquire_owned()
            .ok()
            .map(|_permit| Slot { _permit })
    }
}

fn nothing_to_do(vendor: &str, reason: String) -> Response {
    skidbladnir::functional_status(
        "monetize-server/callback",
        vendor,
        true,
        &format!("verified event ignored: {reason}"),
    );
    Json(serde_json::json!({ "ignored": true, "reason": reason })).into_response()
}

/// **The one writer of a refusal record.** Every arm that keeps a row goes
/// through here, so the ring, the testmatrix line, the journal line and the
/// live feed cannot disagree about what happened or say it in four voices.
///
/// It records first and publishes second: the durable-for-this-run fact is the
/// ring, and a console that is not watching must not be able to change what is
/// kept. Publishing is infallible and never blocks (see [`crate::bus`]), so the
/// vendor's request is not waiting on a browser.
///
/// `detail` is bounded and stripped inside [`crate::refusals::RefusalLog::record`],
/// at the one place a row is made — nothing downstream has to remember to.
fn record_refusal(
    state: &AppState,
    vendor: &str,
    refusal: Refusal,
    detail: String,
    body_bytes: usize,
) -> Refused {
    let row = state
        .refusals
        .record(state.now(), vendor, refusal, &detail, body_bytes as u64);
    skidbladnir::functional_status(
        "monetize-server/callback",
        vendor,
        false,
        &format!("callback refused ({}): {}", refusal.name(), row.detail),
    );
    eprintln!(
        "monetize-server: {vendor} callback REFUSED ({}) #{} — {} [recorded; the console reads CallbackAdmin.ListRefusals]",
        refusal.name(),
        row.seq,
        row.detail
    );
    state
        .bus
        .publish(monetize_proto::event::Payload::CallbackRefused(row.to_pb()));
    row
}

fn callback_blocking(
    state: &Arc<AppState>,
    vendor: &str,
    headers: &[(String, String)],
    body: &[u8],
    slot: Slot,
) -> Response {
    let Some(pay) = state.registry.find_vendor(vendor) else {
        return (
            StatusCode::NOT_FOUND,
            format!("no vendor {vendor:?} is linked into this server"),
        )
            .into_response();
    };
    let event = match pay.callback(RawCallback { headers, body }) {
        Ok(e) => e,
        Err(PayError::Unverified(why)) => {
            // **401, and now a ROW.** The status and the reason for it are
            // untouched: the caller proved nothing, so it is told nothing about
            // what this deployment holds, and the body stays the same four
            // words it always was. What is new is that the refusal survives the
            // request — see the module doc, and [`crate::refusals`] for why an
            // unauthenticated caller can fill a bounded ring and nothing else.
            record_refusal(state, vendor, Refusal::Unverified, why, body.len());
            return (StatusCode::UNAUTHORIZED, "callback did not verify").into_response();
        }
        Err(PayError::Ignored(why)) => {
            // **200, deliberately.** The event VERIFIED — it is simply not about
            // anything this deployment holds: another system's payment on the same
            // merchant account, an event type the ledger has no answer for. A vendor
            // whose endpoint answers 401 to its own genuine traffic retries it for
            // days and then disables the endpoint, taking the events that DO matter
            // with it. Counted as functional: nothing failed here.
            skidbladnir::functional_status(
                "monetize-server/callback",
                vendor,
                true,
                &format!("verified event ignored: {why}"),
            );
            return Json(serde_json::json!({ "ignored": true, "reason": why })).into_response();
        }
        Err(PayError::Unavailable(why)) => {
            // **503: our outage, not their payload.** The delivery verified as
            // far as the parser got and the vendor could not be asked for the
            // rest; a 4xx would tell the vendor to stop retrying the one copy.
            eprintln!("monetize-server: {vendor} callback could not be read for now: {why}");
            skidbladnir::functional_status(
                "monetize-server/callback",
                vendor,
                false,
                &format!("callback unavailable: {why}"),
            );
            return (
                StatusCode::SERVICE_UNAVAILABLE,
                "temporarily unavailable; retry",
            )
                .into_response();
        }
        Err(PayError::Signup(signup)) => {
            // **A customer who did not exist when the money moved.** There is no
            // reference to look up and no attempt to resume — the ledger holds
            // nothing for a stranger — so this arm returns before the ledger is
            // touched at all, and the engine in `crate::signup` takes it.
            //
            // **200 in every verified case, refusals included.** A refused
            // signup is not a failed delivery: the delivery was perfect and the
            // answer is no. Telling Stripe otherwise buys days of retries and
            // then a disabled endpoint (`4cafd1d`), and it would not reach the
            // one party who needs the answer — the customer's return page,
            // which reads it over `Signup.Status`.
            //
            // **Kept first, answered at once** (money-webhook-504-in-growth):
            // the sink creates the principal and pushes the entitlement on the
            // same box a growth can hold for minutes, so the settle runs behind
            // the answer. The row is the durable record and the idempotency
            // key (`(vendor, session)`); a delivery the store could not keep is
            // NOT answered 200, so the vendor's retry stays the only copy.
            // ★ ST-7: a session this box never minted (and holds no row for) is another
            // system's checkout on the same merchant account: verified, answered 200 so the
            // vendor stops, and never settled here.
            let estate = match state.signups.get(&signup.vendor, &signup.session) {
                Ok(Some(rec)) => rec.estate,
                Ok(None) => match state.signups.minted(&signup.vendor, &signup.session) {
                    Ok(Some(estate)) => estate,
                    Ok(None) => {
                        let why = format!("signup {}:{} was not minted by this monetize — another system's checkout on the same account; not settled here", signup.vendor, signup.session);
                        skidbladnir::functional_status(
                            "monetize-server/callback",
                            vendor,
                            true,
                            &format!("verified event ignored: {why}"),
                        );
                        eprintln!("monetize-server: {vendor} callback IGNORED: {why}");
                        return Json(serde_json::json!({ "ignored": true, "reason": why }))
                            .into_response();
                    }
                    Err(e) => return internal(vendor, "signup store", e),
                },
                Err(e) => return internal(vendor, "signup store", e),
            };
            match state
                .signups
                .record_pending_on(&signup, state.now() as i64, estate.as_deref())
            {
                Ok(crate::signup::Recorded::Already(rec)) if !rec.state.is_pending() => {
                    return signup_answer(&signup, &rec.state)
                }
                Ok(_) => {}
                Err(e) => return internal(vendor, "signup store", e),
            }
            let (tx, rx) = std::sync::mpsc::channel();
            let (st, sg) = (Arc::clone(state), signup.clone());
            // A jera job on the signup's own lane (A32-14): a settle that panics is an ALERT line
            // and a FAILED row; the signup is kept, and the poller's re-settle finishes it.
            let key = crate::jobs::of(state, &format!("{}:{}", signup.vendor, signup.session));
            let spawned = crate::jobs::once(SIGNUP_SETTLE_JOB, &key, move || {
                let _slot = slot;
                let decided = crate::signup::settle(&st, &sg);
                eprintln!(
                    "monetize-server: callback signup {}:{} settled behind the answer: {}",
                    sg.vendor,
                    sg.session,
                    signup_word(&decided).0
                );
                let _ = tx.send(decided);
                Ok(())
            });
            match spawned {
                Ok(true) => {}
                // Its settle already runs: this answer waits on nothing and says PENDING.
                Ok(false) => eprintln!("monetize-server: callback signup {}:{}: its settle already runs", signup.vendor, signup.session),
                // Kept, and the poller's re-settle finishes it.
                Err(e) => eprintln!("monetize-server: callback signup {}:{}: not settled now ({e}); the poller's re-settle will", signup.vendor, signup.session),
            }
            let state_of = match rx.recv_timeout(crate::inbox::ANSWER_WITHIN) {
                Ok(decided) => decided,
                Err(_) => {
                    eprintln!(
                        "monetize-server: callback signup {}:{}: kept and answered PENDING within {} ms; the account is made behind the answer",
                        signup.vendor,
                        signup.session,
                        crate::inbox::ANSWER_WITHIN.as_millis()
                    );
                    crate::signup::SignupState::Pending
                }
            };
            return signup_answer(&signup, &state_of);
        }
        Err(PayError::Reversal(r)) => {
            // **Money went back, or is contested** (g-disputes): written into
            // the ledger once, 200 whatever became of it — a delivery we
            // cannot place is somebody else's charge on the same account.
            return match crate::reversal::record(state, &r) {
                Ok(done) => {
                    Json(serde_json::json!({ "reversal": r.kind.name(), "reference": done.reference, "seq": done.seq, "recorded": true })).into_response()
                }
                Err(why) if why.starts_with("ledger:") || why.starts_with("signup store:") || why.starts_with("estate store:") => internal(vendor, "reversal", why),
                Err(why) => nothing_to_do(vendor, why),
            };
        }
        Err(e) => {
            // **400, unchanged**, and a row for the same reason the 401 keeps
            // one: a delivery this server threw away leaves no mark on the
            // ledger, and a vendor whose payload we stopped understanding after
            // an API version bump looks exactly like silence from here.
            //
            // Claimed UNVERIFIED even though a parser may well have checked the
            // signature before failing: this arm cannot tell which happened,
            // and a record that over-claimed whose word a row was would be
            // worse than one that under-claims it.
            record_refusal(
                state,
                vendor,
                Refusal::NotUnderstood,
                e.to_string(),
                body.len(),
            );
            return (
                StatusCode::BAD_REQUEST,
                format!("callback not understood: {e}"),
            )
                .into_response();
        }
    };
    // ★ A32-26: an ESTATE order's payment (`estate/<co>`, its renewals and plan changes) has
    // ONE completion path, the estate driver's (the vendor asked, the money captured, `paid`);
    // its webhook is a thin trigger of it — kept in this same inbox, the run driven now.
    if let Some(company) = crate::estate_order::company_of_reference(&event.reference) {
        if state
            .ledger
            .attempt(&event.reference)
            .ok()
            .flatten()
            .is_none()
        {
            return estate_trigger(state, vendor, body, &event.reference, &company, slot);
        }
    }
    let attempt = match state.ledger.attempt(&event.reference) {
        Ok(Some(a)) if a.is_open() => a,
        Ok(_) => return nothing_to_do(vendor, format!("no open attempt for {}", event.reference)),
        Err(e) => return internal(vendor, "ledger", e),
    };
    if attempt.handle != event.handle {
        // **The loudest of the three, and the only one that VERIFIED.** The
        // vendor's signature stood behind this delivery, and it says a charge
        // exists against a handle this ledger never minted for that reference.
        // 409 unchanged; the row is what puts it in front of somebody, and it
        // is the one refusal whose words are the vendor's rather than a
        // stranger's — [`Refusal::verified`] carries that as a value so the
        // console does not have to read it off the prose.
        let why = format!(
            "{} was reserved as {}:{}, callback names {}:{}",
            event.reference,
            attempt.handle.vendor,
            attempt.handle.id,
            event.handle.vendor,
            event.handle.id
        );
        record_refusal(
            state,
            vendor,
            Refusal::HandleMismatch,
            why.clone(),
            body.len(),
        );
        return (StatusCode::CONFLICT, why).into_response();
    }
    // **Kept, then granted behind the answer** (money-webhook-504-in-growth).
    // The ledger's open attempt is the money's record; this row is the
    // delivery's, keyed by the event's id, so a redelivery is counted and never
    // a second grant. Not kept → not answered 200: the vendor's retry is then
    // the only copy of it.
    let id = crate::inbox::event_id(body);
    let row = match state
        .callbacks
        .receive(vendor, &id, &event.reference, state.now() as i64)
    {
        Ok(r) => r.row().clone(),
        Err(e) => return internal(vendor, "callback inbox", e),
    };
    if let crate::inbox::InboxState::Done {
        state: word,
        reason,
    } = &row.state
    {
        eprintln!(
            "monetize-server: {vendor} callback {id} for {} delivered again (delivery {}): already {word} — answered from the kept row, nothing moves",
            row.reference, row.deliveries
        );
        return Json(serde_json::json!({ "reference": row.reference, "state": word, "reason": reason, "duplicate": true })).into_response();
    }
    if state.resuming.holds(&event.reference) {
        eprintln!(
            "monetize-server: {vendor} callback {id} for {} (delivery {}): its grant is in flight — answered at once, nothing started",
            row.reference, row.deliveries
        );
        return accepted(&event.reference, crate::services::purchase::IN_FLIGHT);
    }
    eprintln!("monetize-server: {vendor} callback {id} for {} (delivery {}): kept; granting behind the answer", row.reference, row.deliveries);
    let (tx, rx) = std::sync::mpsc::channel();
    let (st, v, i, r) = (
        Arc::clone(state),
        vendor.to_owned(),
        id.clone(),
        event.reference.clone(),
    );
    // A jera job on the purchase's own lane (A32-14): a grant that panics is an ALERT line and
    // a FAILED row; the attempt is kept, and the poller's tick resumes it.
    match crate::jobs::once(
        GRANT_JOB,
        &crate::jobs::of(state, &event.reference),
        move || {
            let _slot = slot;
            crate::inbox::grant(&st, &v, &i, &r, Some(tx));
            Ok(())
        },
    ) {
        Ok(true) => {}
        Ok(false) => return accepted(&event.reference, crate::services::purchase::IN_FLIGHT),
        Err(e) => {
            // Kept, and the open attempt is resumed by the poller's tick.
            eprintln!("monetize-server: {vendor} callback {id}: not granted now ({e}); the poller's tick will");
            return accepted(&event.reference, "kept; the poller's tick finishes it");
        }
    }
    match rx.recv_timeout(crate::inbox::ANSWER_WITHIN) {
        Ok(crate::inbox::Granted::Verdict(v))
            if v.state == monetize_proto::PurchaseState::Pending =>
        {
            accepted(&v.reference, &v.reason)
        }
        Ok(crate::inbox::Granted::Verdict(v)) => {
            skidbladnir::functional_status(
                "monetize-server/callback",
                vendor,
                true,
                &format!("{} -> {:?} {}", v.reference, v.state, v.reason),
            );
            Json(serde_json::json!({ "reference": v.reference, "state": format!("{:?}", v.state).to_uppercase(), "reason": v.reason }))
                .into_response()
        }
        // Closed between the two reads — the poller and a webhook race by design.
        Ok(crate::inbox::Granted::Closed) => {
            nothing_to_do(vendor, format!("no open attempt for {}", event.reference))
        }
        // The vendor, the cloud or the product did not answer: kept, and the
        // grant retries on its own — the vendor need not.
        Ok(crate::inbox::Granted::Failed(tonic::Code::Unavailable, why)) => {
            accepted(&event.reference, &format!("retrying: {why}"))
        }
        Ok(crate::inbox::Granted::Failed(_, why)) => internal(vendor, "grant", why),
        Err(_) => {
            eprintln!(
                "monetize-server: {vendor} callback {id} for {}: answered within {} ms; the grant continues behind the answer",
                event.reference,
                crate::inbox::ANSWER_WITHIN.as_millis()
            );
            accepted(
                &event.reference,
                "kept; the grant continues behind this answer",
            )
        }
    }
}

/// ★ **A32-26: an estate's payment webhook, a thin trigger of its one completion path.** The
/// delivery is kept in the callback inbox (its event id: a redelivery is answered from the kept
/// row), then the estate's drive is queued at once — the same jera job the tick queues, whose
/// driver asks the vendor, captures and records `paid`. An estate this box does not run is
/// another system's: answered 200, ignored.
fn estate_trigger(
    state: &Arc<AppState>,
    vendor: &str,
    body: &[u8],
    reference: &str,
    company: &str,
    slot: Slot,
) -> Response {
    if state.estates.get(company).is_none() {
        return nothing_to_do(vendor, format!("no estate order for {reference} here"));
    }
    let id = crate::inbox::event_id(body);
    let row = match state
        .callbacks
        .receive(vendor, &id, reference, state.now() as i64)
    {
        Ok(r) => r.row().clone(),
        Err(e) => return internal(vendor, "callback inbox", e),
    };
    if let crate::inbox::InboxState::Done {
        state: word,
        reason,
    } = &row.state
    {
        eprintln!("monetize-server: {vendor} callback {id} for {reference} delivered again (delivery {}): already {word} — answered from the kept row, nothing moves", row.deliveries);
        return Json(serde_json::json!({ "reference": reference, "state": word, "reason": reason, "duplicate": true })).into_response();
    }
    // The run's drive, queued now on its own jera lane (the tick's very job): the driver asks
    // the vendor, captures and records `paid`. The tick queues it as well when no webhook comes.
    let (word, reason) = match crate::estate_order::drive_now(state, company) {
        Ok(()) => (
            "QUEUED",
            "the estate's order is driven on its one completion path".to_string(),
        ),
        Err(e) => ("KEPT", format!("the estate tick drives it ({e})")),
    };
    let now = state.now() as i64;
    if let Err(e) = state.callbacks.update(vendor, &id, |row| {
        row.attempts += 1;
        row.state = crate::inbox::InboxState::Done {
            state: word.into(),
            reason: reason.clone(),
        };
        row.updated_at_unix_ms = now;
    }) {
        eprintln!("monetize-server: {vendor} callback {id}: the kept row could not be closed ({e}); a redelivery drives the estate again, which is harmless");
    }
    drop(slot);
    eprintln!("monetize-server: {vendor} callback {id} for {reference}: kept; {reason}");
    accepted(reference, &reason)
}

/// **200, kept, not finished yet**: `{reference, state: PENDING, reason, accepted}`.
fn accepted(reference: &str, reason: &str) -> Response {
    Json(serde_json::json!({ "reference": reference, "state": "PENDING", "reason": reason, "accepted": true })).into_response()
}

fn signup_word(state_of: &crate::signup::SignupState) -> (&'static str, String) {
    match state_of {
        crate::signup::SignupState::Pending => ("PENDING", String::new()),
        crate::signup::SignupState::Created { .. } => ("CREATED", String::new()),
        crate::signup::SignupState::Refused { reason, .. } => ("REFUSED", reason.clone()),
    }
}

/// The signup arm's answer: `{signup, vendor, session, state, reason}`.
fn signup_answer(
    signup: &monetize_pay::PaidSignup,
    state_of: &crate::signup::SignupState,
) -> Response {
    let (word, reason) = signup_word(state_of);
    Json(serde_json::json!({
        "signup": true,
        "vendor": signup.vendor,
        "session": signup.session,
        "state": word,
        "reason": reason,
    }))
    .into_response()
}

/// The jera job kind of a callback's signup settle, behind the answer (A32-14).
pub const SIGNUP_SETTLE_JOB: &str = "monetize_callback_signup";
/// The jera job kind of a callback's grant, behind the answer (A32-14).
pub const GRANT_JOB: &str = "monetize_callback_grant";

#[cfg(test)]
mod tests;