diff --git a/crates/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index 177310de18..75af0b19cf 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -10,9 +10,9 @@ use crate::{disputes, enums as api_enums, mandates, payments, refunds}; #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, Copy)] #[serde(rename_all = "snake_case")] pub enum IncomingWebhookEvent { - /// Authorization + Capture success - PaymentIntentFailure, /// Authorization + Capture failure + PaymentIntentFailure, + /// Authorization + Capture success PaymentIntentSuccess, PaymentIntentProcessing, PaymentIntentPartiallyFunded, diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index a8a0baa266..cdf94d6e43 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -6328,8 +6328,7 @@ type="Text" payment_method_type = "Visa" [payload.connector_auth.HeaderKey] api_key="API Key" -[payload.connector_webhook_details] -merchant_secret="Source verification key" + [silverflow] [silverflow.connector_auth.BodyKey] api_key="API Key" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index a22b50a9b4..b9a98f5aed 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -4935,10 +4935,8 @@ type="Text" payment_method_type = "Visa" [payload.connector_auth.HeaderKey] api_key="API Key" -[payload.connector_webhook_details] -merchant_secret="Source verification key" [silverflow] [silverflow.connector_auth.BodyKey] api_key="API Key" -key1="Secret Key" \ No newline at end of file +key1="Secret Key" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index d702612969..21801d380b 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -6302,8 +6302,7 @@ type="Text" payment_method_type = "Visa" [payload.connector_auth.HeaderKey] api_key="API Key" -[payload.connector_webhook_details] -merchant_secret="Source verification key" + [silverflow] [silverflow.connector_auth.BodyKey] api_key="API Key" diff --git a/crates/hyperswitch_connectors/src/connectors/payload.rs b/crates/hyperswitch_connectors/src/connectors/payload.rs index c1f1516e63..03c41d367a 100644 --- a/crates/hyperswitch_connectors/src/connectors/payload.rs +++ b/crates/hyperswitch_connectors/src/connectors/payload.rs @@ -8,12 +8,12 @@ use base64::Engine; use common_enums::enums; use common_utils::{ consts::BASE64_ENGINE, - errors::CustomResult, - ext_traits::BytesExt, + errors::{CustomResult, ReportSwitchExt}, + ext_traits::{ByteSliceExt, BytesExt}, request::{Method, Request, RequestBuilder, RequestContent}, types::{AmountConvertor, StringMajorUnit, StringMajorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, @@ -47,7 +47,7 @@ use hyperswitch_interfaces::{ types::{self, PaymentsVoidType, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{ExposeInterface, Mask, Secret}; use transformers as payload; use crate::{constants::headers, types::ResponseRouterData, utils}; @@ -195,7 +195,18 @@ impl ConnectorIntegration fo impl ConnectorIntegration for Payload {} -impl ConnectorIntegration for Payload {} +impl ConnectorIntegration for Payload { + fn build_request( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err( + errors::ConnectorError::NotImplemented("Setup Mandate flow for Payload".to_string()) + .into(), + ) + } +} impl ConnectorIntegration for Payload { fn get_headers( @@ -701,25 +712,95 @@ impl ConnectorIntegration for Payload { #[async_trait::async_trait] impl webhooks::IncomingWebhook for Payload { - fn get_webhook_object_reference_id( + async fn verify_webhook_source( &self, _request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &common_utils::id_type::MerchantId, + _connector_webhook_details: Option, + _connector_account_details: common_utils::crypto::Encryptable>, + _connector_label: &str, + ) -> CustomResult { + // Payload does not support source verification + // It does, but the client id and client secret generation is not possible at present + // It requires OAuth connect which falls under Access Token flow and it also requires multiple calls to be made + // We return false just so that a PSync call is triggered internally + Ok(false) + } + + fn get_webhook_object_reference_id( + &self, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: responses::PayloadWebhookEvent = request + .body + .parse_struct("PayloadWebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let reference_id = match webhook_body.trigger { + responses::PayloadWebhooksTrigger::Payment + | responses::PayloadWebhooksTrigger::Processed + | responses::PayloadWebhooksTrigger::Authorized + | responses::PayloadWebhooksTrigger::Credit + | responses::PayloadWebhooksTrigger::Reversal + | responses::PayloadWebhooksTrigger::Void + | responses::PayloadWebhooksTrigger::AutomaticPayment + | responses::PayloadWebhooksTrigger::Decline + | responses::PayloadWebhooksTrigger::Deposit + | responses::PayloadWebhooksTrigger::Reject + | responses::PayloadWebhooksTrigger::PaymentActivationStatus + | responses::PayloadWebhooksTrigger::PaymentLinkStatus + | responses::PayloadWebhooksTrigger::ProcessingStatus + | responses::PayloadWebhooksTrigger::BankAccountReject + | responses::PayloadWebhooksTrigger::Chargeback + | responses::PayloadWebhooksTrigger::ChargebackReversal + | responses::PayloadWebhooksTrigger::TransactionOperation + | responses::PayloadWebhooksTrigger::TransactionOperationClear => { + api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + webhook_body + .triggered_on + .transaction_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + ) + } + + responses::PayloadWebhooksTrigger::Refund => { + api_models::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId( + webhook_body + .triggered_on + .transaction_id + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?, + ), + ) + } + }; + + Ok(reference_id) } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: responses::PayloadWebhookEvent = + request.body.parse_struct("PayloadWebhookEvent").switch()?; + + Ok(api_models::webhooks::IncomingWebhookEvent::from( + webhook_body.trigger, + )) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: responses::PayloadWebhookEvent = request + .body + .parse_struct("PayloadWebhookEvent") + .change_context(errors::ConnectorError::WebhookReferenceIdNotFound)?; + Ok(Box::new(webhook_body)) } } @@ -782,7 +863,11 @@ static PAYLOAD_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { connector_type: enums::PaymentConnectorCategory::PaymentGateway, }; -static PAYLOAD_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 0] = []; +static PAYLOAD_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 3] = [ + enums::EventClass::Disputes, + enums::EventClass::Payments, + enums::EventClass::Refunds, +]; impl ConnectorSpecifications for Payload { fn get_connector_about(&self) -> Option<&'static ConnectorInfo> { diff --git a/crates/hyperswitch_connectors/src/connectors/payload/responses.rs b/crates/hyperswitch_connectors/src/connectors/payload/responses.rs index 63699d0981..444e794e6b 100644 --- a/crates/hyperswitch_connectors/src/connectors/payload/responses.rs +++ b/crates/hyperswitch_connectors/src/connectors/payload/responses.rs @@ -38,6 +38,7 @@ pub struct PayloadCardsResponseData { #[serde(rename = "id")] pub transaction_id: String, pub payment_method_id: Option>, + // Connector customer id pub processing_id: Option, pub processing_method_id: Option, pub ref_number: Option, @@ -84,6 +85,7 @@ pub struct PayloadRefundResponse { pub transaction_id: String, pub ledger: Vec, pub payment_method_id: Option>, + // Connector customer id pub processing_id: Option, pub ref_number: Option, pub status: RefundStatus, @@ -99,3 +101,51 @@ pub struct PayloadErrorResponse { /// Payload returns arbitrary details in JSON format pub details: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum PayloadWebhooksTrigger { + Payment, + Processed, + Authorized, + Credit, + Refund, + Reversal, + Void, + AutomaticPayment, + Decline, + Deposit, + Reject, + #[serde(rename = "payment_activation:status")] + PaymentActivationStatus, + #[serde(rename = "payment_link:status")] + PaymentLinkStatus, + ProcessingStatus, + BankAccountReject, + Chargeback, + ChargebackReversal, + #[serde(rename = "transaction:operation")] + TransactionOperation, + #[serde(rename = "transaction:operation:clear")] + TransactionOperationClear, +} + +// Webhook response structures +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PayloadWebhookEvent { + pub object: String, // Added to match actual webhook structure + pub trigger: PayloadWebhooksTrigger, + pub webhook_id: String, + pub triggered_at: String, // Added to match actual webhook structure + // Webhooks Payload + pub triggered_on: PayloadEventDetails, + pub url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PayloadEventDetails { + #[serde(rename = "id")] + pub transaction_id: Option, + pub object: String, + pub value: Option, // Changed to handle any value type including null +} diff --git a/crates/hyperswitch_connectors/src/connectors/payload/transformers.rs b/crates/hyperswitch_connectors/src/connectors/payload/transformers.rs index 994b4a7cfc..707a8db936 100644 --- a/crates/hyperswitch_connectors/src/connectors/payload/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/payload/transformers.rs @@ -1,20 +1,24 @@ +use api_models::webhooks::IncomingWebhookEvent; use common_enums::enums; use common_utils::types::StringMajorUnit; use hyperswitch_domain_models::{ payment_method_data::PaymentMethodData, - router_data::{ConnectorAuthType, RouterData}, + router_data::{ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::refunds::{Execute, RSync}, router_request_types::ResponseId, router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, RefundsRouterData}, }; -use hyperswitch_interfaces::errors; +use hyperswitch_interfaces::{ + consts::{NO_ERROR_CODE, NO_ERROR_MESSAGE}, + errors, +}; use masking::Secret; use super::{requests, responses}; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, - utils::{is_manual_capture, CardData, RouterData as OtherRouterData}, + utils::{is_manual_capture, AddressDetailsData, CardData, RouterData as OtherRouterData}, }; //TODO: Fill the struct with respective fields @@ -56,12 +60,20 @@ impl TryFrom<&PayloadRouterData<&PaymentsAuthorizeRouterData>> cvc: req_card.card_cvc, }; let address = item.router_data.get_billing_address()?; + + // Check for required fields and fail if they're missing + let city = address.get_city()?.to_owned(); + let country = address.get_country()?.to_owned(); + let postal_code = address.get_zip()?.to_owned(); + let state_province = address.get_state()?.to_owned(); + let street_address = address.get_line1()?.to_owned(); + let billing_address = requests::BillingAddress { - city: address.city.clone().unwrap_or_default(), - country: address.country.unwrap_or_default(), - postal_code: address.zip.clone().unwrap_or_default(), - state_province: address.state.clone().unwrap_or_default(), - street_address: address.line1.clone().unwrap_or_default(), + city, + country, + postal_code, + state_province, + street_address, }; // For manual capture, set status to "authorized" @@ -127,13 +139,29 @@ impl ) -> Result { match item.response.clone() { responses::PayloadPaymentsResponse::PayloadCardsResponse(response) => { - let payment_status = response.status; - let transaction_id = response.transaction_id; - - Ok(Self { - status: common_enums::AttemptStatus::from(payment_status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(transaction_id), + let status = enums::AttemptStatus::from(response.status); + let connector_customer = response.processing_id.clone(); + let response_result = if status == enums::AttemptStatus::Failure { + Err(ErrorResponse { + attempt_status: None, + code: response + .status_code + .clone() + .unwrap_or_else(|| NO_ERROR_CODE.to_string()), + message: response + .status_message + .clone() + .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), + reason: response.status_message, + status_code: item.http_code, + connector_transaction_id: Some(response.transaction_id.clone()), + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(response.transaction_id), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, @@ -141,7 +169,12 @@ impl connector_response_reference_id: response.ref_number, incremental_authorization_allowed: None, charges: None, - }), + }) + }; + Ok(Self { + status, + response: response_result, + connector_customer, ..item.data }) } @@ -225,3 +258,41 @@ impl TryFrom> }) } } + +// Webhook transformations +impl From for IncomingWebhookEvent { + fn from(trigger: responses::PayloadWebhooksTrigger) -> Self { + match trigger { + // Payment Success Events + responses::PayloadWebhooksTrigger::Processed => Self::PaymentIntentSuccess, + responses::PayloadWebhooksTrigger::Authorized => { + Self::PaymentIntentAuthorizationSuccess + } + // Payment Processing Events + responses::PayloadWebhooksTrigger::Payment + | responses::PayloadWebhooksTrigger::AutomaticPayment => Self::PaymentIntentProcessing, + // Payment Failure Events + responses::PayloadWebhooksTrigger::Decline + | responses::PayloadWebhooksTrigger::Reject + | responses::PayloadWebhooksTrigger::BankAccountReject => Self::PaymentIntentFailure, + responses::PayloadWebhooksTrigger::Void + | responses::PayloadWebhooksTrigger::Reversal => Self::PaymentIntentCancelled, + // Refund Events + responses::PayloadWebhooksTrigger::Refund => Self::RefundSuccess, + // Dispute Events + responses::PayloadWebhooksTrigger::Chargeback => Self::DisputeOpened, + responses::PayloadWebhooksTrigger::ChargebackReversal => Self::DisputeWon, + // Other payment-related events + // Events not supported by our standard flows + responses::PayloadWebhooksTrigger::PaymentActivationStatus + | responses::PayloadWebhooksTrigger::Credit + | responses::PayloadWebhooksTrigger::Deposit + | responses::PayloadWebhooksTrigger::PaymentLinkStatus + | responses::PayloadWebhooksTrigger::ProcessingStatus + | responses::PayloadWebhooksTrigger::TransactionOperation + | responses::PayloadWebhooksTrigger::TransactionOperationClear => { + Self::EventNotSupported + } + } + } +} diff --git a/cypress-tests/cypress/e2e/configs/Payment/Payload.js b/cypress-tests/cypress/e2e/configs/Payment/Payload.js index 3c77533fd6..6f45a71e50 100644 --- a/cypress-tests/cypress/e2e/configs/Payment/Payload.js +++ b/cypress-tests/cypress/e2e/configs/Payment/Payload.js @@ -1,4 +1,8 @@ -import { customerAcceptance } from "./Commons"; +import { + customerAcceptance, + singleUseMandateData, + multiUseMandateData, +} from "./Commons"; const successfulNo3DSCardDetails = { card_number: "4242424242424242", @@ -8,43 +12,18 @@ const successfulNo3DSCardDetails = { card_cvc: "123", }; -// Note: Payload may not support 3DS authentication - using same card for consistency const successfulThreeDSTestCardDetails = { - card_number: "4242424242424242", - card_exp_month: "12", - card_exp_year: "25", - card_holder_name: "John Doe", - card_cvc: "123", + ...successfulNo3DSCardDetails, }; const failedNo3DSCardDetails = { - card_number: "4000000000000002", + card_number: "4111111111119903", card_exp_month: "01", card_exp_year: "25", card_holder_name: "John Doe", card_cvc: "123", }; -const singleUseMandateData = { - customer_acceptance: customerAcceptance, - mandate_type: { - single_use: { - amount: 8000, - currency: "USD", - }, - }, -}; - -const multiUseMandateData = { - customer_acceptance: customerAcceptance, - mandate_type: { - multi_use: { - amount: 8000, - currency: "USD", - }, - }, -}; - const payment_method_data_no3ds = { card: { last4: "4242", @@ -95,14 +74,6 @@ export const connectorDetails = { }, }, }, - SessionToken: { - Response: { - status: 200, - body: { - session_token: [], - }, - }, - }, PaymentIntentWithShippingCost: { Request: { currency: "USD", @@ -151,7 +122,7 @@ export const connectorDetails = { setup_future_usage: "on_session", }, Response: { - status: 501, + status: 400, body: { error: { type: "invalid_request", @@ -175,7 +146,7 @@ export const connectorDetails = { setup_future_usage: "on_session", }, Response: { - status: 501, + status: 400, body: { error: { type: "invalid_request", @@ -433,24 +404,25 @@ export const connectorDetails = { }, SaveCardUse3DSAutoCaptureOffSession: { Configs: { - DELAY: { - STATUS: true, - TIMEOUT: 15000, - }, + TRIGGER_SKIP: true, }, Request: { payment_method: "card", - payment_method_type: "debit", payment_method_data: { card: successfulThreeDSTestCardDetails, }, - setup_future_usage: "off_session", - customer_acceptance: customerAcceptance, + currency: "USD", + customer_acceptance: null, + setup_future_usage: "on_session", }, Response: { - status: 200, + status: 400, body: { - status: "requires_customer_action", + error: { + type: "invalid_request", + message: "3DS authentication is not supported by Payload", + code: "IR_00", + }, }, }, }, @@ -693,33 +665,11 @@ export const connectorDetails = { mandate_data: singleUseMandateData, }, Response: { - status: 200, + status: 501, body: { - status: "succeeded", - }, - }, - }, - ZeroAuthConfirmPayment: { - Configs: { - DELAY: { - STATUS: true, - TIMEOUT: 15000, - }, - TRIGGER_SKIP: true, - }, - Request: { - payment_type: "setup_mandate", - payment_method: "card", - payment_method_type: "credit", - payment_method_data: { - card: successfulNo3DSCardDetails, - }, - }, - Response: { - status: 200, - body: { - status: "succeeded", - setup_future_usage: "off_session", + code: "IR_00", + message: "Setup Mandate flow for Payload is not implemented", + type: "invalid_request", }, }, },