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());
}