From e93f5a0dcd70f578ba17d785984ab9d1b761b85a Mon Sep 17 00:00:00 2001 From: Chris Kruger Date: Fri, 29 Dec 2023 06:42:51 +0800 Subject: [PATCH] add subscription (#12) * add subscription * subscription tests and fixtures --- src/resources.rs | 2 + src/resources/subscription.rs | 92 +++++++ tests/fixtures/create-subscription.json | 14 + tests/fixtures/delete-subscription.json | 14 + tests/fixtures/get-subscription.json | 14 + tests/fixtures/get-subscriptions.json | 22 ++ .../list-subscription-ledger-entries.json | 17 ++ tests/fixtures/reactivate-subscription.json | 14 + tests/subscription.rs | 253 ++++++++++++++++++ 9 files changed, 442 insertions(+) create mode 100644 src/resources/subscription.rs create mode 100644 tests/fixtures/create-subscription.json create mode 100644 tests/fixtures/delete-subscription.json create mode 100644 tests/fixtures/get-subscription.json create mode 100644 tests/fixtures/get-subscriptions.json create mode 100644 tests/fixtures/list-subscription-ledger-entries.json create mode 100644 tests/fixtures/reactivate-subscription.json create mode 100644 tests/subscription.rs diff --git a/src/resources.rs b/src/resources.rs index 993acd6..3cd0fc0 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -8,6 +8,7 @@ mod bank_account; mod recipient; mod transfer; mod plan; +mod subscription; pub use currency::*; pub use charge::*; @@ -19,3 +20,4 @@ pub use bank_account::*; pub use recipient::*; pub use transfer::*; pub use plan::*; +pub use subscription::*; diff --git a/src/resources/subscription.rs b/src/resources/subscription.rs new file mode 100644 index 0000000..d3f6161 --- /dev/null +++ b/src/resources/subscription.rs @@ -0,0 +1,92 @@ +use time::{OffsetDateTime}; +use serde::{Deserialize, Serialize}; + +use crate::client::{Client, Response, StatusOnlyResponse}; +use crate::error::PinError; +use crate::ids::{PlanId, CustomerId, SubscriptionId, CardId}; +use crate::params::{unpack_contained, Page, Paginator, paginate}; +use crate::resources::{Currency}; +use crate::build_map; + +#[derive(Debug, Default, Serialize)] +pub struct CreateSubscription { + pub plan_token: PlanId, + pub customer_token: CustomerId, + + #[serde(skip_serializing_if = "Option::is_none")] + pub include_setup_fee: Option +} + + +#[derive(Debug, Default, Deserialize)] +pub struct Subscription { + pub token: SubscriptionId, + pub plan_token: PlanId, + pub customer_token: CustomerId, + pub card_token: CardId, + pub state: String, + + #[serde(with = "time::serde::iso8601::option")] + pub next_billing_date: Option, + + #[serde(with = "time::serde::iso8601::option")] + pub active_interval_started_at: Option, + + #[serde(with = "time::serde::iso8601::option")] + pub active_interval_finishes_at: Option, + + #[serde(with = "time::serde::iso8601::option")] + pub cancelled_at: Option, + + #[serde(with = "time::serde::iso8601::option")] + pub created_at: Option +} + +#[derive(Debug, Default, Deserialize)] +pub struct LedgerEntry { + pub r#type: String, + pub amount: i64, + pub currency: Currency, + pub annotation: String, + + #[serde(with = "time::serde::iso8601::option")] + pub created_at: Option +} + +impl Subscription { + pub fn create(client: &Client, params: CreateSubscription) -> Response { + unpack_contained(client.post_form("/subscriptions", ¶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("/subscriptions", ¶ms) + } + + pub fn retrieve(client: &Client, token: &SubscriptionId) -> Response { + unpack_contained(client.get(&format!("/subscriptions/{}", token))) + } + + pub fn delete(client: &Client, token: &SubscriptionId) -> Response { + unpack_contained(client.delete(&format!("/subscriptions/{}", token))) + } + + pub fn reactivate(client: &Client, token: &SubscriptionId) -> Response { + unpack_contained(client.put(&format!("/subscriptions/{}/reactivate", token))) + } + + pub fn list_ledger_entries(client: &Client, token: &SubscriptionId, 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(&format!("/subscriptions/{}/ledger", token), ¶ms) + } +} diff --git a/tests/fixtures/create-subscription.json b/tests/fixtures/create-subscription.json new file mode 100644 index 0000000..89d686f --- /dev/null +++ b/tests/fixtures/create-subscription.json @@ -0,0 +1,14 @@ +{ + "response": { + "state": "active", + "next_billing_date": "2023-12-28T22:05:08Z", + "active_interval_started_at": "2023-12-28T22:05:08Z", + "active_interval_finishes_at": "2023-12-28T22:05:08Z", + "cancelled_at": null, + "created_at": "2023-12-28T22:05:08Z", + "token": "sub_bZWXhTzHooKpk9FZjQfzqQ", + "plan_token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_token": "cus_XZg1ULpWaROQCOT5PdwLkQ", + "card_token": "card_nytGw7koRg23EEp9NTmz9w" + } +} diff --git a/tests/fixtures/delete-subscription.json b/tests/fixtures/delete-subscription.json new file mode 100644 index 0000000..eee5a35 --- /dev/null +++ b/tests/fixtures/delete-subscription.json @@ -0,0 +1,14 @@ +{ + "response": { + "state": "cancelled", + "next_billing_date": null, + "active_interval_started_at": "2023-12-28T22:05:08Z", + "active_interval_finishes_at": "2023-12-28T22:05:08Z", + "cancelled_at": "2023-12-28T22:05:08Z", + "created_at": "2023-12-28T22:05:08Z", + "token": "sub_bZWXhTzHooKpk9FZjQfzqQ", + "plan_token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_token": "cus_XZg1ULpWaROQCOT5PdwLkQ", + "card_token": "card_nytGw7koRg23EEp9NTmz9w" + } +} diff --git a/tests/fixtures/get-subscription.json b/tests/fixtures/get-subscription.json new file mode 100644 index 0000000..89d686f --- /dev/null +++ b/tests/fixtures/get-subscription.json @@ -0,0 +1,14 @@ +{ + "response": { + "state": "active", + "next_billing_date": "2023-12-28T22:05:08Z", + "active_interval_started_at": "2023-12-28T22:05:08Z", + "active_interval_finishes_at": "2023-12-28T22:05:08Z", + "cancelled_at": null, + "created_at": "2023-12-28T22:05:08Z", + "token": "sub_bZWXhTzHooKpk9FZjQfzqQ", + "plan_token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_token": "cus_XZg1ULpWaROQCOT5PdwLkQ", + "card_token": "card_nytGw7koRg23EEp9NTmz9w" + } +} diff --git a/tests/fixtures/get-subscriptions.json b/tests/fixtures/get-subscriptions.json new file mode 100644 index 0000000..c65b538 --- /dev/null +++ b/tests/fixtures/get-subscriptions.json @@ -0,0 +1,22 @@ +{ + "response": [ + { + "state": "active", + "next_billing_date": "2023-12-28T22:05:08Z", + "active_interval_started_at": "2023-12-28T22:05:08Z", + "active_interval_finishes_at": "2023-12-28T22:05:08Z", + "cancelled_at": null, + "created_at": "2023-12-28T22:05:08Z", + "token": "sub_bZWXhTzHooKpk9FZjQfzqQ", + "plan_token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_token": "cus_XZg1ULpWaROQCOT5PdwLkQ", + "card_token": "card_nytGw7koRg23EEp9NTmz9w" + } + ], + "count": 1, + "pagination": { + "count": 1, + "per_page": 25, + "current": 1 + } +} diff --git a/tests/fixtures/list-subscription-ledger-entries.json b/tests/fixtures/list-subscription-ledger-entries.json new file mode 100644 index 0000000..a47c680 --- /dev/null +++ b/tests/fixtures/list-subscription-ledger-entries.json @@ -0,0 +1,17 @@ +{ + "response": [ + { + "created_at": "2023-12-28T22:05:08Z", + "type": "credit", + "amount": 1000, + "currency": "AUD", + "annotation": "charge_credit" + } + ], + "count": 1, + "pagination": { + "count": 1, + "per_page": 25, + "current": 1 + } +} diff --git a/tests/fixtures/reactivate-subscription.json b/tests/fixtures/reactivate-subscription.json new file mode 100644 index 0000000..89d686f --- /dev/null +++ b/tests/fixtures/reactivate-subscription.json @@ -0,0 +1,14 @@ +{ + "response": { + "state": "active", + "next_billing_date": "2023-12-28T22:05:08Z", + "active_interval_started_at": "2023-12-28T22:05:08Z", + "active_interval_finishes_at": "2023-12-28T22:05:08Z", + "cancelled_at": null, + "created_at": "2023-12-28T22:05:08Z", + "token": "sub_bZWXhTzHooKpk9FZjQfzqQ", + "plan_token": "plan_ZyDee4HNeUHFHC4SpM2idg", + "customer_token": "cus_XZg1ULpWaROQCOT5PdwLkQ", + "card_token": "card_nytGw7koRg23EEp9NTmz9w" + } +} diff --git a/tests/subscription.rs b/tests/subscription.rs new file mode 100644 index 0000000..5e355e8 --- /dev/null +++ b/tests/subscription.rs @@ -0,0 +1,253 @@ +use pinpayments::{Client, Currency, CreateSubscription, Subscription}; +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_subscription_test() { + let json = common::get_fixture("tests/fixtures/create-subscription.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/subscriptions"), + 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 subscription = Subscription::create( + &client, + CreateSubscription { + plan_token: "plan_ZyDee4HNeUHFHC4SpM2idg".parse().unwrap(), + customer_token: "cus_XZg1ULpWaROQCOT5PdwLkQ".parse().unwrap(), + ..Default::default() + } + ) + .await + .unwrap(); + + assert_eq!(subscription.token, "sub_bZWXhTzHooKpk9FZjQfzqQ"); + assert_eq!(subscription.state, "active"); + assert_eq!(subscription.next_billing_date.unwrap(), datetime!(2023-12-28 22:05:8 UTC)); + assert_eq!(subscription.active_interval_started_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.active_interval_finishes_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.cancelled_at, None); + assert_eq!(subscription.created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.plan_token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(subscription.customer_token, "cus_XZg1ULpWaROQCOT5PdwLkQ"); + assert_eq!(subscription.card_token, "card_nytGw7koRg23EEp9NTmz9w"); +} + +#[tokio::test] +async fn list_subscription_test() { + let json = common::get_fixture("tests/fixtures/get-subscriptions.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/subscriptions"), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::OK.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 subscriptions = Subscription::list(&client, None, None).await.unwrap(); + + assert_eq!(subscriptions.items[0].token, "sub_bZWXhTzHooKpk9FZjQfzqQ"); + assert_eq!(subscriptions.items[0].state, "active"); + assert_eq!(subscriptions.items[0].next_billing_date.unwrap(), datetime!(2023-12-28 22:05:8 UTC)); + assert_eq!(subscriptions.items[0].active_interval_started_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscriptions.items[0].active_interval_finishes_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscriptions.items[0].cancelled_at, None); + assert_eq!(subscriptions.items[0].created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscriptions.items[0].plan_token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(subscriptions.items[0].customer_token, "cus_XZg1ULpWaROQCOT5PdwLkQ"); + assert_eq!(subscriptions.items[0].card_token, "card_nytGw7koRg23EEp9NTmz9w"); +} + +#[tokio::test] +async fn retrieve_subscription_test() { + let json = common::get_fixture("tests/fixtures/get-subscription.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let subscription_token = "sub_bZWXhTzHooKpk9FZjQfzqQ".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("GET", format!("/1/subscriptions/{}", subscription_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::OK.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 subscription = Subscription::retrieve(&client, &subscription_token).await.unwrap(); + + assert_eq!(subscription.token, "sub_bZWXhTzHooKpk9FZjQfzqQ"); + assert_eq!(subscription.state, "active"); + assert_eq!(subscription.next_billing_date.unwrap(), datetime!(2023-12-28 22:05:8 UTC)); + assert_eq!(subscription.active_interval_started_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.active_interval_finishes_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.cancelled_at, None); + assert_eq!(subscription.created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.plan_token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(subscription.customer_token, "cus_XZg1ULpWaROQCOT5PdwLkQ"); + assert_eq!(subscription.card_token, "card_nytGw7koRg23EEp9NTmz9w"); +} + +#[tokio::test] +async fn delete_subscription_test() { + let json = common::get_fixture("tests/fixtures/delete-subscription.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let subscription_token = "sub_bZWXhTzHooKpk9FZjQfzqQ".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("DELETE", format!("/1/subscriptions/{}", subscription_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::OK.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 subscription = Subscription::delete(&client, &subscription_token).await.unwrap(); + + assert_eq!(subscription.token, "sub_bZWXhTzHooKpk9FZjQfzqQ"); + assert_eq!(subscription.state, "cancelled"); + assert_eq!(subscription.next_billing_date, None); + assert_eq!(subscription.active_interval_started_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.active_interval_finishes_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.cancelled_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.plan_token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(subscription.customer_token, "cus_XZg1ULpWaROQCOT5PdwLkQ"); + assert_eq!(subscription.card_token, "card_nytGw7koRg23EEp9NTmz9w"); +} + +#[tokio::test] +async fn reactivate_subscription_test() { + let json = common::get_fixture("tests/fixtures/reactivate-subscription.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let subscription_token = "sub_bZWXhTzHooKpk9FZjQfzqQ".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("PUT", format!("/1/subscriptions/{}/reactivate", subscription_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::OK.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 subscription = Subscription::reactivate(&client, &subscription_token).await.unwrap(); + + assert_eq!(subscription.token, "sub_bZWXhTzHooKpk9FZjQfzqQ"); + assert_eq!(subscription.state, "active"); + assert_eq!(subscription.next_billing_date.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.active_interval_started_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.active_interval_finishes_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.cancelled_at, None); + assert_eq!(subscription.created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(subscription.plan_token, "plan_ZyDee4HNeUHFHC4SpM2idg"); + assert_eq!(subscription.customer_token, "cus_XZg1ULpWaROQCOT5PdwLkQ"); + assert_eq!(subscription.card_token, "card_nytGw7koRg23EEp9NTmz9w"); + +} + +#[tokio::test] +async fn list_subscription_ledger_entries_test() { + let json = common::get_fixture("tests/fixtures/list-subscription-ledger-entries.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let subscription_token = "sub_bZWXhTzHooKpk9FZjQfzqQ".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("GET", format!("/1/subscriptions/{}/ledger", subscription_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with( + status_code(StatusCode::OK.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 ledger_entries = Subscription::list_ledger_entries(&client, &subscription_token, None, None).await.unwrap(); + + + assert_eq!(ledger_entries.items[0].created_at.unwrap(), datetime!(2023-12-28 22:05:08 UTC)); + assert_eq!(ledger_entries.items[0].r#type, "credit"); + assert_eq!(ledger_entries.items[0].amount, 1000); + assert_eq!(ledger_entries.items[0].currency, Currency::AUD); + assert_eq!(ledger_entries.items[0].annotation, "charge_credit"); + + assert_eq!(ledger_entries.pagination.count, 1); + assert_eq!(ledger_entries.pagination.per_page, 25); + assert_eq!(ledger_entries.pagination.current, 1); +}