From acab7671b07fc6b96108321c6079a76b37d9eeb3 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 13 Apr 2023 14:04:49 +0530 Subject: [PATCH] feat(router): added dispute retrieve and dispute list apis (#842) Co-authored-by: Sangamesh Co-authored-by: sai harsha Co-authored-by: Arun Raj M --- crates/api_models/src/disputes.rs | 39 +++++++- .../router/src/compatibility/stripe/errors.rs | 9 +- crates/router/src/core.rs | 1 + crates/router/src/core/disputes.rs | 47 ++++++++++ .../src/core/errors/api_error_response.rs | 8 +- crates/router/src/core/webhooks.rs | 21 ++--- crates/router/src/db/dispute.rs | 56 +++++++++++- crates/router/src/lib.rs | 3 +- crates/router/src/openapi.rs | 6 ++ crates/router/src/routes.rs | 6 +- crates/router/src/routes/app.rs | 14 ++- crates/router/src/routes/disputes.rs | 89 +++++++++++++++++++ crates/router/src/services/authentication.rs | 13 +++ crates/router/src/types/api.rs | 4 +- crates/router/src/types/api/disputes.rs | 7 +- crates/router/src/types/storage/dispute.rs | 74 +++++++++++++++ crates/router/src/types/transformers.rs | 11 ++- crates/router_env/src/logger/types.rs | 4 + crates/storage_models/src/dispute.rs | 2 + crates/storage_models/src/query/dispute.rs | 14 +++ crates/storage_models/src/schema.rs | 1 + .../up.sql | 2 +- .../down.sql | 1 + .../up.sql | 3 + 24 files changed, 404 insertions(+), 31 deletions(-) create mode 100644 crates/router/src/core/disputes.rs create mode 100644 crates/router/src/routes/disputes.rs create mode 100644 migrations/2023-04-06-063047_add_connector_col_in_dispute/down.sql create mode 100644 migrations/2023-04-06-063047_add_connector_col_in_dispute/up.sql diff --git a/crates/api_models/src/disputes.rs b/crates/api_models/src/disputes.rs index e60a6a44fd..df8d9118a1 100644 --- a/crates/api_models/src/disputes.rs +++ b/crates/api_models/src/disputes.rs @@ -1,4 +1,5 @@ -use masking::Serialize; +use masking::{Deserialize, Serialize}; +use time::PrimitiveDateTime; use utoipa::ToSchema; use super::enums::{DisputeStage, DisputeStatus}; @@ -19,6 +20,8 @@ pub struct DisputeResponse { pub dispute_stage: DisputeStage, /// Status of the dispute pub dispute_status: DisputeStatus, + /// connector to which dispute is associated with + pub connector: String, /// Status of the dispute sent by connector pub connector_status: String, /// Dispute id sent by connector @@ -36,3 +39,37 @@ pub struct DisputeResponse { /// Time at which dispute is received 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, + /// status of the dispute + pub dispute_status: Option, + /// stage of the dispute + pub dispute_stage: Option, + /// reason for the dispute + pub reason: Option, + /// connector linked to dispute + pub connector: Option, + /// The time at which dispute is received + #[schema(example = "2022-09-10T10:11:12Z")] + pub received_time: Option, + /// Time less than the dispute received time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "received_time.lt")] + pub received_time_lt: Option, + /// Time greater than the dispute received time + #[schema(example = "2022-09-10T10:11:12Z")] + #[serde(rename = "received_time.gt")] + pub received_time_gt: Option, + /// 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, + /// 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, +} diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 722235d2af..e718651796 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -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")] 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 // Implement the remaining stripe error codes @@ -460,6 +462,10 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::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, } } @@ -507,7 +513,8 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::PaymentIntentMandateInvalid { .. } | Self::PaymentIntentUnexpectedState { .. } | Self::DuplicatePayment { .. } - | Self::IncorrectConnectorNameGiven => StatusCode::BAD_REQUEST, + | Self::IncorrectConnectorNameGiven + | Self::ResourceMissing { .. } => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::InternalServerError | Self::MandateActive diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 58690e8c44..83b321f8ca 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -3,6 +3,7 @@ pub mod api_keys; pub mod cards_info; pub mod configs; pub mod customers; +pub mod disputes; pub mod errors; pub mod mandate; pub mod metrics; diff --git a/crates/router/src/core/disputes.rs b/crates/router/src/core/disputes.rs new file mode 100644 index 0000000000..33639d16e9 --- /dev/null +++ b/crates/router/src/core/disputes.rs @@ -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 { + 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> { + 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)) +} diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 03fa816c40..79a021fcf1 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -158,6 +158,8 @@ pub enum ApiErrorResponse { IncorrectConnectorNameGiven, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "Address does not exist in our records")] 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")] 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")] @@ -253,7 +255,8 @@ impl actix_web::ResponseError for ApiErrorResponse { Self::DuplicateMerchantAccount | Self::DuplicateMerchantConnectorAccount | Self::DuplicatePaymentMethod - | Self::DuplicateMandate => StatusCode::BAD_REQUEST, // 400 + | Self::DuplicateMandate + | Self::DisputeNotFound { .. } => StatusCode::BAD_REQUEST, // 400 Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, // 503 Self::PaymentNotSucceeded => StatusCode::BAD_REQUEST, // 400 Self::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED, // 501 @@ -444,6 +447,9 @@ impl common_utils::errors::ErrorSwitch { 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)) + }, } } } diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index 359937bde3..34c2a77704 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -228,9 +228,9 @@ async fn get_or_update_dispute_object( option_dispute: Option, dispute_details: api::disputes::DisputePayload, merchant_id: &str, - payment_id: &str, - attempt_id: &str, + payment_attempt: &storage_models::payment_attempt::PaymentAttempt, event_type: api_models::webhooks::IncomingWebhookEvent, + connector_name: &str, ) -> CustomResult { let db = &*state.store; match option_dispute { @@ -246,8 +246,9 @@ async fn get_or_update_dispute_object( .foreign_try_into() .into_report() .change_context(errors::WebhooksFlowError::DisputeCoreFailed)?, - payment_id: payment_id.to_owned(), - attempt_id: attempt_id.to_owned(), + payment_id: payment_attempt.payment_id.to_owned(), + connector: connector_name.to_owned(), + attempt_id: payment_attempt.attempt_id.to_owned(), merchant_id: merchant_id.to_owned(), connector_status: dispute_details.connector_status, connector_dispute_id: dispute_details.connector_dispute_id, @@ -327,18 +328,12 @@ async fn disputes_incoming_webhook_flow( option_dispute, dispute_details, &merchant_account.merchant_id, - &payment_attempt.payment_id, - &payment_attempt.attempt_id, + &payment_attempt, event_type.clone(), + connector.id(), ) .await?; - let disputes_response = Box::new( - dispute_object - .clone() - .foreign_try_into() - .into_report() - .change_context(errors::WebhooksFlowError::DisputeCoreFailed)?, - ); + let disputes_response = Box::new(dispute_object.clone().foreign_into()); let event_type: enums::EventType = dispute_object .dispute_status .foreign_try_into() diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index d4ea14d135..596ec20bb0 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -4,7 +4,7 @@ use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, - types::storage, + types::storage::{self, DisputeDbExt}, }; #[async_trait::async_trait] @@ -21,6 +21,18 @@ pub trait DisputeInterface { connector_dispute_id: &str, ) -> CustomResult, errors::StorageError>; + async fn find_dispute_by_merchant_id_dispute_id( + &self, + merchant_id: &str, + dispute_id: &str, + ) -> CustomResult; + + async fn find_disputes_by_merchant_id( + &self, + merchant_id: &str, + dispute_constraints: api_models::disputes::DisputeListConstraints, + ) -> CustomResult, errors::StorageError>; + async fn update_dispute( &self, this: storage::Dispute, @@ -60,6 +72,30 @@ impl DisputeInterface for Store { .into_report() } + async fn find_dispute_by_merchant_id_dispute_id( + &self, + merchant_id: &str, + dispute_id: &str, + ) -> CustomResult { + 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, 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( &self, this: storage::Dispute, @@ -92,6 +128,24 @@ impl DisputeInterface for MockDb { Err(errors::StorageError::MockDbError)? } + async fn find_dispute_by_merchant_id_dispute_id( + &self, + _merchant_id: &str, + _dispute_id: &str, + ) -> CustomResult { + // 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, errors::StorageError> { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + async fn update_dispute( &self, _this: storage::Dispute, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index b38a7c23f1..4c61d25b99 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -115,7 +115,8 @@ pub fn mk_app( { server_app = server_app .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")] diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 2cb824d463..35e3cc9db6 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -57,6 +57,7 @@ Never share your secret api keys. Keep them guarded and secure. (name = "Mandates", description = "Manage mandates"), (name = "Customers", description = "Create and manage 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"), ), 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_revoke, // crate::routes::api_keys::api_key_list, + crate::routes::disputes::retrieve_disputes_list, + crate::routes::disputes::retrieve_dispute, ), components(schemas( 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::BankNames, api_models::enums::CardNetwork, + api_models::enums::DisputeStage, + api_models::enums::DisputeStatus, api_models::enums::CountryCode, api_models::admin::MerchantConnector, api_models::admin::PaymentMethodsEnabled, + api_models::disputes::DisputeResponse, api_models::payments::AddressDetails, api_models::payments::Address, api_models::payments::BankRedirectData, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 915e68a3eb..f0a19e4267 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,7 @@ pub mod app; pub mod cards_info; pub mod configs; pub mod customers; +pub mod disputes; pub mod ephemeral_key; pub mod health; pub mod mandates; @@ -15,8 +16,9 @@ pub mod refunds; pub mod webhooks; pub use self::app::{ - ApiKeys, AppState, Cards, Configs, Customers, EphemeralKey, Health, Mandates, MerchantAccount, - MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, Webhooks, + ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Health, Mandates, + MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, + Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f747d1281d..0634dc9f78 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2,7 +2,7 @@ use actix_web::{web, Scope}; use super::health::*; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*}; +use super::{admin::*, api_keys::*, disputes::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*}; #[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; impl Cards { diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs new file mode 100644 index 0000000000..95ffd9852f --- /dev/null +++ b/crates/router/src/routes/disputes.rs @@ -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, + req: HttpRequest, + path: web::Path, +) -> 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, Query, description = "The maximum number of Dispute Objects to include in the response"), + ("dispute_status" = Option, Query, description = "The status of dispute"), + ("dispute_stage" = Option, Query, description = "The stage of dispute"), + ("reason" = Option, Query, description = "The reason for dispute"), + ("connector" = Option, Query, description = "The connector linked to dispute"), + ("received_time" = Option, Query, description = "The time at which dispute is received"), + ("received_time.lt" = Option, Query, description = "Time less than the dispute received time"), + ("received_time.gt" = Option, Query, description = "Time greater than the dispute received time"), + ("received_time.lte" = Option, Query, description = "Time less than or equals to the dispute received time"), + ("received_time.gte" = Option, Query, description = "Time greater than or equals to the dispute received time"), + ), + responses( + (status = 200, description = "The dispute list was retrieved successfully", body = Vec), + (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, + req: HttpRequest, + payload: web::Query, +) -> 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 +} diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 9ad9de25ae..34a6e8b744 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -391,3 +391,16 @@ pub fn strip_jwt_token(token: &str) -> RouterResult<&str> { .strip_prefix("Bearer ") .ok_or_else(|| errors::ApiErrorResponse::InvalidJwtToken.into()) } + +pub fn auth_type<'a, T, A>( + default_auth: &'a dyn AuthenticateAndFetch, + jwt_auth_type: &'a dyn AuthenticateAndFetch, + headers: &HeaderMap, +) -> &'a dyn AuthenticateAndFetch +where +{ + if is_jwt_auth(headers) { + return jwt_auth_type; + } + default_auth +} diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c7468b88b5..1cfb5e33de 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -15,8 +15,8 @@ use std::{fmt::Debug, str::FromStr}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, payment_methods::*, payments::*, refunds::*, - webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, payment_methods::*, payments::*, + refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ diff --git a/crates/router/src/types/api/disputes.rs b/crates/router/src/types/api/disputes.rs index aba9a915fd..bd0f3b4b92 100644 --- a/crates/router/src/types/api/disputes.rs +++ b/crates/router/src/types/api/disputes.rs @@ -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)] pub struct DisputePayload { diff --git a/crates/router/src/types/storage/dispute.rs b/crates/router/src/types/storage/dispute.rs index 5052feaa27..c85363aae8 100644 --- a/crates/router/src/types/storage/dispute.rs +++ b/crates/router/src/types/storage/dispute.rs @@ -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}; +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, 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, errors::DatabaseError> { + let mut filter = ::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::(&filter).to_string()); + + filter + .get_results_async(conn) + .await + .into_report() + .change_context(errors::DatabaseError::NotFound) + .attach_printable_lazy(|| "Error filtering records by predicate") + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 54a8547e30..80857ddfb8 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -514,11 +514,9 @@ impl ForeignTryFrom for storage_enum } } -impl ForeignTryFrom for api_models::disputes::DisputeResponse { - type Error = errors::ValidationError; - - fn foreign_try_from(dispute: storage::Dispute) -> Result { - Ok(Self { +impl ForeignFrom for api_models::disputes::DisputeResponse { + fn foreign_from(dispute: storage::Dispute) -> Self { + Self { dispute_id: dispute.dispute_id, payment_id: dispute.payment_id, attempt_id: dispute.attempt_id, @@ -526,6 +524,7 @@ impl ForeignTryFrom for api_models::disputes::DisputeResponse currency: dispute.currency, dispute_stage: dispute.dispute_stage.foreign_into(), dispute_status: dispute.dispute_status.foreign_into(), + connector: dispute.connector, connector_status: dispute.connector_status, connector_dispute_id: dispute.connector_dispute_id, connector_reason: dispute.connector_reason, @@ -534,7 +533,7 @@ impl ForeignTryFrom for api_models::disputes::DisputeResponse created_at: dispute.dispute_created_at, updated_at: dispute.updated_at, received_at: dispute.created_at.to_string(), - }) + } } } diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 9438543839..2addcf3a70 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -162,6 +162,10 @@ pub enum Flow { ApiKeyRevoke, /// API Key list flow ApiKeyList, + /// Dispute Retrieve flow + DisputesRetrieve, + /// Dispute List flow + DisputesList, /// Cards Info flow CardsInfo, } diff --git a/crates/storage_models/src/dispute.rs b/crates/storage_models/src/dispute.rs index ec70ca2426..5b4e48601a 100644 --- a/crates/storage_models/src/dispute.rs +++ b/crates/storage_models/src/dispute.rs @@ -17,6 +17,7 @@ pub struct DisputeNew { pub payment_id: String, pub attempt_id: String, pub merchant_id: String, + pub connector: String, pub connector_status: String, pub connector_dispute_id: String, pub connector_reason: Option, @@ -50,6 +51,7 @@ pub struct Dispute { pub created_at: PrimitiveDateTime, #[serde(with = "custom_serde::iso8601")] pub modified_at: PrimitiveDateTime, + pub connector: String, } #[derive(Debug)] diff --git a/crates/storage_models/src/query/dispute.rs b/crates/storage_models/src/query/dispute.rs index a61a58bc8e..5738e29798 100644 --- a/crates/storage_models/src/query/dispute.rs +++ b/crates/storage_models/src/query/dispute.rs @@ -34,6 +34,20 @@ impl Dispute { .await } + pub async fn find_by_merchant_id_dispute_id( + conn: &PgPooledConn, + merchant_id: &str, + dispute_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::dispute_id.eq(dispute_id.to_owned())), + ) + .await + } + #[instrument(skip(conn))] pub async fn update(self, conn: &PgPooledConn, dispute: DisputeUpdate) -> StorageResult { match generics::generic_update_with_unique_predicate_get_result::< diff --git a/crates/storage_models/src/schema.rs b/crates/storage_models/src/schema.rs index 1a2992f2b7..4932246edc 100644 --- a/crates/storage_models/src/schema.rs +++ b/crates/storage_models/src/schema.rs @@ -132,6 +132,7 @@ diesel::table! { updated_at -> Nullable, created_at -> Timestamp, modified_at -> Timestamp, + connector -> Varchar, } } diff --git a/migrations/2023-03-15-185959_add_dispute_table/up.sql b/migrations/2023-03-15-185959_add_dispute_table/up.sql index 046186d753..6255ef1457 100644 --- a/migrations/2023-03-15-185959_add_dispute_table/up.sql +++ b/migrations/2023-03-15-185959_add_dispute_table/up.sql @@ -23,7 +23,7 @@ CREATE TABLE dispute ( 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); diff --git a/migrations/2023-04-06-063047_add_connector_col_in_dispute/down.sql b/migrations/2023-04-06-063047_add_connector_col_in_dispute/down.sql new file mode 100644 index 0000000000..fffb385707 --- /dev/null +++ b/migrations/2023-04-06-063047_add_connector_col_in_dispute/down.sql @@ -0,0 +1 @@ +ALTER TABLE dispute DROP COLUMN connector; \ No newline at end of file diff --git a/migrations/2023-04-06-063047_add_connector_col_in_dispute/up.sql b/migrations/2023-04-06-063047_add_connector_col_in_dispute/up.sql new file mode 100644 index 0000000000..252e9cd962 --- /dev/null +++ b/migrations/2023-04-06-063047_add_connector_col_in_dispute/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE dispute +ADD COLUMN connector VARCHAR(255) NOT NULL; \ No newline at end of file