crates/monetize-server/tests/reconcile.rs
//! **What the vendor took reaches the console over the wire.**
//!
//! The classification itself is held on the host in
//! `monetize_server::reconcile::tests`; this file drives the read side the
//! console uses — `ReconcileAdmin.ListPaidAtVendor`, through the real router,
//! behind the real bearer — because a list the server keeps and never serves
//! is the 2026-09-14 journald line with a nicer name.
use std::sync::Arc;
use monetize_proto::reconcile_admin_client::ReconcileAdminClient;
use monetize_proto::{self as pb};
use monetize_server::poller;
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
use monetize_server::testing::{CallbackPay, TestServer};
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::Request;
const BEARER: &str = "test-bearer-token-of-sufficient-length";
/// **When the vendor says it took the money — relative to NOW, never a
/// literal.**
///
/// This was `1_789_470_600_000`, a wall-clock instant written down on the day
/// the test was authored. `reconcile` asks the vendor from `now −
/// FIRST_WINDOW_MS` (48 h) on a first pass, so the fixture aged out of its own
/// query window and the test began failing on 2026-09-17 — about two days
/// after it was written, with nothing changed. It reported `(0, 0)` against
/// `(2, 2)`: not a regression in the code, the payments were simply no longer
/// inside the window the code correctly asked about.
///
/// A test whose result depends on the date it is RUN is not a test. One hour
/// ago is inside every window this code has, and stays inside it for ever.
fn paid_at() -> i64 {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("a clock at or after the epoch")
.as_millis() as i64;
now - 60 * 60 * 1000
}
fn authed<T>(req: T) -> Request<T> {
let mut r = Request::new(req);
r.metadata_mut().insert("authorization", MetadataValue::try_from(format!("Bearer {BEARER}")).unwrap());
r
}
/// (r) **A case the poller recorded is what the console reads**, field for
/// field, with the counts the health line needs — and an empty store answers
/// an empty list with `last_reconcile` saying it WAS asked.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_cases_the_poller_recorded_are_served_to_the_console() {
let pay = CallbackPay::new("mockpay-shared-secret");
let state = Arc::new(AppState::in_memory(Registry::new().sells("gunnar", pay.clone())));
let server = TestServer::spawn(state.clone(), BEARER).await;
let channel = Channel::from_shared(server.endpoint()).unwrap().connect().await.unwrap();
let mut client = ReconcileAdminClient::new(channel);
// Nothing yet — and "nothing" carries the last pass, so a console can say
// "asked at 11:25, found nothing" rather than "never asked".
let s = state.clone();
tokio::task::spawn_blocking(move || poller::tick(&s)).await.unwrap();
let out = client.list_paid_at_vendor(authed(pb::ListPaidAtVendorRequest { limit: 0 })).await.unwrap().into_inner();
assert!(out.cases.is_empty());
assert_eq!((out.total, out.needs_operator, out.settled), (0, 0, 0));
assert!(out.last_reconcile_unix_ms > 0, "the vendor was asked");
// The vendor took two payments monetize never heard of: one it can place
// (no plan table in this rig, so the plan is not sold — needs an operator)
// and one with no labels at all. Both must reach the console.
// Bound ONCE: the row assertion below compares against this same instant,
// and two calls to `paid_at()` would be two different milliseconds.
let paid = paid_at();
// Both checkouts were minted by this box at Signup.Start (ST-7).
for session in ["cs_live_a1MHA6", "cs_blank"] {
state.signups.record_minted("mockpay", session, None).unwrap();
}
pay.report_paid(CallbackPay::paid_fact("cs_live_a1MHA6", paid, &[("product", "gunnar"), ("plan", "gunnar/10gb-1m"), ("username", "test3")]));
pay.report_paid(CallbackPay::paid_fact("cs_blank", paid + 1, &[]));
let s = state.clone();
let t = tokio::task::spawn_blocking(move || poller::tick(&s)).await.unwrap();
assert_eq!((t.reconciled, t.unfulfilled), (2, 2));
// The counts are APPENDED, never reordered, so this asserts the reconcile
// pair where it sits rather than at the end of the line — the reclamation
// queue's two counts and the signup adoption are newer and hold the tail.
assert!(poller::poll_line(&t).contains(" reconciled=2 unfulfilled=2 grow_resumed=0 "), "{}", poller::poll_line(&t));
assert!(poller::poll_line(&t).contains(" grow_resumed=0 signups_adopted=0 "), "{}", poller::poll_line(&t));
// `contains`, not `ends_with`. This asserted the line ENDED at
// `ladder_superseded=0` — true on the day it was written and false the
// moment `owed_back` and `metered_unproven` were appended after it. The
// comment above already records one round of the same repair ("the
// reclamation queue's two counts and the signup adoption are newer and
// hold the tail"), which is the tell: an assertion that has to be edited
// every time a field is added is pinning the wrong thing. What this test
// is about is that its OWN counts reach the line, and that is what it now
// says.
assert!(poller::poll_line(&t).contains(" ladder_superseded=0"), "{}", poller::poll_line(&t));
let out = client.list_paid_at_vendor(authed(pb::ListPaidAtVendorRequest { limit: 0 })).await.unwrap().into_inner();
assert_eq!(out.cases.len(), 2);
assert_eq!((out.total, out.needs_operator, out.settled), (2, 2, 0));
let row = out.cases.iter().find(|c| c.session == "cs_live_a1MHA6").expect("the incident's row");
assert_eq!((row.vendor.as_str(), row.reference.as_str(), row.state.as_str()), ("mockpay", "pi_cs_live_a1MHA6", "needs-operator"));
assert_eq!((row.amount_minor, row.tax_minor, row.currency.as_str(), row.livemode), (12_500, 2_500, "sek", true));
assert_eq!(row.paid_at_unix_ms, paid);
assert!(row.first_seen_unix_ms > 0 && row.decided_at_unix_ms > 0);
assert!(row.reason.contains("gunnar/10gb-1m"), "names the plan nobody sells: {}", row.reason);
assert_eq!(row.metadata.get("username").map(String::as_str), Some("test3"), "the labels, verbatim");
// The e-mail is NOT on the wire: a console is not a mailing list.
assert!(!format!("{row:?}").contains("buyer@example.test"));
// `limit` cuts from the newest end.
let out = client.list_paid_at_vendor(authed(pb::ListPaidAtVendorRequest { limit: 1 })).await.unwrap().into_inner();
assert_eq!(out.cases.len(), 1);
assert_eq!(out.total, 2, "the counts are the store's, not the page's");
// No bearer, no list: the rows are the vendor's word and the operator's to read.
let refused = client.list_paid_at_vendor(Request::new(pb::ListPaidAtVendorRequest { limit: 0 })).await;
assert_eq!(refused.unwrap_err().code(), tonic::Code::Unauthenticated);
server.stop().await;
}