mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-11-01 02:57:02 +08:00 
			
		
		
		
	feat(router): added dispute accept api, file module apis and dispute evidence submission api (#900)
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
					Sai Harsha Vardhan
				
			
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			 GitHub
						GitHub
					
				
			
						parent
						
							bcbf4c882c
						
					
				
				
					commit
					bdf1e5147e
				
			| @ -182,6 +182,20 @@ pub enum StripeErrorCode { | ||||
|     IncorrectConnectorNameGiven, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "No such {object}: '{id}'")] | ||||
|     ResourceMissing { object: String, id: String }, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File validation failed")] | ||||
|     FileValidationFailed, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File not found in the request")] | ||||
|     MissingFile, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File puropse not found in the request")] | ||||
|     MissingFilePurpose, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File content type not found")] | ||||
|     MissingFileContentType, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Dispute id not found in the request")] | ||||
|     MissingDisputeId, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File does not exists in our records")] | ||||
|     FileNotFound, | ||||
|     #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "File not available")] | ||||
|     FileNotAvailable, | ||||
|     // [#216]: https://github.com/juspay/hyperswitch/issues/216 | ||||
|     // Implement the remaining stripe error codes | ||||
|  | ||||
| @ -466,6 +480,16 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode { | ||||
|                 object: "dispute".to_owned(), | ||||
|                 id: dispute_id, | ||||
|             }, | ||||
|             errors::ApiErrorResponse::DisputeStatusValidationFailed { reason } => { | ||||
|                 Self::InternalServerError | ||||
|             } | ||||
|             errors::ApiErrorResponse::FileValidationFailed { .. } => Self::FileValidationFailed, | ||||
|             errors::ApiErrorResponse::MissingFile => Self::MissingFile, | ||||
|             errors::ApiErrorResponse::MissingFilePurpose => Self::MissingFilePurpose, | ||||
|             errors::ApiErrorResponse::MissingFileContentType => Self::MissingFileContentType, | ||||
|             errors::ApiErrorResponse::MissingDisputeId => Self::MissingDisputeId, | ||||
|             errors::ApiErrorResponse::FileNotFound => Self::FileNotFound, | ||||
|             errors::ApiErrorResponse::FileNotAvailable => Self::FileNotAvailable, | ||||
|             errors::ApiErrorResponse::NotSupported { .. } => Self::InternalServerError, | ||||
|         } | ||||
|     } | ||||
| @ -514,7 +538,14 @@ impl actix_web::ResponseError for StripeErrorCode { | ||||
|             | Self::PaymentIntentUnexpectedState { .. } | ||||
|             | Self::DuplicatePayment { .. } | ||||
|             | Self::IncorrectConnectorNameGiven | ||||
|             | Self::ResourceMissing { .. } => StatusCode::BAD_REQUEST, | ||||
|             | Self::ResourceMissing { .. } | ||||
|             | Self::FileValidationFailed | ||||
|             | Self::MissingFile | ||||
|             | Self::MissingFileContentType | ||||
|             | Self::MissingFilePurpose | ||||
|             | Self::MissingDisputeId | ||||
|             | Self::FileNotFound | ||||
|             | Self::FileNotAvailable => StatusCode::BAD_REQUEST, | ||||
|             Self::RefundFailed | ||||
|             | Self::InternalServerError | ||||
|             | Self::MandateActive | ||||
|  | ||||
| @ -56,6 +56,9 @@ where | ||||
|         } | ||||
|         Ok(api::ApplicationResponse::StatusOk) => api::http_response_ok(), | ||||
|         Ok(api::ApplicationResponse::TextPlain(text)) => api::http_response_plaintext(text), | ||||
|         Ok(api::ApplicationResponse::FileData((file_data, content_type))) => { | ||||
|             api::http_response_file_data(file_data, content_type) | ||||
|         } | ||||
|         Ok(api::ApplicationResponse::JsonForRedirection(response)) => { | ||||
|             match serde_json::to_string(&response) { | ||||
|                 Ok(res) => api::http_redirect_response(res, response), | ||||
|  | ||||
| @ -62,6 +62,8 @@ pub struct Settings { | ||||
|     pub api_keys: ApiKeys, | ||||
|     #[cfg(feature = "kms")] | ||||
|     pub kms: kms::KmsConfig, | ||||
|     #[cfg(feature = "s3")] | ||||
|     pub file_upload_config: FileUploadConfig, | ||||
|     pub tokenization: TokenizationConfig, | ||||
| } | ||||
|  | ||||
| @ -306,7 +308,7 @@ pub struct Connectors { | ||||
|     pub payu: ConnectorParams, | ||||
|     pub rapyd: ConnectorParams, | ||||
|     pub shift4: ConnectorParams, | ||||
|     pub stripe: ConnectorParams, | ||||
|     pub stripe: ConnectorParamsWithFileUploadUrl, | ||||
|     pub worldline: ConnectorParams, | ||||
|     pub worldpay: ConnectorParams, | ||||
|     pub trustpay: ConnectorParamsWithMoreUrls, | ||||
| @ -328,6 +330,13 @@ pub struct ConnectorParamsWithMoreUrls { | ||||
|     pub base_url_bank_redirects: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize, Clone, Default)] | ||||
| #[serde(default)] | ||||
| pub struct ConnectorParamsWithFileUploadUrl { | ||||
|     pub base_url: String, | ||||
|     pub base_url_file_upload: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Deserialize)] | ||||
| #[serde(default)] | ||||
| pub struct SchedulerSettings { | ||||
| @ -387,6 +396,16 @@ pub struct ApiKeys { | ||||
|     pub hash_key: String, | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "s3")] | ||||
| #[derive(Debug, Deserialize, Clone, Default)] | ||||
| #[serde(default)] | ||||
| pub struct FileUploadConfig { | ||||
|     /// The AWS region to send file uploads | ||||
|     pub region: String, | ||||
|     /// The AWS s3 bucket to send file uploads | ||||
|     pub bucket_name: String, | ||||
| } | ||||
|  | ||||
| impl Settings { | ||||
|     pub fn new() -> ApplicationResult<Self> { | ||||
|         Self::with_config_path(None) | ||||
| @ -465,7 +484,8 @@ impl Settings { | ||||
|         self.kms | ||||
|             .validate() | ||||
|             .map_err(|error| ApplicationError::InvalidConfigurationValueError(error.into()))?; | ||||
|  | ||||
|         #[cfg(feature = "s3")] | ||||
|         self.file_upload_config.validate()?; | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -155,6 +155,21 @@ impl super::settings::ConnectorParams { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl super::settings::ConnectorParamsWithFileUploadUrl { | ||||
|     pub fn validate(&self) -> Result<(), ApplicationError> { | ||||
|         common_utils::fp_utils::when(self.base_url.is_default_or_empty(), || { | ||||
|             Err(ApplicationError::InvalidConfigurationValueError( | ||||
|                 "connector base URL must not be empty".into(), | ||||
|             )) | ||||
|         })?; | ||||
|         common_utils::fp_utils::when(self.base_url_file_upload.is_default_or_empty(), || { | ||||
|             Err(ApplicationError::InvalidConfigurationValueError( | ||||
|                 "connector file upload base URL must not be empty".into(), | ||||
|             )) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl super::settings::SchedulerSettings { | ||||
|     pub fn validate(&self) -> Result<(), ApplicationError> { | ||||
|         use common_utils::fp_utils::when; | ||||
| @ -198,6 +213,25 @@ impl super::settings::DrainerSettings { | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(feature = "s3")] | ||||
| impl super::settings::FileUploadConfig { | ||||
|     pub fn validate(&self) -> Result<(), ApplicationError> { | ||||
|         use common_utils::fp_utils::when; | ||||
|  | ||||
|         when(self.region.is_default_or_empty(), || { | ||||
|             Err(ApplicationError::InvalidConfigurationValueError( | ||||
|                 "s3 region must not be empty".into(), | ||||
|             )) | ||||
|         })?; | ||||
|  | ||||
|         when(self.bucket_name.is_default_or_empty(), || { | ||||
|             Err(ApplicationError::InvalidConfigurationValueError( | ||||
|                 "s3 bucket name must not be empty".into(), | ||||
|             )) | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl super::settings::ApiKeys { | ||||
|     pub fn validate(&self) -> Result<(), ApplicationError> { | ||||
|         use common_utils::fp_utils::when; | ||||
|  | ||||
| @ -115,7 +115,9 @@ impl api::PaymentVoid for Checkout {} | ||||
| impl api::PaymentCapture for Checkout {} | ||||
| impl api::PaymentSession for Checkout {} | ||||
| impl api::ConnectorAccessToken for Checkout {} | ||||
| impl api::AcceptDispute for Checkout {} | ||||
| impl api::PaymentToken for Checkout {} | ||||
| impl api::Dispute for Checkout {} | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration< | ||||
| @ -697,6 +699,139 @@ impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponse | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     ConnectorIntegration<api::Accept, types::AcceptDisputeRequestData, types::AcceptDisputeResponse> | ||||
|     for Checkout | ||||
| { | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::AcceptDisputeRouterData, | ||||
|         _connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> { | ||||
|         let mut header = vec![( | ||||
|             headers::CONTENT_TYPE.to_string(), | ||||
|             types::AcceptDisputeType::get_content_type(self).to_string(), | ||||
|         )]; | ||||
|         let mut api_key = self.get_auth_header(&req.connector_auth_type)?; | ||||
|         header.append(&mut api_key); | ||||
|         Ok(header) | ||||
|     } | ||||
|  | ||||
|     fn get_url( | ||||
|         &self, | ||||
|         req: &types::AcceptDisputeRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(format!( | ||||
|             "{}{}{}{}", | ||||
|             self.base_url(connectors), | ||||
|             "disputes/", | ||||
|             req.request.connector_dispute_id, | ||||
|             "/accept" | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn build_request( | ||||
|         &self, | ||||
|         req: &types::AcceptDisputeRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Option<services::Request>, errors::ConnectorError> { | ||||
|         Ok(Some( | ||||
|             services::RequestBuilder::new() | ||||
|                 .method(services::Method::Post) | ||||
|                 .url(&types::AcceptDisputeType::get_url(self, req, connectors)?) | ||||
|                 .attach_default_headers() | ||||
|                 .headers(types::AcceptDisputeType::get_headers( | ||||
|                     self, req, connectors, | ||||
|                 )?) | ||||
|                 .build(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn handle_response( | ||||
|         &self, | ||||
|         data: &types::AcceptDisputeRouterData, | ||||
|         _res: types::Response, | ||||
|     ) -> CustomResult<types::AcceptDisputeRouterData, errors::ConnectorError> { | ||||
|         Ok(types::AcceptDisputeRouterData { | ||||
|             response: Ok(types::AcceptDisputeResponse { | ||||
|                 dispute_status: api::enums::DisputeStatus::DisputeAccepted, | ||||
|                 connector_status: None, | ||||
|             }), | ||||
|             ..data.clone() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_error_response( | ||||
|         &self, | ||||
|         res: types::Response, | ||||
|     ) -> CustomResult<types::ErrorResponse, errors::ConnectorError> { | ||||
|         let response: checkout::ErrorResponse = if res.response.is_empty() { | ||||
|             checkout::ErrorResponse { | ||||
|                 request_id: None, | ||||
|                 error_type: if res.status_code == 401 { | ||||
|                     Some("Invalid Api Key".to_owned()) | ||||
|                 } else { | ||||
|                     None | ||||
|                 }, | ||||
|                 error_codes: None, | ||||
|             } | ||||
|         } else { | ||||
|             res.response | ||||
|                 .parse_struct("ErrorResponse") | ||||
|                 .change_context(errors::ConnectorError::ResponseDeserializationFailed)? | ||||
|         }; | ||||
|  | ||||
|         Ok(types::ErrorResponse { | ||||
|             status_code: res.status_code, | ||||
|             code: response | ||||
|                 .error_codes | ||||
|                 .unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()]) | ||||
|                 .join(" & "), | ||||
|             message: response | ||||
|                 .error_type | ||||
|                 .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), | ||||
|             reason: None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl api::UploadFile for Checkout {} | ||||
|  | ||||
| impl ConnectorIntegration<api::Upload, types::UploadFileRequestData, types::UploadFileResponse> | ||||
|     for Checkout | ||||
| { | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl api::FileUpload for Checkout { | ||||
|     fn validate_file_upload( | ||||
|         &self, | ||||
|         purpose: api::FilePurpose, | ||||
|         file_size: i32, | ||||
|         file_type: mime::Mime, | ||||
|     ) -> CustomResult<(), errors::ConnectorError> { | ||||
|         match purpose { | ||||
|             api::FilePurpose::DisputeEvidence => { | ||||
|                 let supported_file_types = | ||||
|                     vec!["image/jpeg", "image/jpg", "image/png", "application/pdf"]; | ||||
|                 // 4 Megabytes (MB) | ||||
|                 if file_size > 4000000 { | ||||
|                     Err(errors::ConnectorError::FileValidationFailed { | ||||
|                         reason: "file_size exceeded the max file size of 4MB".to_owned(), | ||||
|                     })? | ||||
|                 } | ||||
|                 if !supported_file_types.contains(&file_type.to_string().as_str()) { | ||||
|                     Err(errors::ConnectorError::FileValidationFailed { | ||||
|                         reason: "file_type does not match JPEG, JPG, PNG, or PDF format".to_owned(), | ||||
|                     })? | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl api::IncomingWebhook for Checkout { | ||||
|     fn get_webhook_source_verification_algorithm( | ||||
|  | ||||
| @ -948,6 +948,249 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl api::UploadFile for Stripe {} | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl api::FileUpload for Stripe { | ||||
|     fn validate_file_upload( | ||||
|         &self, | ||||
|         purpose: api::FilePurpose, | ||||
|         file_size: i32, | ||||
|         file_type: mime::Mime, | ||||
|     ) -> CustomResult<(), errors::ConnectorError> { | ||||
|         match purpose { | ||||
|             api::FilePurpose::DisputeEvidence => { | ||||
|                 let supported_file_types = vec!["image/jpeg", "image/png", "application/pdf"]; | ||||
|                 // 5 Megabytes (MB) | ||||
|                 if file_size > 5000000 { | ||||
|                     Err(errors::ConnectorError::FileValidationFailed { | ||||
|                         reason: "file_size exceeded the max file size of 5MB".to_owned(), | ||||
|                     })? | ||||
|                 } | ||||
|                 if !supported_file_types.contains(&file_type.to_string().as_str()) { | ||||
|                     Err(errors::ConnectorError::FileValidationFailed { | ||||
|                         reason: "file_type does not match JPEG, JPG, PNG, or PDF format".to_owned(), | ||||
|                     })? | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl | ||||
|     services::ConnectorIntegration< | ||||
|         api::Upload, | ||||
|         types::UploadFileRequestData, | ||||
|         types::UploadFileResponse, | ||||
|     > for Stripe | ||||
| { | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::RouterData< | ||||
|             api::Upload, | ||||
|             types::UploadFileRequestData, | ||||
|             types::UploadFileResponse, | ||||
|         >, | ||||
|         _connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> { | ||||
|         self.get_auth_header(&req.connector_auth_type) | ||||
|     } | ||||
|  | ||||
|     fn get_content_type(&self) -> &'static str { | ||||
|         "multipart/form-data" | ||||
|     } | ||||
|  | ||||
|     fn get_url( | ||||
|         &self, | ||||
|         _req: &types::UploadFileRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(format!( | ||||
|             "{}{}", | ||||
|             connectors.stripe.base_url_file_upload, "v1/files" | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn get_request_form_data( | ||||
|         &self, | ||||
|         req: &types::UploadFileRouterData, | ||||
|     ) -> CustomResult<Option<reqwest::multipart::Form>, errors::ConnectorError> { | ||||
|         let stripe_req = transformers::construct_file_upload_request(req.clone())?; | ||||
|         Ok(Some(stripe_req)) | ||||
|     } | ||||
|  | ||||
|     fn build_request( | ||||
|         &self, | ||||
|         req: &types::UploadFileRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Option<services::Request>, errors::ConnectorError> { | ||||
|         Ok(Some( | ||||
|             services::RequestBuilder::new() | ||||
|                 .method(services::Method::Post) | ||||
|                 .url(&types::UploadFileType::get_url(self, req, connectors)?) | ||||
|                 .attach_default_headers() | ||||
|                 .headers(types::UploadFileType::get_headers(self, req, connectors)?) | ||||
|                 .form_data(types::UploadFileType::get_request_form_data(self, req)?) | ||||
|                 .content_type(services::request::ContentType::FormData) | ||||
|                 .build(), | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     #[instrument(skip_all)] | ||||
|     fn handle_response( | ||||
|         &self, | ||||
|         data: &types::UploadFileRouterData, | ||||
|         res: types::Response, | ||||
|     ) -> CustomResult< | ||||
|         types::RouterData<api::Upload, types::UploadFileRequestData, types::UploadFileResponse>, | ||||
|         errors::ConnectorError, | ||||
|     > { | ||||
|         let response: stripe::FileUploadResponse = res | ||||
|             .response | ||||
|             .parse_struct("Stripe FileUploadResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|         Ok(types::UploadFileRouterData { | ||||
|             response: Ok(types::UploadFileResponse { | ||||
|                 provider_file_id: response.file_id, | ||||
|             }), | ||||
|             ..data.clone() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_error_response( | ||||
|         &self, | ||||
|         res: types::Response, | ||||
|     ) -> CustomResult<types::ErrorResponse, errors::ConnectorError> { | ||||
|         let response: stripe::ErrorResponse = res | ||||
|             .response | ||||
|             .parse_struct("ErrorResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|         Ok(types::ErrorResponse { | ||||
|             status_code: res.status_code, | ||||
|             code: response | ||||
|                 .error | ||||
|                 .code | ||||
|                 .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), | ||||
|             message: response | ||||
|                 .error | ||||
|                 .message | ||||
|                 .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), | ||||
|             reason: None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl api::SubmitEvidence for Stripe {} | ||||
|  | ||||
| impl | ||||
|     services::ConnectorIntegration< | ||||
|         api::Evidence, | ||||
|         types::SubmitEvidenceRequestData, | ||||
|         types::SubmitEvidenceResponse, | ||||
|     > for Stripe | ||||
| { | ||||
|     fn get_headers( | ||||
|         &self, | ||||
|         req: &types::SubmitEvidenceRouterData, | ||||
|         _connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> { | ||||
|         let mut header = vec![( | ||||
|             headers::CONTENT_TYPE.to_string(), | ||||
|             types::SubmitEvidenceType::get_content_type(self).to_string(), | ||||
|         )]; | ||||
|         let mut api_key = self.get_auth_header(&req.connector_auth_type)?; | ||||
|         header.append(&mut api_key); | ||||
|         Ok(header) | ||||
|     } | ||||
|  | ||||
|     fn get_content_type(&self) -> &'static str { | ||||
|         "application/x-www-form-urlencoded" | ||||
|     } | ||||
|  | ||||
|     fn get_url( | ||||
|         &self, | ||||
|         req: &types::SubmitEvidenceRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<String, errors::ConnectorError> { | ||||
|         Ok(format!( | ||||
|             "{}{}{}", | ||||
|             self.base_url(connectors), | ||||
|             "v1/disputes/", | ||||
|             req.request.connector_dispute_id | ||||
|         )) | ||||
|     } | ||||
|  | ||||
|     fn get_request_body( | ||||
|         &self, | ||||
|         req: &types::SubmitEvidenceRouterData, | ||||
|     ) -> CustomResult<Option<String>, errors::ConnectorError> { | ||||
|         let stripe_req = stripe::Evidence::try_from(req)?; | ||||
|         let stripe_req_string = utils::Encode::<stripe::Evidence>::url_encode(&stripe_req) | ||||
|             .change_context(errors::ConnectorError::RequestEncodingFailed)?; | ||||
|         print!("Stripe request: {:?}", stripe_req_string); | ||||
|         Ok(Some(stripe_req_string)) | ||||
|     } | ||||
|  | ||||
|     fn build_request( | ||||
|         &self, | ||||
|         req: &types::SubmitEvidenceRouterData, | ||||
|         connectors: &settings::Connectors, | ||||
|     ) -> CustomResult<Option<services::Request>, errors::ConnectorError> { | ||||
|         let request = services::RequestBuilder::new() | ||||
|             .method(services::Method::Post) | ||||
|             .url(&types::SubmitEvidenceType::get_url(self, req, connectors)?) | ||||
|             .attach_default_headers() | ||||
|             .headers(types::SubmitEvidenceType::get_headers( | ||||
|                 self, req, connectors, | ||||
|             )?) | ||||
|             .body(types::SubmitEvidenceType::get_request_body(self, req)?) | ||||
|             .build(); | ||||
|         Ok(Some(request)) | ||||
|     } | ||||
|  | ||||
|     #[instrument(skip_all)] | ||||
|     fn handle_response( | ||||
|         &self, | ||||
|         data: &types::SubmitEvidenceRouterData, | ||||
|         res: types::Response, | ||||
|     ) -> CustomResult<types::SubmitEvidenceRouterData, errors::ConnectorError> { | ||||
|         let response: stripe::DisputeObj = res | ||||
|             .response | ||||
|             .parse_struct("Stripe DisputeObj") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|         Ok(types::SubmitEvidenceRouterData { | ||||
|             response: Ok(types::SubmitEvidenceResponse { | ||||
|                 dispute_status: api_models::enums::DisputeStatus::DisputeChallenged, | ||||
|                 connector_status: Some(response.status), | ||||
|             }), | ||||
|             ..data.clone() | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     fn get_error_response( | ||||
|         &self, | ||||
|         res: types::Response, | ||||
|     ) -> CustomResult<types::ErrorResponse, errors::ConnectorError> { | ||||
|         let response: stripe::ErrorResponse = res | ||||
|             .response | ||||
|             .parse_struct("ErrorResponse") | ||||
|             .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; | ||||
|         Ok(types::ErrorResponse { | ||||
|             status_code: res.status_code, | ||||
|             code: response | ||||
|                 .error | ||||
|                 .code | ||||
|                 .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), | ||||
|             message: response | ||||
|                 .error | ||||
|                 .message | ||||
|                 .unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()), | ||||
|             reason: None, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn get_signature_elements_from_header( | ||||
|     headers: &actix_web::http::header::HeaderMap, | ||||
| ) -> CustomResult<HashMap<String, Vec<u8>>, errors::ConnectorError> { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| use api_models::{self, enums as api_enums, payments}; | ||||
| use base64::Engine; | ||||
| use common_utils::{fp_utils, pii}; | ||||
| use common_utils::{errors::CustomResult, fp_utils, pii}; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use masking::{ExposeInterface, ExposeOptionInterface, Secret}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| @ -1681,3 +1681,121 @@ impl | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn construct_file_upload_request( | ||||
|     file_upload_router_data: types::UploadFileRouterData, | ||||
| ) -> CustomResult<reqwest::multipart::Form, errors::ConnectorError> { | ||||
|     let request = file_upload_router_data.request; | ||||
|     let mut multipart = reqwest::multipart::Form::new(); | ||||
|     multipart = multipart.text("purpose", "dispute_evidence"); | ||||
|     let file_data = reqwest::multipart::Part::bytes(request.file) | ||||
|         .file_name(request.file_key) | ||||
|         .mime_str(request.file_type.as_ref()) | ||||
|         .map_err(|_| errors::ConnectorError::RequestEncodingFailed)?; | ||||
|     multipart = multipart.part("file", file_data); | ||||
|     Ok(multipart) | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct FileUploadResponse { | ||||
|     #[serde(rename = "id")] | ||||
|     pub file_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize)] | ||||
| pub struct Evidence { | ||||
|     #[serde(rename = "evidence[access_activity_log]")] | ||||
|     pub access_activity_log: Option<String>, | ||||
|     #[serde(rename = "evidence[billing_address]")] | ||||
|     pub billing_address: Option<String>, | ||||
|     #[serde(rename = "evidence[cancellation_policy]")] | ||||
|     pub cancellation_policy: Option<String>, | ||||
|     #[serde(rename = "evidence[cancellation_policy_disclosure]")] | ||||
|     pub cancellation_policy_disclosure: Option<String>, | ||||
|     #[serde(rename = "evidence[cancellation_rebuttal]")] | ||||
|     pub cancellation_rebuttal: Option<String>, | ||||
|     #[serde(rename = "evidence[customer_communication]")] | ||||
|     pub customer_communication: Option<String>, | ||||
|     #[serde(rename = "evidence[customer_email_address]")] | ||||
|     pub customer_email_address: Option<String>, | ||||
|     #[serde(rename = "evidence[customer_name]")] | ||||
|     pub customer_name: Option<String>, | ||||
|     #[serde(rename = "evidence[customer_purchase_ip]")] | ||||
|     pub customer_purchase_ip: Option<String>, | ||||
|     #[serde(rename = "evidence[customer_signature]")] | ||||
|     pub customer_signature: Option<String>, | ||||
|     #[serde(rename = "evidence[product_description]")] | ||||
|     pub product_description: Option<String>, | ||||
|     #[serde(rename = "evidence[receipt]")] | ||||
|     pub receipt: Option<String>, | ||||
|     #[serde(rename = "evidence[refund_policy]")] | ||||
|     pub refund_policy: Option<String>, | ||||
|     #[serde(rename = "evidence[refund_policy_disclosure]")] | ||||
|     pub refund_policy_disclosure: Option<String>, | ||||
|     #[serde(rename = "evidence[refund_refusal_explanation]")] | ||||
|     pub refund_refusal_explanation: Option<String>, | ||||
|     #[serde(rename = "evidence[service_date]")] | ||||
|     pub service_date: Option<String>, | ||||
|     #[serde(rename = "evidence[service_documentation]")] | ||||
|     pub service_documentation: Option<String>, | ||||
|     #[serde(rename = "evidence[shipping_address]")] | ||||
|     pub shipping_address: Option<String>, | ||||
|     #[serde(rename = "evidence[shipping_carrier]")] | ||||
|     pub shipping_carrier: Option<String>, | ||||
|     #[serde(rename = "evidence[shipping_date]")] | ||||
|     pub shipping_date: Option<String>, | ||||
|     #[serde(rename = "evidence[shipping_documentation]")] | ||||
|     pub shipping_documentation: Option<String>, | ||||
|     #[serde(rename = "evidence[shipping_tracking_number]")] | ||||
|     pub shipping_tracking_number: Option<String>, | ||||
|     #[serde(rename = "evidence[uncategorized_file]")] | ||||
|     pub uncategorized_file: Option<String>, | ||||
|     #[serde(rename = "evidence[uncategorized_text]")] | ||||
|     pub uncategorized_text: Option<String>, | ||||
|     pub submit: bool, | ||||
| } | ||||
|  | ||||
| impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence { | ||||
|     type Error = error_stack::Report<errors::ConnectorError>; | ||||
|     fn try_from(item: &types::SubmitEvidenceRouterData) -> Result<Self, Self::Error> { | ||||
|         let submit_evidence_request_data = item.request.clone(); | ||||
|         Ok(Self { | ||||
|             access_activity_log: submit_evidence_request_data.access_activity_log, | ||||
|             billing_address: submit_evidence_request_data.billing_address, | ||||
|             cancellation_policy: submit_evidence_request_data.cancellation_policy_provider_file_id, | ||||
|             cancellation_policy_disclosure: submit_evidence_request_data | ||||
|                 .cancellation_policy_disclosure, | ||||
|             cancellation_rebuttal: submit_evidence_request_data.cancellation_rebuttal, | ||||
|             customer_communication: submit_evidence_request_data | ||||
|                 .customer_communication_provider_file_id, | ||||
|             customer_email_address: submit_evidence_request_data.customer_email_address, | ||||
|             customer_name: submit_evidence_request_data.customer_name, | ||||
|             customer_purchase_ip: submit_evidence_request_data.customer_purchase_ip, | ||||
|             customer_signature: submit_evidence_request_data.customer_signature_provider_file_id, | ||||
|             product_description: submit_evidence_request_data.product_description, | ||||
|             receipt: submit_evidence_request_data.receipt_provider_file_id, | ||||
|             refund_policy: submit_evidence_request_data.refund_policy_provider_file_id, | ||||
|             refund_policy_disclosure: submit_evidence_request_data.refund_policy_disclosure, | ||||
|             refund_refusal_explanation: submit_evidence_request_data.refund_refusal_explanation, | ||||
|             service_date: submit_evidence_request_data.service_date, | ||||
|             service_documentation: submit_evidence_request_data | ||||
|                 .service_documentation_provider_file_id, | ||||
|             shipping_address: submit_evidence_request_data.shipping_address, | ||||
|             shipping_carrier: submit_evidence_request_data.shipping_carrier, | ||||
|             shipping_date: submit_evidence_request_data.shipping_date, | ||||
|             shipping_documentation: submit_evidence_request_data | ||||
|                 .shipping_documentation_provider_file_id, | ||||
|             shipping_tracking_number: submit_evidence_request_data.shipping_tracking_number, | ||||
|             uncategorized_file: submit_evidence_request_data.uncategorized_file_provider_file_id, | ||||
|             uncategorized_text: submit_evidence_request_data.uncategorized_text, | ||||
|             submit: true, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct DisputeObj { | ||||
|     #[serde(rename = "id")] | ||||
|     pub dispute_id: String, | ||||
|     pub status: String, | ||||
| } | ||||
|  | ||||
| @ -5,6 +5,7 @@ pub mod configs; | ||||
| pub mod customers; | ||||
| pub mod disputes; | ||||
| pub mod errors; | ||||
| pub mod files; | ||||
| pub mod mandate; | ||||
| pub mod metrics; | ||||
| pub mod payment_methods; | ||||
|  | ||||
| @ -1,10 +1,23 @@ | ||||
| use api_models::disputes as dispute_models; | ||||
| use error_stack::ResultExt; | ||||
| use router_env::{instrument, tracing}; | ||||
| pub mod transformers; | ||||
|  | ||||
| use super::errors::{self, RouterResponse, StorageErrorExt}; | ||||
| use super::{ | ||||
|     errors::{self, RouterResponse, StorageErrorExt}, | ||||
|     metrics, | ||||
| }; | ||||
| use crate::{ | ||||
|     core::{payments, utils}, | ||||
|     routes::AppState, | ||||
|     services, | ||||
|     types::{api::disputes, storage, transformers::ForeignFrom}, | ||||
|     types::{ | ||||
|         api::{self, disputes}, | ||||
|         storage::{self, enums as storage_enums}, | ||||
|         transformers::{ForeignFrom, ForeignInto}, | ||||
|         AcceptDisputeRequestData, AcceptDisputeResponse, SubmitEvidenceRequestData, | ||||
|         SubmitEvidenceResponse, | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| #[instrument(skip(state))] | ||||
| @ -34,10 +47,218 @@ pub async fn retrieve_disputes_list( | ||||
|         .store | ||||
|         .find_disputes_by_merchant_id(&merchant_account.merchant_id, constraints) | ||||
|         .await | ||||
|         .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?; | ||||
|         .to_not_found_response(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Unable to retrieve disputes")?; | ||||
|     let disputes_list = disputes | ||||
|         .into_iter() | ||||
|         .map(api_models::disputes::DisputeResponse::foreign_from) | ||||
|         .collect(); | ||||
|     Ok(services::ApplicationResponse::Json(disputes_list)) | ||||
| } | ||||
|  | ||||
| #[instrument(skip(state))] | ||||
| pub async fn accept_dispute( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::MerchantAccount, | ||||
|     req: disputes::DisputeId, | ||||
| ) -> RouterResponse<dispute_models::DisputeResponse> { | ||||
|     let db = &state.store; | ||||
|     let dispute = state | ||||
|         .store | ||||
|         .find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, &req.dispute_id) | ||||
|         .await | ||||
|         .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { | ||||
|             dispute_id: req.dispute_id, | ||||
|         })?; | ||||
|     let dispute_id = dispute.dispute_id.clone(); | ||||
|     common_utils::fp_utils::when( | ||||
|         !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute | ||||
|             && dispute.dispute_status == storage_enums::DisputeStatus::DisputeOpened), | ||||
|         || { | ||||
|             metrics::ACCEPT_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC.add(&metrics::CONTEXT, 1, &[]); | ||||
|             Err(errors::ApiErrorResponse::DisputeStatusValidationFailed { | ||||
|             reason: format!( | ||||
|                 "This dispute cannot be accepted because the dispute is in {} stage and has {} status", | ||||
|                 dispute.dispute_stage, dispute.dispute_status | ||||
|             ), | ||||
|         }) | ||||
|         }, | ||||
|     )?; | ||||
|     let payment_intent = db | ||||
|         .find_payment_intent_by_payment_id_merchant_id( | ||||
|             &dispute.payment_id, | ||||
|             &merchant_account.merchant_id, | ||||
|             merchant_account.storage_scheme, | ||||
|         ) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|     let payment_attempt = db | ||||
|         .find_payment_attempt_by_attempt_id_merchant_id( | ||||
|             &dispute.attempt_id, | ||||
|             &merchant_account.merchant_id, | ||||
|             merchant_account.storage_scheme, | ||||
|         ) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|     let connector_data = api::ConnectorData::get_connector_by_name( | ||||
|         &state.conf.connectors, | ||||
|         &dispute.connector, | ||||
|         api::GetToken::Connector, | ||||
|     )?; | ||||
|     let connector_integration: services::BoxedConnectorIntegration< | ||||
|         '_, | ||||
|         api::Accept, | ||||
|         AcceptDisputeRequestData, | ||||
|         AcceptDisputeResponse, | ||||
|     > = connector_data.connector.get_connector_integration(); | ||||
|     let router_data = utils::construct_accept_dispute_router_data( | ||||
|         state, | ||||
|         &payment_intent, | ||||
|         &payment_attempt, | ||||
|         &merchant_account, | ||||
|         &dispute, | ||||
|     ) | ||||
|     .await?; | ||||
|     let response = services::execute_connector_processing_step( | ||||
|         state, | ||||
|         connector_integration, | ||||
|         &router_data, | ||||
|         payments::CallConnectorAction::Trigger, | ||||
|     ) | ||||
|     .await | ||||
|     .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|     .attach_printable("Failed while calling accept dispute connector api")?; | ||||
|     let accept_dispute_response = | ||||
|         response | ||||
|             .response | ||||
|             .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { | ||||
|                 code: err.code, | ||||
|                 message: err.message, | ||||
|                 connector: dispute.connector.clone(), | ||||
|                 status_code: err.status_code, | ||||
|                 reason: err.reason, | ||||
|             })?; | ||||
|     let update_dispute = storage_models::dispute::DisputeUpdate::StatusUpdate { | ||||
|         dispute_status: accept_dispute_response | ||||
|             .dispute_status | ||||
|             .clone() | ||||
|             .foreign_into(), | ||||
|         connector_status: accept_dispute_response.connector_status.clone(), | ||||
|     }; | ||||
|     let updated_dispute = db | ||||
|         .update_dispute(dispute.clone(), update_dispute) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable_lazy(|| { | ||||
|             format!("Unable to update dispute with dispute_id: {}", dispute_id) | ||||
|         })?; | ||||
|     let dispute_response = api_models::disputes::DisputeResponse::foreign_from(updated_dispute); | ||||
|     Ok(services::ApplicationResponse::Json(dispute_response)) | ||||
| } | ||||
|  | ||||
| #[instrument(skip(state))] | ||||
| pub async fn submit_evidence( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::MerchantAccount, | ||||
|     req: dispute_models::SubmitEvidenceRequest, | ||||
| ) -> RouterResponse<dispute_models::DisputeResponse> { | ||||
|     let db = &state.store; | ||||
|     let dispute = state | ||||
|         .store | ||||
|         .find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, &req.dispute_id) | ||||
|         .await | ||||
|         .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { | ||||
|             dispute_id: req.dispute_id.clone(), | ||||
|         })?; | ||||
|     let dispute_id = dispute.dispute_id.clone(); | ||||
|     common_utils::fp_utils::when( | ||||
|         !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute | ||||
|             && dispute.dispute_status == storage_enums::DisputeStatus::DisputeOpened), | ||||
|         || { | ||||
|             metrics::EVIDENCE_SUBMISSION_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC.add( | ||||
|                 &metrics::CONTEXT, | ||||
|                 1, | ||||
|                 &[], | ||||
|             ); | ||||
|             Err(errors::ApiErrorResponse::DisputeStatusValidationFailed { | ||||
|                 reason: format!( | ||||
|                 "Evidence cannot be submitted because the dispute is in {} stage and has {} status", | ||||
|                 dispute.dispute_stage, dispute.dispute_status | ||||
|             ), | ||||
|             }) | ||||
|         }, | ||||
|     )?; | ||||
|     let submit_evidence_request_data = | ||||
|         transformers::get_evidence_request_data(state, &merchant_account, req, &dispute).await?; | ||||
|     let payment_intent = db | ||||
|         .find_payment_intent_by_payment_id_merchant_id( | ||||
|             &dispute.payment_id, | ||||
|             &merchant_account.merchant_id, | ||||
|             merchant_account.storage_scheme, | ||||
|         ) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|     let payment_attempt = db | ||||
|         .find_payment_attempt_by_attempt_id_merchant_id( | ||||
|             &dispute.attempt_id, | ||||
|             &merchant_account.merchant_id, | ||||
|             merchant_account.storage_scheme, | ||||
|         ) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|     let connector_data = api::ConnectorData::get_connector_by_name( | ||||
|         &state.conf.connectors, | ||||
|         &dispute.connector, | ||||
|         api::GetToken::Connector, | ||||
|     )?; | ||||
|     let connector_integration: services::BoxedConnectorIntegration< | ||||
|         '_, | ||||
|         api::Evidence, | ||||
|         SubmitEvidenceRequestData, | ||||
|         SubmitEvidenceResponse, | ||||
|     > = connector_data.connector.get_connector_integration(); | ||||
|     let router_data = utils::construct_submit_evidence_router_data( | ||||
|         state, | ||||
|         &payment_intent, | ||||
|         &payment_attempt, | ||||
|         &merchant_account, | ||||
|         &dispute, | ||||
|         submit_evidence_request_data, | ||||
|     ) | ||||
|     .await?; | ||||
|     let response = services::execute_connector_processing_step( | ||||
|         state, | ||||
|         connector_integration, | ||||
|         &router_data, | ||||
|         payments::CallConnectorAction::Trigger, | ||||
|     ) | ||||
|     .await | ||||
|     .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|     .attach_printable("Failed while calling submit evidence connector api")?; | ||||
|     let submit_evidence_response = | ||||
|         response | ||||
|             .response | ||||
|             .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { | ||||
|                 code: err.code, | ||||
|                 message: err.message, | ||||
|                 connector: dispute.connector.clone(), | ||||
|                 status_code: err.status_code, | ||||
|                 reason: err.reason, | ||||
|             })?; | ||||
|     let update_dispute = storage_models::dispute::DisputeUpdate::StatusUpdate { | ||||
|         dispute_status: submit_evidence_response | ||||
|             .dispute_status | ||||
|             .clone() | ||||
|             .foreign_into(), | ||||
|         connector_status: submit_evidence_response.connector_status.clone(), | ||||
|     }; | ||||
|     let updated_dispute = db | ||||
|         .update_dispute(dispute.clone(), update_dispute) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable_lazy(|| { | ||||
|             format!("Unable to update dispute with dispute_id: {}", dispute_id) | ||||
|         })?; | ||||
|     let dispute_response = api_models::disputes::DisputeResponse::foreign_from(updated_dispute); | ||||
|     Ok(services::ApplicationResponse::Json(dispute_response)) | ||||
| } | ||||
|  | ||||
							
								
								
									
										106
									
								
								crates/router/src/core/disputes/transformers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								crates/router/src/core/disputes/transformers.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | ||||
| use common_utils::errors::CustomResult; | ||||
|  | ||||
| use crate::{ | ||||
|     core::{errors, files::helpers::retrieve_file_and_provider_file_id_from_file_id}, | ||||
|     routes::AppState, | ||||
|     types::SubmitEvidenceRequestData, | ||||
| }; | ||||
|  | ||||
| pub async fn get_evidence_request_data( | ||||
|     state: &AppState, | ||||
|     merchant_account: &storage_models::merchant_account::MerchantAccount, | ||||
|     evidence_request: api_models::disputes::SubmitEvidenceRequest, | ||||
|     dispute: &storage_models::dispute::Dispute, | ||||
| ) -> CustomResult<SubmitEvidenceRequestData, errors::ApiErrorResponse> { | ||||
|     let (cancellation_policy, cancellation_policy_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.cancellation_policy, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (customer_communication, customer_communication_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.customer_communication, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (customer_signature, customer_signature_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.customer_signature, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (receipt, receipt_provider_file_id) = retrieve_file_and_provider_file_id_from_file_id( | ||||
|         state, | ||||
|         evidence_request.receipt, | ||||
|         merchant_account, | ||||
|     ) | ||||
|     .await?; | ||||
|     let (refund_policy, refund_policy_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.refund_policy, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (service_documentation, service_documentation_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.service_documentation, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (shipping_documentation, shipping_documentation_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.shipping_documentation, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let (uncategorized_file, uncategorized_file_provider_file_id) = | ||||
|         retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             evidence_request.uncategorized_file, | ||||
|             merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     Ok(SubmitEvidenceRequestData { | ||||
|         dispute_id: dispute.dispute_id.clone(), | ||||
|         connector_dispute_id: dispute.connector_dispute_id.clone(), | ||||
|         access_activity_log: evidence_request.access_activity_log, | ||||
|         billing_address: evidence_request.billing_address, | ||||
|         cancellation_policy, | ||||
|         cancellation_policy_provider_file_id, | ||||
|         cancellation_policy_disclosure: evidence_request.cancellation_policy_disclosure, | ||||
|         cancellation_rebuttal: evidence_request.cancellation_rebuttal, | ||||
|         customer_communication, | ||||
|         customer_communication_provider_file_id, | ||||
|         customer_email_address: evidence_request.customer_email_address, | ||||
|         customer_name: evidence_request.customer_name, | ||||
|         customer_purchase_ip: evidence_request.customer_purchase_ip, | ||||
|         customer_signature, | ||||
|         customer_signature_provider_file_id, | ||||
|         product_description: evidence_request.product_description, | ||||
|         receipt, | ||||
|         receipt_provider_file_id, | ||||
|         refund_policy, | ||||
|         refund_policy_provider_file_id, | ||||
|         refund_policy_disclosure: evidence_request.refund_policy_disclosure, | ||||
|         refund_refusal_explanation: evidence_request.refund_refusal_explanation, | ||||
|         service_date: evidence_request.service_date, | ||||
|         service_documentation, | ||||
|         service_documentation_provider_file_id, | ||||
|         shipping_address: evidence_request.shipping_address, | ||||
|         shipping_carrier: evidence_request.shipping_carrier, | ||||
|         shipping_date: evidence_request.shipping_date, | ||||
|         shipping_documentation, | ||||
|         shipping_documentation_provider_file_id, | ||||
|         shipping_tracking_number: evidence_request.shipping_tracking_number, | ||||
|         uncategorized_file, | ||||
|         uncategorized_file_provider_file_id, | ||||
|         uncategorized_text: evidence_request.uncategorized_text, | ||||
|     }) | ||||
| } | ||||
| @ -293,6 +293,8 @@ pub enum ConnectorError { | ||||
|     MismatchedPaymentData, | ||||
|     #[error("Failed to parse Wallet token")] | ||||
|     InvalidWalletToken, | ||||
|     #[error("File Validation failed")] | ||||
|     FileValidationFailed { reason: String }, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, thiserror::Error)] | ||||
|  | ||||
| @ -160,10 +160,26 @@ pub enum ApiErrorResponse { | ||||
|     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::ObjectNotFound, code = "HE_04", message = "File does not exist in our records")] | ||||
|     FileNotFound, | ||||
|     #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "File not available")] | ||||
|     FileNotAvailable, | ||||
|     #[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Dispute status validation failed")] | ||||
|     DisputeStatusValidationFailed { reason: 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")] | ||||
|     InvalidCardIinLength, | ||||
|     #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "File validation failed")] | ||||
|     FileValidationFailed { reason: String }, | ||||
|     #[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "File not found / valid in the request")] | ||||
|     MissingFile, | ||||
|     #[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "Dispute id not found in the request")] | ||||
|     MissingDisputeId, | ||||
|     #[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "File purpose not found in the request or is invalid")] | ||||
|     MissingFilePurpose, | ||||
|     #[error(error_type = ErrorType::InvalidRequestError, code = "HE_04", message = "File content type not found / valid")] | ||||
|     MissingFileContentType, | ||||
| } | ||||
|  | ||||
| #[derive(Clone)] | ||||
| @ -251,12 +267,20 @@ impl actix_web::ResponseError for ApiErrorResponse { | ||||
|             | Self::AddressNotFound | ||||
|             | Self::NotSupported { .. } | ||||
|             | Self::FlowNotSupported { .. } | ||||
|             | Self::ApiKeyNotFound => StatusCode::BAD_REQUEST, // 400 | ||||
|             | Self::ApiKeyNotFound | ||||
|             | Self::DisputeStatusValidationFailed { .. } => StatusCode::BAD_REQUEST, // 400 | ||||
|             Self::DuplicateMerchantAccount | ||||
|             | Self::DuplicateMerchantConnectorAccount | ||||
|             | Self::DuplicatePaymentMethod | ||||
|             | Self::DuplicateMandate | ||||
|             | Self::DisputeNotFound { .. } => StatusCode::BAD_REQUEST, // 400 | ||||
|             | Self::DisputeNotFound { .. } | ||||
|             | Self::MissingFile | ||||
|             | Self::FileValidationFailed { .. } | ||||
|             | Self::MissingFileContentType | ||||
|             | Self::MissingFilePurpose | ||||
|             | Self::MissingDisputeId | ||||
|             | Self::FileNotFound | ||||
|             | Self::FileNotAvailable => StatusCode::BAD_REQUEST, // 400 | ||||
|             Self::ReturnUrlUnavailable => StatusCode::SERVICE_UNAVAILABLE, // 503 | ||||
|             Self::PaymentNotSucceeded => StatusCode::BAD_REQUEST,          // 400 | ||||
|             Self::NotImplemented { .. } => StatusCode::NOT_IMPLEMENTED,    // 501 | ||||
| @ -446,10 +470,34 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon | ||||
|             Self::InvalidCardIinLength  => AER::BadRequest(ApiError::new("HE", 3, "The provided card IIN length is invalid, please provide an IIN with 6 digits", None)), | ||||
|             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 | ||||
|             }, | ||||
|             } | ||||
|             Self::DisputeNotFound { .. } => { | ||||
|                 AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None)) | ||||
|             }, | ||||
|             } | ||||
|             Self::FileNotFound => { | ||||
|                 AER::NotFound(ApiError::new("HE", 2, "File does not exist in our records", None)) | ||||
|             } | ||||
|             Self::FileNotAvailable => { | ||||
|                 AER::NotFound(ApiError::new("HE", 2, "File not available", None)) | ||||
|             } | ||||
|             Self::DisputeStatusValidationFailed { .. } => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, "Dispute status validation failed", None)) | ||||
|             } | ||||
|             Self::FileValidationFailed { reason } => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, format!("File validation failed {reason}"), None)) | ||||
|             } | ||||
|             Self::MissingFile => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, "File not found in the request", None)) | ||||
|             } | ||||
|             Self::MissingFilePurpose => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, "File purpose not found in the request or is invalid", None)) | ||||
|             } | ||||
|             Self::MissingFileContentType => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, "File content type not found", None)) | ||||
|             } | ||||
|             Self::MissingDisputeId => { | ||||
|                 AER::BadRequest(ApiError::new("HE", 2, "Dispute id not found in the request", None)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
							
								
								
									
										120
									
								
								crates/router/src/core/files.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								crates/router/src/core/files.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| pub mod helpers; | ||||
| #[cfg(feature = "s3")] | ||||
| pub mod s3_utils; | ||||
|  | ||||
| #[cfg(not(feature = "s3"))] | ||||
| pub mod fs_utils; | ||||
|  | ||||
| use api_models::files; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
|  | ||||
| use super::errors::{self, RouterResponse}; | ||||
| use crate::{ | ||||
|     consts, | ||||
|     routes::AppState, | ||||
|     services::{self, ApplicationResponse}, | ||||
|     types::{api, storage, transformers::ForeignInto}, | ||||
| }; | ||||
|  | ||||
| pub async fn files_create_core( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::merchant_account::MerchantAccount, | ||||
|     create_file_request: api::CreateFileRequest, | ||||
| ) -> RouterResponse<files::CreateFileResponse> { | ||||
|     helpers::validate_file_upload(state, merchant_account.clone(), create_file_request.clone()) | ||||
|         .await?; | ||||
|     let file_id = common_utils::generate_id(consts::ID_LENGTH, "file"); | ||||
|     #[cfg(feature = "s3")] | ||||
|     let file_key = format!("{}/{}", merchant_account.merchant_id, file_id); | ||||
|     #[cfg(not(feature = "s3"))] | ||||
|     let file_key = format!("{}_{}", merchant_account.merchant_id, file_id); | ||||
|     let file_new = storage_models::file::FileMetadataNew { | ||||
|         file_id: file_id.clone(), | ||||
|         merchant_id: merchant_account.merchant_id.clone(), | ||||
|         file_name: create_file_request.file_name.clone(), | ||||
|         file_size: create_file_request.file_size, | ||||
|         file_type: create_file_request.file_type.to_string(), | ||||
|         provider_file_id: None, | ||||
|         file_upload_provider: None, | ||||
|         available: false, | ||||
|     }; | ||||
|     let file_metadata_object = state | ||||
|         .store | ||||
|         .insert_file_metadata(file_new) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Unable to insert file_metadata")?; | ||||
|     let (provider_file_id, file_upload_provider) = | ||||
|         helpers::upload_and_get_provider_provider_file_id( | ||||
|             state, | ||||
|             &merchant_account, | ||||
|             &create_file_request, | ||||
|             file_key.clone(), | ||||
|         ) | ||||
|         .await?; | ||||
|     //Update file metadata | ||||
|     let update_file_metadata = storage_models::file::FileMetadataUpdate::Update { | ||||
|         provider_file_id: Some(provider_file_id), | ||||
|         file_upload_provider: Some(file_upload_provider.foreign_into()), | ||||
|         available: true, | ||||
|     }; | ||||
|     state | ||||
|         .store | ||||
|         .update_file_metadata(file_metadata_object, update_file_metadata) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable_lazy(|| { | ||||
|             format!("Unable to update file_metadata with file_id: {}", file_id) | ||||
|         })?; | ||||
|     Ok(services::api::ApplicationResponse::Json( | ||||
|         files::CreateFileResponse { file_id }, | ||||
|     )) | ||||
| } | ||||
|  | ||||
| pub async fn files_delete_core( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::MerchantAccount, | ||||
|     req: api::FileId, | ||||
| ) -> RouterResponse<serde_json::Value> { | ||||
|     helpers::delete_file_using_file_id(state, req.file_id.clone(), &merchant_account).await?; | ||||
|     state | ||||
|         .store | ||||
|         .delete_file_metadata_by_merchant_id_file_id(&merchant_account.merchant_id, &req.file_id) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Unable to delete file_metadata")?; | ||||
|     Ok(ApplicationResponse::StatusOk) | ||||
| } | ||||
|  | ||||
| pub async fn files_retrieve_core( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::MerchantAccount, | ||||
|     req: api::FileId, | ||||
| ) -> RouterResponse<serde_json::Value> { | ||||
|     let file_metadata_object = state | ||||
|         .store | ||||
|         .find_file_metadata_by_merchant_id_file_id(&merchant_account.merchant_id, &req.file_id) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::FileNotFound) | ||||
|         .attach_printable("Unable to retrieve file_metadata")?; | ||||
|     let (received_data, _provider_file_id) = | ||||
|         helpers::retrieve_file_and_provider_file_id_from_file_id( | ||||
|             state, | ||||
|             Some(req.file_id), | ||||
|             &merchant_account, | ||||
|         ) | ||||
|         .await?; | ||||
|     let content_type = file_metadata_object | ||||
|         .file_type | ||||
|         .parse::<mime::Mime>() | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed to parse file content type")?; | ||||
|     Ok(ApplicationResponse::FileData(( | ||||
|         received_data | ||||
|             .ok_or(errors::ApiErrorResponse::FileNotAvailable) | ||||
|             .into_report() | ||||
|             .attach_printable("File data not found")?, | ||||
|         content_type, | ||||
|     ))) | ||||
| } | ||||
							
								
								
									
										57
									
								
								crates/router/src/core/files/fs_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								crates/router/src/core/files/fs_utils.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,57 @@ | ||||
| use std::{ | ||||
|     fs::{remove_file, File}, | ||||
|     io::{Read, Write}, | ||||
|     path::PathBuf, | ||||
| }; | ||||
|  | ||||
| use common_utils::errors::CustomResult; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
|  | ||||
| use crate::{core::errors, env}; | ||||
|  | ||||
| pub fn get_file_path(file_key: String) -> PathBuf { | ||||
|     let mut file_path = PathBuf::new(); | ||||
|     file_path.push(env::workspace_path()); | ||||
|     file_path.push("files"); | ||||
|     file_path.push(file_key); | ||||
|     file_path | ||||
| } | ||||
|  | ||||
| pub fn save_file_to_fs( | ||||
|     file_key: String, | ||||
|     file_data: Vec<u8>, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     let file_path = get_file_path(file_key); | ||||
|     let mut file = File::create(file_path) | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed to create file")?; | ||||
|     file.write_all(&file_data) | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed while writing into file")?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn delete_file_from_fs(file_key: String) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     let file_path = get_file_path(file_key); | ||||
|     remove_file(file_path) | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed while deleting the file")?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub fn retrieve_file_from_fs(file_key: String) -> CustomResult<Vec<u8>, errors::ApiErrorResponse> { | ||||
|     let mut received_data: Vec<u8> = Vec::new(); | ||||
|     let file_path = get_file_path(file_key); | ||||
|     let mut file = File::open(file_path) | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed while opening the file")?; | ||||
|     file.read_to_end(&mut received_data) | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Failed while reading the file")?; | ||||
|     Ok(received_data) | ||||
| } | ||||
							
								
								
									
										280
									
								
								crates/router/src/core/files/helpers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										280
									
								
								crates/router/src/core/files/helpers.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,280 @@ | ||||
| use actix_multipart::Field; | ||||
| use common_utils::errors::CustomResult; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use futures::TryStreamExt; | ||||
|  | ||||
| use crate::{ | ||||
|     core::{ | ||||
|         errors::{self, StorageErrorExt}, | ||||
|         files, payments, utils, | ||||
|     }, | ||||
|     routes::AppState, | ||||
|     services, | ||||
|     types::{self, api, storage}, | ||||
| }; | ||||
|  | ||||
| pub async fn read_string(field: &mut Field) -> Option<String> { | ||||
|     let bytes = field.try_next().await; | ||||
|     if let Ok(Some(bytes)) = bytes { | ||||
|         String::from_utf8(bytes.to_vec()).ok() | ||||
|     } else { | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn get_file_purpose(field: &mut Field) -> Option<api::FilePurpose> { | ||||
|     let purpose = read_string(field).await; | ||||
|     match purpose.as_deref() { | ||||
|         Some("dispute_evidence") => Some(api::FilePurpose::DisputeEvidence), | ||||
|         _ => None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn upload_file( | ||||
|     #[cfg(feature = "s3")] state: &AppState, | ||||
|     file_key: String, | ||||
|     file: Vec<u8>, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     #[cfg(feature = "s3")] | ||||
|     return files::s3_utils::upload_file_to_s3(state, file_key, file).await; | ||||
|     #[cfg(not(feature = "s3"))] | ||||
|     return files::fs_utils::save_file_to_fs(file_key, file); | ||||
| } | ||||
|  | ||||
| pub async fn delete_file( | ||||
|     #[cfg(feature = "s3")] state: &AppState, | ||||
|     file_key: String, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     #[cfg(feature = "s3")] | ||||
|     return files::s3_utils::delete_file_from_s3(state, file_key).await; | ||||
|     #[cfg(not(feature = "s3"))] | ||||
|     return files::fs_utils::delete_file_from_fs(file_key); | ||||
| } | ||||
|  | ||||
| pub async fn retrieve_file( | ||||
|     #[cfg(feature = "s3")] state: &AppState, | ||||
|     file_key: String, | ||||
| ) -> CustomResult<Vec<u8>, errors::ApiErrorResponse> { | ||||
|     #[cfg(feature = "s3")] | ||||
|     return files::s3_utils::retrieve_file_from_s3(state, file_key).await; | ||||
|     #[cfg(not(feature = "s3"))] | ||||
|     return files::fs_utils::retrieve_file_from_fs(file_key); | ||||
| } | ||||
|  | ||||
| pub async fn validate_file_upload( | ||||
|     state: &AppState, | ||||
|     merchant_account: storage::merchant_account::MerchantAccount, | ||||
|     create_file_request: api::CreateFileRequest, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     //File Validation based on the purpose of file upload | ||||
|     match create_file_request.purpose { | ||||
|         api::FilePurpose::DisputeEvidence => { | ||||
|             let dispute_id = &create_file_request | ||||
|                 .dispute_id | ||||
|                 .ok_or(errors::ApiErrorResponse::MissingDisputeId)?; | ||||
|             let dispute = state | ||||
|                 .store | ||||
|                 .find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, dispute_id) | ||||
|                 .await | ||||
|                 .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { | ||||
|                     dispute_id: dispute_id.to_string(), | ||||
|                 })?; | ||||
|             let connector_data = api::ConnectorData::get_connector_by_name( | ||||
|                 &state.conf.connectors, | ||||
|                 &dispute.connector, | ||||
|                 api::GetToken::Connector, | ||||
|             )?; | ||||
|             let validation = connector_data.connector.validate_file_upload( | ||||
|                 create_file_request.purpose, | ||||
|                 create_file_request.file_size, | ||||
|                 create_file_request.file_type.clone(), | ||||
|             ); | ||||
|             match validation { | ||||
|                 Ok(()) => Ok(()), | ||||
|                 Err(err) => match err.current_context() { | ||||
|                     errors::ConnectorError::FileValidationFailed { reason } => { | ||||
|                         Err(errors::ApiErrorResponse::FileValidationFailed { | ||||
|                             reason: reason.to_string(), | ||||
|                         } | ||||
|                         .into()) | ||||
|                     } | ||||
|                     //We are using parent error and ignoring this | ||||
|                     _error => Err(err | ||||
|                         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|                         .attach_printable("File validation failed"))?, | ||||
|                 }, | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn delete_file_using_file_id( | ||||
|     state: &AppState, | ||||
|     file_key: String, | ||||
|     merchant_account: &storage_models::merchant_account::MerchantAccount, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     let file_metadata_object = state | ||||
|         .store | ||||
|         .find_file_metadata_by_merchant_id_file_id(&merchant_account.merchant_id, &file_key) | ||||
|         .await | ||||
|         .change_context(errors::ApiErrorResponse::FileNotFound)?; | ||||
|     let (provider, provider_file_id) = match ( | ||||
|         file_metadata_object.file_upload_provider, | ||||
|         file_metadata_object.provider_file_id, | ||||
|         file_metadata_object.available, | ||||
|     ) { | ||||
|         (Some(provider), Some(provider_file_id), true) => (provider, provider_file_id), | ||||
|         _ => Err(errors::ApiErrorResponse::FileNotAvailable) | ||||
|             .into_report() | ||||
|             .attach_printable("File not available")?, | ||||
|     }; | ||||
|     match provider { | ||||
|         storage_models::enums::FileUploadProvider::Router => { | ||||
|             delete_file( | ||||
|                 #[cfg(feature = "s3")] | ||||
|                 state, | ||||
|                 provider_file_id, | ||||
|             ) | ||||
|             .await | ||||
|         } | ||||
|         _ => Err(errors::ApiErrorResponse::NotSupported { | ||||
|             message: "Not Supported if provider is not Router".to_owned(), | ||||
|         } | ||||
|         .into()), | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub async fn retrieve_file_and_provider_file_id_from_file_id( | ||||
|     state: &AppState, | ||||
|     file_id: Option<String>, | ||||
|     merchant_account: &storage_models::merchant_account::MerchantAccount, | ||||
| ) -> CustomResult<(Option<Vec<u8>>, Option<String>), errors::ApiErrorResponse> { | ||||
|     match file_id { | ||||
|         None => Ok((None, None)), | ||||
|         Some(file_key) => { | ||||
|             let file_metadata_object = state | ||||
|                 .store | ||||
|                 .find_file_metadata_by_merchant_id_file_id(&merchant_account.merchant_id, &file_key) | ||||
|                 .await | ||||
|                 .change_context(errors::ApiErrorResponse::FileNotFound)?; | ||||
|             let (provider, provider_file_id) = match ( | ||||
|                 file_metadata_object.file_upload_provider, | ||||
|                 file_metadata_object.provider_file_id, | ||||
|             ) { | ||||
|                 (Some(provider), Some(provider_file_id)) => (provider, provider_file_id), | ||||
|                 _ => Err(errors::ApiErrorResponse::FileNotFound)?, | ||||
|             }; | ||||
|             match provider { | ||||
|                 storage_models::enums::FileUploadProvider::Router => Ok(( | ||||
|                     Some( | ||||
|                         retrieve_file( | ||||
|                             #[cfg(feature = "s3")] | ||||
|                             state, | ||||
|                             provider_file_id.clone(), | ||||
|                         ) | ||||
|                         .await?, | ||||
|                     ), | ||||
|                     Some(provider_file_id), | ||||
|                 )), | ||||
|                 //TODO: Handle Retrieve for other providers | ||||
|                 _ => Ok((None, Some(provider_file_id))), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| //Upload file to connector if it supports / store it in S3 and return file_upload_provider, provider_file_id accordingly | ||||
| pub async fn upload_and_get_provider_provider_file_id( | ||||
|     state: &AppState, | ||||
|     merchant_account: &storage::merchant_account::MerchantAccount, | ||||
|     create_file_request: &api::CreateFileRequest, | ||||
|     file_key: String, | ||||
| ) -> CustomResult<(String, api::FileUploadProvider), errors::ApiErrorResponse> { | ||||
|     match create_file_request.purpose { | ||||
|         api::FilePurpose::DisputeEvidence => { | ||||
|             let dispute_id = create_file_request | ||||
|                 .dispute_id | ||||
|                 .clone() | ||||
|                 .ok_or(errors::ApiErrorResponse::MissingDisputeId)?; | ||||
|             let dispute = state | ||||
|                 .store | ||||
|                 .find_dispute_by_merchant_id_dispute_id(&merchant_account.merchant_id, &dispute_id) | ||||
|                 .await | ||||
|                 .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id })?; | ||||
|             let connector_data = api::ConnectorData::get_connector_by_name( | ||||
|                 &state.conf.connectors, | ||||
|                 &dispute.connector, | ||||
|                 api::GetToken::Connector, | ||||
|             )?; | ||||
|             if connector_data.connector_name.supports_file_storage_module() { | ||||
|                 let payment_intent = state | ||||
|                     .store | ||||
|                     .find_payment_intent_by_payment_id_merchant_id( | ||||
|                         &dispute.payment_id, | ||||
|                         &merchant_account.merchant_id, | ||||
|                         merchant_account.storage_scheme, | ||||
|                     ) | ||||
|                     .await | ||||
|                     .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|                 let payment_attempt = state | ||||
|                     .store | ||||
|                     .find_payment_attempt_by_attempt_id_merchant_id( | ||||
|                         &dispute.attempt_id, | ||||
|                         &merchant_account.merchant_id, | ||||
|                         merchant_account.storage_scheme, | ||||
|                     ) | ||||
|                     .await | ||||
|                     .change_context(errors::ApiErrorResponse::PaymentNotFound)?; | ||||
|                 let connector_integration: services::BoxedConnectorIntegration< | ||||
|                     '_, | ||||
|                     api::Upload, | ||||
|                     types::UploadFileRequestData, | ||||
|                     types::UploadFileResponse, | ||||
|                 > = connector_data.connector.get_connector_integration(); | ||||
|                 let router_data = utils::construct_upload_file_router_data( | ||||
|                     state, | ||||
|                     &payment_intent, | ||||
|                     &payment_attempt, | ||||
|                     merchant_account, | ||||
|                     create_file_request, | ||||
|                     &dispute.connector, | ||||
|                     file_key, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|                 .attach_printable("Failed constructing the upload file router data")?; | ||||
|                 let response = services::execute_connector_processing_step( | ||||
|                     state, | ||||
|                     connector_integration, | ||||
|                     &router_data, | ||||
|                     payments::CallConnectorAction::Trigger, | ||||
|                 ) | ||||
|                 .await | ||||
|                 .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|                 .attach_printable("Failed while calling upload file connector api")?; | ||||
|                 let upload_file_response = response.response.map_err(|err| { | ||||
|                     errors::ApiErrorResponse::ExternalConnectorError { | ||||
|                         code: err.code, | ||||
|                         message: err.message, | ||||
|                         connector: dispute.connector.clone(), | ||||
|                         status_code: err.status_code, | ||||
|                         reason: err.reason, | ||||
|                     } | ||||
|                 })?; | ||||
|                 Ok(( | ||||
|                     upload_file_response.provider_file_id, | ||||
|                     api::FileUploadProvider::try_from(&connector_data.connector_name)?, | ||||
|                 )) | ||||
|             } else { | ||||
|                 upload_file( | ||||
|                     #[cfg(feature = "s3")] | ||||
|                     state, | ||||
|                     file_key.clone(), | ||||
|                     create_file_request.file.clone(), | ||||
|                 ) | ||||
|                 .await?; | ||||
|                 Ok((file_key, api::FileUploadProvider::Router)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										87
									
								
								crates/router/src/core/files/s3_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								crates/router/src/core/files/s3_utils.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | ||||
| use aws_config::{self, meta::region::RegionProviderChain}; | ||||
| use aws_sdk_s3::{config::Region, Client}; | ||||
| use common_utils::errors::CustomResult; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use futures::TryStreamExt; | ||||
|  | ||||
| use crate::{core::errors, routes}; | ||||
|  | ||||
| async fn get_aws_client(state: &routes::AppState) -> Client { | ||||
|     let region_provider = | ||||
|         RegionProviderChain::first_try(Region::new(state.conf.file_upload_config.region.clone())); | ||||
|     let sdk_config = aws_config::from_env().region(region_provider).load().await; | ||||
|     Client::new(&sdk_config) | ||||
| } | ||||
|  | ||||
| pub async fn upload_file_to_s3( | ||||
|     state: &routes::AppState, | ||||
|     file_key: String, | ||||
|     file: Vec<u8>, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     let client = get_aws_client(state).await; | ||||
|     let bucket_name = &state.conf.file_upload_config.bucket_name; | ||||
|     // Upload file to S3 | ||||
|     let upload_res = client | ||||
|         .put_object() | ||||
|         .bucket(bucket_name) | ||||
|         .key(file_key.clone()) | ||||
|         .body(file.into()) | ||||
|         .send() | ||||
|         .await; | ||||
|     upload_res | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("File upload to S3 failed")?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub async fn delete_file_from_s3( | ||||
|     state: &routes::AppState, | ||||
|     file_key: String, | ||||
| ) -> CustomResult<(), errors::ApiErrorResponse> { | ||||
|     let client = get_aws_client(state).await; | ||||
|     let bucket_name = &state.conf.file_upload_config.bucket_name; | ||||
|     // Delete file from S3 | ||||
|     let delete_res = client | ||||
|         .delete_object() | ||||
|         .bucket(bucket_name) | ||||
|         .key(file_key) | ||||
|         .send() | ||||
|         .await; | ||||
|     delete_res | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("File delete from S3 failed")?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub async fn retrieve_file_from_s3( | ||||
|     state: &routes::AppState, | ||||
|     file_key: String, | ||||
| ) -> CustomResult<Vec<u8>, errors::ApiErrorResponse> { | ||||
|     let client = get_aws_client(state).await; | ||||
|     let bucket_name = &state.conf.file_upload_config.bucket_name; | ||||
|     // Get file data from S3 | ||||
|     let get_res = client | ||||
|         .get_object() | ||||
|         .bucket(bucket_name) | ||||
|         .key(file_key) | ||||
|         .send() | ||||
|         .await; | ||||
|     let mut object = get_res | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("File retrieve from S3 failed")?; | ||||
|     let mut received_data: Vec<u8> = Vec::new(); | ||||
|     while let Some(bytes) = object | ||||
|         .body | ||||
|         .try_next() | ||||
|         .await | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("Invalid file data received from S3")? | ||||
|     { | ||||
|         received_data.extend_from_slice(&bytes); // Collect the bytes in the Vec | ||||
|     } | ||||
|     Ok(received_data) | ||||
| } | ||||
| @ -18,3 +18,11 @@ counter_metric!( | ||||
|     INCOMING_DISPUTE_WEBHOOK_MERCHANT_NOTIFIED_METRIC, | ||||
|     GLOBAL_METER | ||||
| ); // No. of incoming dispute webhooks which are notified to merchant | ||||
| counter_metric!( | ||||
|     ACCEPT_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC, | ||||
|     GLOBAL_METER | ||||
| ); //No. of status validation fialures while accpeting a dispute | ||||
| counter_metric!( | ||||
|     EVIDENCE_SUBMISSION_DISPUTE_STATUS_VALIDATION_FAILURE_METRIC, | ||||
|     GLOBAL_METER | ||||
| ); //No. of status validation fialures while submitting evidence for a dispute | ||||
|  | ||||
| @ -190,3 +190,142 @@ default_imp_for_connector_request_id!( | ||||
|     connector::Worldline, | ||||
|     connector::Worldpay | ||||
| ); | ||||
|  | ||||
| macro_rules! default_imp_for_accept_dispute{ | ||||
|     ($($path:ident::$connector:ident),*)=> { | ||||
|         $( | ||||
|             impl api::Dispute for $path::$connector {} | ||||
|             impl api::AcceptDispute for $path::$connector {} | ||||
|             impl | ||||
|                 services::ConnectorIntegration< | ||||
|                 api::Accept, | ||||
|                 types::AcceptDisputeRequestData, | ||||
|                 types::AcceptDisputeResponse, | ||||
|             > for $path::$connector | ||||
|             {} | ||||
|     )* | ||||
|     }; | ||||
| } | ||||
|  | ||||
| default_imp_for_accept_dispute!( | ||||
|     connector::Aci, | ||||
|     connector::Adyen, | ||||
|     connector::Airwallex, | ||||
|     connector::Authorizedotnet, | ||||
|     connector::Bambora, | ||||
|     connector::Bluesnap, | ||||
|     connector::Braintree, | ||||
|     connector::Coinbase, | ||||
|     connector::Cybersource, | ||||
|     connector::Dlocal, | ||||
|     connector::Fiserv, | ||||
|     connector::Forte, | ||||
|     connector::Globalpay, | ||||
|     connector::Klarna, | ||||
|     connector::Mollie, | ||||
|     connector::Multisafepay, | ||||
|     connector::Nexinets, | ||||
|     connector::Nuvei, | ||||
|     connector::Payeezy, | ||||
|     connector::Paypal, | ||||
|     connector::Payu, | ||||
|     connector::Rapyd, | ||||
|     connector::Shift4, | ||||
|     connector::Stripe, | ||||
|     connector::Trustpay, | ||||
|     connector::Opennode, | ||||
|     connector::Worldline, | ||||
|     connector::Worldpay | ||||
| ); | ||||
|  | ||||
| macro_rules! default_imp_for_file_upload{ | ||||
|     ($($path:ident::$connector:ident),*)=> { | ||||
|         $( | ||||
|             impl api::FileUpload for $path::$connector {} | ||||
|             impl api::UploadFile for $path::$connector {} | ||||
|             impl | ||||
|                 services::ConnectorIntegration< | ||||
|                 api::Upload, | ||||
|                 types::UploadFileRequestData, | ||||
|                 types::UploadFileResponse, | ||||
|             > for $path::$connector | ||||
|             {} | ||||
|     )* | ||||
|     }; | ||||
| } | ||||
|  | ||||
| default_imp_for_file_upload!( | ||||
|     connector::Aci, | ||||
|     connector::Adyen, | ||||
|     connector::Airwallex, | ||||
|     connector::Authorizedotnet, | ||||
|     connector::Bambora, | ||||
|     connector::Bluesnap, | ||||
|     connector::Braintree, | ||||
|     connector::Coinbase, | ||||
|     connector::Cybersource, | ||||
|     connector::Dlocal, | ||||
|     connector::Fiserv, | ||||
|     connector::Forte, | ||||
|     connector::Globalpay, | ||||
|     connector::Klarna, | ||||
|     connector::Mollie, | ||||
|     connector::Multisafepay, | ||||
|     connector::Nexinets, | ||||
|     connector::Nuvei, | ||||
|     connector::Payeezy, | ||||
|     connector::Paypal, | ||||
|     connector::Payu, | ||||
|     connector::Rapyd, | ||||
|     connector::Shift4, | ||||
|     connector::Trustpay, | ||||
|     connector::Opennode, | ||||
|     connector::Worldline, | ||||
|     connector::Worldpay | ||||
| ); | ||||
|  | ||||
| macro_rules! default_imp_for_submit_evidence{ | ||||
|     ($($path:ident::$connector:ident),*)=> { | ||||
|         $( | ||||
|             impl api::SubmitEvidence for $path::$connector {} | ||||
|             impl | ||||
|                 services::ConnectorIntegration< | ||||
|                 api::Evidence, | ||||
|                 types::SubmitEvidenceRequestData, | ||||
|                 types::SubmitEvidenceResponse, | ||||
|             > for $path::$connector | ||||
|             {} | ||||
|     )* | ||||
|     }; | ||||
| } | ||||
|  | ||||
| default_imp_for_submit_evidence!( | ||||
|     connector::Aci, | ||||
|     connector::Adyen, | ||||
|     connector::Airwallex, | ||||
|     connector::Authorizedotnet, | ||||
|     connector::Bambora, | ||||
|     connector::Bluesnap, | ||||
|     connector::Braintree, | ||||
|     connector::Checkout, | ||||
|     connector::Cybersource, | ||||
|     connector::Coinbase, | ||||
|     connector::Dlocal, | ||||
|     connector::Fiserv, | ||||
|     connector::Forte, | ||||
|     connector::Globalpay, | ||||
|     connector::Klarna, | ||||
|     connector::Mollie, | ||||
|     connector::Multisafepay, | ||||
|     connector::Nexinets, | ||||
|     connector::Nuvei, | ||||
|     connector::Payeezy, | ||||
|     connector::Paypal, | ||||
|     connector::Payu, | ||||
|     connector::Rapyd, | ||||
|     connector::Shift4, | ||||
|     connector::Trustpay, | ||||
|     connector::Opennode, | ||||
|     connector::Worldline, | ||||
|     connector::Worldpay | ||||
| ); | ||||
|  | ||||
| @ -222,3 +222,181 @@ pub fn validate_dispute_stage_and_dispute_status( | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
|  | ||||
| #[instrument(skip_all)] | ||||
| pub async fn construct_accept_dispute_router_data<'a>( | ||||
|     state: &'a AppState, | ||||
|     payment_intent: &'a storage::PaymentIntent, | ||||
|     payment_attempt: &storage::PaymentAttempt, | ||||
|     merchant_account: &storage::MerchantAccount, | ||||
|     dispute: &storage::Dispute, | ||||
| ) -> RouterResult<types::AcceptDisputeRouterData> { | ||||
|     let db = &*state.store; | ||||
|     let connector_id = &dispute.connector; | ||||
|     let connector_label = helpers::get_connector_label( | ||||
|         payment_intent.business_country, | ||||
|         &payment_intent.business_label, | ||||
|         payment_attempt.business_sub_label.as_ref(), | ||||
|         connector_id, | ||||
|     ); | ||||
|     let merchant_connector_account = helpers::get_merchant_connector_account( | ||||
|         db, | ||||
|         merchant_account.merchant_id.as_str(), | ||||
|         &connector_label, | ||||
|         None, | ||||
|     ) | ||||
|     .await?; | ||||
|     let auth_type: types::ConnectorAuthType = merchant_connector_account | ||||
|         .get_connector_account_details() | ||||
|         .parse_value("ConnectorAuthType") | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError)?; | ||||
|     let payment_method = payment_attempt | ||||
|         .payment_method | ||||
|         .get_required_value("payment_method_type")?; | ||||
|     let router_data = types::RouterData { | ||||
|         flow: PhantomData, | ||||
|         merchant_id: merchant_account.merchant_id.clone(), | ||||
|         connector: connector_id.to_string(), | ||||
|         payment_id: payment_attempt.payment_id.clone(), | ||||
|         attempt_id: payment_attempt.attempt_id.clone(), | ||||
|         status: payment_attempt.status, | ||||
|         payment_method, | ||||
|         connector_auth_type: auth_type, | ||||
|         description: None, | ||||
|         return_url: payment_intent.return_url.clone(), | ||||
|         payment_method_id: payment_attempt.payment_method_id.clone(), | ||||
|         address: PaymentAddress::default(), | ||||
|         auth_type: payment_attempt.authentication_type.unwrap_or_default(), | ||||
|         connector_meta_data: merchant_connector_account.get_metadata(), | ||||
|         amount_captured: payment_intent.amount_captured, | ||||
|         request: types::AcceptDisputeRequestData { | ||||
|             dispute_id: dispute.dispute_id.clone(), | ||||
|             connector_dispute_id: dispute.connector_dispute_id.clone(), | ||||
|         }, | ||||
|         response: Err(types::ErrorResponse::default()), | ||||
|         access_token: None, | ||||
|         session_token: None, | ||||
|         reference_id: None, | ||||
|         payment_method_token: None, | ||||
|     }; | ||||
|     Ok(router_data) | ||||
| } | ||||
|  | ||||
| #[instrument(skip_all)] | ||||
| pub async fn construct_submit_evidence_router_data<'a>( | ||||
|     state: &'a AppState, | ||||
|     payment_intent: &'a storage::PaymentIntent, | ||||
|     payment_attempt: &storage::PaymentAttempt, | ||||
|     merchant_account: &storage::MerchantAccount, | ||||
|     dispute: &storage::Dispute, | ||||
|     submit_evidence_request_data: types::SubmitEvidenceRequestData, | ||||
| ) -> RouterResult<types::SubmitEvidenceRouterData> { | ||||
|     let db = &*state.store; | ||||
|     let connector_id = &dispute.connector; | ||||
|     let connector_label = helpers::get_connector_label( | ||||
|         payment_intent.business_country, | ||||
|         &payment_intent.business_label, | ||||
|         payment_attempt.business_sub_label.as_ref(), | ||||
|         connector_id, | ||||
|     ); | ||||
|     let merchant_connector_account = helpers::get_merchant_connector_account( | ||||
|         db, | ||||
|         merchant_account.merchant_id.as_str(), | ||||
|         &connector_label, | ||||
|         None, | ||||
|     ) | ||||
|     .await?; | ||||
|     let auth_type: types::ConnectorAuthType = merchant_connector_account | ||||
|         .get_connector_account_details() | ||||
|         .parse_value("ConnectorAuthType") | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError)?; | ||||
|     let payment_method = payment_attempt | ||||
|         .payment_method | ||||
|         .get_required_value("payment_method_type")?; | ||||
|     let router_data = types::RouterData { | ||||
|         flow: PhantomData, | ||||
|         merchant_id: merchant_account.merchant_id.clone(), | ||||
|         connector: connector_id.to_string(), | ||||
|         payment_id: payment_attempt.payment_id.clone(), | ||||
|         attempt_id: payment_attempt.attempt_id.clone(), | ||||
|         status: payment_attempt.status, | ||||
|         payment_method, | ||||
|         connector_auth_type: auth_type, | ||||
|         description: None, | ||||
|         return_url: payment_intent.return_url.clone(), | ||||
|         payment_method_id: payment_attempt.payment_method_id.clone(), | ||||
|         address: PaymentAddress::default(), | ||||
|         auth_type: payment_attempt.authentication_type.unwrap_or_default(), | ||||
|         connector_meta_data: merchant_connector_account.get_metadata(), | ||||
|         amount_captured: payment_intent.amount_captured, | ||||
|         request: submit_evidence_request_data, | ||||
|         response: Err(types::ErrorResponse::default()), | ||||
|         access_token: None, | ||||
|         session_token: None, | ||||
|         reference_id: None, | ||||
|         payment_method_token: None, | ||||
|     }; | ||||
|     Ok(router_data) | ||||
| } | ||||
|  | ||||
| #[instrument(skip_all)] | ||||
| pub async fn construct_upload_file_router_data<'a>( | ||||
|     state: &'a AppState, | ||||
|     payment_intent: &'a storage::PaymentIntent, | ||||
|     payment_attempt: &storage::PaymentAttempt, | ||||
|     merchant_account: &storage::MerchantAccount, | ||||
|     create_file_request: &types::api::CreateFileRequest, | ||||
|     connector_id: &str, | ||||
|     file_key: String, | ||||
| ) -> RouterResult<types::UploadFileRouterData> { | ||||
|     let db = &*state.store; | ||||
|     let connector_label = helpers::get_connector_label( | ||||
|         payment_intent.business_country, | ||||
|         &payment_intent.business_label, | ||||
|         payment_attempt.business_sub_label.as_ref(), | ||||
|         connector_id, | ||||
|     ); | ||||
|     let merchant_connector_account = helpers::get_merchant_connector_account( | ||||
|         db, | ||||
|         merchant_account.merchant_id.as_str(), | ||||
|         &connector_label, | ||||
|         None, | ||||
|     ) | ||||
|     .await?; | ||||
|     let auth_type: types::ConnectorAuthType = merchant_connector_account | ||||
|         .get_connector_account_details() | ||||
|         .parse_value("ConnectorAuthType") | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError)?; | ||||
|     let payment_method = payment_attempt | ||||
|         .payment_method | ||||
|         .get_required_value("payment_method_type")?; | ||||
|     let router_data = types::RouterData { | ||||
|         flow: PhantomData, | ||||
|         merchant_id: merchant_account.merchant_id.clone(), | ||||
|         connector: connector_id.to_string(), | ||||
|         payment_id: payment_attempt.payment_id.clone(), | ||||
|         attempt_id: payment_attempt.attempt_id.clone(), | ||||
|         status: payment_attempt.status, | ||||
|         payment_method, | ||||
|         connector_auth_type: auth_type, | ||||
|         description: None, | ||||
|         return_url: payment_intent.return_url.clone(), | ||||
|         payment_method_id: payment_attempt.payment_method_id.clone(), | ||||
|         address: PaymentAddress::default(), | ||||
|         auth_type: payment_attempt.authentication_type.unwrap_or_default(), | ||||
|         connector_meta_data: merchant_connector_account.get_metadata(), | ||||
|         amount_captured: payment_intent.amount_captured, | ||||
|         request: types::UploadFileRequestData { | ||||
|             file_key, | ||||
|             file: create_file_request.file.clone(), | ||||
|             file_type: create_file_request.file_type.clone(), | ||||
|             file_size: create_file_request.file_size, | ||||
|         }, | ||||
|         response: Err(types::ErrorResponse::default()), | ||||
|         access_token: None, | ||||
|         session_token: None, | ||||
|         reference_id: None, | ||||
|         payment_method_token: None, | ||||
|     }; | ||||
|     Ok(router_data) | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ pub mod customers; | ||||
| pub mod dispute; | ||||
| pub mod ephemeral_key; | ||||
| pub mod events; | ||||
| pub mod file; | ||||
| pub mod locker_mock_up; | ||||
| pub mod mandate; | ||||
| pub mod merchant_account; | ||||
| @ -46,6 +47,7 @@ pub trait StorageInterface: | ||||
|     + dispute::DisputeInterface | ||||
|     + ephemeral_key::EphemeralKeyInterface | ||||
|     + events::EventInterface | ||||
|     + file::FileMetadataInterface | ||||
|     + locker_mock_up::LockerMockUpInterface | ||||
|     + mandate::MandateInterface | ||||
|     + merchant_account::MerchantAccountInterface | ||||
|  | ||||
							
								
								
									
										119
									
								
								crates/router/src/db/file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								crates/router/src/db/file.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| use error_stack::IntoReport; | ||||
|  | ||||
| use super::{MockDb, Store}; | ||||
| use crate::{ | ||||
|     connection, | ||||
|     core::errors::{self, CustomResult}, | ||||
|     types::storage, | ||||
| }; | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| pub trait FileMetadataInterface { | ||||
|     async fn insert_file_metadata( | ||||
|         &self, | ||||
|         file: storage::FileMetadataNew, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError>; | ||||
|  | ||||
|     async fn find_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|         file_id: &str, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError>; | ||||
|  | ||||
|     async fn delete_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|         file_id: &str, | ||||
|     ) -> CustomResult<bool, errors::StorageError>; | ||||
|  | ||||
|     async fn update_file_metadata( | ||||
|         &self, | ||||
|         this: storage::FileMetadata, | ||||
|         file_metadata: storage::FileMetadataUpdate, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError>; | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl FileMetadataInterface for Store { | ||||
|     async fn insert_file_metadata( | ||||
|         &self, | ||||
|         file: storage::FileMetadataNew, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         let conn = connection::pg_connection_write(self).await?; | ||||
|         file.insert(&conn).await.map_err(Into::into).into_report() | ||||
|     } | ||||
|  | ||||
|     async fn find_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|         file_id: &str, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         let conn = connection::pg_connection_read(self).await?; | ||||
|         storage::FileMetadata::find_by_merchant_id_file_id(&conn, merchant_id, file_id) | ||||
|             .await | ||||
|             .map_err(Into::into) | ||||
|             .into_report() | ||||
|     } | ||||
|  | ||||
|     async fn delete_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         merchant_id: &str, | ||||
|         file_id: &str, | ||||
|     ) -> CustomResult<bool, errors::StorageError> { | ||||
|         let conn = connection::pg_connection_write(self).await?; | ||||
|         storage::FileMetadata::delete_by_merchant_id_file_id(&conn, merchant_id, file_id) | ||||
|             .await | ||||
|             .map_err(Into::into) | ||||
|             .into_report() | ||||
|     } | ||||
|  | ||||
|     async fn update_file_metadata( | ||||
|         &self, | ||||
|         this: storage::FileMetadata, | ||||
|         file_metadata: storage::FileMetadataUpdate, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         let conn = connection::pg_connection_write(self).await?; | ||||
|         this.update(&conn, file_metadata) | ||||
|             .await | ||||
|             .map_err(Into::into) | ||||
|             .into_report() | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[async_trait::async_trait] | ||||
| impl FileMetadataInterface for MockDb { | ||||
|     async fn insert_file_metadata( | ||||
|         &self, | ||||
|         _file: storage::FileMetadataNew, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         // TODO: Implement function for `MockDb` | ||||
|         Err(errors::StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn find_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         _merchant_id: &str, | ||||
|         _file_id: &str, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         // TODO: Implement function for `MockDb` | ||||
|         Err(errors::StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn delete_file_metadata_by_merchant_id_file_id( | ||||
|         &self, | ||||
|         _merchant_id: &str, | ||||
|         _file_id: &str, | ||||
|     ) -> CustomResult<bool, errors::StorageError> { | ||||
|         // TODO: Implement function for `MockDb` | ||||
|         Err(errors::StorageError::MockDbError)? | ||||
|     } | ||||
|  | ||||
|     async fn update_file_metadata( | ||||
|         &self, | ||||
|         _this: storage::FileMetadata, | ||||
|         _file_metadata: storage::FileMetadataUpdate, | ||||
|     ) -> CustomResult<storage::FileMetadata, errors::StorageError> { | ||||
|         // TODO: Implement function for `MockDb` | ||||
|         Err(errors::StorageError::MockDbError)? | ||||
|     } | ||||
| } | ||||
| @ -120,6 +120,7 @@ pub fn mk_app( | ||||
|         server_app = server_app | ||||
|             .service(routes::MerchantAccount::server(state.clone())) | ||||
|             .service(routes::ApiKeys::server(state.clone())) | ||||
|             .service(routes::Files::server(state.clone())) | ||||
|             .service(routes::Disputes::server(state.clone())); | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -6,6 +6,7 @@ pub mod configs; | ||||
| pub mod customers; | ||||
| pub mod disputes; | ||||
| pub mod ephemeral_key; | ||||
| pub mod files; | ||||
| pub mod health; | ||||
| pub mod mandates; | ||||
| pub mod metrics; | ||||
| @ -16,7 +17,7 @@ pub mod refunds; | ||||
| pub mod webhooks; | ||||
|  | ||||
| pub use self::app::{ | ||||
|     ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Health, Mandates, | ||||
|     ApiKeys, AppState, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Health, Mandates, | ||||
|     MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Payouts, Refunds, | ||||
|     Webhooks, | ||||
| }; | ||||
|  | ||||
| @ -3,7 +3,7 @@ use tokio::sync::oneshot; | ||||
|  | ||||
| use super::health::*; | ||||
| #[cfg(feature = "olap")] | ||||
| use super::{admin::*, api_keys::*, disputes::*}; | ||||
| use super::{admin::*, api_keys::*, disputes::*, files::*}; | ||||
| #[cfg(any(feature = "olap", feature = "oltp"))] | ||||
| use super::{configs::*, customers::*, mandates::*, payments::*, payouts::*, refunds::*}; | ||||
| #[cfg(feature = "oltp")] | ||||
| @ -393,6 +393,8 @@ impl Disputes { | ||||
|         web::scope("/disputes") | ||||
|             .app_data(web::Data::new(state)) | ||||
|             .service(web::resource("/list").route(web::get().to(retrieve_disputes_list))) | ||||
|             .service(web::resource("/accept/{dispute_id}").route(web::post().to(accept_dispute))) | ||||
|             .service(web::resource("/evidence").route(web::post().to(submit_dispute_evidence))) | ||||
|             .service(web::resource("/{dispute_id}").route(web::get().to(retrieve_dispute))) | ||||
|     } | ||||
| } | ||||
| @ -406,3 +408,19 @@ impl Cards { | ||||
|             .service(web::resource("/{bin}").route(web::get().to(card_iin_info))) | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct Files; | ||||
|  | ||||
| #[cfg(feature = "olap")] | ||||
| impl Files { | ||||
|     pub fn server(state: AppState) -> Scope { | ||||
|         web::scope("/files") | ||||
|             .app_data(web::Data::new(state)) | ||||
|             .service(web::resource("").route(web::post().to(files_create))) | ||||
|             .service( | ||||
|                 web::resource("/{file_id}") | ||||
|                     .route(web::delete().to(files_delete)) | ||||
|                     .route(web::get().to(files_retrieve)), | ||||
|             ) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,12 +1,12 @@ | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use api_models::disputes::DisputeListConstraints; | ||||
| use api_models::disputes as dispute_models; | ||||
| 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, | ||||
|     types::api::disputes::{self as dispute_types}, | ||||
| }; | ||||
|  | ||||
| /// Diputes - Retrieve Dispute | ||||
| @ -73,7 +73,7 @@ pub async fn retrieve_dispute( | ||||
| pub async fn retrieve_disputes_list( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     payload: web::Query<DisputeListConstraints>, | ||||
|     payload: web::Query<dispute_models::DisputeListConstraints>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::DisputesList; | ||||
|     let payload = payload.into_inner(); | ||||
| @ -87,3 +87,70 @@ pub async fn retrieve_disputes_list( | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| /// Diputes - Accept Dispute | ||||
| #[utoipa::path( | ||||
|     get, | ||||
|     path = "/disputes/accept/{dispute_id}", | ||||
|     params( | ||||
|         ("dispute_id" = String, Path, description = "The identifier for dispute") | ||||
|     ), | ||||
|     responses( | ||||
|         (status = 200, description = "The dispute was accepted successfully", body = DisputeResponse), | ||||
|         (status = 404, description = "Dispute does not exist in our records") | ||||
|     ), | ||||
|     tag = "Disputes", | ||||
|     operation_id = "Accept a Dispute", | ||||
|     security(("api_key" = [])) | ||||
| )] | ||||
| #[instrument(skip_all, fields(flow = ?Flow::DisputesRetrieve))] | ||||
| pub async fn accept_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::accept_dispute, | ||||
|         auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| /// Diputes - Submit Dispute Evidence | ||||
| #[utoipa::path( | ||||
|     post, | ||||
|     path = "/disputes/evidence", | ||||
|     request_body=AcceptDisputeRequestData, | ||||
|     responses( | ||||
|         (status = 200, description = "The dispute evidence submitted successfully", body = AcceptDisputeResponse), | ||||
|         (status = 404, description = "Dispute does not exist in our records") | ||||
|     ), | ||||
|     tag = "Disputes", | ||||
|     operation_id = "Submit Dispute Evidence", | ||||
|     security(("api_key" = [])) | ||||
| )] | ||||
| #[instrument(skip_all, fields(flow = ?Flow::DisputesEvidenceSubmit))] | ||||
| pub async fn submit_dispute_evidence( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     json_payload: web::Json<dispute_models::SubmitEvidenceRequest>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::DisputesEvidenceSubmit; | ||||
|     api::server_wrap( | ||||
|         flow, | ||||
|         state.get_ref(), | ||||
|         &req, | ||||
|         json_payload.into_inner(), | ||||
|         disputes::submit_evidence, | ||||
|         auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
							
								
								
									
										125
									
								
								crates/router/src/routes/files.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								crates/router/src/routes/files.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,125 @@ | ||||
| use actix_multipart::Multipart; | ||||
| use actix_web::{web, HttpRequest, HttpResponse}; | ||||
| use router_env::{instrument, tracing, Flow}; | ||||
| pub mod transformers; | ||||
|  | ||||
| use super::app::AppState; | ||||
| use crate::{ | ||||
|     core::files::*, | ||||
|     services::{api, authentication as auth}, | ||||
|     types::api::files, | ||||
| }; | ||||
|  | ||||
| /// Files - Create | ||||
| /// | ||||
| /// To create a file | ||||
| #[utoipa::path( | ||||
|     post, | ||||
|     path = "/files", | ||||
|     request_body=MultipartRequestWithFile, | ||||
|     responses( | ||||
|         (status = 200, description = "File created", body = CreateFileResponse), | ||||
|         (status = 400, description = "Bad Request") | ||||
|     ), | ||||
|     tag = "Files", | ||||
|     operation_id = "Create a File", | ||||
|     security(("api_key" = [])) | ||||
| )] | ||||
| #[instrument(skip_all, fields(flow = ?Flow::CreateFile))] | ||||
| pub async fn files_create( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     payload: Multipart, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::CreateFile; | ||||
|     let create_file_request_result = transformers::get_create_file_request(payload).await; | ||||
|     let create_file_request = match create_file_request_result { | ||||
|         Ok(valid_request) => valid_request, | ||||
|         Err(err) => return api::log_and_return_error_response(err), | ||||
|     }; | ||||
|     api::server_wrap( | ||||
|         flow, | ||||
|         state.get_ref(), | ||||
|         &req, | ||||
|         create_file_request, | ||||
|         files_create_core, | ||||
|         auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| /// Files - Delete | ||||
| /// | ||||
| /// To delete a file | ||||
| #[utoipa::path( | ||||
|     delete, | ||||
|     path = "/files/{file_id}", | ||||
|     params( | ||||
|         ("file_id" = String, Path, description = "The identifier for file") | ||||
|     ), | ||||
|     responses( | ||||
|         (status = 200, description = "File deleted"), | ||||
|         (status = 404, description = "File not found") | ||||
|     ), | ||||
|     tag = "Files", | ||||
|     operation_id = "Delete a File", | ||||
|     security(("api_key" = [])) | ||||
| )] | ||||
| #[instrument(skip_all, fields(flow = ?Flow::DeleteFile))] | ||||
| pub async fn files_delete( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     path: web::Path<String>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::DeleteFile; | ||||
|     let file_id = files::FileId { | ||||
|         file_id: path.into_inner(), | ||||
|     }; | ||||
|     api::server_wrap( | ||||
|         flow, | ||||
|         state.get_ref(), | ||||
|         &req, | ||||
|         file_id, | ||||
|         files_delete_core, | ||||
|         auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
|  | ||||
| /// Files - Retrieve | ||||
| /// | ||||
| /// To retrieve a file | ||||
| #[utoipa::path( | ||||
|     get, | ||||
|     path = "/files/{file_id}", | ||||
|     params( | ||||
|         ("file_id" = String, Path, description = "The identifier for file") | ||||
|     ), | ||||
|     responses( | ||||
|         (status = 200, description = "File body"), | ||||
|         (status = 400, description = "Bad Request") | ||||
|     ), | ||||
|     tag = "Files", | ||||
|     operation_id = "Retrieve a File", | ||||
|     security(("api_key" = [])) | ||||
| )] | ||||
| #[instrument(skip_all, fields(flow = ?Flow::RetrieveFile))] | ||||
| pub async fn files_retrieve( | ||||
|     state: web::Data<AppState>, | ||||
|     req: HttpRequest, | ||||
|     path: web::Path<String>, | ||||
| ) -> HttpResponse { | ||||
|     let flow = Flow::RetrieveFile; | ||||
|     let file_id = files::FileId { | ||||
|         file_id: path.into_inner(), | ||||
|     }; | ||||
|     api::server_wrap( | ||||
|         flow, | ||||
|         state.get_ref(), | ||||
|         &req, | ||||
|         file_id, | ||||
|         files_retrieve_core, | ||||
|         auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), | ||||
|     ) | ||||
|     .await | ||||
| } | ||||
							
								
								
									
										89
									
								
								crates/router/src/routes/files/transformers.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								crates/router/src/routes/files/transformers.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | ||||
| use actix_multipart::Multipart; | ||||
| use actix_web::web::Bytes; | ||||
| use common_utils::errors::CustomResult; | ||||
| use error_stack::{IntoReport, ResultExt}; | ||||
| use futures::{StreamExt, TryStreamExt}; | ||||
|  | ||||
| use crate::{ | ||||
|     core::{errors, files::helpers}, | ||||
|     types::api::files::{self, CreateFileRequest}, | ||||
|     utils::OptionExt, | ||||
| }; | ||||
|  | ||||
| pub async fn get_create_file_request( | ||||
|     mut payload: Multipart, | ||||
| ) -> CustomResult<CreateFileRequest, errors::ApiErrorResponse> { | ||||
|     let mut option_purpose: Option<files::FilePurpose> = None; | ||||
|     let mut dispute_id: Option<String> = None; | ||||
|  | ||||
|     let mut file_name: Option<String> = None; | ||||
|     let mut file_content: Option<Vec<Bytes>> = None; | ||||
|  | ||||
|     while let Ok(Some(mut field)) = payload.try_next().await { | ||||
|         let content_disposition = field.content_disposition(); | ||||
|         let field_name = content_disposition.get_name(); | ||||
|         // Parse the different parameters expected in the multipart request | ||||
|         match field_name { | ||||
|             Some("purpose") => { | ||||
|                 option_purpose = helpers::get_file_purpose(&mut field).await; | ||||
|             } | ||||
|             Some("file") => { | ||||
|                 file_name = content_disposition.get_filename().map(String::from); | ||||
|  | ||||
|                 //Collect the file content and throw error if something fails | ||||
|                 let mut file_data = Vec::new(); | ||||
|                 let mut stream = field.into_stream(); | ||||
|                 while let Some(chunk) = stream.next().await { | ||||
|                     match chunk { | ||||
|                         Ok(bytes) => file_data.push(bytes), | ||||
|                         Err(err) => Err(errors::ApiErrorResponse::InternalServerError) | ||||
|                             .into_report() | ||||
|                             .attach_printable(format!("{}{}", "File parsing error: ", err))?, | ||||
|                     } | ||||
|                 } | ||||
|                 file_content = Some(file_data) | ||||
|             } | ||||
|             Some("dispute_id") => { | ||||
|                 dispute_id = helpers::read_string(&mut field).await; | ||||
|             } | ||||
|             // Can ignore other params | ||||
|             _ => (), | ||||
|         } | ||||
|     } | ||||
|     let purpose = option_purpose.get_required_value("purpose")?; | ||||
|     let file = match file_content { | ||||
|         Some(valid_file_content) => valid_file_content.concat().to_vec(), | ||||
|         None => Err(errors::ApiErrorResponse::MissingFile) | ||||
|             .into_report() | ||||
|             .attach_printable("Missing / Invalid file in the request")?, | ||||
|     }; | ||||
|     //Get and validate file size | ||||
|     let file_size: i32 = file | ||||
|         .len() | ||||
|         .try_into() | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::InternalServerError) | ||||
|         .attach_printable("File size error")?; | ||||
|     // Check if empty file and throw error | ||||
|     if file_size <= 0 { | ||||
|         Err(errors::ApiErrorResponse::MissingFile) | ||||
|             .into_report() | ||||
|             .attach_printable("Missing / Invalid file in the request")? | ||||
|     } | ||||
|     // Get file mime type using 'infer' | ||||
|     let kind = infer::get(&file).ok_or(errors::ApiErrorResponse::MissingFileContentType)?; | ||||
|     let file_type = kind | ||||
|         .mime_type() | ||||
|         .parse::<mime::Mime>() | ||||
|         .into_report() | ||||
|         .change_context(errors::ApiErrorResponse::MissingFileContentType) | ||||
|         .attach_printable("File content type error")?; | ||||
|     Ok(CreateFileRequest { | ||||
|         file, | ||||
|         file_name, | ||||
|         file_size, | ||||
|         file_type, | ||||
|         purpose, | ||||
|         dispute_id, | ||||
|     }) | ||||
| } | ||||
| @ -81,6 +81,13 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re | ||||
|         Ok(None) | ||||
|     } | ||||
|  | ||||
|     fn get_request_form_data( | ||||
|         &self, | ||||
|         _req: &types::RouterData<T, Req, Resp>, | ||||
|     ) -> CustomResult<Option<reqwest::multipart::Form>, errors::ConnectorError> { | ||||
|         Ok(None) | ||||
|     } | ||||
|  | ||||
|     /// This module can be called before executing a payment flow where a pre-task is needed | ||||
|     /// Eg: Some connectors requires one-time session token before making a payment, we can add the session token creation logic in this block | ||||
|     async fn execute_pretasks( | ||||
| @ -309,6 +316,12 @@ async fn send_request( | ||||
|             match request.content_type { | ||||
|                 Some(ContentType::Json) => client.json(&request.payload), | ||||
|  | ||||
|                 Some(ContentType::FormData) => client.multipart( | ||||
|                     request | ||||
|                         .form_data | ||||
|                         .unwrap_or_else(reqwest::multipart::Form::new), | ||||
|                 ), | ||||
|  | ||||
|                 // Currently this is not used remove this if not required | ||||
|                 // If using this then handle the serde_part | ||||
|                 Some(ContentType::FormUrlEncoded) => { | ||||
| @ -336,11 +349,9 @@ async fn send_request( | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         Method::Put => { | ||||
|             client | ||||
|                 .put(url) | ||||
|                 .body(request.payload.expose_option().unwrap_or_default()) // If payload needs processing the body cannot have default | ||||
|         } | ||||
|         Method::Put => client | ||||
|             .put(url) | ||||
|             .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default | ||||
|         Method::Delete => client.delete(url), | ||||
|     } | ||||
|     .add_headers(headers) | ||||
| @ -367,7 +378,7 @@ async fn handle_response( | ||||
|             logger::info!(?response); | ||||
|             let status_code = response.status().as_u16(); | ||||
|             match status_code { | ||||
|                 200..=202 | 302 => { | ||||
|                 200..=202 | 302 | 204 => { | ||||
|                     logger::debug!(response=?response); | ||||
|                     // If needed add log line | ||||
|                     // logger:: error!( error_parsing_response=?err); | ||||
| @ -441,6 +452,7 @@ pub enum ApplicationResponse<R> { | ||||
|     TextPlain(String), | ||||
|     JsonForRedirection(api::RedirectionResponse), | ||||
|     Form(RedirectForm), | ||||
|     FileData((Vec<u8>, mime::Mime)), | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Eq, PartialEq)] | ||||
| @ -556,6 +568,9 @@ where | ||||
|         }, | ||||
|         Ok(ApplicationResponse::StatusOk) => http_response_ok(), | ||||
|         Ok(ApplicationResponse::TextPlain(text)) => http_response_plaintext(text), | ||||
|         Ok(ApplicationResponse::FileData((file_data, content_type))) => { | ||||
|             http_response_file_data(file_data, content_type) | ||||
|         } | ||||
|         Ok(ApplicationResponse::JsonForRedirection(response)) => { | ||||
|             match serde_json::to_string(&response) { | ||||
|                 Ok(res) => http_redirect_response(res, response), | ||||
| @ -605,6 +620,13 @@ pub fn http_response_plaintext<T: body::MessageBody + 'static>(res: T) -> HttpRe | ||||
|     HttpResponse::Ok().content_type(mime::TEXT_PLAIN).body(res) | ||||
| } | ||||
|  | ||||
| pub fn http_response_file_data<T: body::MessageBody + 'static>( | ||||
|     res: T, | ||||
|     content_type: mime::Mime, | ||||
| ) -> HttpResponse { | ||||
|     HttpResponse::Ok().content_type(content_type).body(res) | ||||
| } | ||||
|  | ||||
| pub fn http_response_ok() -> HttpResponse { | ||||
|     HttpResponse::Ok().finish() | ||||
| } | ||||
|  | ||||
| @ -28,6 +28,7 @@ pub enum Method { | ||||
| pub enum ContentType { | ||||
|     Json, | ||||
|     FormUrlEncoded, | ||||
|     FormData, | ||||
| } | ||||
|  | ||||
| fn default_request_headers() -> [(String, String); 1] { | ||||
| @ -36,7 +37,7 @@ fn default_request_headers() -> [(String, String); 1] { | ||||
|     [(header::VIA.to_string(), "HyperSwitch".into())] | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[derive(Debug)] | ||||
| pub struct Request { | ||||
|     pub url: String, | ||||
|     pub headers: Headers, | ||||
| @ -45,6 +46,7 @@ pub struct Request { | ||||
|     pub content_type: Option<ContentType>, | ||||
|     pub certificate: Option<String>, | ||||
|     pub certificate_key: Option<String>, | ||||
|     pub form_data: Option<reqwest::multipart::Form>, | ||||
| } | ||||
|  | ||||
| impl Request { | ||||
| @ -57,6 +59,7 @@ impl Request { | ||||
|             content_type: None, | ||||
|             certificate: None, | ||||
|             certificate_key: None, | ||||
|             form_data: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -84,6 +87,10 @@ impl Request { | ||||
|     pub fn add_certificate_key(&mut self, certificate_key: Option<String>) { | ||||
|         self.certificate = certificate_key; | ||||
|     } | ||||
|  | ||||
|     pub fn set_form_data(&mut self, form_data: reqwest::multipart::Form) { | ||||
|         self.form_data = Some(form_data); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct RequestBuilder { | ||||
| @ -94,6 +101,7 @@ pub struct RequestBuilder { | ||||
|     pub content_type: Option<ContentType>, | ||||
|     pub certificate: Option<String>, | ||||
|     pub certificate_key: Option<String>, | ||||
|     pub form_data: Option<reqwest::multipart::Form>, | ||||
| } | ||||
|  | ||||
| impl RequestBuilder { | ||||
| @ -106,6 +114,7 @@ impl RequestBuilder { | ||||
|             content_type: None, | ||||
|             certificate: None, | ||||
|             certificate_key: None, | ||||
|             form_data: None, | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -135,6 +144,11 @@ impl RequestBuilder { | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn form_data(mut self, form_data: Option<reqwest::multipart::Form>) -> Self { | ||||
|         self.form_data = form_data; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     pub fn body(mut self, body: Option<String>) -> Self { | ||||
|         self.payload = body.map(From::from); | ||||
|         self | ||||
| @ -164,6 +178,7 @@ impl RequestBuilder { | ||||
|             content_type: self.content_type, | ||||
|             certificate: self.certificate, | ||||
|             certificate_key: self.certificate_key, | ||||
|             form_data: self.form_data, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -105,8 +105,31 @@ pub type RefundSyncType = | ||||
| pub type RefreshTokenType = | ||||
|     dyn services::ConnectorIntegration<api::AccessTokenAuth, AccessTokenRequestData, AccessToken>; | ||||
|  | ||||
| pub type AcceptDisputeType = dyn services::ConnectorIntegration< | ||||
|     api::Accept, | ||||
|     AcceptDisputeRequestData, | ||||
|     AcceptDisputeResponse, | ||||
| >; | ||||
|  | ||||
| pub type SubmitEvidenceType = dyn services::ConnectorIntegration< | ||||
|     api::Evidence, | ||||
|     SubmitEvidenceRequestData, | ||||
|     SubmitEvidenceResponse, | ||||
| >; | ||||
|  | ||||
| pub type UploadFileType = | ||||
|     dyn services::ConnectorIntegration<api::Upload, UploadFileRequestData, UploadFileResponse>; | ||||
|  | ||||
| pub type VerifyRouterData = RouterData<api::Verify, VerifyRequestData, PaymentsResponseData>; | ||||
|  | ||||
| pub type AcceptDisputeRouterData = | ||||
|     RouterData<api::Accept, AcceptDisputeRequestData, AcceptDisputeResponse>; | ||||
|  | ||||
| pub type SubmitEvidenceRouterData = | ||||
|     RouterData<api::Evidence, SubmitEvidenceRequestData, SubmitEvidenceResponse>; | ||||
|  | ||||
| pub type UploadFileRouterData = RouterData<api::Upload, UploadFileRequestData, UploadFileResponse>; | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct RouterData<Flow, Request, Response> { | ||||
|     pub flow: PhantomData<Flow>, | ||||
| @ -357,6 +380,75 @@ pub enum Redirection { | ||||
|     NoRedirect, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Clone)] | ||||
| pub struct AcceptDisputeRequestData { | ||||
|     pub dispute_id: String, | ||||
|     pub connector_dispute_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Clone, Debug)] | ||||
| pub struct AcceptDisputeResponse { | ||||
|     pub dispute_status: api_models::enums::DisputeStatus, | ||||
|     pub connector_status: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Debug, Clone)] | ||||
| pub struct SubmitEvidenceRequestData { | ||||
|     pub dispute_id: String, | ||||
|     pub connector_dispute_id: String, | ||||
|     pub access_activity_log: Option<String>, | ||||
|     pub billing_address: Option<String>, | ||||
|     pub cancellation_policy: Option<Vec<u8>>, | ||||
|     pub cancellation_policy_provider_file_id: Option<String>, | ||||
|     pub cancellation_policy_disclosure: Option<String>, | ||||
|     pub cancellation_rebuttal: Option<String>, | ||||
|     pub customer_communication: Option<Vec<u8>>, | ||||
|     pub customer_communication_provider_file_id: Option<String>, | ||||
|     pub customer_email_address: Option<String>, | ||||
|     pub customer_name: Option<String>, | ||||
|     pub customer_purchase_ip: Option<String>, | ||||
|     pub customer_signature: Option<Vec<u8>>, | ||||
|     pub customer_signature_provider_file_id: Option<String>, | ||||
|     pub product_description: Option<String>, | ||||
|     pub receipt: Option<Vec<u8>>, | ||||
|     pub receipt_provider_file_id: Option<String>, | ||||
|     pub refund_policy: Option<Vec<u8>>, | ||||
|     pub refund_policy_provider_file_id: Option<String>, | ||||
|     pub refund_policy_disclosure: Option<String>, | ||||
|     pub refund_refusal_explanation: Option<String>, | ||||
|     pub service_date: Option<String>, | ||||
|     pub service_documentation: Option<Vec<u8>>, | ||||
|     pub service_documentation_provider_file_id: Option<String>, | ||||
|     pub shipping_address: Option<String>, | ||||
|     pub shipping_carrier: Option<String>, | ||||
|     pub shipping_date: Option<String>, | ||||
|     pub shipping_documentation: Option<Vec<u8>>, | ||||
|     pub shipping_documentation_provider_file_id: Option<String>, | ||||
|     pub shipping_tracking_number: Option<String>, | ||||
|     pub uncategorized_file: Option<Vec<u8>>, | ||||
|     pub uncategorized_file_provider_file_id: Option<String>, | ||||
|     pub uncategorized_text: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Clone, Debug)] | ||||
| pub struct SubmitEvidenceResponse { | ||||
|     pub dispute_status: api_models::enums::DisputeStatus, | ||||
|     pub connector_status: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Debug)] | ||||
| pub struct UploadFileRequestData { | ||||
|     pub file_key: String, | ||||
|     pub file: Vec<u8>, | ||||
|     pub file_type: mime::Mime, | ||||
|     pub file_size: i32, | ||||
| } | ||||
|  | ||||
| #[derive(Default, Clone, Debug)] | ||||
| pub struct UploadFileResponse { | ||||
|     pub provider_file_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)] | ||||
| pub struct ConnectorResponse { | ||||
|     pub merchant_id: String, | ||||
|  | ||||
| @ -4,6 +4,7 @@ pub mod configs; | ||||
| pub mod customers; | ||||
| pub mod disputes; | ||||
| pub mod enums; | ||||
| pub mod files; | ||||
| pub mod mandates; | ||||
| pub mod payment_methods; | ||||
| pub mod payments; | ||||
| @ -15,8 +16,8 @@ use std::{fmt::Debug, str::FromStr}; | ||||
| use error_stack::{report, IntoReport, ResultExt}; | ||||
|  | ||||
| pub use self::{ | ||||
|     admin::*, api_keys::*, configs::*, customers::*, disputes::*, payment_methods::*, payments::*, | ||||
|     refunds::*, webhooks::*, | ||||
|     admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_methods::*, | ||||
|     payments::*, refunds::*, webhooks::*, | ||||
| }; | ||||
| use super::ErrorResponse; | ||||
| use crate::{ | ||||
| @ -106,6 +107,8 @@ pub trait Connector: | ||||
|     + ConnectorRedirectResponse | ||||
|     + IncomingWebhook | ||||
|     + ConnectorAccessToken | ||||
|     + Dispute | ||||
|     + FileUpload | ||||
|     + ConnectorTransactionId | ||||
| { | ||||
| } | ||||
| @ -122,6 +125,8 @@ impl< | ||||
|             + Send | ||||
|             + IncomingWebhook | ||||
|             + ConnectorAccessToken | ||||
|             + Dispute | ||||
|             + FileUpload | ||||
|             + ConnectorTransactionId, | ||||
|     > Connector for T | ||||
| { | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| use masking::{Deserialize, Serialize}; | ||||
|  | ||||
| use crate::{services, types}; | ||||
|  | ||||
| #[derive(Default, Debug, Deserialize, Serialize)] | ||||
| pub struct DisputeId { | ||||
|     pub dispute_id: String, | ||||
| @ -18,3 +20,29 @@ pub struct DisputePayload { | ||||
|     pub created_at: Option<String>, | ||||
|     pub updated_at: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Accept; | ||||
|  | ||||
| pub trait AcceptDispute: | ||||
|     services::ConnectorIntegration< | ||||
|     Accept, | ||||
|     types::AcceptDisputeRequestData, | ||||
|     types::AcceptDisputeResponse, | ||||
| > | ||||
| { | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Evidence; | ||||
|  | ||||
| pub trait SubmitEvidence: | ||||
|     services::ConnectorIntegration< | ||||
|     Evidence, | ||||
|     types::SubmitEvidenceRequestData, | ||||
|     types::SubmitEvidenceResponse, | ||||
| > | ||||
| { | ||||
| } | ||||
|  | ||||
| pub trait Dispute: super::ConnectorCommon + AcceptDispute + SubmitEvidence {} | ||||
|  | ||||
| @ -0,0 +1,67 @@ | ||||
| use masking::{Deserialize, Serialize}; | ||||
|  | ||||
| use super::ConnectorCommon; | ||||
| use crate::{core::errors, services, types}; | ||||
|  | ||||
| #[derive(Default, Debug, Deserialize, Serialize)] | ||||
| pub struct FileId { | ||||
|     pub file_id: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, frunk::LabelledGeneric)] | ||||
| pub enum FileUploadProvider { | ||||
|     Router, | ||||
|     Stripe, | ||||
| } | ||||
|  | ||||
| impl TryFrom<&types::Connector> for FileUploadProvider { | ||||
|     type Error = error_stack::Report<errors::ApiErrorResponse>; | ||||
|     fn try_from(item: &types::Connector) -> Result<Self, Self::Error> { | ||||
|         match item { | ||||
|             &types::Connector::Stripe => Ok(Self::Stripe), | ||||
|             _ => Err(errors::ApiErrorResponse::NotSupported { | ||||
|                 message: "Connector not supported as file provider".to_owned(), | ||||
|             } | ||||
|             .into()), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct CreateFileRequest { | ||||
|     pub file: Vec<u8>, | ||||
|     pub file_name: Option<String>, | ||||
|     pub file_size: i32, | ||||
|     pub file_type: mime::Mime, | ||||
|     pub purpose: FilePurpose, | ||||
|     pub dispute_id: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, serde::Deserialize, strum::Display, Clone)] | ||||
| #[serde(rename_all = "snake_case")] | ||||
| #[strum(serialize_all = "snake_case")] | ||||
| pub enum FilePurpose { | ||||
|     DisputeEvidence, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct Upload; | ||||
|  | ||||
| pub trait UploadFile: | ||||
|     services::ConnectorIntegration<Upload, types::UploadFileRequestData, types::UploadFileResponse> | ||||
| { | ||||
| } | ||||
|  | ||||
| pub trait FileUpload: ConnectorCommon + Sync + UploadFile { | ||||
|     fn validate_file_upload( | ||||
|         &self, | ||||
|         _purpose: FilePurpose, | ||||
|         _file_size: i32, | ||||
|         _file_type: mime::Mime, | ||||
|     ) -> common_utils::errors::CustomResult<(), errors::ConnectorError> { | ||||
|         Err(errors::ConnectorError::FileValidationFailed { | ||||
|             reason: "".to_owned(), | ||||
|         } | ||||
|         .into()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ pub mod dispute; | ||||
| pub mod enums; | ||||
| pub mod ephemeral_key; | ||||
| pub mod events; | ||||
| pub mod file; | ||||
| pub mod locker_mock_up; | ||||
| pub mod mandate; | ||||
| pub mod merchant_account; | ||||
| @ -26,7 +27,7 @@ pub mod kv; | ||||
|  | ||||
| pub use self::{ | ||||
|     address::*, api_keys::*, cards_info::*, configs::*, connector_response::*, customers::*, | ||||
|     dispute::*, events::*, locker_mock_up::*, mandate::*, merchant_account::*, | ||||
|     dispute::*, events::*, file::*, locker_mock_up::*, mandate::*, merchant_account::*, | ||||
|     merchant_connector_account::*, payment_attempt::*, payment_intent::*, payment_method::*, | ||||
|     process_tracker::*, refund::*, reverse_lookup::*, | ||||
| }; | ||||
|  | ||||
							
								
								
									
										3
									
								
								crates/router/src/types/storage/file.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								crates/router/src/types/storage/file.rs
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| pub use storage_models::file::{ | ||||
|     FileMetadata, FileMetadataNew, FileMetadataUpdate, FileMetadataUpdateInternal, | ||||
| }; | ||||
| @ -457,6 +457,12 @@ impl ForeignFrom<storage_enums::DisputeStatus> for api_enums::DisputeStatus { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ForeignFrom<api_types::FileUploadProvider> for storage_enums::FileUploadProvider { | ||||
|     fn foreign_from(provider: api_types::FileUploadProvider) -> Self { | ||||
|         frunk::labelled_convert_from(provider) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl ForeignTryFrom<api_models::webhooks::IncomingWebhookEvent> for storage_enums::DisputeStatus { | ||||
|     type Error = errors::ValidationError; | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user