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
2023-04-25 01:05:21 +05:30
committed by GitHub
parent bcbf4c882c
commit bdf1e5147e
54 changed files with 2822 additions and 34 deletions

View File

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

View File

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

View File

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