Skip to content

Commit

Permalink
add dispute (#13)
Browse files Browse the repository at this point in the history
* add dispute

* add dispute tests
  • Loading branch information
montdidier authored Dec 29, 2023
1 parent e93f5a0 commit 458114b
Show file tree
Hide file tree
Showing 6 changed files with 431 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/client/pinpayments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ impl Client {
self.client.execute::<T>(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<T: DeserializeOwned + Send + 'static, F: Serialize>(
&self,
Expand Down
2 changes: 2 additions & 0 deletions src/resources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ mod recipient;
mod transfer;
mod plan;
mod subscription;
mod dispute;

pub use currency::*;
pub use charge::*;
Expand All @@ -21,3 +22,4 @@ pub use recipient::*;
pub use transfer::*;
pub use plan::*;
pub use subscription::*;
pub use dispute::*;
80 changes: 80 additions & 0 deletions src/resources/dispute.rs
Original file line number Diff line number Diff line change
@@ -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<OffsetDateTime>,
pub relevant_evidence: Vec<String>,

#[serde(with = "time::serde::iso8601::option")]
pub received_at: Option<OffsetDateTime>
}

#[derive(Debug, Default, Serialize)]
pub struct DisputeSearchParams<'a> {
pub query: Option<&'a str>,
pub status: Option<&'a str>,
pub sort: Option<SortByField>,
pub direction: Option<SortDirection>,
}

#[derive(Debug, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SortByField {
ReceivedAt,
EvidenceRequiredBy,
Amount
}

impl Dispute {
pub fn list(client: &Client, page: Option<u32>, per_page: Option<u32>) -> Response<Page<Dispute>> {
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", &params)
}

pub fn list_with_paginator(client: &Client, per_page: Option<u32>) -> Paginator<Result<Dispute, PinError>> {
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<Page<Dispute>> {
client.get_query("/disputes/search", &search_params)
}

pub fn retrieve(client: &Client, token: &DisputeId) -> Response<Dispute> {
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))
}
}
198 changes: 198 additions & 0 deletions tests/dispute.rs
Original file line number Diff line number Diff line change
@@ -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);
}
67 changes: 67 additions & 0 deletions tests/fixtures/get-dispute.json
Original file line number Diff line number Diff line change
@@ -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": "[email protected]",
"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"
}
}
Loading

0 comments on commit 458114b

Please sign in to comment.