feat(connector): [Noon] Add Card Mandates and Webhooks Support (#1243)

Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
SamraatBansal
2023-06-05 15:06:59 +05:30
committed by GitHub
parent fc6acd04cb
commit ba8a17d66f
16 changed files with 256 additions and 34 deletions

View File

@ -1057,7 +1057,7 @@ impl api::IncomingWebhook for Bluesnap {
let details: bluesnap::BluesnapWebhookObjectResource =
serde_urlencoded::from_bytes(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
let res_json =
utils::Encode::<transformers::BluesnapWebhookObjectResource>::encode_to_value(&details)
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;

View File

@ -3,6 +3,7 @@ mod transformers;
use std::fmt::Debug;
use base64::Engine;
use common_utils::{crypto, ext_traits::ByteSliceExt};
use error_stack::{IntoReport, ResultExt};
use transformers as noon;
@ -14,6 +15,7 @@ use crate::{
errors::{self, CustomResult},
payments,
},
db::StorageInterface,
headers,
services::{
self,
@ -585,24 +587,112 @@ impl services::ConnectorRedirectResponse for Noon {
#[async_trait::async_trait]
impl api::IncomingWebhook for Noon {
fn get_webhook_object_reference_id(
fn get_webhook_source_verification_algorithm(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
Ok(Box::new(crypto::HmacSha512))
}
fn get_webhook_source_verification_signature(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let webhook_body: noon::NoonWebhookSignature = request
.body
.parse_struct("NoonWebhookSignature")
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
let signature = webhook_body.signature;
consts::BASE64_ENGINE
.decode(signature)
.into_report()
.change_context(errors::ConnectorError::WebhookSignatureNotFound)
}
fn get_webhook_source_verification_message(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
_merchant_id: &str,
_secret: &[u8],
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let webhook_body: noon::NoonWebhookBody = request
.body
.parse_struct("NoonWebhookBody")
.change_context(errors::ConnectorError::WebhookSignatureNotFound)?;
let message = format!(
"{},{},{},{},{}",
webhook_body.order_id,
webhook_body.order_status,
webhook_body.event_id,
webhook_body.event_type,
webhook_body.time_stamp,
);
Ok(message.into_bytes())
}
async fn get_webhook_source_verification_merchant_secret(
&self,
db: &dyn StorageInterface,
merchant_id: &str,
) -> CustomResult<Vec<u8>, errors::ConnectorError> {
let key = format!("whsec_verification_{}_{}", self.id(), merchant_id);
let secret = match db.find_config_by_key(&key).await {
Ok(config) => Some(config),
Err(e) => {
crate::logger::warn!("Unable to fetch merchant webhook secret from DB: {:#?}", e);
None
}
};
Ok(secret
.map(|conf| conf.config.into_bytes())
.unwrap_or_default())
}
fn get_webhook_object_reference_id(
&self,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::webhooks::ObjectReferenceId, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let details: noon::NoonWebhookOrderId = request
.body
.parse_struct("NoonWebhookOrderId")
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
Ok(api_models::webhooks::ObjectReferenceId::PaymentId(
api_models::payments::PaymentIdType::ConnectorTransactionId(
details.order_id.to_string(),
),
))
}
fn get_webhook_event_type(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let details: noon::NoonWebhookEvent = request
.body
.parse_struct("NoonWebhookEvent")
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
Ok(match &details.event_type {
noon::NoonWebhookEventTypes::Sale | noon::NoonWebhookEventTypes::Capture => {
match &details.order_status {
noon::NoonPaymentStatus::Captured => {
api::IncomingWebhookEvent::PaymentIntentSuccess
}
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound)?,
}
}
noon::NoonWebhookEventTypes::Fail => api::IncomingWebhookEvent::PaymentIntentFailure,
_ => Err(errors::ConnectorError::WebhookEventTypeNotFound)?,
})
}
fn get_webhook_resource_object(
&self,
_request: &api::IncomingWebhookRequestDetails<'_>,
request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
Err(errors::ConnectorError::WebhooksNotImplemented).into_report()
let reference_object: serde_json::Value = serde_json::from_slice(request.body)
.into_report()
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
Ok(reference_object)
}
}

View File

@ -16,13 +16,27 @@ pub enum NoonChannels {
Web,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum NoonSubscriptionType {
Unscheduled,
}
#[derive(Debug, Serialize)]
pub struct NoonSubscriptionData {
#[serde(rename = "type")]
subscription_type: NoonSubscriptionType,
//Short description about the subscription.
name: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonOrder {
amount: String,
currency: storage_models::enums::Currency,
currency: Option<storage_models::enums::Currency>,
channel: NoonChannels,
category: String,
category: Option<String>,
//Short description of the order.
name: String,
}
@ -37,10 +51,17 @@ pub enum NoonPaymentActions {
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonConfiguration {
tokenize_c_c: Option<bool>,
payment_action: NoonPaymentActions,
return_url: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonSubscription {
subscription_identifier: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonCard {
@ -55,6 +76,7 @@ pub struct NoonCard {
#[serde(tag = "type", content = "data")]
pub enum NoonPaymentData {
Card(NoonCard),
Subscription(NoonSubscription),
}
#[derive(Debug, Serialize)]
@ -72,30 +94,55 @@ pub struct NoonPaymentsRequest {
order: NoonOrder,
configuration: NoonConfiguration,
payment_data: NoonPaymentData,
subscription: Option<NoonSubscriptionData>,
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
let payment_data = match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard {
name_on_card: req_card.card_holder_name,
number_plain: req_card.card_number,
expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year,
cvv: req_card.card_cvc,
})),
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
)),
}?;
let (payment_data, currency, category) = match item.request.connector_mandate_id() {
Some(subscription_identifier) => (
NoonPaymentData::Subscription(NoonSubscription {
subscription_identifier,
}),
None,
None,
),
_ => (
match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => Ok(NoonPaymentData::Card(NoonCard {
name_on_card: req_card.card_holder_name,
number_plain: req_card.card_number,
expiry_month: req_card.card_exp_month,
expiry_year: req_card.card_exp_year,
cvv: req_card.card_cvc,
})),
_ => Err(errors::ConnectorError::NotImplemented(
"Payment methods".to_string(),
)),
}?,
Some(item.request.currency),
item.request.order_category.clone(),
),
};
let name = item.get_description()?;
let (subscription, tokenize_c_c) =
match item.request.setup_future_usage.is_some().then_some((
NoonSubscriptionData {
subscription_type: NoonSubscriptionType::Unscheduled,
name: name.clone(),
},
true,
)) {
Some((a, b)) => (Some(a), Some(b)),
None => (None, None),
};
let order = NoonOrder {
amount: conn_utils::to_currency_base_unit(item.request.amount, item.request.currency)?,
currency: item.request.currency,
currency,
channel: NoonChannels::Web,
category: "pay".to_string(),
name: item.get_description()?,
category,
name,
};
let payment_action = if item.request.is_auto_capture()? {
NoonPaymentActions::Sale
@ -108,8 +155,10 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest {
configuration: NoonConfiguration {
payment_action,
return_url: item.request.router_return_url.clone(),
tokenize_c_c,
},
payment_data,
subscription,
})
}
}
@ -138,13 +187,15 @@ impl TryFrom<&types::ConnectorAuthType> for NoonAuthType {
}
}
}
#[derive(Default, Debug, Deserialize)]
#[derive(Default, Debug, Deserialize, strum::Display)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[strum(serialize_all = "UPPERCASE")]
pub enum NoonPaymentStatus {
Authorized,
Captured,
PartiallyCaptured,
Reversed,
Cancelled,
#[serde(rename = "3DS_ENROLL_INITIATED")]
ThreeDsEnrollInitiated,
Failed,
@ -158,6 +209,7 @@ impl From<NoonPaymentStatus> for enums::AttemptStatus {
NoonPaymentStatus::Authorized => Self::Authorized,
NoonPaymentStatus::Captured | NoonPaymentStatus::PartiallyCaptured => Self::Charged,
NoonPaymentStatus::Reversed => Self::Voided,
NoonPaymentStatus::Cancelled => Self::AuthenticationFailed,
NoonPaymentStatus::ThreeDsEnrollInitiated => Self::AuthenticationPending,
NoonPaymentStatus::Failed => Self::Failure,
NoonPaymentStatus::Pending => Self::Pending,
@ -165,6 +217,11 @@ impl From<NoonPaymentStatus> for enums::AttemptStatus {
}
}
#[derive(Debug, Deserialize)]
pub struct NoonSubscriptionResponse {
identifier: String,
}
#[derive(Default, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonPaymentsOrderResponse {
@ -183,6 +240,7 @@ pub struct NoonCheckoutData {
pub struct NoonPaymentsResponseResult {
order: NoonPaymentsOrderResponse,
checkout_data: Option<NoonCheckoutData>,
subscription: Option<NoonSubscriptionResponse>,
}
#[derive(Debug, Deserialize)]
@ -205,6 +263,14 @@ impl<F, T>
form_fields: std::collections::HashMap::new(),
}
});
let mandate_reference =
item.response
.result
.subscription
.map(|subscription_data| types::MandateReference {
connector_mandate_id: Some(subscription_data.identifier),
payment_method_id: None,
});
Ok(Self {
status: enums::AttemptStatus::from(item.response.result.order.status),
response: Ok(types::PaymentsResponseData::TransactionResponse {
@ -212,7 +278,7 @@ impl<F, T>
item.response.result.order.id.to_string(),
),
redirection_data,
mandate_reference: None,
mandate_reference,
connector_metadata: None,
network_txn_id: None,
}),
@ -399,6 +465,44 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, RefundSyncResponse>>
}
}
#[derive(Debug, Deserialize, strum::Display)]
pub enum NoonWebhookEventTypes {
Authenticate,
Authorize,
Capture,
Fail,
Refund,
Sale,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonWebhookBody {
pub order_id: u64,
pub order_status: NoonPaymentStatus,
pub event_type: NoonWebhookEventTypes,
pub event_id: String,
pub time_stamp: String,
}
#[derive(Debug, Deserialize)]
pub struct NoonWebhookSignature {
pub signature: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonWebhookOrderId {
pub order_id: u64,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonWebhookEvent {
pub order_status: NoonPaymentStatus,
pub event_type: NoonWebhookEventTypes,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NoonErrorResponse {

View File

@ -364,6 +364,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
}),
allowed_payment_method_types: None,
order_category: None,
}),
..Default::default()
};

View File

@ -393,9 +393,13 @@ pub fn validate_request_amount_and_amount_to_capture(
pub fn validate_mandate(
req: impl Into<api::MandateValidationFields>,
is_confirm_operation: bool,
) -> RouterResult<Option<api::MandateTxnType>> {
) -> CustomResult<Option<api::MandateTxnType>, errors::ApiErrorResponse> {
let req: api::MandateValidationFields = req.into();
match req.is_mandate() {
match req.validate_and_get_mandate_type().change_context(
errors::ApiErrorResponse::MandateValidationFailed {
reason: "Expected one out of mandate_id and mandate_data but got both".to_string(),
},
)? {
Some(api::MandateTxnType::NewMandateTxn) => {
validate_new_mandate_request(req, is_confirm_operation)?;
Ok(Some(api::MandateTxnType::NewMandateTxn))

View File

@ -605,6 +605,9 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
.transpose()
.unwrap_or_default();
let order_category = parsed_metadata
.as_ref()
.and_then(|data| data.order_category.clone());
let order_details = parsed_metadata.and_then(|data| data.order_details);
let complete_authorize_url = Some(helpers::create_complete_authorize_url(
router_base_url,
@ -647,6 +650,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
email: payment_data.email,
payment_experience: payment_data.payment_attempt.payment_experience,
order_details,
order_category,
session_token: None,
enrolled_for_3ds: true,
related_transaction_id: None,

View File

@ -226,6 +226,7 @@ pub struct PaymentsAuthorizeData {
pub setup_mandate_details: Option<payments::MandateData>,
pub browser_info: Option<BrowserInformation>,
pub order_details: Option<api_models::payments::OrderDetails>,
pub order_category: Option<String>,
pub session_token: Option<String>,
pub enrolled_for_3ds: bool,
pub related_transaction_id: Option<String>,
@ -729,6 +730,7 @@ impl From<&VerifyRouterData> for PaymentsAuthorizeData {
complete_authorize_url: None,
browser_info: None,
order_details: None,
order_category: None,
session_token: None,
enrolled_for_3ds: true,
related_transaction_id: None,

View File

@ -113,15 +113,23 @@ impl PaymentIdTypeExt for PaymentIdType {
}
pub(crate) trait MandateValidationFieldsExt {
fn is_mandate(&self) -> Option<MandateTxnType>;
fn validate_and_get_mandate_type(
&self,
) -> errors::CustomResult<Option<MandateTxnType>, errors::ValidationError>;
}
impl MandateValidationFieldsExt for MandateValidationFields {
fn is_mandate(&self) -> Option<MandateTxnType> {
fn validate_and_get_mandate_type(
&self,
) -> errors::CustomResult<Option<MandateTxnType>, errors::ValidationError> {
match (&self.mandate_data, &self.mandate_id) {
(None, None) => None,
(_, Some(_)) => Some(MandateTxnType::RecurringMandateTxn),
(Some(_), _) => Some(MandateTxnType::NewMandateTxn),
(None, None) => Ok(None),
(Some(_), Some(_)) => Err(errors::ValidationError::InvalidValue {
message: "Expected one out of mandate_id and mandate_data but got both".to_string(),
})
.into_report(),
(_, Some(_)) => Ok(Some(MandateTxnType::RecurringMandateTxn)),
(Some(_), _) => Ok(Some(MandateTxnType::NewMandateTxn)),
}
}
}

View File

@ -53,6 +53,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData {
capture_method: None,
browser_info: None,
order_details: None,
order_category: None,
email: None,
session_token: None,
enrolled_for_3ds: false,

View File

@ -81,6 +81,7 @@ impl AdyenTest {
capture_method: Some(capture_method),
browser_info: None,
order_details: None,
order_category: None,
email: None,
payment_experience: None,
payment_method_type: None,

View File

@ -75,6 +75,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
// capture_method: Some(capture_method),
browser_info: None,
order_details: None,
order_category: None,
email: None,
payment_experience: None,
payment_method_type: None,

View File

@ -77,6 +77,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
// capture_method: Some(capture_method),
browser_info: None,
order_details: None,
order_category: None,
email: None,
payment_experience: None,
payment_method_type: None,

View File

@ -76,6 +76,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
// capture_method: Some(capture_method),
browser_info: None,
order_details: None,
order_category: None,
email: None,
payment_experience: None,
payment_method_type: None,

View File

@ -508,6 +508,7 @@ impl Default for PaymentAuthorizeType {
setup_mandate_details: None,
browser_info: Some(BrowserInfoType::default().0),
order_details: None,
order_category: None,
email: None,
session_token: None,
enrolled_for_3ds: false,

View File

@ -84,6 +84,7 @@ impl WorldlineTest {
capture_method: Some(capture_method),
browser_info: None,
order_details: None,
order_category: None,
email: None,
session_token: None,
enrolled_for_3ds: false,