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