mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(connectors): implement capture flow for checkout and adyen connectors (#259)
This commit is contained in:
@ -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 {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
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 {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user