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/estate_order/tests/estate_reversal.rs

//! ★ **F-S-01 (A17, 2026-10-23): money that goes back on a private estate's sale.** A refund
//! or a dispute the vendor reports (Stripe's `charge.refunded`, `charge.dispute.*`) on an
//! estate's order, renewal or plan change is written on the ledger AND on the estate's run, said
//! as an operator ALERT, and — when nothing of the sale is kept (all of it refunded, or a
//! dispute lost) — takes its paid time back, so the estate follows the nonpay path (grace, then
//! stopped with every disk kept). Nothing is deleted by it: past the notice the estate stays
//! stopped until the operator acts.

use super::*;

/// One delivery at the webhook, as the vendor POSTs it: `(status, body)`.
fn deliver(
    r: &Rig,
    reference: &str,
    handle: &str,
    event: &str,
    amount_minor: u64,
) -> (u16, serde_json::Value) {
    let body = crate::testing::CallbackPay::reversal_body(reference, handle, event, amount_minor);
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(1)
        .enable_all()
        .build()
        .unwrap();
    let state = r.state.clone();
    rt.block_on(async move {
        let resp = crate::callback::handle(
            axum::extract::State(state),
            axum::extract::Path("holdpay".to_string()),
            axum::http::HeaderMap::new(),
            axum::body::Bytes::from(body),
        )
        .await;
        let status = resp.status().as_u16();
        let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
            .await
            .unwrap();
        (
            status,
            serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null),
        )
    })
}

fn reversal_notes(run: &EstateRun) -> Vec<String> {
    run.history
        .iter()
        .filter(|t| t.note.contains("REVERSAL"))
        .map(|t| t.note.clone())
        .collect()
}

/// **A full refund of the order**: recorded on the ledger and the run, the paid time taken
/// back to now, and the estate goes the nonpay way — grace, then stopped with its data kept —
/// and is never deleted by it.
#[test]
fn a_full_refund_of_an_estate_order_is_recorded_and_the_estate_lapses_like_nonpay_and_is_never_deleted_by_it(
) {
    let r = delivered("rev-full", cfg());
    let run = r.state.estates.get("acme").unwrap();
    let handle = run.handle.clone().unwrap();
    let servers = server_ids(&run);
    let now = r.state.now() + DAY;
    r.clock.store(now, Ordering::SeqCst);

    let (status, said) = deliver(&r, &run.reference, &handle.id, "refunded", run.amount.minor);
    assert_eq!(status, 200, "{said}");
    assert_eq!(
        said["recorded"], true,
        "an estate's refund is ours, never ignored: {said}"
    );
    assert_eq!(said["reference"], run.reference.as_str());
    let rows = r.state.ledger.rows(&run.reference).unwrap();
    assert_eq!(
        rows.iter()
            .filter(|row| matches!(row.step, monetize::Step::Reversed { .. }))
            .count(),
        1,
        "one Reversed row on the order"
    );

    let after = r.state.estates.get("acme").unwrap();
    assert_eq!(
        reversal_notes(&after).len(),
        1,
        "the run's history says it: {:#?}",
        after.history
    );
    assert!(
        reversal_notes(&after)[0].contains("ALERT"),
        "said as an operator alert: {:?}",
        reversal_notes(&after)
    );
    assert_eq!(
        after.paid_until_ms, now,
        "nothing of the sale is kept: paid only up to now"
    );

    // The vendor delivers it again: one row, one note.
    let (status, said) = deliver(&r, &run.reference, &handle.id, "refunded", run.amount.minor);
    assert_eq!(
        (status, said["recorded"].clone()),
        (200, serde_json::Value::Bool(true))
    );
    assert_eq!(
        reversal_notes(&r.state.estates.get("acme").unwrap()).len(),
        1,
        "a repeat is one note"
    );

    // The nonpay path, as for a renewal nobody paid.
    let run = at(&r, now + 60_000);
    assert_eq!(run.phase, phase::GRACE, "{:#?}", run.history);
    let run = at(&r, now + 15 * DAY);
    assert_eq!(run.phase, phase::SUSPENDED);
    assert!(servers.iter().all(|s| !r.cloud.running(s)), "stopped");
    // Past the notice: never deleted by a reversal.
    let run = at(&r, now + 400 * DAY);
    assert_eq!(
        run.phase,
        phase::SUSPENDED,
        "a reversed estate is never deleted by itself: {:#?}",
        run.history
    );
    assert_eq!(
        r.cloud.live_under("estate/acme"),
        11,
        "every server and disk is kept"
    );
    assert!(
        run.why.contains("operator"),
        "the operator decides: {}",
        run.why
    );
}

/// **A lost dispute** takes the paid time back whatever its amount; an opened one and a
/// partial refund are recorded and said, and leave the paid period as it is.
#[test]
fn a_partial_refund_and_an_opened_dispute_are_said_and_a_lost_dispute_takes_the_time_back() {
    let r = delivered("rev-dispute", cfg());
    let run = r.state.estates.get("acme").unwrap();
    let handle = run.handle.clone().unwrap();
    let paid = run.paid_until_ms;

    assert_eq!(
        deliver(&r, &run.reference, &handle.id, "refunded", 100).1["recorded"],
        true
    );
    assert_eq!(
        deliver(
            &r,
            &run.reference,
            &handle.id,
            "dispute_opened",
            run.amount.minor
        )
        .1["recorded"],
        true
    );
    let mid = r.state.estates.get("acme").unwrap();
    assert_eq!(
        mid.paid_until_ms, paid,
        "goodwill and an open dispute keep the paid period"
    );
    assert_eq!(reversal_notes(&mid).len(), 2);

    assert_eq!(
        deliver(
            &r,
            &run.reference,
            &handle.id,
            "dispute_lost",
            run.amount.minor
        )
        .1["recorded"],
        true
    );
    let lost = r.state.estates.get("acme").unwrap();
    assert_eq!(
        lost.paid_until_ms,
        r.state.now(),
        "a chargeback lost: paid only up to now"
    );
    assert_eq!(at(&r, r.state.now() + 60_000).phase, phase::GRACE);
}

/// **A renewal refunded**: only the time it bought goes; the period paid before it stays.
#[test]
fn a_refunded_renewal_takes_back_only_the_time_it_bought() {
    let r = delivered("rev-renewal", cfg());
    let paid = r.state.estates.get("acme").unwrap().paid_until_ms;
    at(&r, paid - 3 * DAY);
    let renewal = term::renew(&r.state, "acme").unwrap();
    r.pay.authorize(&renewal.handle.id);
    let run = at(&r, paid - 3 * DAY + 60_000);
    assert_eq!(run.renewals.len(), 1);
    assert!(run.paid_until_ms > paid);

    let (status, said) = deliver(
        &r,
        &renewal.reference,
        &renewal.handle.id,
        "refunded",
        renewal.amount.minor,
    );
    assert_eq!(
        (status, said["recorded"].clone()),
        (200, serde_json::Value::Bool(true)),
        "{said}"
    );
    assert_eq!(said["reference"], renewal.reference.as_str());
    let run = r.state.estates.get("acme").unwrap();
    assert_eq!(
        run.paid_until_ms, paid,
        "the renewal's months are taken back, the period before it kept"
    );
    assert_eq!(at(&r, paid - 1).phase, phase::CAPTURED);
    assert_eq!(at(&r, paid).phase, phase::GRACE);
}

/// A handle the run never held is not this estate's charge: ignored, nothing changes.
#[test]
fn a_reversal_naming_an_estate_reference_under_a_handle_it_never_held_is_ignored() {
    let r = delivered("rev-stranger", cfg());
    let run = r.state.estates.get("acme").unwrap();
    let (status, said) = deliver(
        &r,
        &run.reference,
        "cs_somebody_else",
        "refunded",
        run.amount.minor,
    );
    assert_eq!(status, 200);
    assert_eq!(said["ignored"], true, "{said}");
    let after = r.state.estates.get("acme").unwrap();
    assert_eq!(after.paid_until_ms, run.paid_until_ms);
    assert!(reversal_notes(&after).is_empty());
}