mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 03:13:56 +08:00
feat(router): added dispute retrieve and dispute list apis (#842)
Co-authored-by: Sangamesh <sangamesh.kulkarni@juspay.in> Co-authored-by: sai harsha <sai.harsha@sai.harsha-MacBookPro> Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
committed by
GitHub
parent
d1d58e33b7
commit
acab7671b0
@ -1,4 +1,5 @@
|
|||||||
use masking::Serialize;
|
use masking::{Deserialize, Serialize};
|
||||||
|
use time::PrimitiveDateTime;
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
use super::enums::{DisputeStage, DisputeStatus};
|
use super::enums::{DisputeStage, DisputeStatus};
|
||||||
@ -19,6 +20,8 @@ pub struct DisputeResponse {
|
|||||||
pub dispute_stage: DisputeStage,
|
pub dispute_stage: DisputeStage,
|
||||||
/// Status of the dispute
|
/// Status of the dispute
|
||||||
pub dispute_status: DisputeStatus,
|
pub dispute_status: DisputeStatus,
|
||||||
|
/// connector to which dispute is associated with
|
||||||
|
pub connector: String,
|
||||||
/// Status of the dispute sent by connector
|
/// Status of the dispute sent by connector
|
||||||
pub connector_status: String,
|
pub connector_status: String,
|
||||||
/// Dispute id sent by connector
|
/// Dispute id sent by connector
|
||||||
@ -36,3 +39,37 @@ pub struct DisputeResponse {
|
|||||||
/// Time at which dispute is received
|
/// Time at which dispute is received
|
||||||
pub received_at: String,
|
pub received_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct DisputeListConstraints {
|
||||||
|
/// limit on the number of objects to return
|
||||||
|
pub limit: Option<i64>,
|
||||||
|
/// status of the dispute
|
||||||
|
pub dispute_status: Option<DisputeStatus>,
|
||||||
|
/// stage of the dispute
|
||||||
|
pub dispute_stage: Option<DisputeStage>,
|
||||||
|
/// reason for the dispute
|
||||||
|
pub reason: Option<String>,
|
||||||
|
/// connector linked to dispute
|
||||||
|
pub connector: Option<String>,
|
||||||
|
/// The time at which dispute is received
|
||||||
|
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||||
|
pub received_time: Option<PrimitiveDateTime>,
|
||||||
|
/// Time less than the dispute received time
|
||||||
|
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||||
|
#[serde(rename = "received_time.lt")]
|
||||||
|
pub received_time_lt: Option<PrimitiveDateTime>,
|
||||||
|
/// Time greater than the dispute received time
|
||||||
|
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||||
|
#[serde(rename = "received_time.gt")]
|
||||||
|
pub received_time_gt: Option<PrimitiveDateTime>,
|
||||||
|
/// Time less than or equals to the dispute received time
|
||||||
|
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||||
|
#[serde(rename = "received_time.lte")]
|
||||||
|
pub received_time_lte: Option<PrimitiveDateTime>,
|
||||||
|
/// Time greater than or equals to the dispute received time
|
||||||
|
#[schema(example = "2022-09-10T10:11:12Z")]
|
||||||
|
#[serde(rename = "received_time.gte")]
|
||||||
|
pub received_time_gte: Option<PrimitiveDateTime>,
|
||||||
|
}
|
||||||
|
|||||||
@ -180,6 +180,8 @@ pub enum StripeErrorCode {
|
|||||||
|
|
||||||
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "The connector provided in the request is incorrect or not available")]
|
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "The connector provided in the request is incorrect or not available")]
|
||||||
IncorrectConnectorNameGiven,
|
IncorrectConnectorNameGiven,
|
||||||
|
#[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "No such {object}: '{id}'")]
|
||||||
|
ResourceMissing { object: String, id: String },
|
||||||
// [#216]: https://github.com/juspay/hyperswitch/issues/216
|
// [#216]: https://github.com/juspay/hyperswitch/issues/216
|
||||||
// Implement the remaining stripe error codes
|
// Implement the remaining stripe error codes
|
||||||
|
|
||||||
@ -460,6 +462,10 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
|
|||||||
errors::ApiErrorResponse::DuplicatePayment { payment_id } => {
|
errors::ApiErrorResponse::DuplicatePayment { payment_id } => {
|
||||||
Self::DuplicatePayment { payment_id }
|
Self::DuplicatePayment { payment_id }
|
||||||
}
|
}
|
||||||
|
errors::ApiErrorResponse::DisputeNotFound { dispute_id } => Self::ResourceMissing {
|
||||||
|
object: "dispute".to_owned(),
|
||||||
|
id: dispute_id,
|
||||||
|
},
|
||||||
errors::ApiErrorResponse::NotSupported { .. } => Self::InternalServerError,
|
errors::ApiErrorResponse::NotSupported { .. } => Self::InternalServerError,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -507,7 +513,8 @@ impl actix_web::ResponseError for StripeErrorCode {
|
|||||||
| Self::PaymentIntentMandateInvalid { .. }
|
| Self::PaymentIntentMandateInvalid { .. }
|
||||||
| Self::PaymentIntentUnexpectedState { .. }
|
| Self::PaymentIntentUnexpectedState { .. }
|
||||||
| Self::DuplicatePayment { .. }
|
| Self::DuplicatePayment { .. }
|
||||||
| Self::IncorrectConnectorNameGiven => StatusCode::BAD_REQUEST,
|
| Self::IncorrectConnectorNameGiven
|
||||||
|
| Self::ResourceMissing { .. } => StatusCode::BAD_REQUEST,
|
||||||
Self::RefundFailed
|
Self::RefundFailed
|
||||||
| Self::InternalServerError
|
| Self::InternalServerError
|
||||||
| Self::MandateActive
|
| Self::MandateActive
|
||||||
|
|||||||
@ -3,6 +3,7 @@ pub mod api_keys;
|
|||||||
pub mod cards_info;
|
pub mod cards_info;
|
||||||
pub mod configs;
|
pub mod configs;
|
||||||
pub mod customers;
|
pub mod customers;
|
||||||
|
pub mod disputes;
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod mandate;
|
pub mod mandate;
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
|||||||
47
crates/router/src/core/disputes.rs
Normal file
47
crates/router/src/core/disputes.rs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
use router_env::{instrument, tracing};
|
||||||
|
|
||||||
|
use super::errors::{self, RouterResponse, StorageErrorExt};
|
||||||
|
use crate::{
|
||||||
|
routes::AppState,
|
||||||
|
services,
|
||||||
|
types::{api::disputes, storage, transformers::ForeignFrom},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
|
pub async fn retrieve_dispute(
|
||||||
|
state: &AppState,
|
||||||
|
merchant_account: storage::MerchantAccount,
|
||||||
|
req: disputes::DisputeId,
|
||||||
|
) -> RouterResponse<api_models::disputes::DisputeResponse> {
|
||||||
|
let dispute = state
|
||||||
|
.store
|
||||||
|
.find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, &req.dispute_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
error.to_not_found_response(errors::ApiErrorResponse::DisputeNotFound {
|
||||||
|
dispute_id: req.dispute_id,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let dispute_response = api_models::disputes::DisputeResponse::foreign_from(dispute);
|
||||||
|
Ok(services::ApplicationResponse::Json(dispute_response))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(state))]
|
||||||
|
pub async fn retrieve_disputes_list(
|
||||||
|
state: &AppState,
|
||||||
|
merchant_account: storage::MerchantAccount,
|
||||||
|
constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> RouterResponse<Vec<api_models::disputes::DisputeResponse>> {
|
||||||
|
let disputes = state
|
||||||
|
.store
|
||||||
|
.find_disputes_by_merchant_id(&merchant_account.merchant_id, constraints)
|
||||||
|
.await
|
||||||
|
.map_err(|error| {
|
||||||
|
error.to_not_found_response(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
})?;
|
||||||
|
let disputes_list = disputes
|
||||||
|
.into_iter()
|
||||||
|
.map(api_models::disputes::DisputeResponse::foreign_from)
|
||||||
|
.collect();
|
||||||
|
Ok(services::ApplicationResponse::Json(disputes_list))
|
||||||
|
}
|
||||||
@ -158,6 +158,8 @@ pub enum ApiErrorResponse {
|
|||||||
IncorrectConnectorNameGiven,
|
IncorrectConnectorNameGiven,
|
||||||
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")]
|
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")]
|
||||||
AddressNotFound,
|
AddressNotFound,
|
||||||
|
#[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Dispute does not exist in our records")]
|
||||||
|
DisputeNotFound { dispute_id: String },
|
||||||
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Card with the provided iin does not exist")]
|
#[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Card with the provided iin does not exist")]
|
||||||
InvalidCardIin,
|
InvalidCardIin,
|
||||||
#[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")]
|
#[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 {
|
|||||||
Self::DuplicateMerchantAccount
|
Self::DuplicateMerchantAccount
|
||||||
| Self::DuplicateMerchantConnectorAccount
|
| Self::DuplicateMerchantConnectorAccount
|
||||||
| Self::DuplicatePaymentMethod
|
| Self::DuplicatePaymentMethod
|
||||||
| Self::DuplicateMandate => StatusCode::BAD_REQUEST, // 400
|
| Self::DuplicateMandate
|
||||||
|
| Self::DisputeNotFound { .. } => StatusCode::BAD_REQUEST, // 400
|
||||||
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, // 503
|
Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, // 503
|
||||||
Self::PaymentNotSucceeded => StatusCode::BAD_REQUEST, // 400
|
Self::PaymentNotSucceeded => StatusCode::BAD_REQUEST, // 400
|
||||||
Self::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, // 501
|
Self::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, // 501
|
||||||
@ -444,6 +447,9 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
|||||||
Self::FlowNotSupported { flow, connector } => {
|
Self::FlowNotSupported { flow, connector } => {
|
||||||
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) //FIXME: error message
|
AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) //FIXME: error message
|
||||||
},
|
},
|
||||||
|
Self::DisputeNotFound { .. } => {
|
||||||
|
AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -228,9 +228,9 @@ async fn get_or_update_dispute_object(
|
|||||||
option_dispute: Option<storage_models::dispute::Dispute>,
|
option_dispute: Option<storage_models::dispute::Dispute>,
|
||||||
dispute_details: api::disputes::DisputePayload,
|
dispute_details: api::disputes::DisputePayload,
|
||||||
merchant_id: &str,
|
merchant_id: &str,
|
||||||
payment_id: &str,
|
payment_attempt: &storage_models::payment_attempt::PaymentAttempt,
|
||||||
attempt_id: &str,
|
|
||||||
event_type: api_models::webhooks::IncomingWebhookEvent,
|
event_type: api_models::webhooks::IncomingWebhookEvent,
|
||||||
|
connector_name: &str,
|
||||||
) -> CustomResult<storage_models::dispute::Dispute, errors::WebhooksFlowError> {
|
) -> CustomResult<storage_models::dispute::Dispute, errors::WebhooksFlowError> {
|
||||||
let db = &*state.store;
|
let db = &*state.store;
|
||||||
match option_dispute {
|
match option_dispute {
|
||||||
@ -246,8 +246,9 @@ async fn get_or_update_dispute_object(
|
|||||||
.foreign_try_into()
|
.foreign_try_into()
|
||||||
.into_report()
|
.into_report()
|
||||||
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
|
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
|
||||||
payment_id: payment_id.to_owned(),
|
payment_id: payment_attempt.payment_id.to_owned(),
|
||||||
attempt_id: attempt_id.to_owned(),
|
connector: connector_name.to_owned(),
|
||||||
|
attempt_id: payment_attempt.attempt_id.to_owned(),
|
||||||
merchant_id: merchant_id.to_owned(),
|
merchant_id: merchant_id.to_owned(),
|
||||||
connector_status: dispute_details.connector_status,
|
connector_status: dispute_details.connector_status,
|
||||||
connector_dispute_id: dispute_details.connector_dispute_id,
|
connector_dispute_id: dispute_details.connector_dispute_id,
|
||||||
@ -327,18 +328,12 @@ async fn disputes_incoming_webhook_flow<W: api::OutgoingWebhookType>(
|
|||||||
option_dispute,
|
option_dispute,
|
||||||
dispute_details,
|
dispute_details,
|
||||||
&merchant_account.merchant_id,
|
&merchant_account.merchant_id,
|
||||||
&payment_attempt.payment_id,
|
&payment_attempt,
|
||||||
&payment_attempt.attempt_id,
|
|
||||||
event_type.clone(),
|
event_type.clone(),
|
||||||
|
connector.id(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let disputes_response = Box::new(
|
let disputes_response = Box::new(dispute_object.clone().foreign_into());
|
||||||
dispute_object
|
|
||||||
.clone()
|
|
||||||
.foreign_try_into()
|
|
||||||
.into_report()
|
|
||||||
.change_context(errors::WebhooksFlowError::DisputeCoreFailed)?,
|
|
||||||
);
|
|
||||||
let event_type: enums::EventType = dispute_object
|
let event_type: enums::EventType = dispute_object
|
||||||
.dispute_status
|
.dispute_status
|
||||||
.foreign_try_into()
|
.foreign_try_into()
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use super::{MockDb, Store};
|
|||||||
use crate::{
|
use crate::{
|
||||||
connection,
|
connection,
|
||||||
core::errors::{self, CustomResult},
|
core::errors::{self, CustomResult},
|
||||||
types::storage,
|
types::storage::{self, DisputeDbExt},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
@ -21,6 +21,18 @@ pub trait DisputeInterface {
|
|||||||
connector_dispute_id: &str,
|
connector_dispute_id: &str,
|
||||||
) -> CustomResult<Option<storage::Dispute>, errors::StorageError>;
|
) -> CustomResult<Option<storage::Dispute>, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn find_dispute_by_merchant_id_dispute_id(
|
||||||
|
&self,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_id: &str,
|
||||||
|
) -> CustomResult<storage::Dispute, errors::StorageError>;
|
||||||
|
|
||||||
|
async fn find_disputes_by_merchant_id(
|
||||||
|
&self,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError>;
|
||||||
|
|
||||||
async fn update_dispute(
|
async fn update_dispute(
|
||||||
&self,
|
&self,
|
||||||
this: storage::Dispute,
|
this: storage::Dispute,
|
||||||
@ -60,6 +72,30 @@ impl DisputeInterface for Store {
|
|||||||
.into_report()
|
.into_report()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_dispute_by_merchant_id_dispute_id(
|
||||||
|
&self,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_id: &str,
|
||||||
|
) -> CustomResult<storage::Dispute, errors::StorageError> {
|
||||||
|
let conn = connection::pg_connection_read(self).await?;
|
||||||
|
storage::Dispute::find_by_merchant_id_dispute_id(&conn, merchant_id, dispute_id)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
.into_report()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_disputes_by_merchant_id(
|
||||||
|
&self,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError> {
|
||||||
|
let conn = connection::pg_connection_read(self).await?;
|
||||||
|
storage::Dispute::filter_by_constraints(&conn, merchant_id, dispute_constraints)
|
||||||
|
.await
|
||||||
|
.map_err(Into::into)
|
||||||
|
.into_report()
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_dispute(
|
async fn update_dispute(
|
||||||
&self,
|
&self,
|
||||||
this: storage::Dispute,
|
this: storage::Dispute,
|
||||||
@ -92,6 +128,24 @@ impl DisputeInterface for MockDb {
|
|||||||
Err(errors::StorageError::MockDbError)?
|
Err(errors::StorageError::MockDbError)?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn find_dispute_by_merchant_id_dispute_id(
|
||||||
|
&self,
|
||||||
|
_merchant_id: &str,
|
||||||
|
_dispute_id: &str,
|
||||||
|
) -> CustomResult<storage::Dispute, errors::StorageError> {
|
||||||
|
// TODO: Implement function for `MockDb`
|
||||||
|
Err(errors::StorageError::MockDbError)?
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_disputes_by_merchant_id(
|
||||||
|
&self,
|
||||||
|
_merchant_id: &str,
|
||||||
|
_dispute_constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> CustomResult<Vec<storage::Dispute>, errors::StorageError> {
|
||||||
|
// TODO: Implement function for `MockDb`
|
||||||
|
Err(errors::StorageError::MockDbError)?
|
||||||
|
}
|
||||||
|
|
||||||
async fn update_dispute(
|
async fn update_dispute(
|
||||||
&self,
|
&self,
|
||||||
_this: storage::Dispute,
|
_this: storage::Dispute,
|
||||||
|
|||||||
@ -115,7 +115,8 @@ pub fn mk_app(
|
|||||||
{
|
{
|
||||||
server_app = server_app
|
server_app = server_app
|
||||||
.service(routes::MerchantAccount::server(state.clone()))
|
.service(routes::MerchantAccount::server(state.clone()))
|
||||||
.service(routes::ApiKeys::server(state.clone()));
|
.service(routes::ApiKeys::server(state.clone()))
|
||||||
|
.service(routes::Disputes::server(state.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "stripe")]
|
#[cfg(feature = "stripe")]
|
||||||
|
|||||||
@ -57,6 +57,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
(name = "Mandates", description = "Manage mandates"),
|
(name = "Mandates", description = "Manage mandates"),
|
||||||
(name = "Customers", description = "Create and manage customers"),
|
(name = "Customers", description = "Create and manage customers"),
|
||||||
(name = "Payment Methods", description = "Create and manage payment methods of customers"),
|
(name = "Payment Methods", description = "Create and manage payment methods of customers"),
|
||||||
|
(name = "Disputes", description = "Manage disputes"),
|
||||||
// (name = "API Key", description = "Create and manage API Keys"),
|
// (name = "API Key", description = "Create and manage API Keys"),
|
||||||
),
|
),
|
||||||
paths(
|
paths(
|
||||||
@ -100,6 +101,8 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
// crate::routes::api_keys::api_key_update,
|
// crate::routes::api_keys::api_key_update,
|
||||||
// crate::routes::api_keys::api_key_revoke,
|
// crate::routes::api_keys::api_key_revoke,
|
||||||
// crate::routes::api_keys::api_key_list,
|
// crate::routes::api_keys::api_key_list,
|
||||||
|
crate::routes::disputes::retrieve_disputes_list,
|
||||||
|
crate::routes::disputes::retrieve_dispute,
|
||||||
),
|
),
|
||||||
components(schemas(
|
components(schemas(
|
||||||
crate::types::api::refunds::RefundRequest,
|
crate::types::api::refunds::RefundRequest,
|
||||||
@ -143,9 +146,12 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
api_models::enums::PaymentExperience,
|
api_models::enums::PaymentExperience,
|
||||||
api_models::enums::BankNames,
|
api_models::enums::BankNames,
|
||||||
api_models::enums::CardNetwork,
|
api_models::enums::CardNetwork,
|
||||||
|
api_models::enums::DisputeStage,
|
||||||
|
api_models::enums::DisputeStatus,
|
||||||
api_models::enums::CountryCode,
|
api_models::enums::CountryCode,
|
||||||
api_models::admin::MerchantConnector,
|
api_models::admin::MerchantConnector,
|
||||||
api_models::admin::PaymentMethodsEnabled,
|
api_models::admin::PaymentMethodsEnabled,
|
||||||
|
api_models::disputes::DisputeResponse,
|
||||||
api_models::payments::AddressDetails,
|
api_models::payments::AddressDetails,
|
||||||
api_models::payments::Address,
|
api_models::payments::Address,
|
||||||
api_models::payments::BankRedirectData,
|
api_models::payments::BankRedirectData,
|
||||||
|
|||||||
@ -4,6 +4,7 @@ pub mod app;
|
|||||||
pub mod cards_info;
|
pub mod cards_info;
|
||||||
pub mod configs;
|
pub mod configs;
|
||||||
pub mod customers;
|
pub mod customers;
|
||||||
|
pub mod disputes;
|
||||||
pub mod ephemeral_key;
|
pub mod ephemeral_key;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod mandates;
|
pub mod mandates;
|
||||||
@ -15,8 +16,9 @@ pub mod refunds;
|
|||||||
pub mod webhooks;
|
pub mod webhooks;
|
||||||
|
|
||||||
pub use self::app::{
|
pub use self::app::{
|
||||||
ApiKeys, AppState, Cards, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount,
|
ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Health, Mandates,
|
||||||
MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks,
|
MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds,
|
||||||
|
Webhooks,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "stripe")]
|
#[cfg(feature = "stripe")]
|
||||||
pub use super::compatibility::stripe::StripeApis;
|
pub use super::compatibility::stripe::StripeApis;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use actix_web::{web, Scope};
|
|||||||
|
|
||||||
use super::health::*;
|
use super::health::*;
|
||||||
#[cfg(feature = "olap")]
|
#[cfg(feature = "olap")]
|
||||||
use super::{admin::*, api_keys::*};
|
use super::{admin::*, api_keys::*, disputes::*};
|
||||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||||
use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*};
|
use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*};
|
||||||
#[cfg(feature = "oltp")]
|
#[cfg(feature = "oltp")]
|
||||||
@ -379,6 +379,18 @@ impl ApiKeys {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Disputes;
|
||||||
|
|
||||||
|
#[cfg(feature = "olap")]
|
||||||
|
impl Disputes {
|
||||||
|
pub fn server(state: AppState) -> Scope {
|
||||||
|
web::scope("/disputes")
|
||||||
|
.app_data(web::Data::new(state))
|
||||||
|
.service(web::resource("/list").route(web::get().to(retrieve_disputes_list)))
|
||||||
|
.service(web::resource("/{dispute_id}").route(web::get().to(retrieve_dispute)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Cards;
|
pub struct Cards;
|
||||||
|
|
||||||
impl Cards {
|
impl Cards {
|
||||||
|
|||||||
89
crates/router/src/routes/disputes.rs
Normal file
89
crates/router/src/routes/disputes.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
use actix_web::{web, HttpRequest, HttpResponse};
|
||||||
|
use api_models::disputes::DisputeListConstraints;
|
||||||
|
use router_env::{instrument, tracing, Flow};
|
||||||
|
|
||||||
|
use super::app::AppState;
|
||||||
|
use crate::{
|
||||||
|
core::disputes,
|
||||||
|
services::{api, authentication as auth},
|
||||||
|
types::api::disputes as dispute_types,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Diputes - Retrieve Dispute
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/disputes/{dispute_id}",
|
||||||
|
params(
|
||||||
|
("dispute_id" = String, Path, description = "The identifier for dispute")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "The dispute was retrieved successfully", body = DisputeResponse),
|
||||||
|
(status = 404, description = "Dispute does not exist in our records")
|
||||||
|
),
|
||||||
|
tag = "Disputes",
|
||||||
|
operation_id = "Retrieve a Dispute",
|
||||||
|
security(("api_key" = []))
|
||||||
|
)]
|
||||||
|
#[instrument(skip_all, fields(flow = ?Flow::DisputesRetrieve))]
|
||||||
|
pub async fn retrieve_dispute(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
path: web::Path<String>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::DisputesRetrieve;
|
||||||
|
let dispute_id = dispute_types::DisputeId {
|
||||||
|
dispute_id: path.into_inner(),
|
||||||
|
};
|
||||||
|
api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.get_ref(),
|
||||||
|
&req,
|
||||||
|
dispute_id,
|
||||||
|
disputes::retrieve_dispute,
|
||||||
|
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Diputes - List Disputes
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
path = "/disputes/list",
|
||||||
|
params(
|
||||||
|
("limit" = Option<i64>, Query, description = "The maximum number of Dispute Objects to include in the response"),
|
||||||
|
("dispute_status" = Option<DisputeStatus>, Query, description = "The status of dispute"),
|
||||||
|
("dispute_stage" = Option<DisputeStage>, Query, description = "The stage of dispute"),
|
||||||
|
("reason" = Option<String>, Query, description = "The reason for dispute"),
|
||||||
|
("connector" = Option<String>, Query, description = "The connector linked to dispute"),
|
||||||
|
("received_time" = Option<PrimitiveDateTime>, Query, description = "The time at which dispute is received"),
|
||||||
|
("received_time.lt" = Option<PrimitiveDateTime>, Query, description = "Time less than the dispute received time"),
|
||||||
|
("received_time.gt" = Option<PrimitiveDateTime>, Query, description = "Time greater than the dispute received time"),
|
||||||
|
("received_time.lte" = Option<PrimitiveDateTime>, Query, description = "Time less than or equals to the dispute received time"),
|
||||||
|
("received_time.gte" = Option<PrimitiveDateTime>, Query, description = "Time greater than or equals to the dispute received time"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "The dispute list was retrieved successfully", body = Vec<DisputeResponse>),
|
||||||
|
(status = 401, description = "Unauthorized request")
|
||||||
|
),
|
||||||
|
tag = "Disputes",
|
||||||
|
operation_id = "List Disputes",
|
||||||
|
security(("api_key" = []))
|
||||||
|
)]
|
||||||
|
#[instrument(skip_all, fields(flow = ?Flow::DisputesList))]
|
||||||
|
pub async fn retrieve_disputes_list(
|
||||||
|
state: web::Data<AppState>,
|
||||||
|
req: HttpRequest,
|
||||||
|
payload: web::Query<DisputeListConstraints>,
|
||||||
|
) -> HttpResponse {
|
||||||
|
let flow = Flow::DisputesList;
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
api::server_wrap(
|
||||||
|
flow,
|
||||||
|
state.get_ref(),
|
||||||
|
&req,
|
||||||
|
payload,
|
||||||
|
disputes::retrieve_disputes_list,
|
||||||
|
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
@ -391,3 +391,16 @@ pub fn strip_jwt_token(token: &str) -> RouterResult<&str> {
|
|||||||
.strip_prefix("Bearer ")
|
.strip_prefix("Bearer ")
|
||||||
.ok_or_else(|| errors::ApiErrorResponse::InvalidJwtToken.into())
|
.ok_or_else(|| errors::ApiErrorResponse::InvalidJwtToken.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auth_type<'a, T, A>(
|
||||||
|
default_auth: &'a dyn AuthenticateAndFetch<T, A>,
|
||||||
|
jwt_auth_type: &'a dyn AuthenticateAndFetch<T, A>,
|
||||||
|
headers: &HeaderMap,
|
||||||
|
) -> &'a dyn AuthenticateAndFetch<T, A>
|
||||||
|
where
|
||||||
|
{
|
||||||
|
if is_jwt_auth(headers) {
|
||||||
|
return jwt_auth_type;
|
||||||
|
}
|
||||||
|
default_auth
|
||||||
|
}
|
||||||
|
|||||||
@ -15,8 +15,8 @@ use std::{fmt::Debug, str::FromStr};
|
|||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
admin::*, api_keys::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*,
|
admin::*, api_keys::*, configs::*, customers::*, disputes::*, payment_methods::*, payments::*,
|
||||||
webhooks::*,
|
refunds::*, webhooks::*,
|
||||||
};
|
};
|
||||||
use super::ErrorResponse;
|
use super::ErrorResponse;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
use masking::Deserialize;
|
use masking::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct DisputeId {
|
||||||
|
pub dispute_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Deserialize)]
|
#[derive(Default, Debug, Deserialize)]
|
||||||
pub struct DisputePayload {
|
pub struct DisputePayload {
|
||||||
|
|||||||
@ -1 +1,75 @@
|
|||||||
|
use async_bb8_diesel::AsyncRunQueryDsl;
|
||||||
|
use common_utils::errors::CustomResult;
|
||||||
|
use diesel::{associations::HasTable, ExpressionMethods, QueryDsl};
|
||||||
|
use error_stack::{IntoReport, ResultExt};
|
||||||
pub use storage_models::dispute::{Dispute, DisputeNew, DisputeUpdate};
|
pub use storage_models::dispute::{Dispute, DisputeNew, DisputeUpdate};
|
||||||
|
use storage_models::{errors, schema::dispute::dsl};
|
||||||
|
|
||||||
|
use crate::{connection::PgPooledConn, logger, types::transformers::ForeignInto};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait DisputeDbExt: Sized {
|
||||||
|
async fn filter_by_constraints(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_list_constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> CustomResult<Vec<Self>, errors::DatabaseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl DisputeDbExt for Dispute {
|
||||||
|
async fn filter_by_constraints(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_list_constraints: api_models::disputes::DisputeListConstraints,
|
||||||
|
) -> CustomResult<Vec<Self>, errors::DatabaseError> {
|
||||||
|
let mut filter = <Self as HasTable>::table()
|
||||||
|
.filter(dsl::merchant_id.eq(merchant_id.to_owned()))
|
||||||
|
.order(dsl::modified_at.desc())
|
||||||
|
.into_boxed();
|
||||||
|
|
||||||
|
if let Some(received_time) = dispute_list_constraints.received_time {
|
||||||
|
filter = filter.filter(dsl::created_at.eq(received_time));
|
||||||
|
}
|
||||||
|
if let Some(received_time_lt) = dispute_list_constraints.received_time_lt {
|
||||||
|
filter = filter.filter(dsl::created_at.lt(received_time_lt));
|
||||||
|
}
|
||||||
|
if let Some(received_time_gt) = dispute_list_constraints.received_time_gt {
|
||||||
|
filter = filter.filter(dsl::created_at.gt(received_time_gt));
|
||||||
|
}
|
||||||
|
if let Some(received_time_lte) = dispute_list_constraints.received_time_lte {
|
||||||
|
filter = filter.filter(dsl::created_at.le(received_time_lte));
|
||||||
|
}
|
||||||
|
if let Some(received_time_gte) = dispute_list_constraints.received_time_gte {
|
||||||
|
filter = filter.filter(dsl::created_at.ge(received_time_gte));
|
||||||
|
}
|
||||||
|
if let Some(connector) = dispute_list_constraints.connector {
|
||||||
|
filter = filter.filter(dsl::connector.eq(connector));
|
||||||
|
}
|
||||||
|
if let Some(reason) = dispute_list_constraints.reason {
|
||||||
|
filter = filter.filter(dsl::connector_reason.eq(reason));
|
||||||
|
}
|
||||||
|
if let Some(dispute_stage) = dispute_list_constraints.dispute_stage {
|
||||||
|
let storage_dispute_stage: storage_models::enums::DisputeStage =
|
||||||
|
dispute_stage.foreign_into();
|
||||||
|
filter = filter.filter(dsl::dispute_stage.eq(storage_dispute_stage));
|
||||||
|
}
|
||||||
|
if let Some(dispute_status) = dispute_list_constraints.dispute_status {
|
||||||
|
let storage_dispute_status: storage_models::enums::DisputeStatus =
|
||||||
|
dispute_status.foreign_into();
|
||||||
|
filter = filter.filter(dsl::dispute_status.eq(storage_dispute_status));
|
||||||
|
}
|
||||||
|
if let Some(limit) = dispute_list_constraints.limit {
|
||||||
|
filter = filter.limit(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger::debug!(query = %diesel::debug_query::<diesel::pg::Pg, _>(&filter).to_string());
|
||||||
|
|
||||||
|
filter
|
||||||
|
.get_results_async(conn)
|
||||||
|
.await
|
||||||
|
.into_report()
|
||||||
|
.change_context(errors::DatabaseError::NotFound)
|
||||||
|
.attach_printable_lazy(|| "Error filtering records by predicate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -514,11 +514,9 @@ impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for storage_enum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForeignTryFrom<storage::Dispute> for api_models::disputes::DisputeResponse {
|
impl ForeignFrom<storage::Dispute> for api_models::disputes::DisputeResponse {
|
||||||
type Error = errors::ValidationError;
|
fn foreign_from(dispute: storage::Dispute) -> Self {
|
||||||
|
Self {
|
||||||
fn foreign_try_from(dispute: storage::Dispute) -> Result<Self, Self::Error> {
|
|
||||||
Ok(Self {
|
|
||||||
dispute_id: dispute.dispute_id,
|
dispute_id: dispute.dispute_id,
|
||||||
payment_id: dispute.payment_id,
|
payment_id: dispute.payment_id,
|
||||||
attempt_id: dispute.attempt_id,
|
attempt_id: dispute.attempt_id,
|
||||||
@ -526,6 +524,7 @@ impl ForeignTryFrom<storage::Dispute> for api_models::disputes::DisputeResponse
|
|||||||
currency: dispute.currency,
|
currency: dispute.currency,
|
||||||
dispute_stage: dispute.dispute_stage.foreign_into(),
|
dispute_stage: dispute.dispute_stage.foreign_into(),
|
||||||
dispute_status: dispute.dispute_status.foreign_into(),
|
dispute_status: dispute.dispute_status.foreign_into(),
|
||||||
|
connector: dispute.connector,
|
||||||
connector_status: dispute.connector_status,
|
connector_status: dispute.connector_status,
|
||||||
connector_dispute_id: dispute.connector_dispute_id,
|
connector_dispute_id: dispute.connector_dispute_id,
|
||||||
connector_reason: dispute.connector_reason,
|
connector_reason: dispute.connector_reason,
|
||||||
@ -534,7 +533,7 @@ impl ForeignTryFrom<storage::Dispute> for api_models::disputes::DisputeResponse
|
|||||||
created_at: dispute.dispute_created_at,
|
created_at: dispute.dispute_created_at,
|
||||||
updated_at: dispute.updated_at,
|
updated_at: dispute.updated_at,
|
||||||
received_at: dispute.created_at.to_string(),
|
received_at: dispute.created_at.to_string(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,6 +162,10 @@ pub enum Flow {
|
|||||||
ApiKeyRevoke,
|
ApiKeyRevoke,
|
||||||
/// API Key list flow
|
/// API Key list flow
|
||||||
ApiKeyList,
|
ApiKeyList,
|
||||||
|
/// Dispute Retrieve flow
|
||||||
|
DisputesRetrieve,
|
||||||
|
/// Dispute List flow
|
||||||
|
DisputesList,
|
||||||
/// Cards Info flow
|
/// Cards Info flow
|
||||||
CardsInfo,
|
CardsInfo,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,6 +17,7 @@ pub struct DisputeNew {
|
|||||||
pub payment_id: String,
|
pub payment_id: String,
|
||||||
pub attempt_id: String,
|
pub attempt_id: String,
|
||||||
pub merchant_id: String,
|
pub merchant_id: String,
|
||||||
|
pub connector: String,
|
||||||
pub connector_status: String,
|
pub connector_status: String,
|
||||||
pub connector_dispute_id: String,
|
pub connector_dispute_id: String,
|
||||||
pub connector_reason: Option<String>,
|
pub connector_reason: Option<String>,
|
||||||
@ -50,6 +51,7 @@ pub struct Dispute {
|
|||||||
pub created_at: PrimitiveDateTime,
|
pub created_at: PrimitiveDateTime,
|
||||||
#[serde(with = "custom_serde::iso8601")]
|
#[serde(with = "custom_serde::iso8601")]
|
||||||
pub modified_at: PrimitiveDateTime,
|
pub modified_at: PrimitiveDateTime,
|
||||||
|
pub connector: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -34,6 +34,20 @@ impl Dispute {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_merchant_id_dispute_id(
|
||||||
|
conn: &PgPooledConn,
|
||||||
|
merchant_id: &str,
|
||||||
|
dispute_id: &str,
|
||||||
|
) -> StorageResult<Self> {
|
||||||
|
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(
|
||||||
|
conn,
|
||||||
|
dsl::merchant_id
|
||||||
|
.eq(merchant_id.to_owned())
|
||||||
|
.and(dsl::dispute_id.eq(dispute_id.to_owned())),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(conn))]
|
#[instrument(skip(conn))]
|
||||||
pub async fn update(self, conn: &PgPooledConn, dispute: DisputeUpdate) -> StorageResult<Self> {
|
pub async fn update(self, conn: &PgPooledConn, dispute: DisputeUpdate) -> StorageResult<Self> {
|
||||||
match generics::generic_update_with_unique_predicate_get_result::<
|
match generics::generic_update_with_unique_predicate_get_result::<
|
||||||
|
|||||||
@ -132,6 +132,7 @@ diesel::table! {
|
|||||||
updated_at -> Nullable<Varchar>,
|
updated_at -> Nullable<Varchar>,
|
||||||
created_at -> Timestamp,
|
created_at -> Timestamp,
|
||||||
modified_at -> Timestamp,
|
modified_at -> Timestamp,
|
||||||
|
connector -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ CREATE TABLE dispute (
|
|||||||
modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP
|
modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX dispute_id_index ON dispute (dispute_id);
|
CREATE UNIQUE INDEX merchant_id_dispute_id_index ON dispute (merchant_id, dispute_id);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX merchant_id_payment_id_connector_dispute_id_index ON dispute (merchant_id, payment_id, connector_dispute_id);
|
CREATE UNIQUE INDEX merchant_id_payment_id_connector_dispute_id_index ON dispute (merchant_id, payment_id, connector_dispute_id);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE dispute DROP COLUMN connector;
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
-- Your SQL goes here
|
||||||
|
ALTER TABLE dispute
|
||||||
|
ADD COLUMN connector VARCHAR(255) NOT NULL;
|
||||||
Reference in New Issue
Block a user