crates/monetize-server/src/returns.rs
//! **The buyer comes back to the site they bought on** (A16, owner 2026-10-23): a vendor
//! instance (one Stripe account) may serve several sites — test3.holger.rs pays through
//! test2's — and its configured return page names ONE site. Every checkout a site's buyer opens
//! therefore returns to THAT site's own pages (`https://<site>/paid/<reference>`, `/cancel/…`),
//! whichever instance takes the money; the instance's own page is only for a buyer no site
//! asked for (an operator's order on the ONE box).
use monetize_pay::payment::{Charge, Of, Page, PayManifest, Return, Standing};
use monetize_pay::{reconcile, CallbackEvent, ChargeHandle, ChargeStatus, Money, PayError, Payment, RawCallback, Settlement, Vendor};
/// Pure: **the site's own ways back for `reference`**: `(paid, cancelled)`. `None` for a site
/// that names no host a browser can be sent to.
pub fn site_returns(site: &str, reference: &str) -> Option<(String, String)> {
let host = site.trim().trim_end_matches('/').trim_start_matches("https://").to_ascii_lowercase();
let ok = !host.is_empty() && host.len() <= 253 && host.contains('.') && host.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'.' || b == b'-' || b == b':');
ok.then(|| (format!("https://{host}/paid/{reference}"), format!("https://{host}/cancel/{reference}")))
}
/// Pure: **where the buyer of `reference` comes back to**: the site's own pages when `site`
/// names one, else the vendor's own.
pub fn back_for(site: Option<&str>, reference: &str) -> Return {
match site.and_then(|s| site_returns(s, reference)) {
Some((paid, cancelled)) => Return::to(&paid, &cancelled),
None => Return::vendors_own(),
}
}
/// **A vendor whose every order page returns to `site`** (the caller's own), for a caller that
/// asks for the vendor's own pages (the core's transaction). Everything else is the vendor's.
pub struct ReturningTo<'a> {
pub inner: Vendor<'a>,
pub site: String,
}
impl<'a> ReturningTo<'a> {
/// `vendor`, returning to the declared site of the caller's estate when there is one.
pub fn of(vendor: Vendor<'a>, site: Option<&str>) -> ReturningTo<'a> {
ReturningTo { inner: vendor, site: site.unwrap_or_default().to_owned() }
}
/// It, as the host holds a vendor: the inner vendor's name and markets.
pub fn vendor(&self) -> Vendor<'_> {
Vendor::new(self.inner.name, self.inner.markets, self)
}
}
impl Payment for ReturningTo<'_> {
fn manifest(&self) -> PayManifest {
self.inner.manifest()
}
fn page(&self, charge: &Charge<'_>, back: &Return) -> Result<Page, PayError> {
match charge {
Charge::Order(req) if back.paid.trim().is_empty() && back.cancel.trim().is_empty() => self.inner.page(charge, &back_for(Some(&self.site), &req.reference)),
_ => self.inner.page(charge, back),
}
}
fn status(&self, of: &Of<'_>) -> Result<Standing, PayError> {
self.inner.status(of)
}
fn capture(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
self.inner.capture(handle)
}
fn release(&self, handle: &ChargeHandle) -> Result<ChargeStatus, PayError> {
self.inner.release(handle)
}
fn refund(&self, handle: &ChargeHandle, amount: Money) -> Result<ChargeStatus, PayError> {
self.inner.refund(handle, amount)
}
fn callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
self.inner.callback(raw)
}
fn settlement(&self, handle: &ChargeHandle) -> Option<Settlement> {
self.inner.settlement(handle)
}
fn paid_since(&self, since_unix_ms: i64) -> Result<Vec<reconcile::PaidAtVendor>, PayError> {
self.inner.paid_since(since_unix_ms)
}
fn livemode(&self) -> Option<bool> {
self.inner.livemode()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
/// A vendor that remembers where it was told to send the buyer back.
#[derive(Default)]
struct Heard(Mutex<Vec<(String, String)>>);
impl Payment for Heard {
fn manifest(&self) -> PayManifest {
PayManifest { vendor: "stripe/gunnar-test2".into(), markets: vec![monetize_pay::Market::Se], settings: vec![], holds: true, test_payer: None }
}
fn page(&self, charge: &Charge<'_>, back: &Return) -> Result<Page, PayError> {
let heard = if back.paid.is_empty() { ("the instance's own page".to_string(), String::new()) } else { (back.paid.clone(), back.cancel.clone()) };
self.0.lock().unwrap().push(heard);
match charge {
Charge::Order(_) => Ok(Page::Order { handle: ChargeHandle { vendor: "stripe".into(), id: "pi_1".into() }, url: None }),
_ => Ok(Page::Signup(None)),
}
}
fn status(&self, _: &Of<'_>) -> Result<Standing, PayError> {
unimplemented!()
}
fn capture(&self, _: &ChargeHandle) -> Result<ChargeStatus, PayError> {
unimplemented!()
}
fn release(&self, _: &ChargeHandle) -> Result<ChargeStatus, PayError> {
unimplemented!()
}
fn refund(&self, _: &ChargeHandle, _: Money) -> Result<ChargeStatus, PayError> {
unimplemented!()
}
fn callback(&self, _: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
unimplemented!()
}
fn settlement(&self, _: &ChargeHandle) -> Option<Settlement> {
None
}
fn paid_since(&self, _: i64) -> Result<Vec<reconcile::PaidAtVendor>, PayError> {
Ok(vec![])
}
fn livemode(&self) -> Option<bool> {
None
}
}
fn req(reference: &str) -> monetize_pay::ChargeRequest {
monetize_pay::ChargeRequest { reference: reference.into(), amount: Money { minor: 100, currency: monetize_pay::Currency::Sek }, market: monetize_pay::Market::Se, description: "Holger 5 GiB".into(), payer: None }
}
/// ★ **A site on a shared Stripe instance gets its own way back** (owner 2026-10-23,
/// test3.holger.rs on test2's instance): the checkout returns to the buying site's
/// `/paid/<reference>` and `/cancel/<reference>`, never to the instance's own site.
#[test]
fn a_site_on_a_shared_stripe_instance_gets_its_own_return() {
let stripe = Heard::default();
let own = Return::vendors_own();
ReturningTo::of(Vendor::of(&stripe), Some("test3.holger.rs")).vendor().reserve(&req("holger/x14/2026-10-23+5gib"), &own).unwrap();
Vendor::of(&stripe).reserve(&req("estate/acme/change-2"), &back_for(Some("test2.gunnar.rs"), "estate/acme/change-2")).unwrap();
ReturningTo::of(Vendor::of(&stripe), None).vendor().reserve(&req("op-1"), &own).unwrap();
let heard = stripe.0.lock().unwrap().clone();
assert_eq!(heard[0], ("https://test3.holger.rs/paid/holger/x14/2026-10-23+5gib".to_owned(), "https://test3.holger.rs/cancel/holger/x14/2026-10-23+5gib".to_owned()));
assert_eq!(heard[1].0, "https://test2.gunnar.rs/paid/estate/acme/change-2");
assert_eq!(heard[2].0, "the instance's own page", "no site asked: the instance's own page");
assert_eq!(site_returns("javascript:alert(1)", "x"), None);
assert_eq!(site_returns("", "x"), None);
}
}