Skip to content

Commit acab767

Browse files
sai-harsha-vardhanSangamesh26sai harshajarnura
authored
feat(router): added dispute retrieve and dispute list apis (#842)
Co-authored-by: Sangamesh <[email protected]> Co-authored-by: sai harsha <[email protected]> Co-authored-by: Arun Raj M <[email protected]>
1 parent d1d58e3 commit acab767

File tree

24 files changed

+404
-31
lines changed

24 files changed

+404
-31
lines changed

crates/api_models/src/disputes.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
use masking::Serialize;
1+
use masking::{Deserialize, Serialize};
2+
use time::PrimitiveDateTime;
23
use utoipa::ToSchema;
34

45
use super::enums::{DisputeStage, DisputeStatus};
@@ -19,6 +20,8 @@ pub struct DisputeResponse {
1920
pub dispute_stage: DisputeStage,
2021
/// Status of the dispute
2122
pub dispute_status: DisputeStatus,
23+
/// connector to which dispute is associated with
24+
pub connector: String,
2225
/// Status of the dispute sent by connector
2326
pub connector_status: String,
2427
/// Dispute id sent by connector
@@ -36,3 +39,37 @@ pub struct DisputeResponse {
3639
/// Time at which dispute is received
3740
pub received_at: String,
3841
}
42+
43+
#[derive(Clone, Debug, Deserialize, ToSchema)]
44+
#[serde(deny_unknown_fields)]
45+
pub struct DisputeListConstraints {
46+
/// limit on the number of objects to return
47+
pub limit: Option<i64>,
48+
/// status of the dispute
49+
pub dispute_status: Option<DisputeStatus>,
50+
/// stage of the dispute
51+
pub dispute_stage: Option<DisputeStage>,
52+
/// reason for the dispute
53+
pub reason: Option<String>,
54+
/// connector linked to dispute
55+
pub connector: Option<String>,
56+
/// The time at which dispute is received
57+
#[schema(example = "2022-09-10T10:11:12Z")]
58+
pub received_time: Option<PrimitiveDateTime>,
59+
/// Time less than the dispute received time
60+
#[schema(example = "2022-09-10T10:11:12Z")]
61+
#[serde(rename = "received_time.lt")]
62+
pub received_time_lt: Option<PrimitiveDateTime>,
63+
/// Time greater than the dispute received time
64+
#[schema(example = "2022-09-10T10:11:12Z")]
65+
#[serde(rename = "received_time.gt")]
66+
pub received_time_gt: Option<PrimitiveDateTime>,
67+
/// Time less than or equals to the dispute received time
68+
#[schema(example = "2022-09-10T10:11:12Z")]
69+
#[serde(rename = "received_time.lte")]
70+
pub received_time_lte: Option<PrimitiveDateTime>,
71+
/// Time greater than or equals to the dispute received time
72+
#[schema(example = "2022-09-10T10:11:12Z")]
73+
#[serde(rename = "received_time.gte")]
74+
pub received_time_gte: Option<PrimitiveDateTime>,
75+
}

crates/router/src/compatibility/stripe/errors.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ pub enum StripeErrorCode {
180180

181181
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "The connector provided in the request is incorrect or not available")]
182182
IncorrectConnectorNameGiven,
183+
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "No such {object}: '{id}'")]
184+
ResourceMissing { object: String, id: String },
183185
// [#216]: https://github.com/juspay/hyperswitch/issues/216
184186
// Implement the remaining stripe error codes
185187

@@ -460,6 +462,10 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
460462
errors::ApiErrorResponse::DuplicatePayment { payment_id } => {
461463
Self::DuplicatePayment { payment_id }
462464
}
465+
errors::ApiErrorResponse::DisputeNotFound { dispute_id } => Self::ResourceMissing {
466+
object: "dispute".to_owned(),
467+
id: dispute_id,
468+
},
463469
errors::ApiErrorResponse::NotSupported { .. } => Self::InternalServerError,
464470
}
465471
}
@@ -507,7 +513,8 @@ impl actix_web::ResponseError for StripeErrorCode {
507513
| Self::PaymentIntentMandateInvalid { .. }
508514
| Self::PaymentIntentUnexpectedState { .. }
509515
| Self::DuplicatePayment { .. }
510-
| Self::IncorrectConnectorNameGiven => StatusCode::BAD_REQUEST,
516+
| Self::IncorrectConnectorNameGiven
517+
| Self::ResourceMissing { .. } => StatusCode::BAD_REQUEST,
511518
Self::RefundFailed
512519
| Self::InternalServerError
513520
| Self::MandateActive

crates/router/src/core.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod api_keys;
33
pub mod cards_info;
44
pub mod configs;
55
pub mod customers;
6+
pub mod disputes;
67
pub mod errors;
78
pub mod mandate;
89
pub mod metrics;

crates/router/src/core/disputes.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
use router_env::{instrument, tracing};
2+
3+
use super::errors::{self, RouterResponse, StorageErrorExt};
4+
use crate::{
5+
routes::AppState,
6+
services,
7+
types::{api::disputes, storage, transformers::ForeignFrom},
8+
};
9+
10+
#[instrument(skip(state))]
11+
pub async fn retrieve_dispute(
12+
state: &AppState,
13+
merchant_account: storage::MerchantAccount,
14+
req: disputes::DisputeId,
15+
) -> RouterResponse<api_models::disputes::DisputeResponse> {
16+
let dispute = state
17+
.store
18+
.find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, &req.dispute_id)
19+
.await
20+
.map_err(|error| {
21+
error.to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
22+
dispute_id: req.dispute_id,
23+
})
24+
})?;
25+
let dispute_response = api_models::disputes::DisputeResponse::foreign_from(dispute);
26+
Ok(services::ApplicationResponse::Json(dispute_response))
27+
}
28+
29+
#[instrument(skip(state))]
30+
pub async fn retrieve_disputes_list(
31+
state: &AppState,
32+
merchant_account: storage::MerchantAccount,
33+
constraints: api_models::disputes::DisputeListConstraints,
34+
) -> RouterResponse<Vec<api_models::disputes::DisputeResponse>> {
35+
let disputes = state
36+
.store
37+
.find_disputes_by_merchant_id(&merchant_account.merchant_id, constraints)
38+
.await
39+
.map_err(|error| {
40+
error.to_not_found_response(errors::ApiErrorResponse::InternalServerError)
41+
})?;
42+
let disputes_list = disputes
43+
.into_iter()
44+
.map(api_models::disputes::DisputeResponse::foreign_from)
45+
.collect();
46+
Ok(services::ApplicationResponse::Json(disputes_list))
47+
}

crates/router/src/core/errors/api_error_response.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ pub enum ApiErrorResponse {
158158
IncorrectConnectorNameGiven,
159159
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")]
160160
AddressNotFound,
161+
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Dispute does not exist in our records")]
162+
DisputeNotFound { dispute_id: String },
161163
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Card with the provided iin does not exist")]
162164
InvalidCardIin,
163165
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "The provided card IIN length is invalid, please provide an iin with 6 or 8 digits")]
@@ -253,7 +255,8 @@ impl actix_web::ResponseError for ApiErrorResponse {
253255
Self::DuplicateMerchantAccount
254256
| Self::DuplicateMerchantConnectorAccount
255257
| Self::DuplicatePaymentMethod
256-
| Self::DuplicateMandate => StatusCode::BAD_REQUEST, // 400
258+
| Self::DuplicateMandate
259+
| Self::DisputeNotFound { .. } => StatusCode::BAD_REQUEST, // 400
257260
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, // 503
258261
Self::PaymentNotSucceeded => StatusCode::BAD_REQUEST, // 400
259262
Self::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, // 501
@@ -444,6 +447,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
444447
Self::FlowNotSupported { flow, connector } => {
445448
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) //FIXME: error message
446449
},
450+
Self::DisputeNotFound { .. } => {
451+
AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None))
452+
},
447453
}
448454
}
449455
}

crates/router/src/core/webhooks.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,9 @@ async fn get_or_update_dispute_object(
228228
option_dispute: Option<storage_models::dispute::Dispute>,
229229
dispute_details: api::disputes::DisputePayload,
230230
merchant_id: &str,
231-
payment_id: &str,
232-
attempt_id: &str,
231+
payment_attempt: &storage_models::payment_attempt::PaymentAttempt,
233232
event_type: api_models::webhooks::IncomingWebhookEvent,
233+
connector_name: &str,
234234
) -> CustomResult<storage_models::dispute::Dispute, errors::WebhooksFlowError> {
235235
let db = &*state.store;
236236
match option_dispute {
@@ -246,8 +246,9 @@ async fn get_or_update_dispute_object(
246246
.foreign_try_into()
247247
.into_report()
248248
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
249-
payment_id: payment_id.to_owned(),
250-
attempt_id: attempt_id.to_owned(),
249+
payment_id: payment_attempt.payment_id.to_owned(),
250+
connector: connector_name.to_owned(),
251+
attempt_id: payment_attempt.attempt_id.to_owned(),
251252
merchant_id: merchant_id.to_owned(),
252253
connector_status: dispute_details.connector_status,
253254
connector_dispute_id: dispute_details.connector_dispute_id,
@@ -327,18 +328,12 @@ async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
327328
option_dispute,
328329
dispute_details,
329330
&merchant_account.merchant_id,
330-
&payment_attempt.payment_id,
331-
&payment_attempt.attempt_id,
331+
&payment_attempt,
332332
event_type.clone(),
333+
connector.id(),
333334
)
334335
.await?;
335-
let disputes_response = Box::new(
336-
dispute_object
337-
.clone()
338-
.foreign_try_into()
339-
.into_report()
340-
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
341-
);
336+
let disputes_response = Box::new(dispute_object.clone().foreign_into());
342337
let event_type: enums::EventType = dispute_object
343338
.dispute_status
344339
.foreign_try_into()

crates/router/src/db/dispute.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use super::{MockDb, Store};
44
use crate::{
55
connection,
66
core::errors::{self, CustomResult},
7-
types::storage,
7+
types::storage::{self, DisputeDbExt},
88
};
99

1010
#[async_trait::async_trait]
@@ -21,6 +21,18 @@ pub trait DisputeInterface {
2121
connector_dispute_id: &str,
2222
) -> CustomResult<Option<storage::Dispute>, errors::StorageError>;
2323

24+
async fn find_dispute_by_merchant_id_dispute_id(
25+
&self,
26+
merchant_id: &str,
27+
dispute_id: &str,
28+
) -> CustomResult<storage::Dispute, errors::StorageError>;
29+
30+
async fn find_disputes_by_merchant_id(
31+
&self,
32+
merchant_id: &str,
33+
dispute_constraints: api_models::disputes::DisputeListConstraints,
34+
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError>;
35+
2436
async fn update_dispute(
2537
&self,
2638
this: storage::Dispute,
@@ -60,6 +72,30 @@ impl DisputeInterface for Store {
6072
.into_report()
6173
}
6274

75+
async fn find_dispute_by_merchant_id_dispute_id(
76+
&self,
77+
merchant_id: &str,
78+
dispute_id: &str,
79+
) -> CustomResult<storage::Dispute, errors::StorageError> {
80+
let conn = connection::pg_connection_read(self).await?;
81+
storage::Dispute::find_by_merchant_id_dispute_id(&conn, merchant_id, dispute_id)
82+
.await
83+
.map_err(Into::into)
84+
.into_report()
85+
}
86+
87+
async fn find_disputes_by_merchant_id(
88+
&self,
89+
merchant_id: &str,
90+
dispute_constraints: api_models::disputes::DisputeListConstraints,
91+
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError> {
92+
let conn = connection::pg_connection_read(self).await?;
93+
storage::Dispute::filter_by_constraints(&conn, merchant_id, dispute_constraints)
94+
.await
95+
.map_err(Into::into)
96+
.into_report()
97+
}
98+
6399
async fn update_dispute(
64100
&self,
65101
this: storage::Dispute,
@@ -92,6 +128,24 @@ impl DisputeInterface for MockDb {
92128
Err(errors::StorageError::MockDbError)?
93129
}
94130

131+
async fn find_dispute_by_merchant_id_dispute_id(
132+
&self,
133+
_merchant_id: &str,
134+
_dispute_id: &str,
135+
) -> CustomResult<storage::Dispute, errors::StorageError> {
136+
// TODO: Implement function for `MockDb`
137+
Err(errors::StorageError::MockDbError)?
138+
}
139+
140+
async fn find_disputes_by_merchant_id(
141+
&self,
142+
_merchant_id: &str,
143+
_dispute_constraints: api_models::disputes::DisputeListConstraints,
144+
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError> {
145+
// TODO: Implement function for `MockDb`
146+
Err(errors::StorageError::MockDbError)?
147+
}
148+
95149
async fn update_dispute(
96150
&self,
97151
_this: storage::Dispute,

crates/router/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ pub fn mk_app(
115115
{
116116
server_app = server_app
117117
.service(routes::MerchantAccount::server(state.clone()))
118-
.service(routes::ApiKeys::server(state.clone()));
118+
.service(routes::ApiKeys::server(state.clone()))
119+
.service(routes::Disputes::server(state.clone()));
119120
}
120121

121122
#[cfg(feature = "stripe")]

crates/router/src/openapi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Never share your secret api keys. Keep them guarded and secure.
5757
(name = "Mandates", description = "Manage mandates"),
5858
(name = "Customers", description = "Create and manage customers"),
5959
(name = "Payment Methods", description = "Create and manage payment methods of customers"),
60+
(name = "Disputes", description = "Manage disputes"),
6061
// (name = "API Key", description = "Create and manage API Keys"),
6162
),
6263
paths(
@@ -100,6 +101,8 @@ Never share your secret api keys. Keep them guarded and secure.
100101
// crate::routes::api_keys::api_key_update,
101102
// crate::routes::api_keys::api_key_revoke,
102103
// crate::routes::api_keys::api_key_list,
104+
crate::routes::disputes::retrieve_disputes_list,
105+
crate::routes::disputes::retrieve_dispute,
103106
),
104107
components(schemas(
105108
crate::types::api::refunds::RefundRequest,
@@ -143,9 +146,12 @@ Never share your secret api keys. Keep them guarded and secure.
143146
api_models::enums::PaymentExperience,
144147
api_models::enums::BankNames,
145148
api_models::enums::CardNetwork,
149+
api_models::enums::DisputeStage,
150+
api_models::enums::DisputeStatus,
146151
api_models::enums::CountryCode,
147152
api_models::admin::MerchantConnector,
148153
api_models::admin::PaymentMethodsEnabled,
154+
api_models::disputes::DisputeResponse,
149155
api_models::payments::AddressDetails,
150156
api_models::payments::Address,
151157
api_models::payments::BankRedirectData,

crates/router/src/routes.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pub mod app;
44
pub mod cards_info;
55
pub mod configs;
66
pub mod customers;
7+
pub mod disputes;
78
pub mod ephemeral_key;
89
pub mod health;
910
pub mod mandates;
@@ -15,8 +16,9 @@ pub mod refunds;
1516
pub mod webhooks;
1617

1718
pub use self::app::{
18-
ApiKeys, AppState, Cards, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
19-
MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
19+
ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Health, Mandates,
20+
MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds,
21+
Webhooks,
2022
};
2123
#[cfg(feature = "stripe")]
2224
pub use super::compatibility::stripe::StripeApis;

0 commit comments

Comments
 (0)