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:
Sai Harsha Vardhan
2023-04-13 14:04:49 +05:30
committed by GitHub
parent d1d58e33b7
commit acab7671b0
24 changed files with 404 additions and 31 deletions

View File

@ -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>,
}

View File

@ -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

View File

@ -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;

View 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))
}

View File

@ -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))
},
} }
} }
} }

View File

@ -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()

View File

@ -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,

View File

@ -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")]

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View 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
}

View File

@ -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
}

View File

@ -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::{

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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(),
}) }
} }
} }

View File

@ -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,
} }

View File

@ -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)]

View File

@ -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::<

View File

@ -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,
} }
} }

View File

@ -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);

View File

@ -0,0 +1 @@
ALTER TABLE dispute DROP COLUMN connector;

View File

@ -0,0 +1,3 @@
-- Your SQL goes here
ALTER TABLE dispute
ADD COLUMN connector VARCHAR(255) NOT NULL;