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/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;
}