Files
Sahkal Poddar 2ba186b7d1 feat(compatibility): add mandates support in stripe compatibility (#897)
Co-authored-by: Sahkal Poddar <sahkal.poddar@juspay.in>
Co-authored-by: Sarthak Soni <sarthak.soni@juspay.in>
Co-authored-by: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com>
Co-authored-by: Abhishek Marrivagu <abhi.codes10@gmail.com>
2023-05-08 09:11:27 +00:00

468 lines
17 KiB
Rust

use std::marker::PhantomData;
use api_models::enums::{DisputeStage, DisputeStatus};
use common_utils::errors::CustomResult;
use error_stack::ResultExt;
use router_env::{instrument, tracing};
use super::payments::{helpers, PaymentAddress};
use crate::{
consts,
core::errors::{self, RouterResult},
routes::AppState,
types::{
self,
storage::{self, enums},
ErrorResponse,
},
utils::{generate_id, OptionExt, ValueExt},
};
#[instrument(skip_all)]
#[allow(clippy::too_many_arguments)]
pub async fn construct_refund_router_data<'a, F>(
state: &'a AppState,
connector_id: &str,
merchant_account: &storage::MerchantAccount,
money: (i64, enums::Currency),
payment_intent: &'a storage::PaymentIntent,
payment_attempt: &storage::PaymentAttempt,
refund: &'a storage::Refund,
creds_identifier: Option<String>,
) -> RouterResult<types::RefundsRouterData<F>> {
let connector_label = helpers::get_connector_label(
payment_intent.business_country,
&payment_intent.business_label,
None,
connector_id,
);
let merchant_connector_account = helpers::get_merchant_connector_account(
state,
merchant_account.merchant_id.as_str(),
&connector_label,
creds_identifier,
)
.await?;
let auth_type: types::ConnectorAuthType = merchant_connector_account
.get_connector_account_details()
.parse_value("ConnectorAuthType")
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let status = payment_attempt.status;
let (amount, currency) = money;
let payment_method_type = payment_attempt
.payment_method
.get_required_value("payment_method_type")?;
let router_data = types::RouterData {
flow: PhantomData,
merchant_id: merchant_account.merchant_id.clone(),
customer_id: payment_intent.customer_id.to_owned(),
connector: connector_id.to_string(),
payment_id: payment_attempt.payment_id.clone(),
attempt_id: payment_attempt.attempt_id.clone(),
status,
payment_method: payment_method_type,
connector_auth_type: auth_type,
description: None,
return_url: payment_intent.return_url.clone(),
payment_method_id: payment_attempt.payment_method_id.clone(),
// Does refund need shipping/billing address ?
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::RefundsData {
refund_id: refund.refund_id.clone(),
connector_transaction_id: refund.connector_transaction_id.clone(),
refund_amount: refund.refund_amount,
currency,
amount,
connector_metadata: payment_attempt.connector_metadata.clone(),
reason: refund.refund_reason.clone(),
connector_refund_id: refund.connector_refund_id.clone(),
},
response: Ok(types::RefundsResponseData {
connector_refund_id: refund.connector_refund_id.clone().unwrap_or_default(),
refund_status: refund.refund_status,
}),
access_token: None,
session_token: None,
reference_id: None,
payment_method_token: None,
connector_customer: None,
};
Ok(router_data)
}
pub fn get_or_generate_id(
key: &str,
provided_id: &Option<String>,
prefix: &str,
) -> Result<String, errors::ApiErrorResponse> {
let validate_id = |id| validate_id(id, key);
provided_id
.clone()
.map_or(Ok(generate_id(consts::ID_LENGTH, prefix)), validate_id)
}
fn invalid_id_format_error(key: &str) -> errors::ApiErrorResponse {
errors::ApiErrorResponse::InvalidDataFormat {
field_name: key.to_string(),
expected_format: format!(
"length should be less than {} characters",
consts::MAX_ID_LENGTH
),
}
}
pub fn validate_id(id: String, key: &str) -> Result<String, errors::ApiErrorResponse> {
if id.len() > consts::MAX_ID_LENGTH {
Err(invalid_id_format_error(key))
} else {
Ok(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_id_length_constraint() {
let payment_id =
"abcdefghijlkmnopqrstuvwzyzabcdefghijknlmnopsjkdnfjsknfkjsdnfspoig".to_string(); //length = 65
let result = validate_id(payment_id, "payment_id");
assert!(result.is_err());
}
#[test]
fn validate_id_proper_response() {
let payment_id = "abcdefghijlkmnopqrstjhbjhjhkhbhgcxdfxvmhb".to_string();
let result = validate_id(payment_id.clone(), "payment_id");
assert!(result.is_ok());
let result = result.unwrap_or_default();
assert_eq!(result, payment_id);
}
#[test]
fn test_generate_id() {
let generated_id = generate_id(consts::ID_LENGTH, "ref");
assert_eq!(generated_id.len(), consts::ID_LENGTH + 4)
}
}
// Dispute Stage can move linearly from PreDispute -> Dispute -> PreArbitration
pub fn validate_dispute_stage(
prev_dispute_stage: &DisputeStage,
dispute_stage: &DisputeStage,
) -> bool {
match prev_dispute_stage {
DisputeStage::PreDispute => true,
DisputeStage::Dispute => !matches!(dispute_stage, DisputeStage::PreDispute),
DisputeStage::PreArbitration => matches!(dispute_stage, DisputeStage::PreArbitration),
}
}
//Dispute status can go from Opened -> (Expired | Accepted | Cancelled | Challenged -> (Won | Lost))
pub fn validate_dispute_status(
prev_dispute_status: DisputeStatus,
dispute_status: DisputeStatus,
) -> bool {
match prev_dispute_status {
DisputeStatus::DisputeOpened => true,
DisputeStatus::DisputeExpired => {
matches!(dispute_status, DisputeStatus::DisputeExpired)
}
DisputeStatus::DisputeAccepted => {
matches!(dispute_status, DisputeStatus::DisputeAccepted)
}
DisputeStatus::DisputeCancelled => {
matches!(dispute_status, DisputeStatus::DisputeCancelled)
}
DisputeStatus::DisputeChallenged => matches!(
dispute_status,
DisputeStatus::DisputeChallenged
| DisputeStatus::DisputeWon
| DisputeStatus::DisputeLost
),
DisputeStatus::DisputeWon => matches!(dispute_status, DisputeStatus::DisputeWon),
DisputeStatus::DisputeLost => matches!(dispute_status, DisputeStatus::DisputeLost),
}
}
pub fn validate_dispute_stage_and_dispute_status(
prev_dispute_stage: DisputeStage,
prev_dispute_status: DisputeStatus,
dispute_stage: DisputeStage,
dispute_status: DisputeStatus,
) -> CustomResult<(), errors::WebhooksFlowError> {
let dispute_stage_validation = validate_dispute_stage(&prev_dispute_stage, &dispute_stage);
let dispute_status_validation = if dispute_stage == prev_dispute_stage {
validate_dispute_status(prev_dispute_status, dispute_status)
} else {
true
};
common_utils::fp_utils::when(
!(dispute_stage_validation && dispute_status_validation),
|| {
super::metrics::INCOMING_DISPUTE_WEBHOOK_VALIDATION_FAILURE_METRIC.add(
&super::metrics::CONTEXT,
1,
&[],
);
Err(errors::WebhooksFlowError::DisputeWebhookValidationFailed)?
},
)
}
#[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 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(
state,
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,
connector_customer: None,
customer_id: 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 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(
state,
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,
connector_customer: None,
customer_id: 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 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(
state,
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,
connector_customer: None,
customer_id: None,
};
Ok(router_data)
}
#[instrument(skip_all)]
pub async fn construct_defend_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::DefendDisputeRouterData> {
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(
state,
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::DefendDisputeRequestData {
dispute_id: dispute.dispute_id.clone(),
connector_dispute_id: dispute.connector_dispute_id.clone(),
},
response: Err(ErrorResponse::get_not_implemented()),
access_token: None,
session_token: None,
reference_id: None,
payment_method_token: None,
customer_id: None,
connector_customer: None,
};
Ok(router_data)
}