mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 04:04:43 +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:
committed by
GitHub
parent
bcbf4c882c
commit
bdf1e5147e
@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user