crates/monetize-pay/src/conformance.rs
//! **The `Payment` conformance suite** — one test every [`Payment`] runs against its
//! fake (Stripe at the fake Stripe, Klarna at the fake Klarna, Swish at the fake
//! Swish, the host's guest adapter over a signed module at any of them). It holds a
//! vendor to the contract the ledger relies on, and to its own word on `holds`:
//!
//! 1. the manifest names the vendor and sells in the order's market; the fake is
//! never live money;
//! 2. `page(Order)` mints a page: a handle of this vendor, nothing drawn —
//! `status` is `AwaitingCustomer` and `capture` draws nothing;
//! 3. the customer pays (the fixture plays the customer and, where the vendor
//! learns of it by a notice, delivers that notice through `callback`);
//! 4. **`holds` honesty**: a vendor that says it holds now answers `Reserved`; one
//! that says it does not answers `Captured` at once;
//! 5. `capture` answers `Captured`, and again `Captured` (idempotent, never drawn twice);
//! 6. `refund` of a part answers the charge's standing after;
//! 7. `settlement` and `paid_since` answer without panicking;
//! 8. a second order is RELEASED: before the customer pays where the vendor cannot
//! hold, after the customer paid where it can — and reads `Released` after;
//! 9. an unverifiable notice is `Unverified` or `Ignored`, never an event.
//!
//! The suite is a function, not a `#[test]`, so each vendor's own test builds its
//! fixture (its fake, its order, its customer) and calls it. It panics with the step
//! named.
use crate::payment::{Charge, Of, Page, Payment, Return, Standing};
use crate::{ChargeHandle, ChargeRequest, ChargeStatus, Money, PayError, RawCallback};
/// **What one implementation hands the suite.**
pub struct PayFixture<'a> {
pub payment: &'a dyn Payment,
/// An order in a market the vendor sells in; the suite derives a second one from it.
pub order: ChargeRequest,
/// **The customer pays the page behind `handle`** at the fake — and, where the
/// vendor learns of the payment by a notice, delivers that notice to
/// `payment.callback` (the fixture knows the fake's notice; the suite does not).
pub customer_pays: &'a dyn Fn(&dyn Payment, &ChargeHandle),
/// Less than the order's amount.
pub refund: Money,
/// A notice nobody signed, for step 9. `None` skips the step.
pub forged_notice: Option<Notice>,
}
/// A notice as it arrives: its headers and its body.
pub type Notice = (Vec<(String, String)>, Vec<u8>);
fn standing(p: &dyn Payment, h: &ChargeHandle, step: &str) -> ChargeStatus {
match p.status(&Of::Charge(h)).unwrap_or_else(|e| panic!("{step}: status answers: {e:?}")) {
Standing::Charge(s) => s,
other => panic!("{step}: status answered {other:?} to a charge"),
}
}
fn mint(p: &dyn Payment, order: &ChargeRequest, step: &str) -> (ChargeHandle, Option<String>) {
match p.page(&Charge::Order(order), &Return::vendors_own()).unwrap_or_else(|e| panic!("{step}: page(Order) mints: {e:?}")) {
Page::Order { handle, url } => (handle, url),
other => panic!("{step}: page answered {other:?} to an order"),
}
}
/// **pay → hold or capture → capture → refund; release; `holds` honesty.**
pub fn pay_hold_capture_refund(f: &PayFixture<'_>) {
let p = f.payment;
// 1. the manifest
let m = p.manifest();
assert!(!m.vendor.trim().is_empty(), "manifest: the vendor is named");
assert!(m.markets.contains(&f.order.market), "manifest: the order's market {:?} is sold: {:?}", f.order.market, m.markets);
assert_ne!(p.livemode(), Some(true), "livemode: a fake is never live money");
assert!(f.refund.minor < f.order.amount.minor, "fixture: the refund is a part of the order");
// 2. the page, nothing drawn
let (h, url) = mint(p, &f.order, "page");
assert!(!h.id.is_empty(), "page: a handle");
if let Some(u) = &url {
assert!(u.contains("://"), "page: the URL is a URL (https or the app's own scheme): {u}");
}
assert_eq!(standing(p, &h, "status.before"), ChargeStatus::AwaitingCustomer, "status: nothing held before the customer pays");
assert_eq!(p.capture(&h).expect("capture.before: answers"), ChargeStatus::AwaitingCustomer, "capture: draws nothing before the customer pays");
let page_again = match p.status(&Of::Page(&h)) {
Ok(Standing::Page(u)) => u,
Ok(other) => panic!("status(Page): answered {other:?}"),
Err(e) => panic!("status(Page): {e:?}"),
};
if url.is_some() {
assert_eq!(page_again, url, "status(Page): the page's URL, read again");
}
// 3. the customer pays
(f.customer_pays)(p, &h);
// 4. holds honesty
let after = standing(p, &h, "status.paid");
if m.holds {
assert_eq!(after, ChargeStatus::Reserved, "holds = true: the money is HELD once the customer paid");
} else {
assert!(matches!(after, ChargeStatus::Captured { .. }), "holds = false: the money MOVED when the customer paid: {after:?}");
}
// 5. capture, twice
assert!(matches!(p.capture(&h).expect("capture: answers"), ChargeStatus::Captured { .. }), "capture: drawn");
assert!(matches!(p.capture(&h).expect("capture.again: answers"), ChargeStatus::Captured { .. }), "capture: idempotent");
assert!(matches!(standing(p, &h, "status.captured"), ChargeStatus::Captured { .. }));
// 6. a partial refund
let r = p.refund(&h, f.refund).expect("refund: a part is given back");
assert!(matches!(r, ChargeStatus::Captured { .. }), "refund: the charge stands captured after a partial refund: {r:?}");
// 7. the back office
// A settlement row, when the vendor says one, is in the order's currency (it was `let _ =`).
if let Some(s) = p.settlement(&h) {
assert_eq!(s.fee.currency, f.order.amount.currency, "settlement: the fee is in the order's currency");
assert_eq!(s.net.currency, f.order.amount.currency, "settlement: the net is in the order's currency");
}
match p.paid_since(0) {
Ok(_) => {}
Err(PayError::Unavailable(_)) => {}
Err(e) => panic!("paid_since: an outage or a list, never {e:?}"),
}
// 8. a second order, released
let second = ChargeRequest { reference: format!("{}+2", f.order.reference), ..f.order.clone() };
let (h2, _) = mint(p, &second, "page.second");
if m.holds {
(f.customer_pays)(p, &h2);
assert_eq!(standing(p, &h2, "status.second"), ChargeStatus::Reserved);
}
assert_eq!(p.release(&h2).expect("release: answers"), ChargeStatus::Released, "release: unlocked without drawing");
assert_eq!(standing(p, &h2, "status.released"), ChargeStatus::Released, "status: released, read again");
assert_eq!(p.release(&h2).expect("release.again: answers"), ChargeStatus::Released, "release: idempotent");
// 9. a forged notice
if let Some((headers, body)) = &f.forged_notice {
match p.callback(RawCallback { headers, body }) {
Err(PayError::Unverified(_)) | Err(PayError::Ignored(_)) => {}
Ok(ev) => panic!("callback: a notice nobody signed became an event: {ev:?}"),
Err(e) => panic!("callback: a notice nobody signed is Unverified or Ignored, not {e:?}"),
}
}
}