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>
This commit is contained in:
Sahkal Poddar
2023-05-08 14:41:27 +05:30
committed by GitHub
parent 982c27fce7
commit 2ba186b7d1
13 changed files with 329 additions and 76 deletions

View File

@ -20,6 +20,7 @@ impl StripeApis {
.service(app::PaymentIntents::server(state.clone()))
.service(app::Refunds::server(state.clone()))
.service(app::Customers::server(state.clone()))
.service(app::Webhooks::server(state))
.service(app::Webhooks::server(state.clone()))
.service(app::Mandates::server(state))
}
}

View File

@ -1,7 +1,7 @@
use actix_web::{web, Scope};
use super::{customers::*, payment_intents::*, refunds::*, setup_intents::*, webhooks::*};
use crate::routes::{self, webhooks};
use crate::routes::{self, mandates, webhooks};
pub struct PaymentIntents;
@ -111,3 +111,13 @@ impl Webhooks {
)
}
}
pub struct Mandates;
impl Mandates {
pub fn server(config: routes::AppState) -> Scope {
web::scope("/payment_methods")
.app_data(web::Data::new(config))
.service(web::resource("/{id}/detach").route(web::post().to(mandates::revoke_mandate)))
}
}

View File

@ -1,6 +1,6 @@
use api_models::payments;
use common_utils::{date_time, ext_traits::StringExt, pii as secret};
use error_stack::ResultExt;
use error_stack::{IntoReport, ResultExt};
use serde::{Deserialize, Serialize};
use crate::{
@ -10,7 +10,7 @@ use crate::{
pii::{self, Email, PeekInterface},
types::{
api::{admin, enums as api_enums},
transformers::{ForeignFrom, ForeignInto},
transformers::{ForeignFrom, ForeignTryFrom},
},
};
@ -59,6 +59,7 @@ impl From<StripePaymentMethodType> for api_enums::PaymentMethod {
}
}
}
#[derive(Default, PartialEq, Eq, Deserialize, Clone)]
pub struct StripePaymentMethodData {
#[serde(rename = "type")]
@ -143,12 +144,32 @@ pub struct StripePaymentIntentRequest {
pub client_secret: Option<pii::Secret<String>>,
pub payment_method_options: Option<StripePaymentMethodOptions>,
pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>,
pub mandate_id: Option<String>,
pub off_session: Option<bool>,
}
impl TryFrom<StripePaymentIntentRequest> for payments::PaymentsRequest {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(item: StripePaymentIntentRequest) -> errors::RouterResult<Self> {
Ok(Self {
let (mandate_options, authentication_type) = match item.payment_method_options {
Some(pmo) => {
let StripePaymentMethodOptions::Card {
request_three_d_secure,
mandate_options,
}: StripePaymentMethodOptions = pmo;
(
Option::<payments::MandateData>::foreign_try_from((
mandate_options,
item.currency.to_owned(),
))?,
Some(api_enums::AuthenticationType::foreign_from(
request_three_d_secure,
)),
)
}
None => (None, None),
};
let request = Ok(Self {
payment_id: item.id.map(payments::PaymentIdType::PaymentIntentId),
amount: item.amount.map(|amount| amount.into()),
connector: item.connector,
@ -188,16 +209,15 @@ impl TryFrom<StripePaymentIntentRequest> for payments::PaymentsRequest {
statement_descriptor_suffix: item.statement_descriptor_suffix,
metadata: item.metadata,
client_secret: item.client_secret.map(|s| s.peek().clone()),
authentication_type: item.payment_method_options.map(|pmo| {
let StripePaymentMethodOptions::Card {
request_three_d_secure,
} = pmo;
request_three_d_secure.foreign_into()
}),
authentication_type,
mandate_data: mandate_options,
merchant_connector_details: item.merchant_connector_details,
setup_future_usage: item.setup_future_usage,
mandate_id: item.mandate_id,
off_session: item.off_session,
..Self::default()
})
});
request
}
}
@ -374,7 +394,7 @@ impl From<payments::PaymentsResponse> for StripePaymentIntentResponse {
created: u64::try_from(date_time::now().assume_utc().unix_timestamp())
.unwrap_or_default(),
method_type: "card".to_string(),
live_mode: false,
livemode: false,
},
error_type: code,
}),
@ -391,7 +411,7 @@ pub struct StripePaymentMethod {
created: u64,
#[serde(rename = "type")]
method_type: String,
live_mode: bool,
livemode: bool,
}
#[derive(Default, Eq, PartialEq, Serialize)]
@ -404,7 +424,7 @@ pub struct Charges {
}
impl Charges {
fn new() -> Self {
pub fn new() -> Self {
Self {
object: "list",
data: vec![],
@ -491,15 +511,83 @@ impl From<payments::PaymentListResponse> for StripePaymentIntentListResponse {
}
}
#[derive(PartialEq, Eq, Deserialize, Clone)]
#[derive(PartialEq, Eq, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum StripePaymentMethodOptions {
Card {
request_three_d_secure: Option<Request3DS>,
mandate_options: Option<MandateOption>,
},
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize, Clone)]
#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone)]
#[serde(rename_all = "snake_case")]
pub enum StripeMandateType {
SingleUse,
MultiUse,
}
#[derive(PartialEq, Eq, Clone, Default, Deserialize, Serialize, Debug)]
pub struct MandateOption {
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
pub accepted_at: Option<time::PrimitiveDateTime>,
pub user_agent: Option<String>,
pub ip_address: Option<pii::Secret<String, common_utils::pii::IpAddress>>,
pub mandate_type: Option<StripeMandateType>,
pub amount: Option<i64>,
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
pub start_date: Option<time::PrimitiveDateTime>,
#[serde(default, with = "common_utils::custom_serde::timestamp::option")]
pub end_date: Option<time::PrimitiveDateTime>,
}
impl ForeignTryFrom<(Option<MandateOption>, Option<String>)> for Option<payments::MandateData> {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn foreign_try_from(
(mandate_options, currency): (Option<MandateOption>, Option<String>),
) -> errors::RouterResult<Self> {
let currency = currency
.ok_or(errors::ApiErrorResponse::MissingRequiredField {
field_name: "currency",
})
.into_report()
.and_then(|c| {
c.to_uppercase().parse_enum("currency").change_context(
errors::ApiErrorResponse::InvalidDataValue {
field_name: "currency",
},
)
})?;
let mandate_data = mandate_options.map(|mandate| payments::MandateData {
mandate_type: match mandate.mandate_type {
Some(item) => match item {
StripeMandateType::SingleUse => {
payments::MandateType::SingleUse(payments::MandateAmountData {
amount: mandate.amount.unwrap_or_default(),
currency,
start_date: mandate.start_date,
end_date: mandate.end_date,
metadata: None,
})
}
StripeMandateType::MultiUse => payments::MandateType::MultiUse(None),
},
None => api_models::payments::MandateType::MultiUse(None),
},
customer_acceptance: payments::CustomerAcceptance {
acceptance_type: payments::AcceptanceType::Online,
accepted_at: mandate.accepted_at,
online: Some(payments::OnlineMandate {
ip_address: mandate.ip_address.unwrap_or_default(),
user_agent: mandate.user_agent.unwrap_or_default(),
}),
},
});
Ok(mandate_data)
}
}
#[derive(Default, Eq, PartialEq, Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Request3DS {
#[default]

View File

@ -28,7 +28,11 @@ pub async fn setup_intents_create(
}
};
let create_payment_req: payment_types::PaymentsRequest = payload.into();
let create_payment_req: payment_types::PaymentsRequest =
match payment_types::PaymentsRequest::try_from(payload) {
Ok(req) => req,
Err(err) => return api::log_and_return_error_response(err),
};
wrap::compatibility_api_wrap::<
_,
@ -124,7 +128,11 @@ pub async fn setup_intents_update(
}
};
let mut payload: payment_types::PaymentsRequest = stripe_payload.into();
let mut payload: payment_types::PaymentsRequest =
match payment_types::PaymentsRequest::try_from(stripe_payload) {
Ok(req) => req,
Err(err) => return api::log_and_return_error_response(err),
};
payload.payment_id = Some(api_types::PaymentIdType::PaymentIntentId(setup_id));
let (auth_type, auth_flow) =
@ -179,7 +187,11 @@ pub async fn setup_intents_confirm(
}
};
let mut payload: payment_types::PaymentsRequest = stripe_payload.into();
let mut payload: payment_types::PaymentsRequest =
match payment_types::PaymentsRequest::try_from(stripe_payload) {
Ok(req) => req,
Err(err) => return api::log_and_return_error_response(err),
};
payload.payment_id = Some(api_types::PaymentIdType::PaymentIntentId(setup_id));
payload.confirm = Some(true);

View File

@ -1,12 +1,21 @@
use api_models::{payments, refunds};
use api_models::payments;
use common_utils::{date_time, ext_traits::StringExt};
use error_stack::ResultExt;
use router_env::logger;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
compatibility::stripe::{
payment_intents::types as payment_intent, refunds::types as stripe_refunds,
},
consts,
core::errors,
pii::{self, PeekInterface},
types::api::{self as api_types, enums as api_enums},
types::{
api::{self as api_types, admin, enums as api_enums},
transformers::{ForeignFrom, ForeignTryFrom},
},
};
#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)]
@ -108,11 +117,14 @@ impl From<Shipping> for payments::Address {
}
}
}
#[derive(Default, PartialEq, Eq, Deserialize, Clone)]
pub struct StripeSetupIntentRequest {
pub confirm: Option<bool>,
pub customer: Option<String>,
pub description: Option<String>,
pub currency: Option<String>,
pub payment_method_data: Option<StripePaymentMethodData>,
pub receipt_email: Option<pii::Email>,
pub return_url: Option<url::Url>,
@ -123,17 +135,46 @@ pub struct StripeSetupIntentRequest {
pub statement_descriptor_suffix: Option<String>,
pub metadata: Option<api_models::payments::Metadata>,
pub client_secret: Option<pii::Secret<String>>,
pub payment_method_options: Option<payment_intent::StripePaymentMethodOptions>,
pub payment_method: Option<String>,
pub merchant_connector_details: Option<admin::MerchantConnectorDetailsWrap>,
}
impl From<StripeSetupIntentRequest> for payments::PaymentsRequest {
fn from(item: StripeSetupIntentRequest) -> Self {
Self {
impl TryFrom<StripeSetupIntentRequest> for payments::PaymentsRequest {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(item: StripeSetupIntentRequest) -> errors::RouterResult<Self> {
let (mandate_options, authentication_type) = match item.payment_method_options {
Some(pmo) => {
let payment_intent::StripePaymentMethodOptions::Card {
request_three_d_secure,
mandate_options,
}: payment_intent::StripePaymentMethodOptions = pmo;
(
Option::<payments::MandateData>::foreign_try_from((
mandate_options,
item.currency.to_owned(),
))?,
Some(api_enums::AuthenticationType::foreign_from(
request_three_d_secure,
)),
)
}
None => (None, None),
};
let request = Ok(Self {
amount: Some(api_types::Amount::Zero),
currency: Some(api_enums::Currency::default()),
capture_method: None,
amount_to_capture: None,
confirm: item.confirm,
customer_id: item.customer,
currency: item
.currency
.as_ref()
.map(|c| c.to_uppercase().parse_enum("currency"))
.transpose()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "currency",
})?,
email: item.receipt_email,
name: item
.billing_details
@ -163,8 +204,13 @@ impl From<StripeSetupIntentRequest> for payments::PaymentsRequest {
statement_descriptor_suffix: item.statement_descriptor_suffix,
metadata: item.metadata,
client_secret: item.client_secret.map(|s| s.peek().clone()),
setup_future_usage: item.setup_future_usage,
merchant_connector_details: item.merchant_connector_details,
authentication_type,
mandate_data: mandate_options,
..Default::default()
}
});
request
}
}
@ -232,6 +278,32 @@ impl From<StripePaymentCancelRequest> for payments::PaymentsCancelRequest {
}
}
}
#[derive(Default, Eq, PartialEq, Serialize)]
pub struct RedirectUrl {
pub return_url: Option<String>,
pub url: Option<String>,
}
#[derive(Eq, PartialEq, Serialize)]
pub struct StripeNextAction {
#[serde(rename = "type")]
stype: payments::NextActionType,
redirect_to_url: RedirectUrl,
}
pub(crate) fn into_stripe_next_action(
next_action: Option<payments::NextAction>,
return_url: Option<String>,
) -> Option<StripeNextAction> {
next_action.map(|n| StripeNextAction {
stype: n.next_action_type,
redirect_to_url: RedirectUrl {
return_url,
url: n.redirect_to_url,
},
})
}
#[derive(Default, Eq, PartialEq, Serialize)]
pub struct StripeSetupIntentResponse {
pub id: Option<String>,
@ -241,8 +313,35 @@ pub struct StripeSetupIntentResponse {
#[serde(with = "common_utils::custom_serde::iso8601::option")]
pub created: Option<time::PrimitiveDateTime>,
pub customer: Option<String>,
pub refunds: Option<Vec<refunds::RefundResponse>>,
pub refunds: Option<Vec<stripe_refunds::StripeRefundResponse>>,
pub mandate_id: Option<String>,
pub next_action: Option<StripeNextAction>,
pub last_payment_error: Option<LastPaymentError>,
pub charges: payment_intent::Charges,
}
#[derive(Default, Eq, PartialEq, Serialize)]
pub struct LastPaymentError {
charge: Option<String>,
code: Option<String>,
decline_code: Option<String>,
message: String,
param: Option<String>,
payment_method: StripePaymentMethod,
#[serde(rename = "type")]
error_type: String,
}
#[derive(Default, Eq, PartialEq, Serialize)]
pub struct StripePaymentMethod {
#[serde(rename = "id")]
payment_method_id: String,
object: &'static str,
card: Option<StripeCard>,
created: u64,
#[serde(rename = "type")]
method_type: String,
livemode: bool,
}
impl From<payments::PaymentsResponse> for StripeSetupIntentResponse {
@ -251,11 +350,36 @@ impl From<payments::PaymentsResponse> for StripeSetupIntentResponse {
object: "setup_intent".to_owned(),
status: StripeSetupStatus::from(resp.status),
client_secret: resp.client_secret,
charges: payment_intent::Charges::new(),
created: resp.created,
customer: resp.customer_id,
id: resp.payment_id,
refunds: resp.refunds,
refunds: resp
.refunds
.map(|a| a.into_iter().map(Into::into).collect()),
mandate_id: resp.mandate_id,
next_action: into_stripe_next_action(resp.next_action, resp.return_url),
last_payment_error: resp.error_code.map(|code| -> LastPaymentError {
LastPaymentError {
charge: None,
code: Some(code.to_owned()),
decline_code: None,
message: resp
.error_message
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
param: None,
payment_method: StripePaymentMethod {
payment_method_id: "place_holder_id".to_string(),
object: "payment_method",
card: None,
created: u64::try_from(date_time::now().assume_utc().unix_timestamp())
.unwrap_or_default(),
method_type: "card".to_string(),
livemode: false,
},
error_type: code,
}
}),
}
}
}