payrust 0.1.0

PayPal REST API client for Rust
Documentation
use crate::models::common::{Intent, LandingPage, Link, Money, ShippingPreference, UserAction};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum OrderStatus {
    Created,
    Approved,
    Saved,
    Voided,
    Completed,
    PayerActionRequired,
}

#[derive(Debug, Serialize)]
pub struct CreateOrderRequest {
    pub intent: Intent,
    pub purchase_units: Vec<PurchaseUnit>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payment_source: Option<PaymentSource>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub application_context: Option<ApplicationContext>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct PurchaseUnit {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub reference_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub custom_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub invoice_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub soft_descriptor: Option<String>,
    pub amount: PurchaseAmount,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub items: Option<Vec<Item>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shipping: Option<Shipping>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payments: Option<PaymentCollection>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct PurchaseAmount {
    pub currency_code: String,
    pub value: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub breakdown: Option<AmountBreakdown>,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AmountBreakdown {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub item_total: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shipping: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub handling: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tax_total: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub insurance: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shipping_discount: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub discount: Option<Money>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
    pub name: String,
    pub quantity: String,
    pub unit_amount: Money,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub description: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sku: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub category: Option<ItemCategory>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tax: Option<Money>,
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ItemCategory {
    DigitalGoods,
    PhysicalGoods,
    Donation,
}

#[derive(Debug, Default, Serialize, Deserialize)]
pub struct Shipping {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<ShippingName>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address: Option<Address>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShippingName {
    pub full_name: String,
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Address {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address_line_1: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub address_line_2: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub admin_area_1: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub admin_area_2: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub postal_code: Option<String>,
    pub country_code: String,
}

#[derive(Debug, Default, Serialize)]
pub struct ApplicationContext {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub locale: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub landing_page: Option<LandingPage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shipping_preference: Option<ShippingPreference>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_action: Option<UserAction>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub return_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cancel_url: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct PaymentSource {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub paypal: Option<PayPalWallet>,
}

#[derive(Debug, Default, Serialize)]
pub struct PayPalWallet {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub experience_context: Option<ExperienceContext>,
}

#[derive(Debug, Default, Serialize)]
pub struct ExperienceContext {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub brand_name: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub shipping_preference: Option<ShippingPreference>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub landing_page: Option<LandingPage>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub user_action: Option<UserAction>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub return_url: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub cancel_url: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct Order {
    pub id: String,
    pub status: OrderStatus,
    #[serde(default)]
    pub intent: Option<Intent>,
    #[serde(default)]
    pub purchase_units: Vec<PurchaseUnit>,
    #[serde(default)]
    pub links: Vec<Link>,
    pub create_time: Option<String>,
    pub update_time: Option<String>,
}

impl Order {
    pub fn approve_url(&self) -> Option<&str> {
        self.links
            .iter()
            .find(|l| l.rel == "approve" || l.rel == "payer-action")
            .map(|l| l.href.as_str())
    }

    pub fn capture_url(&self) -> Option<&str> {
        self.links
            .iter()
            .find(|l| l.rel == "capture")
            .map(|l| l.href.as_str())
    }

    pub fn self_url(&self) -> Option<&str> {
        self.links
            .iter()
            .find(|l| l.rel == "self")
            .map(|l| l.href.as_str())
    }

    pub fn is_approved(&self) -> bool {
        self.status == OrderStatus::Approved
    }
    pub fn is_completed(&self) -> bool {
        self.status == OrderStatus::Completed
    }

    pub fn total(&self) -> Option<f64> {
        let mut total = 0.0;
        for pu in &self.purchase_units {
            total += pu.amount.value.parse::<f64>().ok()?;
        }
        Some(total)
    }

    pub fn currency(&self) -> Option<&str> {
        let mut iter = self.purchase_units.iter();
        let first = iter.next()?;
        let code = first.amount.currency_code.as_str();

        if iter.all(|pu| pu.amount.currency_code == code) {
            Some(code)
        } else {
            None
        }
    }
}

#[derive(Debug, Deserialize, Serialize)]
pub struct PaymentCollection {
    #[serde(default)]
    pub captures: Vec<Capture>,
    #[serde(default)]
    pub refunds: Vec<Refund>,
    #[serde(default)]
    pub authorizations: Vec<Authorization>,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Capture {
    pub id: String,
    pub status: CaptureStatus,
    #[serde(default)]
    pub amount: Option<Money>,
    pub create_time: Option<String>,
    pub update_time: Option<String>,
    #[serde(default)]
    pub links: Vec<Link>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum CaptureStatus {
    Completed,
    Declined,
    PartiallyRefunded,
    Pending,
    Refunded,
    Failed,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Refund {
    pub id: String,
    pub status: RefundStatus,
    #[serde(default)]
    pub amount: Option<Money>,
    pub create_time: Option<String>,
    #[serde(default)]
    pub links: Vec<Link>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RefundStatus {
    Cancelled,
    Failed,
    Pending,
    Completed,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct Authorization {
    pub id: String,
    pub status: AuthorizationStatus,
    #[serde(default)]
    pub amount: Option<Money>,
    pub create_time: Option<String>,
    pub expiration_time: Option<String>,
    #[serde(default)]
    pub links: Vec<Link>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum AuthorizationStatus {
    Created,
    Captured,
    Denied,
    Expired,
    PartiallyCaptured,
    Voided,
    Pending,
}

#[derive(Debug, Default, Serialize)]
pub struct CaptureOrderRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub payment_source: Option<PaymentSource>,
}

#[derive(Debug, Serialize)]
pub struct RefundRequest {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub amount: Option<Money>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub note_to_payer: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub invoice_id: Option<String>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::common::Intent;

    fn make_purchase_unit(currency: &str, value: &str) -> PurchaseUnit {
        PurchaseUnit {
            reference_id: None,
            description: None,
            custom_id: None,
            invoice_id: None,
            soft_descriptor: None,
            amount: PurchaseAmount {
                currency_code: currency.to_string(),
                value: value.to_string(),
                breakdown: None,
            },
            items: None,
            shipping: None,
            payments: None,
        }
    }

    fn base_order(units: Vec<PurchaseUnit>) -> Order {
        Order {
            id: "id".into(),
            status: OrderStatus::Created,
            intent: Some(Intent::Capture),
            purchase_units: units,
            links: vec![],
            create_time: None,
            update_time: None,
        }
    }

    #[test]
    fn total_sums_multiple_units() {
        let units = vec![
            make_purchase_unit("USD", "10.00"),
            make_purchase_unit("USD", "5.50"),
        ];
        let order = base_order(units);

        assert_eq!(order.total(), Some(15.5));
    }

    #[test]
    fn currency_conflict_returns_none() {
        let units = vec![
            make_purchase_unit("USD", "10.00"),
            make_purchase_unit("EUR", "5.00"),
        ];
        let order = base_order(units);

        assert_eq!(order.currency(), None);
    }
}