feat(connector): (checkout.com) add support for multiple captures PSync (#2043)

This commit is contained in:
Hrithikesh
2023-09-08 12:47:43 +05:30
committed by GitHub
parent 7777c5ca84
commit 517c5c4165
12 changed files with 259 additions and 47 deletions

View File

@ -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(

View File

@ -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> {

View File

@ -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>,

View File

@ -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(

View File

@ -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,
},
})
}

View File

@ -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)]