feat(setup_intent): add setup_intent stripe compatibility (#102)

This commit is contained in:
Nishant Joshi
2022-12-12 14:34:53 +05:30
committed by GitHub
parent 044613dcf1
commit 01cafe753b
4 changed files with 558 additions and 2 deletions

View File

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

View File

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

View File

@ -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<AppState>,
qs_config: web::Data<serde_qs::Config>,
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::<Verify, api_types::PaymentsResponse, _, _, _>(
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<AppState>,
req: HttpRequest,
path: web::Path<String>,
) -> 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::<PSync, api_types::PaymentsResponse, _, _, _>(
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<AppState>,
qs_config: web::Data<serde_qs::Config>,
req: HttpRequest,
form_payload: web::Bytes,
path: web::Path<String>,
) -> 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::<Verify, api_types::PaymentsResponse, _, _, _>(
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<AppState>,
qs_config: web::Data<serde_qs::Config>,
req: HttpRequest,
form_payload: web::Bytes,
path: web::Path<String>,
) -> 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::<Verify, api_types::PaymentsResponse, _, _, _>(
state,
merchant_account,
payments::PaymentConfirm,
req,
auth_flow,
payments::CallConnectorAction::Trigger,
)
},
auth_type,
)
.await
}

View File

@ -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<AddressDetails>,
pub(crate) email: Option<String>,
pub(crate) name: Option<String>,
pub(crate) phone: Option<String>,
}
impl From<StripeBillingDetails> 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<StripePaymentMethodType> 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<StripeBillingDetails>,
#[serde(flatten)]
pub(crate) payment_method_details: Option<StripePaymentMethodDetails>, // enum
pub(crate) metadata: Option<Value>,
}
#[derive(Default, PartialEq, Eq, Deserialize, Clone)]
#[serde(rename_all = "snake_case")]
pub(crate) enum StripePaymentMethodDetails {
Card(StripeCard),
#[default]
BankTransfer,
}
impl From<StripeCard> 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<StripePaymentMethodDetails> 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<AddressDetails>,
pub(crate) name: Option<String>,
pub(crate) carrier: Option<String>,
pub(crate) phone: Option<String>,
pub(crate) tracking_number: Option<String>,
}
impl From<Shipping> 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<bool>,
pub(crate) customer: Option<String>,
pub(crate) description: Option<String>,
pub(crate) payment_method_data: Option<StripePaymentMethodData>,
pub(crate) receipt_email: Option<String>,
pub(crate) return_url: Option<String>,
pub(crate) setup_future_usage: Option<api_enums::FutureUsage>,
pub(crate) shipping: Option<Shipping>,
pub(crate) billing_details: Option<StripeBillingDetails>,
pub(crate) statement_descriptor: Option<String>,
pub(crate) statement_descriptor_suffix: Option<String>,
pub(crate) metadata: Option<Value>,
pub(crate) client_secret: Option<String>,
}
impl From<StripeSetupIntentRequest> 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<api_enums::IntentStatus> 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<CancellationReason>,
}
impl From<StripePaymentCancelRequest> 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<String>,
pub(crate) object: String,
pub(crate) status: StripeSetupStatus,
pub(crate) client_secret: Option<Secret<String>>,
#[serde(with = "common_utils::custom_serde::iso8601::option")]
pub(crate) created: Option<time::PrimitiveDateTime>,
pub(crate) customer: Option<String>,
pub(crate) refunds: Option<Vec<RefundResponse>>,
pub(crate) mandate_id: Option<String>,
}
impl From<PaymentsResponse> 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<String>,
pub starting_after: Option<String>,
pub ending_before: Option<String>,
#[serde(default = "default_limit")]
pub limit: i64,
pub created: Option<i64>,
#[serde(rename = "created[lt]")]
pub created_lt: Option<i64>,
#[serde(rename = "created[gt]")]
pub created_gt: Option<i64>,
#[serde(rename = "created[lte]")]
pub created_lte: Option<i64>,
#[serde(rename = "created[gte]")]
pub created_gte: Option<i64>,
}
fn default_limit() -> i64 {
10
}
impl TryFrom<StripePaymentListConstraints> for PaymentListConstraints {
type Error = error_stack::Report<errors::ApiErrorResponse>;
fn try_from(item: StripePaymentListConstraints) -> Result<Self, Self::Error> {
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<i64>,
) -> Result<Option<time::PrimitiveDateTime>, 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)
}
}