Skip to content

Commit 6d235d7

Browse files
feat(connector): [NMI] Add mandates flow (#8652)
1 parent 190d136 commit 6d235d7

File tree

3 files changed

+290
-98
lines changed

3 files changed

+290
-98
lines changed

crates/hyperswitch_connectors/src/connectors/nmi.rs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use hyperswitch_interfaces::{
3838
types::{
3939
PaymentsAuthorizeType, PaymentsCaptureType, PaymentsCompleteAuthorizeType,
4040
PaymentsPreProcessingType, PaymentsSyncType, PaymentsVoidType, RefundExecuteType,
41-
RefundSyncType, Response,
41+
RefundSyncType, Response, SetupMandateType,
4242
},
4343
webhooks::{IncomingWebhook, IncomingWebhookRequestDetails},
4444
};
@@ -48,7 +48,7 @@ use transformers as nmi;
4848

4949
use crate::{
5050
types::ResponseRouterData,
51-
utils::{construct_not_supported_error_report, convert_amount, get_header_key_value},
51+
utils::{self, construct_not_supported_error_report, convert_amount, get_header_key_value},
5252
};
5353

5454
#[derive(Clone)]
@@ -161,6 +161,16 @@ impl ConnectorValidation for Nmi {
161161
// in case we dont have transaction id, we can make psync using attempt id
162162
Ok(())
163163
}
164+
165+
fn validate_mandate_payment(
166+
&self,
167+
pm_type: Option<enums::PaymentMethodType>,
168+
pm_data: hyperswitch_domain_models::payment_method_data::PaymentMethodData,
169+
) -> CustomResult<(), ConnectorError> {
170+
let mandate_supported_pmd =
171+
std::collections::HashSet::from([utils::PaymentMethodDataType::Card]);
172+
utils::is_mandate_supported(pm_data, pm_type, mandate_supported_pmd, self.id())
173+
}
164174
}
165175

166176
impl ConnectorIntegration<PaymentMethodToken, PaymentMethodTokenizationData, PaymentsResponseData>
@@ -194,27 +204,23 @@ impl ConnectorIntegration<SetupMandate, SetupMandateRequestData, PaymentsRespons
194204
req: &SetupMandateRouterData,
195205
_connectors: &Connectors,
196206
) -> CustomResult<RequestContent, ConnectorError> {
197-
let connector_req = nmi::NmiPaymentsRequest::try_from(req)?;
207+
let connector_req = nmi::NmiValidateRequest::try_from(req)?;
198208
Ok(RequestContent::FormUrlEncoded(Box::new(connector_req)))
199209
}
200210

201211
fn build_request(
202212
&self,
203-
_req: &SetupMandateRouterData,
204-
_connectors: &Connectors,
213+
req: &SetupMandateRouterData,
214+
connectors: &Connectors,
205215
) -> CustomResult<Option<Request>, ConnectorError> {
206-
Err(ConnectorError::NotImplemented("Setup Mandate flow for Nmi".to_string()).into())
207-
208-
// Ok(Some(
209-
// RequestBuilder::new()
210-
// .method(Method::Post)
211-
// .url(&SetupMandateType::get_url(self, req, connectors)?)
212-
// .headers(SetupMandateType::get_headers(self, req, connectors)?)
213-
// .set_body(SetupMandateType::get_request_body(
214-
// self, req, connectors,
215-
// )?)
216-
// .build(),
217-
// ))
216+
Ok(Some(
217+
RequestBuilder::new()
218+
.method(Method::Post)
219+
.url(&SetupMandateType::get_url(self, req, connectors)?)
220+
.headers(SetupMandateType::get_headers(self, req, connectors)?)
221+
.set_body(SetupMandateType::get_request_body(self, req, connectors)?)
222+
.build(),
223+
))
218224
}
219225

220226
fn handle_response(

crates/hyperswitch_connectors/src/connectors/nmi/transformers.rs

Lines changed: 154 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use api_models::webhooks::IncomingWebhookEvent;
22
use cards::CardNumber;
3-
use common_enums::{
4-
AttemptStatus, AuthenticationType, CaptureMethod, CountryAlpha2, Currency, RefundStatus,
5-
};
3+
use common_enums::{AttemptStatus, AuthenticationType, CountryAlpha2, Currency, RefundStatus};
64
use common_utils::{errors::CustomResult, ext_traits::XmlExt, pii::Email, types::FloatMajorUnit};
75
use error_stack::{report, Report, ResultExt};
86
use hyperswitch_domain_models::{
@@ -351,6 +349,7 @@ pub struct NmiCompleteResponse {
351349
pub cvvresponse: Option<String>,
352350
pub orderid: String,
353351
pub response_code: String,
352+
customer_vault_id: Option<Secret<String>>,
354353
}
355354

356355
impl
@@ -377,17 +376,27 @@ impl
377376
Ok(PaymentsResponseData::TransactionResponse {
378377
resource_id: ResponseId::ConnectorTransactionId(item.response.transactionid),
379378
redirection_data: Box::new(None),
380-
mandate_reference: Box::new(None),
379+
mandate_reference: match item.response.customer_vault_id {
380+
Some(vault_id) => Box::new(Some(
381+
hyperswitch_domain_models::router_response_types::MandateReference {
382+
connector_mandate_id: Some(vault_id.expose()),
383+
payment_method_id: None,
384+
mandate_metadata: None,
385+
connector_mandate_request_reference_id: None,
386+
},
387+
)),
388+
None => Box::new(None),
389+
},
381390
connector_metadata: None,
382391
network_txn_id: None,
383392
connector_response_reference_id: Some(item.response.orderid),
384393
incremental_authorization_allowed: None,
385394
charges: None,
386395
}),
387-
if let Some(CaptureMethod::Automatic) = item.data.request.capture_method {
388-
AttemptStatus::CaptureInitiated
396+
if item.data.request.is_auto_capture()? {
397+
AttemptStatus::Charged
389398
} else {
390-
AttemptStatus::Authorizing
399+
AttemptStatus::Authorized
391400
},
392401
),
393402
Response::Declined | Response::Error => (
@@ -417,6 +426,18 @@ fn get_nmi_error_response(response: NmiCompleteResponse, http_code: u16) -> Erro
417426
}
418427
}
419428

429+
#[derive(Debug, Serialize)]
430+
pub struct NmiValidateRequest {
431+
#[serde(rename = "type")]
432+
transaction_type: TransactionType,
433+
security_key: Secret<String>,
434+
ccnumber: CardNumber,
435+
ccexp: Secret<String>,
436+
cvv: Secret<String>,
437+
orderid: String,
438+
customer_vault: CustomerAction,
439+
}
440+
420441
#[derive(Debug, Serialize)]
421442
pub struct NmiPaymentsRequest {
422443
#[serde(rename = "type")]
@@ -429,6 +450,8 @@ pub struct NmiPaymentsRequest {
429450
#[serde(flatten)]
430451
merchant_defined_field: Option<NmiMerchantDefinedField>,
431452
orderid: String,
453+
#[serde(skip_serializing_if = "Option::is_none")]
454+
customer_vault: Option<CustomerAction>,
432455
}
433456

434457
#[derive(Debug, Serialize)]
@@ -462,6 +485,12 @@ pub enum PaymentMethod {
462485
CardThreeDs(Box<CardThreeDsData>),
463486
GPay(Box<GooglePayData>),
464487
ApplePay(Box<ApplePayData>),
488+
MandatePayment(Box<MandatePayment>),
489+
}
490+
491+
#[derive(Debug, Serialize)]
492+
pub struct MandatePayment {
493+
customer_vault_id: Secret<String>,
465494
}
466495

467496
#[derive(Debug, Serialize)]
@@ -503,25 +532,70 @@ impl TryFrom<&NmiRouterData<&PaymentsAuthorizeRouterData>> for NmiPaymentsReques
503532
};
504533
let auth_type: NmiAuthType = (&item.router_data.connector_auth_type).try_into()?;
505534
let amount = item.amount;
506-
let payment_method = PaymentMethod::try_from((
507-
&item.router_data.request.payment_method_data,
508-
Some(item.router_data),
509-
))?;
510535

511-
Ok(Self {
512-
transaction_type,
513-
security_key: auth_type.api_key,
514-
amount,
515-
currency: item.router_data.request.currency,
516-
payment_method,
517-
merchant_defined_field: item
518-
.router_data
519-
.request
520-
.metadata
521-
.as_ref()
522-
.map(NmiMerchantDefinedField::new),
523-
orderid: item.router_data.connector_request_reference_id.clone(),
524-
})
536+
match item
537+
.router_data
538+
.request
539+
.mandate_id
540+
.clone()
541+
.and_then(|mandate_ids| mandate_ids.mandate_reference_id)
542+
{
543+
Some(api_models::payments::MandateReferenceId::ConnectorMandateId(
544+
connector_mandate_id,
545+
)) => Ok(Self {
546+
transaction_type,
547+
security_key: auth_type.api_key,
548+
amount,
549+
currency: item.router_data.request.currency,
550+
payment_method: PaymentMethod::MandatePayment(Box::new(MandatePayment {
551+
customer_vault_id: Secret::new(
552+
connector_mandate_id
553+
.get_connector_mandate_id()
554+
.ok_or(ConnectorError::MissingConnectorMandateID)?,
555+
),
556+
})),
557+
merchant_defined_field: item
558+
.router_data
559+
.request
560+
.metadata
561+
.as_ref()
562+
.map(NmiMerchantDefinedField::new),
563+
orderid: item.router_data.connector_request_reference_id.clone(),
564+
customer_vault: None,
565+
}),
566+
Some(api_models::payments::MandateReferenceId::NetworkMandateId(_))
567+
| Some(api_models::payments::MandateReferenceId::NetworkTokenWithNTI(_)) => {
568+
Err(ConnectorError::NotImplemented(
569+
get_unimplemented_payment_method_error_message("nmi"),
570+
))?
571+
}
572+
None => {
573+
let payment_method = PaymentMethod::try_from((
574+
&item.router_data.request.payment_method_data,
575+
Some(item.router_data),
576+
))?;
577+
578+
Ok(Self {
579+
transaction_type,
580+
security_key: auth_type.api_key,
581+
amount,
582+
currency: item.router_data.request.currency,
583+
payment_method,
584+
merchant_defined_field: item
585+
.router_data
586+
.request
587+
.metadata
588+
.as_ref()
589+
.map(NmiMerchantDefinedField::new),
590+
orderid: item.router_data.connector_request_reference_id.clone(),
591+
customer_vault: item
592+
.router_data
593+
.request
594+
.is_mandate_payment()
595+
.then_some(CustomerAction::AddCustomer),
596+
})
597+
}
598+
}
525599
}
526600
}
527601

@@ -670,20 +744,36 @@ impl TryFrom<&ApplePayWalletData> for PaymentMethod {
670744
}
671745
}
672746

673-
impl TryFrom<&SetupMandateRouterData> for NmiPaymentsRequest {
747+
impl TryFrom<&SetupMandateRouterData> for NmiValidateRequest {
674748
type Error = Error;
675749
fn try_from(item: &SetupMandateRouterData) -> Result<Self, Self::Error> {
676-
let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?;
677-
let payment_method = PaymentMethod::try_from((&item.request.payment_method_data, None))?;
678-
Ok(Self {
679-
transaction_type: TransactionType::Validate,
680-
security_key: auth_type.api_key,
681-
amount: FloatMajorUnit::zero(),
682-
currency: item.request.currency,
683-
payment_method,
684-
merchant_defined_field: None,
685-
orderid: item.connector_request_reference_id.clone(),
686-
})
750+
match item.request.amount {
751+
Some(amount) if amount > 0 => Err(ConnectorError::FlowNotSupported {
752+
flow: "Setup Mandate with non zero amount".to_string(),
753+
connector: "NMI".to_string(),
754+
}
755+
.into()),
756+
_ => {
757+
if let PaymentMethodData::Card(card_details) = &item.request.payment_method_data {
758+
let auth_type: NmiAuthType = (&item.connector_auth_type).try_into()?;
759+
Ok(Self {
760+
transaction_type: TransactionType::Validate,
761+
security_key: auth_type.api_key,
762+
ccnumber: card_details.card_number.clone(),
763+
ccexp: card_details
764+
.get_card_expiry_month_year_2_digit_with_delimiter("".to_string())?,
765+
cvv: card_details.card_cvc.clone(),
766+
orderid: item.connector_request_reference_id.clone(),
767+
customer_vault: CustomerAction::AddCustomer,
768+
})
769+
} else {
770+
Err(ConnectorError::NotImplemented(
771+
get_unimplemented_payment_method_error_message("Nmi"),
772+
)
773+
.into())
774+
}
775+
}
776+
}
687777
}
688778
}
689779

@@ -754,7 +844,7 @@ impl
754844
incremental_authorization_allowed: None,
755845
charges: None,
756846
}),
757-
AttemptStatus::CaptureInitiated,
847+
AttemptStatus::Charged,
758848
),
759849
Response::Declined | Response::Error => (
760850
Err(get_standard_error_response(item.response, item.http_code)),
@@ -835,6 +925,7 @@ pub struct StandardResponse {
835925
pub cvvresponse: Option<String>,
836926
pub orderid: String,
837927
pub response_code: String,
928+
pub customer_vault_id: Option<Secret<String>>,
838929
}
839930

840931
impl<T> TryFrom<ResponseRouterData<SetupMandate, StandardResponse, T, PaymentsResponseData>>
@@ -851,7 +942,17 @@ impl<T> TryFrom<ResponseRouterData<SetupMandate, StandardResponse, T, PaymentsRe
851942
item.response.transactionid.clone(),
852943
),
853944
redirection_data: Box::new(None),
854-
mandate_reference: Box::new(None),
945+
mandate_reference: match item.response.customer_vault_id {
946+
Some(vault_id) => Box::new(Some(
947+
hyperswitch_domain_models::router_response_types::MandateReference {
948+
connector_mandate_id: Some(vault_id.expose()),
949+
payment_method_id: None,
950+
mandate_metadata: None,
951+
connector_mandate_request_reference_id: None,
952+
},
953+
)),
954+
None => Box::new(None),
955+
},
855956
connector_metadata: None,
856957
network_txn_id: None,
857958
connector_response_reference_id: Some(item.response.orderid),
@@ -905,15 +1006,25 @@ impl TryFrom<PaymentsResponseRouterData<StandardResponse>>
9051006
item.response.transactionid.clone(),
9061007
),
9071008
redirection_data: Box::new(None),
908-
mandate_reference: Box::new(None),
1009+
mandate_reference: match item.response.customer_vault_id {
1010+
Some(vault_id) => Box::new(Some(
1011+
hyperswitch_domain_models::router_response_types::MandateReference {
1012+
connector_mandate_id: Some(vault_id.expose()),
1013+
payment_method_id: None,
1014+
mandate_metadata: None,
1015+
connector_mandate_request_reference_id: None,
1016+
},
1017+
)),
1018+
None => Box::new(None),
1019+
},
9091020
connector_metadata: None,
9101021
network_txn_id: None,
9111022
connector_response_reference_id: Some(item.response.orderid),
9121023
incremental_authorization_allowed: None,
9131024
charges: None,
9141025
}),
915-
if let Some(CaptureMethod::Automatic) = item.data.request.capture_method {
916-
AttemptStatus::CaptureInitiated
1026+
if item.data.request.is_auto_capture()? {
1027+
AttemptStatus::Charged
9171028
} else {
9181029
AttemptStatus::Authorized
9191030
},
@@ -1102,7 +1213,7 @@ impl TryFrom<RefundsResponseRouterData<Capture, StandardResponse>> for RefundsRo
11021213
impl From<Response> for RefundStatus {
11031214
fn from(item: Response) -> Self {
11041215
match item {
1105-
Response::Approved => Self::Pending,
1216+
Response::Approved => Self::Success,
11061217
Response::Declined | Response::Error => Self::Failure,
11071218
}
11081219
}

0 commit comments

Comments
 (0)