mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
feat(connector): (checkout.com) add support for multiple captures PSync (#2043)
This commit is contained in:
@ -375,14 +375,19 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
let suffix = match req.request.sync_type {
|
||||
types::SyncRequestType::MultipleCaptureSync(_) => "/actions",
|
||||
types::SyncRequestType::SinglePaymentSync => "",
|
||||
};
|
||||
Ok(format!(
|
||||
"{}{}{}",
|
||||
"{}{}{}{}",
|
||||
self.base_url(connectors),
|
||||
"payments/",
|
||||
req.request
|
||||
.connector_transaction_id
|
||||
.get_connector_transaction_id()
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?
|
||||
.change_context(errors::ConnectorError::MissingConnectorTransactionID)?,
|
||||
suffix
|
||||
))
|
||||
}
|
||||
|
||||
@ -412,17 +417,40 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
|
||||
types::PaymentsSyncData: Clone,
|
||||
types::PaymentsResponseData: Clone,
|
||||
{
|
||||
let response: checkout::PaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
match &data.request.sync_type {
|
||||
types::SyncRequestType::MultipleCaptureSync(_) => {
|
||||
let response: checkout::PaymentsResponseEnum = res
|
||||
.response
|
||||
.parse_struct("checkout::PaymentsResponseEnum")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
types::SyncRequestType::SinglePaymentSync => {
|
||||
let response: checkout::PaymentsResponse = res
|
||||
.response
|
||||
.parse_struct("PaymentsResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_multiple_capture_sync_method(
|
||||
&self,
|
||||
) -> CustomResult<services::CaptureSyncMethod, errors::ConnectorError> {
|
||||
Ok(services::CaptureSyncMethod::Bulk)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -1185,12 +1213,26 @@ impl api::IncomingWebhook for Checkout {
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<serde_json::Value, errors::ConnectorError> {
|
||||
let details: checkout::CheckoutWebhookObjectResource = request
|
||||
let event_type_data: checkout::CheckoutWebhookEventTypeBody = request
|
||||
.body
|
||||
.parse_struct("CheckoutWebhookObjectResource")
|
||||
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?;
|
||||
|
||||
Ok(details.data)
|
||||
.parse_struct("CheckoutWebhookBody")
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
let resource_object = if checkout::is_chargeback_event(&event_type_data.transaction_type)
|
||||
&& checkout::is_refund_event(&event_type_data.transaction_type)
|
||||
{
|
||||
// if other event, just return the json data.
|
||||
let resource_object_data: checkout::CheckoutWebhookObjectResource = request
|
||||
.body
|
||||
.parse_struct("CheckoutWebhookObjectResource")
|
||||
.change_context(errors::ConnectorError::WebhookEventTypeNotFound)?;
|
||||
resource_object_data.data
|
||||
} else {
|
||||
// if payment_event, construct PaymentResponse and then serialize it to json and return.
|
||||
let payment_response = checkout::PaymentsResponse::try_from(request)?;
|
||||
utils::Encode::<checkout::PaymentsResponse>::encode_to_value(&payment_response)
|
||||
.change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?
|
||||
};
|
||||
Ok(resource_object)
|
||||
}
|
||||
|
||||
fn get_dispute_details(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
use common_utils::{errors::CustomResult, ext_traits::ByteSliceExt};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -394,7 +394,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentsRequest {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Default, Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub enum CheckoutPaymentStatus {
|
||||
Authorized,
|
||||
#[default]
|
||||
@ -405,6 +405,35 @@ pub enum CheckoutPaymentStatus {
|
||||
Captured,
|
||||
}
|
||||
|
||||
impl TryFrom<CheckoutWebhookEventType> for CheckoutPaymentStatus {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(value: CheckoutWebhookEventType) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
CheckoutWebhookEventType::PaymentApproved => Ok(Self::Authorized),
|
||||
CheckoutWebhookEventType::PaymentCaptured => Ok(Self::Captured),
|
||||
CheckoutWebhookEventType::PaymentDeclined => Ok(Self::Declined),
|
||||
CheckoutWebhookEventType::AuthenticationStarted
|
||||
| CheckoutWebhookEventType::AuthenticationApproved => Ok(Self::Pending),
|
||||
CheckoutWebhookEventType::PaymentRefunded
|
||||
| CheckoutWebhookEventType::PaymentRefundDeclined
|
||||
| CheckoutWebhookEventType::DisputeReceived
|
||||
| CheckoutWebhookEventType::DisputeExpired
|
||||
| CheckoutWebhookEventType::DisputeAccepted
|
||||
| CheckoutWebhookEventType::DisputeCanceled
|
||||
| CheckoutWebhookEventType::DisputeEvidenceSubmitted
|
||||
| CheckoutWebhookEventType::DisputeEvidenceAcknowledgedByScheme
|
||||
| CheckoutWebhookEventType::DisputeEvidenceRequired
|
||||
| CheckoutWebhookEventType::DisputeArbitrationLost
|
||||
| CheckoutWebhookEventType::DisputeArbitrationWon
|
||||
| CheckoutWebhookEventType::DisputeWon
|
||||
| CheckoutWebhookEventType::DisputeLost
|
||||
| CheckoutWebhookEventType::Unknown => {
|
||||
Err(errors::ConnectorError::WebhookEventTypeNotFound.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<(CheckoutPaymentStatus, Option<enums::CaptureMethod>)> for enums::AttemptStatus {
|
||||
fn foreign_from(item: (CheckoutPaymentStatus, Option<enums::CaptureMethod>)) -> Self {
|
||||
let (status, capture_method) = item;
|
||||
@ -449,20 +478,21 @@ impl ForeignFrom<(CheckoutPaymentStatus, Option<Balances>)> for enums::AttemptSt
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Href {
|
||||
#[serde(rename = "href")]
|
||||
redirection_url: Url,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Links {
|
||||
redirect: Option<Href>,
|
||||
}
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct PaymentsResponse {
|
||||
id: String,
|
||||
amount: Option<i32>,
|
||||
action_id: Option<String>,
|
||||
status: CheckoutPaymentStatus,
|
||||
#[serde(rename = "_links")]
|
||||
links: Links,
|
||||
@ -472,7 +502,14 @@ pub struct PaymentsResponse {
|
||||
response_summary: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PaymentsResponseEnum {
|
||||
ActionResponse(Vec<ActionResponse>),
|
||||
PaymentResponse(Box<PaymentsResponse>),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Balances {
|
||||
available_to_capture: i32,
|
||||
}
|
||||
@ -573,6 +610,33 @@ impl TryFrom<types::PaymentsSyncResponseRouterData<PaymentsResponse>>
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<types::PaymentsSyncResponseRouterData<PaymentsResponseEnum>>
|
||||
for types::PaymentsSyncRouterData
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
fn try_from(
|
||||
item: types::PaymentsSyncResponseRouterData<PaymentsResponseEnum>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
let capture_sync_response_list = match item.response {
|
||||
PaymentsResponseEnum::PaymentResponse(payments_response) => {
|
||||
// for webhook consumption flow
|
||||
utils::construct_captures_response_hashmap(vec![payments_response])
|
||||
}
|
||||
PaymentsResponseEnum::ActionResponse(action_list) => {
|
||||
// for captures sync
|
||||
utils::construct_captures_response_hashmap(action_list)
|
||||
}
|
||||
};
|
||||
Ok(Self {
|
||||
response: Ok(types::PaymentsResponseData::MultipleCaptureResponse {
|
||||
capture_sync_response_list,
|
||||
}),
|
||||
..item.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct PaymentVoidRequest {
|
||||
reference: String,
|
||||
@ -795,7 +859,7 @@ pub struct ErrorResponse {
|
||||
pub error_codes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, PartialEq)]
|
||||
pub enum ActionType {
|
||||
Authorization,
|
||||
Void,
|
||||
@ -828,6 +892,46 @@ impl From<&ActionResponse> for enums::RefundStatus {
|
||||
}
|
||||
}
|
||||
|
||||
impl utils::MultipleCaptureSyncResponse for ActionResponse {
|
||||
fn get_connector_capture_id(&self) -> String {
|
||||
self.action_id.clone()
|
||||
}
|
||||
|
||||
fn get_capture_attempt_status(&self) -> enums::AttemptStatus {
|
||||
match self.approved {
|
||||
Some(true) => enums::AttemptStatus::Charged,
|
||||
Some(false) => enums::AttemptStatus::Failure,
|
||||
None => enums::AttemptStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_connector_reference_id(&self) -> Option<String> {
|
||||
self.reference.clone()
|
||||
}
|
||||
|
||||
fn is_capture_response(&self) -> bool {
|
||||
self.action_type == ActionType::Capture
|
||||
}
|
||||
}
|
||||
|
||||
impl utils::MultipleCaptureSyncResponse for Box<PaymentsResponse> {
|
||||
fn get_connector_capture_id(&self) -> String {
|
||||
self.action_id.clone().unwrap_or("".into())
|
||||
}
|
||||
|
||||
fn get_capture_attempt_status(&self) -> enums::AttemptStatus {
|
||||
enums::AttemptStatus::foreign_from((self.status.clone(), self.balances.clone()))
|
||||
}
|
||||
|
||||
fn get_connector_reference_id(&self) -> Option<String> {
|
||||
self.reference.clone()
|
||||
}
|
||||
|
||||
fn is_capture_response(&self) -> bool {
|
||||
self.status == CheckoutPaymentStatus::Captured
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum CheckoutRedirectResponseStatus {
|
||||
@ -947,7 +1051,11 @@ pub struct CheckoutWebhookData {
|
||||
pub id: String,
|
||||
pub payment_id: Option<String>,
|
||||
pub action_id: Option<String>,
|
||||
pub reference: Option<String>,
|
||||
pub amount: i32,
|
||||
pub balances: Option<Balances>,
|
||||
pub response_code: Option<String>,
|
||||
pub response_summary: Option<String>,
|
||||
pub currency: String,
|
||||
}
|
||||
|
||||
@ -956,6 +1064,8 @@ pub struct CheckoutWebhookBody {
|
||||
#[serde(rename = "type")]
|
||||
pub transaction_type: CheckoutWebhookEventType,
|
||||
pub data: CheckoutWebhookData,
|
||||
#[serde(rename = "_links")]
|
||||
pub links: Links,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@ -1087,6 +1197,31 @@ pub struct Evidence {
|
||||
pub additional_evidence_file: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&api::IncomingWebhookRequestDetails<'_>> for PaymentsResponse {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
|
||||
fn try_from(request: &api::IncomingWebhookRequestDetails<'_>) -> Result<Self, Self::Error> {
|
||||
let details: CheckoutWebhookBody = request
|
||||
.body
|
||||
.parse_struct("CheckoutWebhookBody")
|
||||
.change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?;
|
||||
let data = details.data;
|
||||
let psync_struct = Self {
|
||||
id: data.payment_id.unwrap_or(data.id),
|
||||
amount: Some(data.amount),
|
||||
status: CheckoutPaymentStatus::try_from(details.transaction_type)?,
|
||||
links: details.links,
|
||||
balances: data.balances,
|
||||
reference: data.reference,
|
||||
response_code: data.response_code,
|
||||
response_summary: data.response_summary,
|
||||
action_id: data.action_id,
|
||||
};
|
||||
|
||||
Ok(psync_struct)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&types::SubmitEvidenceRouterData> for Evidence {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::SubmitEvidenceRouterData) -> Result<Self, Self::Error> {
|
||||
|
||||
@ -21,7 +21,10 @@ use crate::{
|
||||
consts,
|
||||
core::errors::{self, CustomResult},
|
||||
pii::PeekInterface,
|
||||
types::{self, api, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId},
|
||||
types::{
|
||||
self, api, storage::enums as storage_enums, transformers::ForeignTryFrom,
|
||||
PaymentsCancelData, ResponseId,
|
||||
},
|
||||
utils::{OptionExt, ValueExt},
|
||||
};
|
||||
|
||||
@ -1332,6 +1335,41 @@ mod error_code_error_message_tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait MultipleCaptureSyncResponse {
|
||||
fn get_connector_capture_id(&self) -> String;
|
||||
fn get_capture_attempt_status(&self) -> storage_enums::AttemptStatus;
|
||||
fn is_capture_response(&self) -> bool;
|
||||
fn get_connector_reference_id(&self) -> Option<String> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn construct_captures_response_hashmap<T>(
|
||||
capture_sync_response_list: Vec<T>,
|
||||
) -> HashMap<String, types::CaptureSyncResponse>
|
||||
where
|
||||
T: MultipleCaptureSyncResponse,
|
||||
{
|
||||
let mut hashmap = HashMap::new();
|
||||
capture_sync_response_list
|
||||
.into_iter()
|
||||
.for_each(|capture_sync_response| {
|
||||
let connector_capture_id = capture_sync_response.get_connector_capture_id();
|
||||
if capture_sync_response.is_capture_response() {
|
||||
hashmap.insert(
|
||||
connector_capture_id.clone(),
|
||||
types::CaptureSyncResponse::Success {
|
||||
resource_id: ResponseId::ConnectorTransactionId(connector_capture_id),
|
||||
status: capture_sync_response.get_capture_attempt_status(),
|
||||
connector_response_reference_id: capture_sync_response
|
||||
.get_connector_reference_id(),
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
hashmap
|
||||
}
|
||||
|
||||
pub fn validate_currency(
|
||||
request_currency: types::storage::enums::Currency,
|
||||
merchant_config_currency: Option<types::storage::enums::Currency>,
|
||||
|
||||
@ -66,12 +66,9 @@ impl Feature<api::PSync, types::PaymentsSyncData>
|
||||
.get_multiple_capture_sync_method()
|
||||
.to_payment_failed_response();
|
||||
|
||||
match (
|
||||
self.request.capture_sync_type.clone(),
|
||||
capture_sync_method_result,
|
||||
) {
|
||||
match (self.request.sync_type.clone(), capture_sync_method_result) {
|
||||
(
|
||||
types::CaptureSyncType::MultipleCaptureSync(pending_connector_capture_id_list),
|
||||
types::SyncRequestType::MultipleCaptureSync(pending_connector_capture_id_list),
|
||||
Ok(services::CaptureSyncMethod::Individual),
|
||||
) => {
|
||||
let resp = self
|
||||
@ -84,7 +81,7 @@ impl Feature<api::PSync, types::PaymentsSyncData>
|
||||
.await?;
|
||||
Ok(resp)
|
||||
}
|
||||
(types::CaptureSyncType::MultipleCaptureSync(_), Err(err)) => Err(err),
|
||||
(types::SyncRequestType::MultipleCaptureSync(_), Err(err)) => Err(err),
|
||||
_ => {
|
||||
// for bulk sync of captures, above logic needs to be handled at connector end
|
||||
let resp = services::execute_connector_processing_step(
|
||||
|
||||
@ -978,11 +978,11 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
|
||||
encoded_data: payment_data.connector_response.encoded_data,
|
||||
capture_method: payment_data.payment_attempt.capture_method,
|
||||
connector_meta: payment_data.payment_attempt.connector_metadata,
|
||||
capture_sync_type: match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => types::CaptureSyncType::MultipleCaptureSync(
|
||||
sync_type: match payment_data.multiple_capture_data {
|
||||
Some(multiple_capture_data) => types::SyncRequestType::MultipleCaptureSync(
|
||||
multiple_capture_data.get_pending_connector_capture_ids(),
|
||||
),
|
||||
None => types::CaptureSyncType::SingleCaptureSync,
|
||||
None => types::SyncRequestType::SinglePaymentSync,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -452,15 +452,15 @@ pub struct PaymentsSyncData {
|
||||
pub encoded_data: Option<String>,
|
||||
pub capture_method: Option<storage_enums::CaptureMethod>,
|
||||
pub connector_meta: Option<serde_json::Value>,
|
||||
pub capture_sync_type: CaptureSyncType,
|
||||
pub sync_type: SyncRequestType,
|
||||
pub mandate_id: Option<api_models::payments::MandateIds>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum CaptureSyncType {
|
||||
pub enum SyncRequestType {
|
||||
MultipleCaptureSync(Vec<String>),
|
||||
#[default]
|
||||
SingleCaptureSync,
|
||||
SinglePaymentSync,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
|
||||
Reference in New Issue
Block a user