diff --git a/examples/plan.rs b/examples/plan.rs new file mode 100644 index 0000000..416fca0 --- /dev/null +++ b/examples/plan.rs @@ -0,0 +1,17 @@ +use pinpayments::{Client, Plan}; + +#[tokio::main] +async fn main() { + let secret_key = std::env::var("PINPAYMENTS_SECRET_KEY").expect("Missing PINPAYMENTS_SECRET_KEY in env"); + let client = Client::from_url(pinpayments::DEFAULT_TEST_API_BASE_URL, secret_key); + + let plans = match Plan::list(&client, None, None).await { + Ok(c) => Some(c), + Err(e) => { + println!("{e:?}"); + None + }, + }; + + println!("Results are {plans:?}"); +} diff --git a/src/params.rs b/src/params.rs index c77089d..dc6cf49 100644 --- a/src/params.rs +++ b/src/params.rs @@ -76,7 +76,7 @@ pub struct PaginationDetails { pub previous: Option, pub next: Option, pub per_page: u32, - pub pages: u32, + pub pages: Option, pub count: u64 } diff --git a/src/resources.rs b/src/resources.rs index e728aa7..993acd6 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -7,6 +7,7 @@ mod balance; mod bank_account; mod recipient; mod transfer; +mod plan; pub use currency::*; pub use charge::*; @@ -17,3 +18,4 @@ pub use balance::*; pub use bank_account::*; pub use recipient::*; pub use transfer::*; +pub use plan::*; diff --git a/src/resources/plan.rs b/src/resources/plan.rs new file mode 100644 index 0000000..17b3c48 --- /dev/null +++ b/src/resources/plan.rs @@ -0,0 +1,113 @@ +use time::{OffsetDateTime}; +use serde::{Deserialize, Serialize}; + +use crate::client::{Client, Response, StatusOnlyResponse}; +use crate::error::PinError; +use crate::ids::{PlanId}; +use crate::params::{unpack_contained, Page, Paginator, paginate}; +use crate::resources::{Currency}; +use crate::build_map; + +#[derive(PartialEq, Debug, Serialize, Default, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum IntervalUnit { + #[default] + Day, + Week, + Month, + Year +} + +#[derive(PartialEq, Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CustomerPermission { + Cancel +} + +#[derive(Debug, Default, Serialize)] +pub struct CreatePlan<'a> { + pub name: &'a str, + pub amount: i64, + pub currency: Currency, + pub interval: u32, + pub interval_unit: IntervalUnit, + + #[serde(skip_serializing_if = "Option::is_none")] + pub intervals: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub setup_amount: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub trial_amount: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub trial_interval: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub trial_interval_unit: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub customer_permissions: Option> +} + +#[derive(Debug, Default, Deserialize)] +pub struct SubscriptionCounts { + pub trial: u32, + pub active: u32, + pub cancelling: u32, + pub cancelled: u32 +} + +#[derive(Debug, Default, Deserialize)] +pub struct Plan { + pub token: PlanId, + pub name: String, + pub amount: i64, + pub currency: Currency, + pub interval: u32, + pub interval_unit: IntervalUnit, + pub intervals: u32, + pub setup_amount: u32, + pub trial_amount: u32, + pub trial_interval: u32, + pub trial_interval_unit: IntervalUnit, + pub customer_permissions: Vec, + pub subscription_counts: SubscriptionCounts, + + #[serde(with = "time::serde::iso8601::option")] + pub created_at: Option +} + +impl Plan { + pub fn create(client: &Client, params: CreatePlan<'_>) -> Response { + unpack_contained(client.post_form("/plans", ¶ms)) + } + + pub fn list(client: &Client, page: Option, per_page: Option) -> Response> { + let page = page.map(|s| s.to_string()); + let per_page = per_page.map(|s| s.to_string()); + let params = build_map([ + ("page", page.as_deref()), + ("per_page", per_page.as_deref()) + ]); + client.get_query("/plans", ¶ms) + } + + pub fn list_with_paginator(client: &Client, per_page: Option) -> Paginator> { + paginate( + move |page, per_page| { + Plan::list(client, Some(page), Some(per_page)) + }, + per_page.unwrap_or(25) + ) + } + + pub fn retrieve(client: &Client, token: &PlanId) -> Response { + unpack_contained(client.get(&format!("/plans/{}", token))) + } + + pub fn delete(client: &Client, token: &PlanId) -> StatusOnlyResponse { + client.delete_status_only(&format!("/plans/{}", token)) + } +} diff --git a/tests/fixtures/create-plan.json b/tests/fixtures/create-plan.json new file mode 100644 index 0000000..d1628d4 --- /dev/null +++ b/tests/fixtures/create-plan.json @@ -0,0 +1,25 @@ +{ + "response": { + "name": "Coffee Plan", + "amount": 1000, + "currency": "USD", + "setup_amount": 0, + "trial_amount": 0, + "interval": 30, + "interval_unit": "day", + "intervals": 6, + "trial_interval": 7, + "trial_interval_unit": "day", + "created_at": "2023-12-28T05:13:07Z", + "token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_permissions": [ + "cancel" + ], + "subscription_counts": { + "trial": 0, + "active": 0, + "cancelling": 0, + "cancelled": 0 + } + } +} diff --git a/tests/fixtures/get-plan.json b/tests/fixtures/get-plan.json new file mode 100644 index 0000000..15a20d9 --- /dev/null +++ b/tests/fixtures/get-plan.json @@ -0,0 +1,25 @@ +{ + "response": { + "name": "Coffee Plan", + "amount": 1000, + "currency": "USD", + "setup_amount": 0, + "trial_amount": 0, + "interval": 30, + "interval_unit": "day", + "intervals": 0, + "trial_interval": 7, + "trial_interval_unit": "day", + "created_at": "2023-12-28T05:44:36Z", + "token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_permissions": [ + "cancel" + ], + "subscription_counts": { + "trial": 0, + "active": 0, + "cancelling": 0, + "cancelled": 0 + } + } +} diff --git a/tests/fixtures/get-plans.json b/tests/fixtures/get-plans.json new file mode 100644 index 0000000..41acfa9 --- /dev/null +++ b/tests/fixtures/get-plans.json @@ -0,0 +1,32 @@ +{ + "response": [ + { + "name": "Coffee Plan", + "amount": 1000, + "currency": "USD", + "setup_amount": 0, + "trial_amount": 0, + "interval": 30, + "interval_unit": "day", + "intervals": 0, + "trial_interval": 7, + "trial_interval_unit": "day", + "created_at": "2023-12-28T05:31:34Z", + "token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_permissions": [ + "cancel" + ], + "subscription_counts": { + "trial": 0, + "active": 0, + "cancelling": 0, + "cancelled": 0 + } + } + ], + "pagination": { + "count": 1, + "per_page": 25, + "current": 1 + } +} diff --git a/tests/plan.rs b/tests/plan.rs new file mode 100644 index 0000000..402fb91 --- /dev/null +++ b/tests/plan.rs @@ -0,0 +1,171 @@ +use pinpayments::{Client, Currency, CreatePlan, Plan, IntervalUnit, CustomerPermission}; +use httptest::{Expectation, matchers::*, responders::*}; +use surf::http::auth::BasicAuth; +use time::macros::datetime; +use http::StatusCode; + +pub mod common; + +#[tokio::test] +async fn create_plan_test() { + let json = common::get_fixture("tests/fixtures/create-plan.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("POST", "/1/plans"), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::CREATED.into()) + .append_header("Content-Type", "application/json") + .body(serde_json::to_string(&json).expect("failed to serialize body"))), + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let plan = Plan::create( + &client, + CreatePlan { + name: "Coffee Plan", + amount: 1000, + currency: Currency::USD, + interval: 30, + interval_unit: IntervalUnit::Day, + intervals: Some(6), + setup_amount: Some(0), + trial_amount: Some(0), + trial_interval: Some(7), + trial_interval_unit: Some(IntervalUnit::Day), + customer_permissions: Some(vec![CustomerPermission::Cancel]), + ..Default::default() + } + ) + .await + .unwrap(); + + assert_eq!(plan.token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(plan.name, "Coffee Plan"); + assert_eq!(plan.currency, Currency::USD); + assert_eq!(plan.setup_amount, 0); + assert_eq!(plan.trial_amount, 0); + assert_eq!(plan.interval, 30); + assert_eq!(plan.interval_unit, IntervalUnit::Day); + assert_eq!(plan.intervals, 6); + assert_eq!(plan.trial_interval, 7); + assert_eq!(plan.trial_interval_unit, IntervalUnit::Day); + assert_eq!(plan.created_at.unwrap(), datetime!(2023-12-28 5:13:07 UTC)); + assert_eq!(plan.customer_permissions, vec![CustomerPermission::Cancel]); + assert_eq!(plan.subscription_counts.trial, 0); + assert_eq!(plan.subscription_counts.active, 0); + assert_eq!(plan.subscription_counts.cancelling, 0); + assert_eq!(plan.subscription_counts.cancelled, 0); +} + +#[tokio::test] +async fn list_plans_test() { + let json = common::get_fixture("tests/fixtures/get-plans.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("GET", "/1/plans"), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with(json_encoded(json)) + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let plans = Plan::list(&client, None, None) + .await + .unwrap(); + + assert_eq!(plans.items[0].token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(plans.items[0].name, "Coffee Plan"); + assert_eq!(plans.items[0].amount, 1000); + assert_eq!(plans.items[0].setup_amount, 0); + assert_eq!(plans.items[0].trial_amount, 0); + assert_eq!(plans.items[0].interval, 30); + assert_eq!(plans.items[0].interval_unit, IntervalUnit::Day); + assert_eq!(plans.items[0].trial_interval, 7); + assert_eq!(plans.items[0].trial_interval_unit, IntervalUnit::Day); + assert_eq!(plans.items[0].created_at.unwrap(), datetime!(2023-12-28 5:31:34 UTC)); +} + +#[tokio::test] +async fn retrieve_plan_test() { + let json = common::get_fixture("tests/fixtures/get-plan.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let plan_token = "plan_ZyDee4HNeUHFHC4SpM2idg".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("GET", format!("/1/plans/{}", plan_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with(json_encoded(json)) + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let plan = Plan::retrieve(&client, &plan_token).await.unwrap(); + + assert_eq!(plan.token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(plan.name, "Coffee Plan"); + assert_eq!(plan.amount, 1000); + assert_eq!(plan.setup_amount, 0); + assert_eq!(plan.trial_amount, 0); + assert_eq!(plan.interval, 30); + assert_eq!(plan.interval_unit, IntervalUnit::Day); + assert_eq!(plan.trial_interval, 7); + assert_eq!(plan.trial_interval_unit, IntervalUnit::Day); + assert_eq!(plan.created_at.unwrap(), datetime!(2023-12-28 5:44:36 UTC)); +} + +#[tokio::test] +async fn delete_plan_test() { + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let plan_token = "plan_lfUYEBK14zotCTykezJkfg".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("DELETE", format!("/1/plans/{}", plan_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with(status_code(StatusCode::NO_CONTENT.into())) + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let result = Plan::delete(&client, &plan_token) + .await + .unwrap(); + + assert_eq!(result, StatusCode::NO_CONTENT); +}