feat(connectors): implement capture flow for checkout and adyen connectors (#259)

This commit is contained in:
Abhishek
2023-01-05 18:25:46 +05:30
committed by GitHub
parent c7c3f94a76
commit adb048dbc6
7 changed files with 369 additions and 18 deletions

View File

@ -81,7 +81,90 @@ impl
types::PaymentsResponseData,
> for Adyen
{
// Not Implemented (R)
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
self.common_get_content_type().to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.as_str();
Ok(format!(
"{}{}/{}/captures",
self.base_url(connectors),
"v68/payments",
id
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let adyen_req = utils::Encode::<adyen::AdyenCaptureRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(adyen_req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: adyen::AdyenCaptureResponse = res
.response
.parse_struct("AdyenCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: adyen::ErrorResponse = res
.parse_struct("adyen::ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
code: response.error_code,
message: response.message,
reason: None,
})
}
}
impl
@ -189,12 +272,15 @@ impl
.response
.parse_struct("AdyenPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
let is_manual_capture = false;
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
},
is_manual_capture,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
@ -294,12 +380,16 @@ impl
.response
.parse_struct("AdyenPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
let is_manual_capture =
data.request.capture_method == Some(storage_models::enums::CaptureMethod::Manual);
types::RouterData::try_from((
types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
},
is_manual_capture,
))
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}

View File

@ -35,6 +35,16 @@ pub enum AdyenRecurringModel {
UnscheduledCardOnFile,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub enum AuthType {
#[default]
PreAuth,
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct AdditionalData {
authorisation_type: AuthType,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenPaymentRequest {
@ -47,6 +57,7 @@ pub struct AdyenPaymentRequest {
shopper_interaction: AdyenShopperInteraction,
#[serde(skip_serializing_if = "Option::is_none")]
recurring_processing_model: Option<AdyenRecurringModel>,
additional_data: Option<AdditionalData>,
}
#[derive(Debug, Serialize)]
@ -363,6 +374,13 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest {
None
};
let additional_data = match item.request.capture_method {
Some(storage_models::enums::CaptureMethod::Manual) => Some(AdditionalData {
authorisation_type: AuthType::PreAuth,
}),
_ => None,
};
Ok(Self {
amount,
merchant_account: auth_type.merchant_account,
@ -376,6 +394,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for AdyenPaymentRequest {
shopper_interaction,
recurring_processing_model,
browser_info,
additional_data,
})
}
}
@ -419,6 +438,7 @@ impl TryFrom<types::PaymentsCancelResponseRouterData<AdyenCancelResponse>>
pub fn get_adyen_response(
response: AdyenResponse,
is_capture_manual: bool,
) -> errors::CustomResult<
(
storage_enums::AttemptStatus,
@ -429,7 +449,13 @@ pub fn get_adyen_response(
> {
let result = response.result_code;
let status = match result.as_str() {
"Authorised" => storage_enums::AttemptStatus::Charged,
"Authorised" => {
if is_capture_manual {
storage_enums::AttemptStatus::Authorized
} else {
storage_enums::AttemptStatus::Charged
}
}
"Refused" => storage_enums::AttemptStatus::Failure,
_ => storage_enums::AttemptStatus::Pending,
};
@ -522,15 +548,24 @@ pub fn get_redirection_response(
}
impl<F, Req>
TryFrom<types::ResponseRouterData<F, AdyenPaymentResponse, Req, types::PaymentsResponseData>>
for types::RouterData<F, Req, types::PaymentsResponseData>
TryFrom<(
types::ResponseRouterData<F, AdyenPaymentResponse, Req, types::PaymentsResponseData>,
bool,
)> for types::RouterData<F, Req, types::PaymentsResponseData>
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::ResponseRouterData<F, AdyenPaymentResponse, Req, types::PaymentsResponseData>,
items: (
types::ResponseRouterData<F, AdyenPaymentResponse, Req, types::PaymentsResponseData>,
bool,
),
) -> Result<Self, Self::Error> {
let item = items.0;
let is_manual_capture = items.1;
let (status, error, payment_response_data) = match item.response {
AdyenPaymentResponse::AdyenResponse(response) => get_adyen_response(response)?,
AdyenPaymentResponse::AdyenResponse(response) => {
get_adyen_response(response, is_manual_capture)?
}
AdyenPaymentResponse::AdyenRedirectResponse(response) => {
get_redirection_response(response)?
}
@ -539,7 +574,70 @@ impl<F, Req>
Ok(Self {
status,
response: error.map_or_else(|| Ok(payment_response_data), Err),
..item.data
})
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenCaptureRequest {
merchant_account: String,
amount: Amount,
reference: String,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for AdyenCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let auth_type = AdyenAuthType::try_from(&item.connector_auth_type)?;
Ok(Self {
merchant_account: auth_type.merchant_account,
reference: item.payment_id.to_string(),
amount: Amount {
currency: item.request.currency.to_string(),
value: item
.request
.amount_to_capture
.unwrap_or(item.request.amount),
},
})
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdyenCaptureResponse {
merchant_account: String,
payment_psp_reference: String,
psp_reference: String,
reference: String,
status: String,
amount: Amount,
}
impl TryFrom<types::PaymentsCaptureResponseRouterData<AdyenCaptureResponse>>
for types::PaymentsCaptureRouterData
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::PaymentsCaptureResponseRouterData<AdyenCaptureResponse>,
) -> Result<Self, Self::Error> {
let (status, amount_captured) = match item.response.status.as_str() {
"received" => (
storage_enums::AttemptStatus::Charged,
Some(item.response.amount.value),
),
_ => (storage_enums::AttemptStatus::Pending, None),
};
Ok(Self {
status,
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(item.response.psp_reference),
redirect: false,
redirection_data: None,
mandate_reference: None,
}),
amount_captured,
..item.data
})
}

View File

@ -87,6 +87,97 @@ impl
types::PaymentsResponseData,
> for Checkout
{
fn get_headers(
&self,
req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, String)>, errors::ConnectorError> {
let mut header = vec![
(
headers::CONTENT_TYPE.to_string(),
self.common_get_content_type().to_string(),
),
(headers::X_ROUTER.to_string(), "test".to_string()),
];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header)
}
fn get_url(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> {
let id = req.request.connector_transaction_id.as_str();
Ok(format!(
"{}payments/{id}/captures",
self.base_url(connectors)
))
}
fn get_request_body(
&self,
req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<String>, errors::ConnectorError> {
let checkout_req =
utils::Encode::<checkout::PaymentCaptureRequest>::convert_and_encode(req)
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(checkout_req))
}
fn build_request(
&self,
req: &types::PaymentsCaptureRouterData,
connectors: &settings::Connectors,
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some(
services::RequestBuilder::new()
.method(services::Method::Post)
.url(&types::PaymentsCaptureType::get_url(self, req, connectors)?)
.headers(types::PaymentsCaptureType::get_headers(
self, req, connectors,
)?)
.body(types::PaymentsCaptureType::get_request_body(self, req)?)
.build(),
))
}
fn handle_response(
&self,
data: &types::PaymentsCaptureRouterData,
res: types::Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: checkout::PaymentCaptureResponse = res
.response
.parse_struct("CaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
fn get_error_response(
&self,
res: Bytes,
) -> CustomResult<types::ErrorResponse, errors::ConnectorError> {
let response: checkout::ErrorResponse = res
.parse_struct("ErrorResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
Ok(types::ErrorResponse {
code: response
.error_codes
.unwrap_or_else(|| vec![consts::NO_ERROR_CODE.to_string()])
.join(" & "),
message: response
.error_type
.unwrap_or_else(|| consts::NO_ERROR_MESSAGE.to_string()),
reason: None,
})
}
}
impl

View File

@ -276,6 +276,7 @@ pub struct PaymentVoidResponse {
action_id: String,
reference: String,
}
impl From<&PaymentVoidResponse> for enums::AttemptStatus {
fn from(item: &PaymentVoidResponse) -> Self {
if item.status == 202 {
@ -316,6 +317,69 @@ impl TryFrom<&types::PaymentsCancelRouterData> for PaymentVoidRequest {
}
}
#[derive(Debug, Serialize)]
pub enum CaptureType {
Final,
NonFinal,
}
#[derive(Debug, Serialize)]
pub struct PaymentCaptureRequest {
pub amount: Option<i64>,
pub capture_type: Option<CaptureType>,
pub processing_channel_id: String,
}
impl TryFrom<&types::PaymentsCaptureRouterData> for PaymentCaptureRequest {
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
let connector_auth = &item.connector_auth_type;
let auth_type: CheckoutAuthType = connector_auth.try_into()?;
let processing_channel_id = auth_type.processing_channel_id;
Ok(Self {
amount: item.request.amount_to_capture,
capture_type: Some(CaptureType::Final),
processing_channel_id,
})
}
}
#[derive(Debug, Deserialize)]
pub struct PaymentCaptureResponse {
pub action_id: String,
}
impl TryFrom<types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>>
for types::PaymentsCaptureRouterData
{
type Error = error_stack::Report<errors::ParsingError>;
fn try_from(
item: types::PaymentsCaptureResponseRouterData<PaymentCaptureResponse>,
) -> Result<Self, Self::Error> {
let (status, amount_captured) = if item.http_code == 202 {
(
enums::AttemptStatus::Charged,
item.data.request.amount_to_capture,
)
} else {
(enums::AttemptStatus::Pending, None)
};
Ok(Self {
response: Ok(types::PaymentsResponseData::TransactionResponse {
resource_id: types::ResponseId::ConnectorTransactionId(
item.data.request.connector_transaction_id.to_owned(),
),
redirect: false,
redirection_data: None,
mandate_reference: None,
}),
status,
amount_captured,
..item.data
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RefundRequest {
amount: Option<i64>,

View File

@ -463,6 +463,8 @@ impl<F: Clone> TryFrom<PaymentData<F>> for types::PaymentsCaptureData {
.payment_attempt
.connector_transaction_id
.ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?,
currency: payment_data.currency,
amount: payment_data.amount.into(),
})
}
}

View File

@ -40,6 +40,8 @@ pub type PaymentsSyncResponseRouterData<R> =
ResponseRouterData<api::PSync, R, PaymentsSyncData, PaymentsResponseData>;
pub type PaymentsSessionResponseRouterData<R> =
ResponseRouterData<api::Session, R, PaymentsSessionData, PaymentsResponseData>;
pub type PaymentsCaptureResponseRouterData<R> =
ResponseRouterData<api::Capture, R, PaymentsCaptureData, PaymentsResponseData>;
pub type RefundsResponseRouterData<F, R> =
ResponseRouterData<F, R, RefundsData, RefundsResponseData>;
@ -112,6 +114,8 @@ pub struct PaymentsAuthorizeData {
pub struct PaymentsCaptureData {
pub amount_to_capture: Option<i64>,
pub connector_transaction_id: String,
pub currency: storage_enums::Currency,
pub amount: i64,
}
#[derive(Debug, Clone)]

View File

@ -56,6 +56,8 @@ pub trait ConnectorActions: Connector {
payment_data.unwrap_or(types::PaymentsCaptureData {
amount_to_capture: Some(100),
connector_transaction_id: transaction_id,
currency: enums::Currency::USD,
amount: 100,
}),
);
call_connector(request, integration).await