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_webhook.rs

//! ★ **A32-26: ONE completion path for an estate's payment.** A sign-up's and a purchase's money
//! is settled from the webhook inbox; an estate's was settled only by the tick polling the vendor
//! — its verified webhook was answered "ignored" (no ledger attempt holds `estate/<co>`). Now the
//! estate's ONE path stays the driver's (the vendor asked, the money captured, `paid`), and its
//! webhook is a thin trigger of it: kept in the same inbox (a redelivery answered from the kept
//! row), the run driven at once. The tick still drives it when no webhook comes.

use super::*;

fn deliver_paid(
    r: &Rig,
    reference: &str,
    handle: &str,
    event_id: &str,
) -> (u16, serde_json::Value) {
    let body = serde_json::to_vec(&serde_json::json!({ "id": event_id, "event": "paid", "reference": reference, "handle": handle })).unwrap();
    let rt = tokio::runtime::Builder::new_multi_thread()
        .worker_threads(1)
        .enable_all()
        .build()
        .unwrap();
    let state = r.state.clone();
    let company = reference.trim_start_matches("estate/").to_string();
    rt.block_on(async move {
        let resp = crate::callback::handle(
            axum::extract::State(state.clone()),
            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();
        // The drive it queued runs on this runtime: let it finish before the runtime goes.
        for _ in 0..200 {
            if state
                .estates
                .get(&company)
                .is_some_and(|r| r.captured_at_ms > 0)
                && !state.estates.driving.lock().unwrap().contains(&company)
            {
                break;
            }
            tokio::time::sleep(std::time::Duration::from_millis(25)).await;
        }
        (
            status,
            serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null),
        )
    })
}

#[test]
fn an_estates_paid_webhook_is_kept_and_drives_its_one_completion_path_at_once() {
    let r = rig_with("estate-webhook", cfg(), "PASS");
    start_for(
        &r.state,
        "acme",
        "buyer@example.com",
        "",
        "alice",
        &EstateSizes::default(),
    )
    .unwrap();
    let run = r.state.estates.get("acme").unwrap();
    assert_eq!(run.phase, phase::ORDERED);
    let handle = handle_of(&r, "acme");
    r.pay.authorize(&handle);

    // No tick: the webhook alone sets the one path going.
    let (status, said) = deliver_paid(&r, &run.reference, &handle, "evt_acme_1");
    assert_eq!(status, 200, "{said}");
    assert!(
        said.get("ignored").is_none(),
        "an estate's payment is ours, never ignored: {said}"
    );
    assert_eq!(said["reference"], run.reference.as_str(), "{said}");
    let after = r.state.estates.get("acme").unwrap();
    assert!(
        after.captured_at_ms > 0,
        "captured on the driver's own path, at once: {:#?}",
        after.history
    );
    assert_ne!(after.phase, phase::ORDERED);
    let rows = r.state.callbacks.rows().unwrap();
    assert!(
        rows.iter().any(|row| row.reference == run.reference),
        "the delivery is kept in the one inbox: {rows:?}"
    );

    // Delivered again: answered from the kept row; nothing captured twice.
    let captures = r.pay.captures();
    let (status, again) = deliver_paid(&r, &run.reference, &handle, "evt_acme_1");
    assert_eq!(
        (status, again["duplicate"].clone()),
        (200, serde_json::Value::Bool(true)),
        "{again}"
    );
    assert_eq!(r.pay.captures(), captures, "one capture, whoever asks");
}

/// ★ **RA5-43: the webhook's trigger says QUEUED only when the drive was queued.** `drive_now`
/// threw away its submit's answer (`let _ = lane.submit(spec)`), so a refused submit was kept in
/// the inbox as Done "QUEUED … driven on its one completion path" and a redelivery was answered
/// from that false row. A refused submit (here a newer drive already holds the estate's lane) is
/// kept as KEPT: the tick drives it.
#[test]
fn a_webhook_whose_drive_is_not_queued_is_kept_not_queued() {
    let r = rig_with("estate-webhook-kept", cfg(), "PASS");
    start_for(
        &r.state,
        "acme",
        "buyer@example.com",
        "",
        "alice",
        &EstateSizes::default(),
    )
    .unwrap();
    let run = r.state.estates.get("acme").unwrap();
    let handle = handle_of(&r, "acme");
    r.pay.authorize(&handle);
    // The estate's lane, on a runtime nobody drives: a newer drive waits in it and never runs.
    let idle = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap();
    {
        let _in = idle.enter();
        let lane = crate::estate_order::drive_lane(&r.state, "acme").unwrap();
        let newer = jera::jobs::chain::ChainSpec::new(
            crate::estate_order::DRIVE_JOB,
            "drive=acme-newer",
            "drive-acme",
            5,
            "monetize",
            serde_json::json!({ "company": "acme" }),
        );
        assert!(lane.submit(newer).is_new());
    }
    let (status, said) = deliver_paid(&r, &run.reference, &handle, "evt_acme_kept");
    assert_eq!(status, 200, "{said}");
    let rows = r.state.callbacks.rows().unwrap();
    let row = rows
        .iter()
        .find(|row| row.reference == run.reference)
        .expect("kept in the inbox");
    match &row.state {
        crate::inbox::InboxState::Done { state, .. } => assert_eq!(state, "KEPT", "{row:?}"),
        other => panic!("not closed: {other:?}"),
    }
}