feat(connector): [Authorizedotnet] implement Capture flow and webhooks for Authorizedotnet (#1171)

Signed-off-by: chikke srujan <121822803+srujanchikke@users.noreply.github.com>
Co-authored-by: Jagan <jaganelavarasan@gmail.com>
This commit is contained in:
chikke srujan
2023-05-18 20:11:52 +05:30
committed by GitHub
parent 9cc1ceec69
commit 2d49ce56de
3 changed files with 1085 additions and 473 deletions

View File

@ -3,7 +3,7 @@ use error_stack::ResultExt;
use serde::{Deserialize, Serialize};
use crate::{
connector::utils::{CardData, RefundsRequestData},
connector::utils::{CardData, PaymentsSyncRequestData, RefundsRequestData},
core::errors,
types::{self, api, storage::enums},
utils::OptionExt,
@ -13,6 +13,10 @@ use crate::{
pub enum TransactionType {
#[serde(rename = "authCaptureTransaction")]
Payment,
#[serde(rename = "authOnlyTransaction")]
Authorization,
#[serde(rename = "priorAuthCaptureTransaction")]
Capture,
#[serde(rename = "refundTransaction")]
Refund,
#[serde(rename = "voidTransaction")]
@ -191,8 +195,9 @@ struct AuthorizationIndicator {
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct TransactionVoidRequest {
struct TransactionVoidOrCaptureRequest {
transaction_type: TransactionType,
amount: Option<i64>,
ref_trans_id: String,
}
@ -205,9 +210,9 @@ pub struct AuthorizedotnetPaymentsRequest {
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedotnetPaymentCancelRequest {
pub struct AuthorizedotnetPaymentCancelOrCaptureRequest {
merchant_authentication: MerchantAuthentication,
transaction_request: TransactionVoidRequest,
transaction_request: TransactionVoidOrCaptureRequest,
}
#[derive(Debug, Serialize, PartialEq)]
@ -219,8 +224,8 @@ pub struct CreateTransactionRequest {
#[derive(Debug, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CancelTransactionRequest {
create_transaction_request: AuthorizedotnetPaymentCancelRequest,
pub struct CancelOrCaptureTransactionRequest {
create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest,
}
#[derive(Debug, Serialize, PartialEq, Eq)]
@ -249,7 +254,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest {
authorization_indicator: c.into(),
});
let transaction_request = TransactionRequest {
transaction_type: TransactionType::Payment,
transaction_type: TransactionType::from(item.request.capture_method),
amount: item.request.amount,
payment: payment_details,
currency_code: item.request.currency.to_string(),
@ -269,10 +274,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for CreateTransactionRequest {
}
}
impl TryFrom<&types::PaymentsCancelRouterData> for CancelTransactionRequest {
impl TryFrom<&types::PaymentsCancelRouterData> for CancelOrCaptureTransactionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCancelRouterData) -> Result<Self, Self::Error> {
let transaction_request = TransactionVoidRequest {
let transaction_request = TransactionVoidOrCaptureRequest {
amount: item.request.amount,
transaction_type: TransactionType::Void,
ref_trans_id: item.request.connector_transaction_id.to_string(),
};
@ -280,7 +286,27 @@ impl TryFrom<&types::PaymentsCancelRouterData> for CancelTransactionRequest {
let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?;
Ok(Self {
create_transaction_request: AuthorizedotnetPaymentCancelRequest {
create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest {
merchant_authentication,
transaction_request,
},
})
}
}
impl TryFrom<&types::PaymentsCaptureRouterData> for CancelOrCaptureTransactionRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let transaction_request = TransactionVoidOrCaptureRequest {
amount: Some(item.request.amount_to_capture),
transaction_type: TransactionType::Capture,
ref_trans_id: item.request.connector_transaction_id.to_string(),
};
let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?;
Ok(Self {
create_transaction_request: AuthorizedotnetPaymentCancelOrCaptureRequest {
merchant_authentication,
transaction_request,
},
@ -306,7 +332,7 @@ pub type AuthorizedotnetRefundStatus = AuthorizedotnetPaymentStatus;
impl From<AuthorizedotnetPaymentStatus> for enums::AttemptStatus {
fn from(item: AuthorizedotnetPaymentStatus) -> Self {
match item {
AuthorizedotnetPaymentStatus::Approved => Self::Charged,
AuthorizedotnetPaymentStatus::Approved => Self::Pending,
AuthorizedotnetPaymentStatus::Declined | AuthorizedotnetPaymentStatus::Error => {
Self::Failure
}
@ -316,9 +342,9 @@ impl From<AuthorizedotnetPaymentStatus> for enums::AttemptStatus {
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
struct ResponseMessage {
pub struct ResponseMessage {
code: String,
text: String,
pub text: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
@ -331,14 +357,14 @@ enum ResultCode {
#[serde(rename_all = "camelCase")]
pub struct ResponseMessages {
result_code: ResultCode,
message: Vec<ResponseMessage>,
pub message: Vec<ResponseMessage>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub(super) struct ErrorMessage {
pub(super) error_code: String,
pub(super) error_text: String,
pub struct ErrorMessage {
pub error_code: String,
pub error_text: String,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
@ -356,10 +382,51 @@ pub struct TransactionResponse {
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedotnetPaymentsResponse {
pub transaction_response: TransactionResponse,
pub transaction_response: Option<TransactionResponse>,
pub messages: ResponseMessages,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedotnetVoidResponse {
pub transaction_response: Option<VoidResponse>,
pub messages: ResponseMessages,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VoidResponse {
response_code: AuthorizedotnetVoidStatus,
auth_code: String,
#[serde(rename = "transId")]
transaction_id: String,
network_trans_id: Option<String>,
pub account_number: Option<String>,
pub errors: Option<Vec<ErrorMessage>>,
}
#[derive(Debug, Clone, Deserialize)]
pub enum AuthorizedotnetVoidStatus {
#[serde(rename = "1")]
Approved,
#[serde(rename = "2")]
Declined,
#[serde(rename = "3")]
Error,
#[serde(rename = "4")]
HeldForReview,
}
impl From<AuthorizedotnetVoidStatus> for enums::AttemptStatus {
fn from(item: AuthorizedotnetVoidStatus) -> Self {
match item {
AuthorizedotnetVoidStatus::Approved => Self::VoidInitiated,
AuthorizedotnetVoidStatus::Declined | AuthorizedotnetVoidStatus::Error => Self::Failure,
AuthorizedotnetVoidStatus::HeldForReview => Self::Pending,
}
}
}
impl<F, T>
TryFrom<
types::ResponseRouterData<
@ -379,50 +446,115 @@ impl<F, T>
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let status = enums::AttemptStatus::from(item.response.transaction_response.response_code);
let error = item
.response
.transaction_response
.errors
.and_then(|errors| {
errors.into_iter().next().map(|error| types::ErrorResponse {
code: error.error_code,
message: error.error_text,
reason: None,
status_code: item.http_code,
match &item.response.transaction_response {
Some(transaction_response) => {
let status = enums::AttemptStatus::from(transaction_response.response_code.clone());
let error = transaction_response.errors.as_ref().and_then(|errors| {
errors.iter().next().map(|error| types::ErrorResponse {
code: error.error_code.clone(),
message: error.error_text.clone(),
reason: None,
status_code: item.http_code,
})
});
let metadata = transaction_response
.account_number
.as_ref()
.map(|acc_no| {
Encode::<'_, PaymentDetails>::encode_to_value(
&construct_refund_payment_details(acc_no.clone()),
)
})
.transpose()
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "connector_metadata",
})?;
Ok(Self {
status,
response: match error {
Some(err) => Err(err),
None => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_response.transaction_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: metadata,
network_txn_id: transaction_response.network_trans_id.clone(),
}),
},
..item.data
})
});
}
None => Ok(Self {
status: enums::AttemptStatus::Failure,
response: Err(get_err_response(item.http_code, item.response.messages)),
..item.data
}),
}
}
}
let metadata = item
.response
.transaction_response
.account_number
.map(|acc_no| {
Encode::<'_, PaymentDetails>::encode_to_value(&construct_refund_payment_details(
acc_no,
))
})
.transpose()
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "connector_metadata",
})?;
Ok(Self {
status,
response: match error {
Some(err) => Err(err),
None => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.transaction_response.transaction_id,
),
redirection_data: None,
mandate_reference: None,
connector_metadata: metadata,
network_txn_id: item.response.transaction_response.network_trans_id,
}),
},
..item.data
})
impl<F, T>
TryFrom<
types::ResponseRouterData<F, AuthorizedotnetVoidResponse, T, types::PaymentsResponseData>,
> for types::RouterData<F, T, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(
item: types::ResponseRouterData<
F,
AuthorizedotnetVoidResponse,
T,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
match &item.response.transaction_response {
Some(transaction_response) => {
let status = enums::AttemptStatus::from(transaction_response.response_code.clone());
let error = transaction_response.errors.as_ref().and_then(|errors| {
errors.iter().next().map(|error| types::ErrorResponse {
code: error.error_code.clone(),
message: error.error_text.clone(),
reason: None,
status_code: item.http_code,
})
});
let metadata = transaction_response
.account_number
.as_ref()
.map(|acc_no| {
Encode::<'_, PaymentDetails>::encode_to_value(
&construct_refund_payment_details(acc_no.clone()),
)
})
.transpose()
.change_context(errors::ConnectorError::MissingRequiredField {
field_name: "connector_metadata",
})?;
Ok(Self {
status,
response: match error {
Some(err) => Err(err),
None => Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction_response.transaction_id.clone(),
),
redirection_data: None,
mandate_reference: None,
connector_metadata: metadata,
network_txn_id: transaction_response.network_trans_id.clone(),
}),
},
..item.data
})
}
None => Ok(Self {
status: enums::AttemptStatus::Failure,
response: Err(get_err_response(item.http_code, item.response.messages)),
..item.data
}),
}
}
}
@ -571,22 +703,11 @@ impl TryFrom<&types::PaymentsSyncRouterData> for AuthorizedotnetCreateSyncReques
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsSyncRouterData) -> Result<Self, Self::Error> {
let transaction_id = item
.response
.as_ref()
.ok()
.map(|payment_response_data| match payment_response_data {
types::PaymentsResponseData::TransactionResponse { resource_id, .. } => {
resource_id.get_connector_transaction_id()
}
_ => Err(error_stack::report!(
errors::ValidationError::MissingRequiredField {
field_name: "transaction_id".to_string()
}
)),
})
.transpose()
.change_context(errors::ConnectorError::ResponseHandlingFailed)?;
let transaction_id = Some(
item.request
.get_connector_transaction_id()
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
);
let merchant_authentication = MerchantAuthentication::try_from(&item.connector_auth_type)?;
@ -612,6 +733,8 @@ pub enum SyncStatus {
Voided,
CouldNotVoid,
GeneralError,
#[serde(rename = "FDSPendingReview")]
FDSPendingReview,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
@ -623,7 +746,8 @@ pub struct SyncTransactionResponse {
#[derive(Debug, Deserialize)]
pub struct AuthorizedotnetSyncResponse {
transaction: SyncTransactionResponse,
transaction: Option<SyncTransactionResponse>,
messages: ResponseMessages,
}
impl From<SyncStatus> for enums::RefundStatus {
@ -639,9 +763,9 @@ impl From<SyncStatus> for enums::RefundStatus {
impl From<SyncStatus> for enums::AttemptStatus {
fn from(transaction_status: SyncStatus) -> Self {
match transaction_status {
SyncStatus::SettledSuccessfully | SyncStatus::CapturedPendingSettlement => {
Self::Charged
}
SyncStatus::SettledSuccessfully => Self::Charged,
SyncStatus::CapturedPendingSettlement => Self::CaptureInitiated,
SyncStatus::AuthorizedPendingCapture => Self::Authorized,
SyncStatus::Declined => Self::AuthenticationFailed,
SyncStatus::Voided => Self::Voided,
SyncStatus::CouldNotVoid => Self::VoidFailed,
@ -659,14 +783,22 @@ impl TryFrom<types::RefundsResponseRouterData<api::RSync, AuthorizedotnetSyncRes
fn try_from(
item: types::RefundsResponseRouterData<api::RSync, AuthorizedotnetSyncResponse>,
) -> Result<Self, Self::Error> {
let refund_status = enums::RefundStatus::from(item.response.transaction.transaction_status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.transaction.transaction_id.clone(),
refund_status,
match item.response.transaction {
Some(transaction) => {
let refund_status = enums::RefundStatus::from(transaction.transaction_status);
Ok(Self {
response: Ok(types::RefundsResponseData {
connector_refund_id: transaction.transaction_id,
refund_status,
}),
..item.data
})
}
None => Ok(Self {
response: Err(get_err_response(item.http_code, item.response.messages)),
..item.data
}),
..item.data
})
}
}
}
@ -685,21 +817,28 @@ impl<F, Req>
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> {
let payment_status =
enums::AttemptStatus::from(item.response.transaction.transaction_status);
Ok(Self {
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.response.transaction.transaction_id,
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
match item.response.transaction {
Some(transaction) => {
let payment_status = enums::AttemptStatus::from(transaction.transaction_status);
Ok(Self {
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
transaction.transaction_id,
),
redirection_data: None,
mandate_reference: None,
connector_metadata: None,
network_txn_id: None,
}),
status: payment_status,
..item.data
})
}
None => Ok(Self {
response: Err(get_err_response(item.http_code, item.response.messages)),
..item.data
}),
status: payment_status,
..item.data
})
}
}
}
@ -724,3 +863,78 @@ fn construct_refund_payment_details(masked_number: String) -> PaymentDetails {
card_code: None,
})
}
impl From<Option<enums::CaptureMethod>> for TransactionType {
fn from(capture_method: Option<enums::CaptureMethod>) -> Self {
match capture_method {
Some(enums::CaptureMethod::Manual) => Self::Authorization,
_ => Self::Payment,
}
}
}
fn get_err_response(status_code: u16, message: ResponseMessages) -> types::ErrorResponse {
types::ErrorResponse {
code: message.message[0].code.clone(),
message: message.message[0].text.clone(),
reason: None,
status_code,
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedotnetWebhookObjectId {
pub webhook_id: String,
pub event_type: AuthorizedotnetWebhookEvent,
pub payload: AuthorizedotnetWebhookPayload,
}
#[derive(Debug, Deserialize)]
pub struct AuthorizedotnetWebhookPayload {
pub id: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AuthorizedotnetWebhookEventType {
pub event_type: AuthorizedotnetWebhookEvent,
}
#[derive(Debug, Deserialize)]
pub enum AuthorizedotnetWebhookEvent {
#[serde(rename = "net.authorize.payment.authorization.created")]
AuthorizationCreated,
#[serde(rename = "net.authorize.payment.priorAuthCapture.created")]
PriorAuthCapture,
#[serde(rename = "net.authorize.payment.authcapture.created")]
AuthCapCreated,
#[serde(rename = "net.authorize.payment.capture.created")]
CaptureCreated,
#[serde(rename = "net.authorize.payment.void.created")]
VoidCreated,
#[serde(rename = "net.authorize.payment.refund.created")]
RefundCreated,
}
impl From<AuthorizedotnetWebhookEvent> for api::IncomingWebhookEvent {
fn from(event_type: AuthorizedotnetWebhookEvent) -> Self {
match event_type {
AuthorizedotnetWebhookEvent::AuthorizationCreated
| AuthorizedotnetWebhookEvent::PriorAuthCapture
| AuthorizedotnetWebhookEvent::AuthCapCreated
| AuthorizedotnetWebhookEvent::CaptureCreated
| AuthorizedotnetWebhookEvent::VoidCreated => Self::PaymentIntentSuccess,
AuthorizedotnetWebhookEvent::RefundCreated => Self::RefundSuccess,
}
}
}
pub fn get_trans_id(
details: AuthorizedotnetWebhookObjectId,
) -> Result<String, errors::ConnectorError> {
details
.payload
.id
.ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)
}