diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index c281e2c2b2..5b3eeff8dd 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -81,7 +81,90 @@ impl types::PaymentsResponseData, > for Adyen { - // Not Implemented (R) + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + let adyen_req = utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(adyen_req)) + } + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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 { + 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) } diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 815b1f98c7..0161f7ca06 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -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, + additional_data: Option, } #[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> 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 - TryFrom> - for types::RouterData + TryFrom<( + types::ResponseRouterData, + bool, + )> for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + items: ( + types::ResponseRouterData, + bool, + ), ) -> Result { + 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 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; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + 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> + for types::PaymentsCaptureRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsCaptureResponseRouterData, + ) -> Result { + 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 }) } diff --git a/crates/router/src/connector/checkout.rs b/crates/router/src/connector/checkout.rs index 4c0116d6ee..35af78e64e 100644 --- a/crates/router/src/connector/checkout.rs +++ b/crates/router/src/connector/checkout.rs @@ -87,6 +87,97 @@ impl types::PaymentsResponseData, > for Checkout { + fn get_headers( + &self, + req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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, errors::ConnectorError> { + let checkout_req = + utils::Encode::::convert_and_encode(req) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(checkout_req)) + } + + fn build_request( + &self, + req: &types::PaymentsCaptureRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, 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 { + 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 { + 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 diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index ce9844d653..798cf5ca83 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -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, + pub capture_type: Option, + pub processing_channel_id: String, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PaymentCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + 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> + for types::PaymentsCaptureRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::PaymentsCaptureResponseRouterData, + ) -> Result { + 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, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index ba45535677..c863c50126 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -463,6 +463,8 @@ impl TryFrom> for types::PaymentsCaptureData { .payment_attempt .connector_transaction_id .ok_or(errors::ApiErrorResponse::MerchantConnectorAccountNotFound)?, + currency: payment_data.currency, + amount: payment_data.amount.into(), }) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 268b99f269..1f6e444ce7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -40,6 +40,8 @@ pub type PaymentsSyncResponseRouterData = ResponseRouterData; pub type PaymentsSessionResponseRouterData = ResponseRouterData; +pub type PaymentsCaptureResponseRouterData = + ResponseRouterData; pub type RefundsResponseRouterData = ResponseRouterData; @@ -112,6 +114,8 @@ pub struct PaymentsAuthorizeData { pub struct PaymentsCaptureData { pub amount_to_capture: Option, pub connector_transaction_id: String, + pub currency: storage_enums::Currency, + pub amount: i64, } #[derive(Debug, Clone)] diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index f1a360846a..e7aa570a7a 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -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