From 01cafe753bc4ea0cf33ec604dcbe0017abfebcad Mon Sep 17 00:00:00 2001 From: Nishant Joshi Date: Mon, 12 Dec 2022 14:34:53 +0530 Subject: [PATCH] feat(setup_intent): add setup_intent stripe compatibility (#102) --- crates/router/src/compatibility/stripe.rs | 4 +- crates/router/src/compatibility/stripe/app.rs | 15 +- .../src/compatibility/stripe/setup_intents.rs | 215 ++++++++++++ .../stripe/setup_intents/types.rs | 326 ++++++++++++++++++ 4 files changed, 558 insertions(+), 2 deletions(-) create mode 100644 crates/router/src/compatibility/stripe/setup_intents.rs create mode 100644 crates/router/src/compatibility/stripe/setup_intents/types.rs diff --git a/crates/router/src/compatibility/stripe.rs b/crates/router/src/compatibility/stripe.rs index bacf2e19d4..9781f45452 100644 --- a/crates/router/src/compatibility/stripe.rs +++ b/crates/router/src/compatibility/stripe.rs @@ -2,11 +2,12 @@ mod app; mod customers; mod payment_intents; mod refunds; +mod setup_intents; use actix_web::{web, Scope}; mod errors; pub(crate) use errors::ErrorCode; -pub(crate) use self::app::{Customers, PaymentIntents, Refunds}; +pub(crate) use self::app::{Customers, PaymentIntents, Refunds, SetupIntents}; use crate::routes::AppState; pub struct StripeApis; @@ -16,6 +17,7 @@ impl StripeApis { let strict = false; web::scope("/vs/v1") .app_data(web::Data::new(serde_qs::Config::new(max_depth, strict))) + .service(SetupIntents::server(state.clone())) .service(PaymentIntents::server(state.clone())) .service(Refunds::server(state.clone())) .service(Customers::server(state)) diff --git a/crates/router/src/compatibility/stripe/app.rs b/crates/router/src/compatibility/stripe/app.rs index b236f26a4e..4e3ddab4c1 100644 --- a/crates/router/src/compatibility/stripe/app.rs +++ b/crates/router/src/compatibility/stripe/app.rs @@ -1,6 +1,6 @@ use actix_web::{web, Scope}; -use super::{customers::*, payment_intents::*, refunds::*}; +use super::{customers::*, payment_intents::*, refunds::*, setup_intents::*}; use crate::routes::AppState; pub struct PaymentIntents; @@ -17,6 +17,19 @@ impl PaymentIntents { } } +pub struct SetupIntents; + +impl SetupIntents { + pub fn server(state: AppState) -> Scope { + web::scope("/setup_intents") + .app_data(web::Data::new(state)) + .service(setup_intents_create) + .service(setup_intents_retrieve) + .service(setup_intents_update) + .service(setup_intents_confirm) + } +} + pub struct Refunds; impl Refunds { diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs new file mode 100644 index 0000000000..bf410e1930 --- /dev/null +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -0,0 +1,215 @@ +mod types; + +use actix_web::{get, post, web, HttpRequest, HttpResponse}; +use error_stack::report; +use router_env::{tracing, tracing::instrument}; + +use crate::{ + compatibility::{stripe, wrap}, + core::payments, + routes::AppState, + services::api, + types::api::{self as api_types, PSync, PaymentsRequest, PaymentsRetrieveRequest, Verify}, +}; + +#[post("")] +#[instrument(skip_all)] +pub async fn setup_intents_create( + state: web::Data, + qs_config: web::Data, + req: HttpRequest, + form_payload: web::Bytes, +) -> HttpResponse { + let payload: types::StripeSetupIntentRequest = match qs_config.deserialize_bytes(&form_payload) + { + Ok(p) => p, + Err(err) => { + return api::log_and_return_error_response(report!(stripe::ErrorCode::from(err))) + } + }; + + let create_payment_req: PaymentsRequest = payload.into(); + + wrap::compatibility_api_wrap::< + _, + _, + _, + _, + _, + types::StripeSetupIntentResponse, + stripe::ErrorCode, + >( + &state, + &req, + create_payment_req, + |state, merchant_account, req| { + payments::payments_core::( + state, + merchant_account, + payments::PaymentCreate, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + ) + }, + api::MerchantAuthentication::ApiKey, + ) + .await +} + +#[instrument(skip_all)] +#[get("/{setup_id}")] +pub async fn setup_intents_retrieve( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let payload = PaymentsRetrieveRequest { + resource_id: api_types::PaymentIdType::PaymentIntentId(path.to_string()), + merchant_id: None, + force_sync: true, + connector: None, + param: None, + }; + + let auth_type = match api::get_auth_type(&req) { + Ok(auth_type) => auth_type, + Err(err) => return api::log_and_return_error_response(report!(err)), + }; + let auth_flow = api::get_auth_flow(&auth_type); + + wrap::compatibility_api_wrap::< + _, + _, + _, + _, + _, + types::StripeSetupIntentResponse, + stripe::ErrorCode, + >( + &state, + &req, + payload, + |state, merchant_account, payload| { + payments::payments_core::( + state, + merchant_account, + payments::PaymentStatus, + payload, + auth_flow, + payments::CallConnectorAction::Trigger, + ) + }, + auth_type, + ) + .await +} + +#[instrument(skip_all)] +#[post("/{setup_id}")] +pub async fn setup_intents_update( + state: web::Data, + qs_config: web::Data, + req: HttpRequest, + form_payload: web::Bytes, + path: web::Path, +) -> HttpResponse { + let setup_id = path.into_inner(); + let stripe_payload: types::StripeSetupIntentRequest = + match qs_config.deserialize_bytes(&form_payload) { + Ok(p) => p, + Err(err) => { + return api::log_and_return_error_response(report!(stripe::ErrorCode::from(err))) + } + }; + + let mut payload: PaymentsRequest = stripe_payload.into(); + payload.payment_id = Some(api_types::PaymentIdType::PaymentIntentId(setup_id)); + + let auth_type; + (payload, auth_type) = match api::get_auth_type_and_check_client_secret(&req, payload) { + Ok(values) => values, + Err(err) => return api::log_and_return_error_response(err), + }; + let auth_flow = api::get_auth_flow(&auth_type); + wrap::compatibility_api_wrap::< + _, + _, + _, + _, + _, + types::StripeSetupIntentResponse, + stripe::ErrorCode, + >( + &state, + &req, + payload, + |state, merchant_account, req| { + payments::payments_core::( + state, + merchant_account, + payments::PaymentUpdate, + req, + auth_flow, + payments::CallConnectorAction::Trigger, + ) + }, + auth_type, + ) + .await +} + +#[instrument(skip_all)] +#[post("/{setup_id}/confirm")] +pub async fn setup_intents_confirm( + state: web::Data, + qs_config: web::Data, + req: HttpRequest, + form_payload: web::Bytes, + path: web::Path, +) -> HttpResponse { + let setup_id = path.into_inner(); + let stripe_payload: types::StripeSetupIntentRequest = + match qs_config.deserialize_bytes(&form_payload) { + Ok(p) => p, + Err(err) => { + return api::log_and_return_error_response(report!(stripe::ErrorCode::from(err))) + } + }; + + let mut payload: PaymentsRequest = stripe_payload.into(); + payload.payment_id = Some(api_types::PaymentIdType::PaymentIntentId(setup_id)); + payload.confirm = Some(true); + + let auth_type; + (payload, auth_type) = match api::get_auth_type_and_check_client_secret(&req, payload) { + Ok(values) => values, + Err(err) => return api::log_and_return_error_response(err), + }; + let auth_flow = api::get_auth_flow(&auth_type); + wrap::compatibility_api_wrap::< + _, + _, + _, + _, + _, + types::StripeSetupIntentResponse, + stripe::ErrorCode, + >( + &state, + &req, + payload, + |state, merchant_account, req| { + payments::payments_core::( + state, + merchant_account, + payments::PaymentConfirm, + req, + auth_flow, + payments::CallConnectorAction::Trigger, + ) + }, + auth_type, + ) + .await +} diff --git a/crates/router/src/compatibility/stripe/setup_intents/types.rs b/crates/router/src/compatibility/stripe/setup_intents/types.rs new file mode 100644 index 0000000000..3a058696e2 --- /dev/null +++ b/crates/router/src/compatibility/stripe/setup_intents/types.rs @@ -0,0 +1,326 @@ +use router_env::logger; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + core::errors, + pii::Secret, + types::api::{ + self as api_types, enums as api_enums, Address, AddressDetails, CCard, + PaymentListConstraints, PaymentMethod, PaymentsCancelRequest, PaymentsRequest, + PaymentsResponse, PhoneDetails, RefundResponse, + }, +}; + +#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)] +pub(crate) struct StripeBillingDetails { + pub(crate) address: Option, + pub(crate) email: Option, + pub(crate) name: Option, + pub(crate) phone: Option, +} + +impl From for Address { + fn from(details: StripeBillingDetails) -> Self { + Self { + address: details.address, + phone: Some(PhoneDetails { + number: details.phone.map(Secret::new), + country_code: None, + }), + } + } +} + +#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)] +pub(crate) struct StripeCard { + pub(crate) number: String, + pub(crate) exp_month: String, + pub(crate) exp_year: String, + pub(crate) cvc: String, +} + +#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub(crate) enum StripePaymentMethodType { + #[default] + Card, +} + +impl From for api_enums::PaymentMethodType { + fn from(item: StripePaymentMethodType) -> Self { + match item { + StripePaymentMethodType::Card => api_enums::PaymentMethodType::Card, + } + } +} +#[derive(Default, PartialEq, Eq, Deserialize, Clone)] +pub(crate) struct StripePaymentMethodData { + #[serde(rename = "type")] + pub(crate) stype: StripePaymentMethodType, + pub(crate) billing_details: Option, + #[serde(flatten)] + pub(crate) payment_method_details: Option, // enum + pub(crate) metadata: Option, +} + +#[derive(Default, PartialEq, Eq, Deserialize, Clone)] +#[serde(rename_all = "snake_case")] +pub(crate) enum StripePaymentMethodDetails { + Card(StripeCard), + #[default] + BankTransfer, +} + +impl From for CCard { + fn from(card: StripeCard) -> Self { + Self { + card_number: Secret::new(card.number), + card_exp_month: Secret::new(card.exp_month), + card_exp_year: Secret::new(card.exp_year), + card_holder_name: Secret::new("stripe_cust".to_owned()), + card_cvc: Secret::new(card.cvc), + } + } +} +impl From for PaymentMethod { + fn from(item: StripePaymentMethodDetails) -> Self { + match item { + StripePaymentMethodDetails::Card(card) => PaymentMethod::Card(CCard::from(card)), + StripePaymentMethodDetails::BankTransfer => PaymentMethod::BankTransfer, + } + } +} + +#[derive(Default, Serialize, PartialEq, Eq, Deserialize, Clone)] +pub(crate) struct Shipping { + pub(crate) address: Option, + pub(crate) name: Option, + pub(crate) carrier: Option, + pub(crate) phone: Option, + pub(crate) tracking_number: Option, +} + +impl From for Address { + fn from(details: Shipping) -> Self { + Self { + address: details.address, + phone: Some(PhoneDetails { + number: details.phone.map(Secret::new), + country_code: None, + }), + } + } +} +#[derive(Default, PartialEq, Eq, Deserialize, Clone)] +pub(crate) struct StripeSetupIntentRequest { + pub(crate) confirm: Option, + pub(crate) customer: Option, + pub(crate) description: Option, + pub(crate) payment_method_data: Option, + pub(crate) receipt_email: Option, + pub(crate) return_url: Option, + pub(crate) setup_future_usage: Option, + pub(crate) shipping: Option, + pub(crate) billing_details: Option, + pub(crate) statement_descriptor: Option, + pub(crate) statement_descriptor_suffix: Option, + pub(crate) metadata: Option, + pub(crate) client_secret: Option, +} + +impl From for PaymentsRequest { + fn from(item: StripeSetupIntentRequest) -> Self { + PaymentsRequest { + amount: Some(api_types::Amount::Zero), + currency: Some(api_enums::Currency::default().to_string()), + capture_method: None, + amount_to_capture: None, + confirm: item.confirm, + customer_id: item.customer, + email: item.receipt_email.map(Secret::new), + name: item + .billing_details + .as_ref() + .and_then(|b| b.name.as_ref().map(|x| Secret::new(x.to_owned()))), + phone: item + .shipping + .as_ref() + .and_then(|s| s.phone.as_ref().map(|x| Secret::new(x.to_owned()))), + description: item.description, + return_url: item.return_url, + payment_method_data: item.payment_method_data.as_ref().and_then(|pmd| { + pmd.payment_method_details + .as_ref() + .map(|spmd| PaymentMethod::from(spmd.to_owned())) + }), + payment_method: item + .payment_method_data + .as_ref() + .map(|pmd| api_enums::PaymentMethodType::from(pmd.stype.to_owned())), + shipping: item.shipping.as_ref().map(|s| Address::from(s.to_owned())), + billing: item + .billing_details + .as_ref() + .map(|b| Address::from(b.to_owned())), + statement_descriptor_name: item.statement_descriptor, + statement_descriptor_suffix: item.statement_descriptor_suffix, + metadata: item.metadata, + client_secret: item.client_secret, + ..Default::default() + } + } +} + +#[derive(Clone, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum StripeSetupStatus { + Succeeded, + Canceled, + #[default] + Processing, + RequiresAction, + RequiresPaymentMethod, + RequiresConfirmation, +} + +// TODO: Verify if the status are correct +impl From for StripeSetupStatus { + fn from(item: api_enums::IntentStatus) -> Self { + match item { + api_enums::IntentStatus::Succeeded => StripeSetupStatus::Succeeded, + api_enums::IntentStatus::Failed => StripeSetupStatus::Canceled, // TODO: should we show canceled or processing + api_enums::IntentStatus::Processing => StripeSetupStatus::Processing, + api_enums::IntentStatus::RequiresCustomerAction => StripeSetupStatus::RequiresAction, + api_enums::IntentStatus::RequiresPaymentMethod => { + StripeSetupStatus::RequiresPaymentMethod + } + api_enums::IntentStatus::RequiresConfirmation => { + StripeSetupStatus::RequiresConfirmation + } + api_enums::IntentStatus::RequiresCapture => { + logger::error!("Invalid status change"); + StripeSetupStatus::Canceled + } + api_enums::IntentStatus::Cancelled => StripeSetupStatus::Canceled, + } + } +} + +#[derive(Debug, Serialize, Deserialize, Copy, Clone)] +#[serde(rename_all = "snake_case")] +pub(crate) enum CancellationReason { + Duplicate, + Fraudulent, + RequestedByCustomer, + Abandoned, +} + +impl ToString for CancellationReason { + fn to_string(&self) -> String { + String::from(match self { + Self::Duplicate => "duplicate", + Self::Fraudulent => "fradulent", + Self::RequestedByCustomer => "requested_by_customer", + Self::Abandoned => "abandoned", + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Copy, Clone)] +pub(crate) struct StripePaymentCancelRequest { + cancellation_reason: Option, +} + +impl From for PaymentsCancelRequest { + fn from(item: StripePaymentCancelRequest) -> Self { + Self { + cancellation_reason: item.cancellation_reason.map(|c| c.to_string()), + ..Self::default() + } + } +} +#[derive(Default, Eq, PartialEq, Serialize)] +pub(crate) struct StripeSetupIntentResponse { + pub(crate) id: Option, + pub(crate) object: String, + pub(crate) status: StripeSetupStatus, + pub(crate) client_secret: Option>, + #[serde(with = "common_utils::custom_serde::iso8601::option")] + pub(crate) created: Option, + pub(crate) customer: Option, + pub(crate) refunds: Option>, + pub(crate) mandate_id: Option, +} + +impl From for StripeSetupIntentResponse { + fn from(resp: PaymentsResponse) -> Self { + Self { + object: "setup_intent".to_owned(), + status: StripeSetupStatus::from(resp.status), + client_secret: resp.client_secret, + created: resp.created, + customer: resp.customer_id, + id: resp.payment_id, + refunds: resp.refunds, + mandate_id: resp.mandate_id, + } + } +} +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StripePaymentListConstraints { + pub customer: Option, + pub starting_after: Option, + pub ending_before: Option, + #[serde(default = "default_limit")] + pub limit: i64, + pub created: Option, + #[serde(rename = "created[lt]")] + pub created_lt: Option, + #[serde(rename = "created[gt]")] + pub created_gt: Option, + #[serde(rename = "created[lte]")] + pub created_lte: Option, + #[serde(rename = "created[gte]")] + pub created_gte: Option, +} + +fn default_limit() -> i64 { + 10 +} + +impl TryFrom for PaymentListConstraints { + type Error = error_stack::Report; + fn try_from(item: StripePaymentListConstraints) -> Result { + Ok(Self { + customer_id: item.customer, + starting_after: item.starting_after, + ending_before: item.ending_before, + limit: item.limit, + created: from_timestamp_to_datetime(item.created)?, + created_lt: from_timestamp_to_datetime(item.created_lt)?, + created_gt: from_timestamp_to_datetime(item.created_gt)?, + created_lte: from_timestamp_to_datetime(item.created_lte)?, + created_gte: from_timestamp_to_datetime(item.created_gte)?, + }) + } +} + +#[inline] +fn from_timestamp_to_datetime( + time: Option, +) -> Result, errors::ApiErrorResponse> { + if let Some(time) = time { + let time = time::OffsetDateTime::from_unix_timestamp(time).map_err(|err| { + logger::error!("Error: from_unix_timestamp: {}", err); + errors::ApiErrorResponse::InvalidRequestData { + message: "Error while converting timestamp".to_string(), + } + })?; + + Ok(Some(time::PrimitiveDateTime::new(time.date(), time.time()))) + } else { + Ok(None) + } +}