From 458114bc10a7f3d6271d22b556f0e4a103fb0a23 Mon Sep 17 00:00:00 2001 From: Chris Kruger Date: Sat, 30 Dec 2023 07:52:15 +0800 Subject: [PATCH] add dispute (#13) * add dispute * add dispute tests --- src/client/pinpayments.rs | 6 + src/resources.rs | 2 + src/resources/dispute.rs | 80 +++++++++++++ tests/dispute.rs | 198 +++++++++++++++++++++++++++++++ tests/fixtures/get-dispute.json | 67 +++++++++++ tests/fixtures/get-disputes.json | 78 ++++++++++++ 6 files changed, 431 insertions(+) create mode 100644 src/resources/dispute.rs create mode 100644 tests/dispute.rs create mode 100644 tests/fixtures/get-dispute.json create mode 100644 tests/fixtures/get-disputes.json diff --git a/src/client/pinpayments.rs b/src/client/pinpayments.rs index 412d29e..684d933 100644 --- a/src/client/pinpayments.rs +++ b/src/client/pinpayments.rs @@ -112,6 +112,12 @@ impl Client { self.client.execute::(self.create_request(Method::Post, url)) } + /// Make a http `POST` request using presented path returning only the status + pub fn post_status_only(&self, path: &str) -> StatusOnlyResponse { + let url = self.url(path); + self.client.execute_status_only(self.create_request(Method::Post, url)) + } + /// Make a http `POST` request urlencoding the body pub fn post_form( &self, diff --git a/src/resources.rs b/src/resources.rs index 3cd0fc0..c4fe373 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -9,6 +9,7 @@ mod recipient; mod transfer; mod plan; mod subscription; +mod dispute; pub use currency::*; pub use charge::*; @@ -21,3 +22,4 @@ pub use recipient::*; pub use transfer::*; pub use plan::*; pub use subscription::*; +pub use dispute::*; diff --git a/src/resources/dispute.rs b/src/resources/dispute.rs new file mode 100644 index 0000000..be31ff9 --- /dev/null +++ b/src/resources/dispute.rs @@ -0,0 +1,80 @@ +use time::{OffsetDateTime}; +use serde::{Deserialize, Serialize}; + +use crate::client::{Client, Response, StatusOnlyResponse}; +use crate::error::PinError; +use crate::ids::{DisputeId}; +use crate::params::{unpack_contained, SortDirection, Page, Paginator, paginate}; +use crate::resources::{Currency, Charge}; +use crate::build_map; + + +#[derive(Debug, Default, Deserialize)] +pub struct Dispute { + pub token: DisputeId, + pub category: String, + pub status: String, + pub amount: i64, + pub currency: Currency, + pub charge: Charge, + + #[serde(with = "time::serde::iso8601::option")] + pub evidence_required_by: Option, + pub relevant_evidence: Vec, + + #[serde(with = "time::serde::iso8601::option")] + pub received_at: Option +} + +#[derive(Debug, Default, Serialize)] +pub struct DisputeSearchParams<'a> { + pub query: Option<&'a str>, + pub status: Option<&'a str>, + pub sort: Option, + pub direction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SortByField { + ReceivedAt, + EvidenceRequiredBy, + Amount +} + +impl Dispute { + 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("/disputes", ¶ms) + } + + pub fn list_with_paginator(client: &Client, per_page: Option) -> Paginator> { + paginate( + move |page, per_page| { + Dispute::list(client, Some(page), Some(per_page)) + }, + per_page.unwrap_or(25) + ) + } + + pub fn search(client: &Client, search_params: DisputeSearchParams<'_>) -> Response> { + client.get_query("/disputes/search", &search_params) + } + + pub fn retrieve(client: &Client, token: &DisputeId) -> Response { + unpack_contained(client.get(&format!("/disputes/{}", token))) + } + + pub fn submit_evidence(client: &Client, token: &DisputeId) -> StatusOnlyResponse { + client.post_status_only(&format!("/disputes/{}/evidence", token)) + } + + pub fn accept(client: &Client, token: &DisputeId) -> StatusOnlyResponse { + client.post_status_only(&format!("/disputes/{}/accept", token)) + } +} diff --git a/tests/dispute.rs b/tests/dispute.rs new file mode 100644 index 0000000..d7cccfd --- /dev/null +++ b/tests/dispute.rs @@ -0,0 +1,198 @@ +use pinpayments::{Client, Currency, Dispute, DisputeSearchParams}; +use httptest::{Expectation, matchers::*, responders::*}; +use surf::http::auth::BasicAuth; +use time::macros::datetime; +use http::StatusCode; + +pub mod common; + +#[tokio::test] +async fn list_dispute_test() { + let json = common::get_fixture("tests/fixtures/get-disputes.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/disputes"), + 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 disputes = Dispute::list(&client, None, None).await.unwrap(); + + assert_eq!(disputes.items[0].token, "dis_JRs6Xgk4jMyF33yGijQ7Nw"); + assert_eq!(disputes.items[0].category, "general"); + assert_eq!(disputes.items[0].status, "evidence_required"); + assert_eq!(disputes.items[0].amount, 100); + assert_eq!(disputes.items[0].currency, Currency::AUD); + assert_eq!(disputes.items[0].evidence_required_by.unwrap(), datetime!(2023-10-15 00:00:00 UTC)); + assert_eq!(disputes.items[0].relevant_evidence, vec![ + "proof_of_delivery_or_service", + "invoice_or_receipt", + "invoice_showing_distinct_transactions", + "customer_communication", + "refund_or_cancellation_policy", + "recurring_transaction_agreement", + "additional_evidence" + ]); + assert_eq!(disputes.items[0].received_at.unwrap(), datetime!(2023-09-25 9:23:58 UTC)); + + assert_eq!(disputes.items[0].charge.token, "ch_yJM0U_NaAsyY2A7Se3IFYQ"); +} + +#[tokio::test] +async fn search_dispute_test() { + let json = common::get_fixture("tests/fixtures/get-disputes.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/disputes/search"), + 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 disputes = Dispute::search( + &client, + DisputeSearchParams { + query: Some("evidence_required"), + ..Default::default() + } + ) + .await + .unwrap(); + + assert_eq!(disputes.items[0].token, "dis_JRs6Xgk4jMyF33yGijQ7Nw"); + assert_eq!(disputes.items[0].category, "general"); + assert_eq!(disputes.items[0].status, "evidence_required"); + assert_eq!(disputes.items[0].amount, 100); + assert_eq!(disputes.items[0].currency, Currency::AUD); + assert_eq!(disputes.items[0].evidence_required_by.unwrap(), datetime!(2023-10-15 00:00:00 UTC)); + assert_eq!(disputes.items[0].relevant_evidence, vec![ + "proof_of_delivery_or_service", + "invoice_or_receipt", + "invoice_showing_distinct_transactions", + "customer_communication", + "refund_or_cancellation_policy", + "recurring_transaction_agreement", + "additional_evidence" + ]); + assert_eq!(disputes.items[0].received_at.unwrap(), datetime!(2023-09-25 9:23:58 UTC)); + + assert_eq!(disputes.items[0].charge.token, "ch_yJM0U_NaAsyY2A7Se3IFYQ"); +} + +#[tokio::test] +async fn retrieve_dispute_test() { + let json = common::get_fixture("tests/fixtures/get-dispute.json"); + + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let dispute_token = "dis_JRs6Xgk4jMyF33yGijQ7Nw".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("GET", format!("/1/disputes/{}", dispute_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 dispute = Dispute::retrieve(&client, &dispute_token).await.unwrap(); + + assert_eq!(dispute.token, "dis_JRs6Xgk4jMyF33yGijQ7Nw"); + assert_eq!(dispute.category, "general"); + assert_eq!(dispute.status, "evidence_required"); + assert_eq!(dispute.amount, 100); + assert_eq!(dispute.currency, Currency::AUD); + assert_eq!(dispute.evidence_required_by.unwrap(), datetime!(2023-10-15 00:00:00 UTC)); + assert_eq!(dispute.relevant_evidence, vec![ + "proof_of_delivery_or_service", + "invoice_or_receipt", + "invoice_showing_distinct_transactions", + "customer_communication", + "refund_or_cancellation_policy", + "recurring_transaction_agreement", + "additional_evidence" + ]); + assert_eq!(dispute.received_at.unwrap(), datetime!(2023-09-25 9:23:58 UTC)); + + assert_eq!(dispute.charge.token, "ch_yJM0U_NaAsyY2A7Se3IFYQ"); +} + +#[tokio::test] +async fn submit_evidence_dispute_test() { + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let dispute_token = "dis_JRs6Xgk4jMyF33yGijQ7Nw".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("POST", format!("/1/disputes/{}/evidence", dispute_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with(status_code(StatusCode::OK.into())) + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let result = Dispute::submit_evidence(&client, &dispute_token).await.unwrap(); + + assert_eq!(result, StatusCode::OK); +} + +#[tokio::test] +async fn accept_dispute_test() { + let auth = BasicAuth::new("sk_test_12345", ""); + + let server = common::SERVER_POOL.get_server(); + + let dispute_token = "dis_JRs6Xgk4jMyF33yGijQ7Nw".parse().unwrap(); + + server.expect( + Expectation::matching( + all_of![ + request::method_path("POST", format!("/1/disputes/{}/accept", dispute_token)), + request::headers( + contains((String::from(auth.name().as_str()), String::from(auth.value().as_str()))) + ), + ]). + respond_with(status_code(StatusCode::OK.into())) + ); + + let client = Client::from_url(server.url_str("/1/").as_str(), "sk_test_12345"); + + let result = Dispute::accept(&client, &dispute_token).await.unwrap(); + + assert_eq!(result, StatusCode::OK); +} diff --git a/tests/fixtures/get-dispute.json b/tests/fixtures/get-dispute.json new file mode 100644 index 0000000..b26401c --- /dev/null +++ b/tests/fixtures/get-dispute.json @@ -0,0 +1,67 @@ +{ + "response": { + "token": "dis_JRs6Xgk4jMyF33yGijQ7Nw", + "category": "general", + "status": "evidence_required", + "amount": 100, + "currency": "AUD", + "charge": { + "token": "ch_yJM0U_NaAsyY2A7Se3IFYQ", + "success": true, + "amount": 100, + "currency": "AUD", + "description": "test charge", + "email": "roland@pinpayments.com", + "ip_address": "203.192.1.172", + "created_at": "2023-09-25T09:23:58Z", + "status_message": "Success", + "error_message": null, + "card": { + "token": "card_pIQJKMs93GsCc9vLSLevbw", + "scheme": "master", + "display_number": "XXXX-XXXX-XXXX-0000", + "issuing_country": "US", + "expiry_month": 5, + "expiry_year": 2024, + "name": "Roland Robot", + "address_line1": "42 Sevenoaks St", + "address_line2": "", + "address_city": "Lathlain", + "address_postcode": "6454", + "address_state": "WA", + "address_country": "Australia", + "network_type": null, + "network_format": null, + "customer_token": null, + "primary": null + }, + "transfer": [], + "amount_refunded": 0, + "total_fees": 33, + "merchant_entitlement": 67, + "refund_pending": false, + "authorisation_token": null, + "authorisation_expired": false, + "authorisation_voided": false, + "captured": true, + "captured_at": "2023-09-25T09:23:58Z", + "settlement_currency": "AUD", + "active_chargebacks": false, + "metadata": { + "OrderNumber": "123456", + "CustomerName": "Roland Robot" + } + }, + "evidence_required_by": "2023-10-15T00:00:00Z", + "relevant_evidence": [ + "proof_of_delivery_or_service", + "invoice_or_receipt", + "invoice_showing_distinct_transactions", + "customer_communication", + "refund_or_cancellation_policy", + "recurring_transaction_agreement", + "additional_evidence" + ], + "received_at": "2023-09-25T09:23:58Z" + } +} diff --git a/tests/fixtures/get-disputes.json b/tests/fixtures/get-disputes.json new file mode 100644 index 0000000..eeeac1d --- /dev/null +++ b/tests/fixtures/get-disputes.json @@ -0,0 +1,78 @@ +{ + "response": [ + { + "token": "dis_JRs6Xgk4jMyF33yGijQ7Nw", + "category": "general", + "status": "evidence_required", + "amount": 100, + "currency": "AUD", + "charge": { + "token": "ch_yJM0U_NaAsyY2A7Se3IFYQ", + "success": true, + "amount": 100, + "currency": "AUD", + "description": "test charge", + "email": "roland@pinpayments.com", + "ip_address": "203.192.1.172", + "created_at": "2023-09-25T09:23:58Z", + "status_message": "Success", + "error_message": null, + "card": { + "token": "card_pIQJKMs93GsCc9vLSLevbw", + "scheme": "master", + "display_number": "XXXX-XXXX-XXXX-0000", + "issuing_country": "US", + "expiry_month": 5, + "expiry_year": 2024, + "name": "Roland Robot", + "address_line1": "42 Sevenoaks St", + "address_line2": "", + "address_city": "Lathlain", + "address_postcode": "6454", + "address_state": "WA", + "address_country": "Australia", + "network_type": null, + "network_format": null, + "customer_token": null, + "primary": null + }, + "transfer": [], + "amount_refunded": 0, + "total_fees": 33, + "merchant_entitlement": 67, + "refund_pending": false, + "authorisation_token": null, + "authorisation_expired": false, + "authorisation_voided": false, + "captured": true, + "captured_at": "2023-09-25T09:23:58Z", + "settlement_currency": "AUD", + "active_chargebacks": false, + "metadata": { + "OrderNumber": "123456", + "CustomerName": "Roland Robot" + } + }, + "evidence_required_by": "2023-10-15T00:00:00Z", + "relevant_evidence": [ + "proof_of_delivery_or_service", + "invoice_or_receipt", + "invoice_showing_distinct_transactions", + "customer_communication", + "refund_or_cancellation_policy", + "recurring_transaction_agreement", + "additional_evidence" + ], + "received_at": "2023-09-25T09:23:58Z" + } + ], + "count": 1, + "pagination": { + "current": 1, + "previous": null, + "next": null, + "per_page": 25, + "pages": 1, + "count": 1 + } +}