Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

crates/monetize-pay/src/instance.rs

//! **One vendor KIND, several INSTANCES** (ONE monetize, 5.4).
//!
//! A monetize that serves several products bills each through its own
//! account at the same vendor — gunnar's Stripe and holger's Stripe. The
//! config says so with `[vendors.<kind>.<instance>]`, and the server knows the
//! vendor by its INSTANCE id `<kind>/<instance>` (`stripe/gunnar`): that is
//! the name in `vendor()`, on every handle the ledger keeps, on a paid
//! signup, in the webhook path `/callback/<kind>/<instance>`.
//!
//! The vendor's own code knows only its KIND: Stripe's logic checks that a
//! handle is `stripe`'s and answers handles named `stripe`. [`Names`] is the
//! one translation between the two, at the edge: a handle the server hands in
//! must name THIS instance (another instance's is
//! [`PayError::UnknownHandle`], never passed on) and reaches the vendor under
//! its kind; everything the vendor hands out is renamed to the instance.
//! [`Instance`] wraps a linked provider in it; a vendor guest uses the same
//! [`Names`] inside its host.

use crate::payment::{Charge, Of, Page, PayManifest, Payment, Return, Standing};
use crate::reconcile::PaidAtVendor;
use crate::{CallbackEvent, ChargeHandle, ChargeStatus, Money, PayError, RawCallback, Settlement};

/// The placeholder a vendor's `config_envs` may carry:
/// `MONETIZE_STRIPE_{INSTANCE}_SECRET_KEY`, expanded per configured instance
/// by [`expand_env`].
pub const INSTANCE_PLACEHOLDER: &str = "{INSTANCE}";

/// `<kind>/<instance>` → `(kind, instance)`, both checked: lower-case ASCII
/// letters, digits and `-`, non-empty, exactly one `/`. The refusal carries
/// `[vendor-instance-name]`.
pub fn split(id: &str) -> Result<(&str, &str), String> {
    let ok = |s: &str| {
        !s.is_empty()
            && s.bytes()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'-')
    };
    match id.split_once('/') {
        Some((kind, instance)) if ok(kind) && ok(instance) => Ok((kind, instance)),
        _ => Err(format!(
            "[vendor-instance-name] `{id}` is not `<kind>/<instance>` (lower-case letters, digits and `-`): a vendor is configured as [vendors.<kind>.<instance>], one instance per account"
        )),
    }
}

/// The KIND part of a vendor name: `stripe/gunnar` → `stripe`; a name with no
/// instance is its own kind (a code-built test double, `none`).
pub fn kind_of(id: &str) -> &str {
    id.split_once('/').map_or(id, |(k, _)| k)
}

/// `[vendors.stripe.gunnar]` for `stripe/gunnar` — how a refusal names the block.
pub fn block_of(id: &str) -> String {
    format!("[vendors.{}]", id.replace('/', "."))
}

/// An instance as it appears in an environment name: `holger-test2` → `HOLGER_TEST2`.
pub fn env_key(instance: &str) -> String {
    instance
        .chars()
        .map(|c| {
            if c == '-' {
                '_'
            } else {
                c.to_ascii_uppercase()
            }
        })
        .collect()
}

/// `MONETIZE_STRIPE_{INSTANCE}_SECRET_KEY` for `gunnar` →
/// `MONETIZE_STRIPE_GUNNAR_SECRET_KEY`. A name with no placeholder is itself.
pub fn expand_env(template: &str, instance: &str) -> String {
    template.replace(INSTANCE_PLACEHOLDER, &env_key(instance))
}

/// A set of markets interned for the life of the process, once per distinct set, so a
/// [`crate::Vendor`] can borrow it however often a manifest is read.
pub fn intern_markets(m: &[crate::Market]) -> &'static [crate::Market] {
    static SETS: std::sync::Mutex<Vec<&'static [crate::Market]>> =
        std::sync::Mutex::new(Vec::new());
    let mut p = SETS.lock().unwrap_or_else(|e| e.into_inner());
    if let Some(found) = p.iter().find(|s| **s == m) {
        return found;
    }
    let leaked: &'static [crate::Market] = Box::leak(m.to_vec().into_boxed_slice());
    p.push(leaked);
    leaked
}

/// Interned for the life of the process: a vendor's name is `&'static str`
/// on the trait, and the same instance built again (a live replacement) must
/// not leak again.
pub fn intern(s: &str) -> &'static str {
    use std::collections::BTreeSet;
    use std::sync::Mutex;
    static NAMES: Mutex<BTreeSet<&'static str>> = Mutex::new(BTreeSet::new());
    let mut names = NAMES.lock().unwrap_or_else(|p| p.into_inner());
    if let Some(n) = names.get(s) {
        return n;
    }
    let n: &'static str = Box::leak(s.to_owned().into_boxed_str());
    names.insert(n);
    n
}

/// **The translation between an instance and its kind.**
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Names {
    /// `stripe/gunnar` — what the server sees.
    pub id: &'static str,
    /// `stripe` — what the vendor's own code sees.
    pub kind: &'static str,
}

impl Names {
    /// From a checked `<kind>/<instance>`.
    pub fn of(id: &str) -> Result<Names, String> {
        let (kind, _) = split(id)?;
        Ok(Names {
            id: intern(id),
            kind: intern(kind),
        })
    }

    /// A handle the server hands in: this instance's, or not ours at all.
    pub fn inbound(&self, h: &ChargeHandle) -> Result<ChargeHandle, PayError> {
        if h.vendor == self.id {
            Ok(ChargeHandle {
                vendor: self.kind.into(),
                id: h.id.clone(),
            })
        } else {
            Err(PayError::UnknownHandle(h.clone()))
        }
    }

    /// A handle the vendor hands out, named for the instance.
    pub fn outbound(&self, h: ChargeHandle) -> ChargeHandle {
        if h.vendor == self.kind {
            ChargeHandle {
                vendor: self.id.into(),
                id: h.id,
            }
        } else {
            h
        }
    }

    fn name(&self, vendor: String) -> String {
        if vendor == self.kind {
            self.id.into()
        } else {
            vendor
        }
    }

    /// Every name an error carries, renamed.
    pub fn error(&self, e: PayError) -> PayError {
        match e {
            PayError::UnknownHandle(h) => PayError::UnknownHandle(self.outbound(h)),
            PayError::Signup(mut s) => {
                s.vendor = self.name(std::mem::take(&mut s.vendor));
                PayError::Signup(s)
            }
            PayError::Reversal(mut r) => {
                r.vendor = self.name(std::mem::take(&mut r.vendor));
                r.handle = self.outbound(r.handle);
                PayError::Reversal(r)
            }
            other => other,
        }
    }

    pub fn event(&self, e: CallbackEvent) -> CallbackEvent {
        CallbackEvent {
            handle: self.outbound(e.handle),
            ..e
        }
    }

    pub fn paid(&self, mut p: PaidAtVendor) -> PaidAtVendor {
        p.vendor = self.name(p.vendor);
        p
    }
}

/// **A linked vendor as one instance**: its manifest's `vendor` is the instance id, and
/// every handle is translated by [`Names`].
pub struct Instance {
    names: Names,
    inner: Box<dyn Payment>,
}

impl Instance {
    /// `inner` is spoken to under the name it gives itself (normally the
    /// kind: the `stripe` factory builds a vendor called `stripe`); the
    /// server only ever sees `id`.
    pub fn wrap(id: &str, inner: Box<dyn Payment>) -> Result<Instance, String> {
        Names::of(id)?;
        let names = Names {
            id: intern(id),
            kind: intern(&inner.manifest().vendor),
        };
        Ok(Instance { names, inner })
    }
    pub fn names(&self) -> Names {
        self.names
    }
}

impl Payment for Instance {
    fn manifest(&self) -> PayManifest {
        PayManifest {
            vendor: self.names.id.to_string(),
            ..self.inner.manifest()
        }
    }
    fn page(&self, charge: &Charge<'_>, back: &Return) -> Result<Page, PayError> {
        let id = self.names.id;
        match self
            .inner
            .page(charge, back)
            .map_err(|e| self.names.error(e))?
        {
            Page::Order { handle, url } => Ok(Page::Order {
                handle: self.names.outbound(handle),
                url,
            }),
            // The paid signup names THIS instance, as its webhook's would.
            Page::TestSignup(p) => Ok(Page::TestSignup(p.map(|mut p| {
                p.vendor = id.to_string();
                p
            }))),
            other => Ok(other),
        }
    }
    fn status(&self, of: &Of<'_>) -> Result<Standing, PayError> {
        let inbound;
        let of = match of {
            Of::Charge(h) => {
                inbound = self.names.inbound(h)?;
                Of::Charge(&inbound)
            }
            Of::Page(h) => match self.names.inbound(h) {
                Ok(h) => {
                    inbound = h;
                    Of::Page(&inbound)
                }
                Err(_) => return Ok(Standing::Page(None)),
            },
            other => other.clone(),
        };
        let id = self.names.id;
        match self.inner.status(&of).map_err(|e| self.names.error(e))? {
            // A paid signup names THIS instance, as its webhook's would.
            Standing::Signup(Some(crate::signup::SignupAtVendor::Paid(mut p))) => {
                p.vendor = id.to_string();
                Ok(Standing::Signup(Some(crate::signup::SignupAtVendor::Paid(
                    p,
                ))))
            }
            other => Ok(other),
        }
    }
    fn capture(&self, h: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.inner
            .capture(&self.names.inbound(h)?)
            .map_err(|e| self.names.error(e))
    }
    fn release(&self, h: &ChargeHandle) -> Result<ChargeStatus, PayError> {
        self.inner
            .release(&self.names.inbound(h)?)
            .map_err(|e| self.names.error(e))
    }
    fn refund(&self, h: &ChargeHandle, amount: Money) -> Result<ChargeStatus, PayError> {
        self.inner
            .refund(&self.names.inbound(h)?, amount)
            .map_err(|e| self.names.error(e))
    }
    fn callback(&self, raw: RawCallback<'_>) -> Result<CallbackEvent, PayError> {
        self.inner
            .callback(raw)
            .map(|e| self.names.event(e))
            .map_err(|e| self.names.error(e))
    }
    fn settlement(&self, h: &ChargeHandle) -> Option<Settlement> {
        self.inner.settlement(&self.names.inbound(h).ok()?)
    }
    fn paid_since(&self, since_unix_ms: i64) -> Result<Vec<PaidAtVendor>, PayError> {
        self.inner
            .paid_since(since_unix_ms)
            .map(|v| v.into_iter().map(|p| self.names.paid(p)).collect())
            .map_err(|e| self.names.error(e))
    }
    fn livemode(&self) -> Option<bool> {
        self.inner.livemode()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn names_split_check_and_expand() {
        assert_eq!(
            split("stripe/holger-test2").unwrap(),
            ("stripe", "holger-test2")
        );
        for bad in [
            "stripe",
            "stripe/",
            "/a",
            "Stripe/a",
            "stripe/a/b",
            "stripe/a b",
        ] {
            assert!(
                split(bad).unwrap_err().contains("[vendor-instance-name]"),
                "{bad}"
            );
        }
        assert_eq!(kind_of("stripe/gunnar"), "stripe");
        assert_eq!(kind_of("mockpay"), "mockpay");
        assert_eq!(block_of("stripe/gunnar"), "[vendors.stripe.gunnar]");
        assert_eq!(
            expand_env("MONETIZE_STRIPE_{INSTANCE}_SECRET_KEY", "holger-test2"),
            "MONETIZE_STRIPE_HOLGER_TEST2_SECRET_KEY"
        );
        assert_eq!(
            expand_env("MONETIZE_STRIPE_BASE_URL", "gunnar"),
            "MONETIZE_STRIPE_BASE_URL"
        );
        assert!(
            std::ptr::eq(intern("stripe/x"), intern("stripe/x")),
            "interned once"
        );
    }

    #[test]
    fn handles_cross_the_edge_renamed_and_another_instances_are_refused() {
        let n = Names::of("stripe/gunnar").unwrap();
        let mine = ChargeHandle {
            vendor: "stripe/gunnar".into(),
            id: "cs_1".into(),
        };
        assert_eq!(
            n.inbound(&mine).unwrap(),
            ChargeHandle {
                vendor: "stripe".into(),
                id: "cs_1".into()
            }
        );
        for other in ["stripe/holger", "stripe"] {
            let h = ChargeHandle {
                vendor: other.into(),
                id: "cs_1".into(),
            };
            assert!(
                matches!(n.inbound(&h), Err(PayError::UnknownHandle(x)) if x == h),
                "{other}"
            );
        }
        assert_eq!(
            n.outbound(ChargeHandle {
                vendor: "stripe".into(),
                id: "cs_2".into()
            })
            .vendor,
            "stripe/gunnar"
        );
        match n.error(PayError::UnknownHandle(ChargeHandle {
            vendor: "stripe".into(),
            id: "x".into(),
        })) {
            PayError::UnknownHandle(h) => assert_eq!(h.vendor, "stripe/gunnar"),
            other => panic!("{other:?}"),
        }
    }
}