diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 95b928d579..89c1277975 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -653,7 +653,7 @@ pub enum Connector { // Opayo, added as template code for future usage Opennode, // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage - // Payme, + Payme, Paypal, Payu, Rapyd, @@ -758,7 +758,7 @@ pub enum RoutableConnectors { // Opayo, added as template code for future usage Opennode, // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage - // Payme, + Payme, Paypal, Payu, Rapyd, diff --git a/crates/router/src/connector/payme.rs b/crates/router/src/connector/payme.rs index e18f4be06b..593eae7c17 100644 --- a/crates/router/src/connector/payme.rs +++ b/crates/router/src/connector/payme.rs @@ -2,19 +2,21 @@ mod transformers; use std::fmt::Debug; +use common_utils::{crypto, ext_traits::ByteSliceExt}; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::ExposeInterface; use transformers as payme; use crate::{ configs::settings, - core::errors::{self, CustomResult}, - headers, - services::{ - self, - request::{self, Mask}, - ConnectorIntegration, + connector::utils as conn_utils, + core::{ + errors::{self, CustomResult}, + payments, }, + db::StorageInterface, + headers, routes, + services::{self, request, ConnectorIntegration}, types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, @@ -46,7 +48,6 @@ impl types::PaymentsResponseData, > for Payme { - // Not Implemented (R) } impl ConnectorCommonExt for Payme @@ -55,17 +56,13 @@ where { fn build_headers( &self, - req: &types::RouterData, + _req: &types::RouterData, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( + let header = vec![( headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self) - .to_string() - .into(), + Self::get_content_type(self).to_string().into(), )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); Ok(header) } } @@ -83,18 +80,6 @@ impl ConnectorCommon for Payme { connectors.payme.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &types::ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = payme::PaymeAuthType::try_from(auth_type) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.peek().to_string().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, @@ -116,7 +101,6 @@ impl ConnectorCommon for Payme { impl ConnectorIntegration for Payme { - //TODO: implement sessions flow } impl ConnectorIntegration @@ -129,9 +113,138 @@ impl ConnectorIntegration for Payme +{ + fn get_headers( + &self, + req: &types::PaymentsInitRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &types::RouterData< + api::InitPayment, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}api/generate-sale", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsInitRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = payme::GenerateSaleRequest::try_from(req)?; + let payme_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payme_req)) + } + + fn build_request( + &self, + req: &types::PaymentsInitRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsInitType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PaymentsInitType::get_headers(self, req, connectors)?) + .body(types::PaymentsInitType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsInitRouterData, + res: Response, + ) -> CustomResult< + types::RouterData< + api::InitPayment, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > + where + api::InitPayment: Clone, + types::PaymentsAuthorizeData: Clone, + types::PaymentsResponseData: Clone, + { + let response: payme::GenerateSaleResponse = res + .response + .parse_struct("Payme GenerateSaleResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] impl ConnectorIntegration for Payme { + async fn execute_pretasks( + &self, + router_data: &mut types::PaymentsAuthorizeRouterData, + app_state: &routes::AppState, + ) -> CustomResult<(), errors::ConnectorError> { + if router_data.request.mandate_id.is_none() { + let integ: Box< + &(dyn ConnectorIntegration< + api::InitPayment, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + > + Send + + Sync + + 'static), + > = Box::new(&Self); + let init_data = &types::PaymentsInitRouterData::from(( + &router_data.to_owned(), + router_data.request.clone(), + )); + let init_res = services::execute_connector_processing_step( + app_state, + integ, + init_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await?; + router_data.request.related_transaction_id = init_res.request.related_transaction_id; + } + Ok(()) + } + fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, @@ -146,20 +259,26 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + if req.request.mandate_id.is_some() { + // For recurring mandate payments + Ok(format!("{}api/generate-sale", self.base_url(connectors))) + } else { + // For Normal & first mandate payments + Ok(format!("{}api/pay-sale", self.base_url(connectors))) + } } fn get_request_body( &self, req: &types::PaymentsAuthorizeRouterData, ) -> CustomResult, errors::ConnectorError> { - let req_obj = payme::PaymePaymentsRequest::try_from(req)?; + let req_obj = payme::PaymePaymentRequest::try_from(req)?; let payme_req = types::RequestBody::log_and_get_request_body( &req_obj, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(payme_req)) @@ -190,7 +309,7 @@ impl ConnectorIntegration CustomResult { - let response: payme::PaymePaymentsResponse = res + let response: payme::PaymePaySaleResponse = res .response .parse_struct("Payme PaymentsAuthorizeResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -199,7 +318,6 @@ impl ConnectorIntegration for Payme { - fn get_headers( - &self, - req: &types::PaymentsSyncRouterData, - connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( + fn build_request( &self, _req: &types::PaymentsSyncRouterData, _connectors: &settings::Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &types::PaymentsSyncRouterData, - connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .build(), - )) + Err(errors::ConnectorError::FlowNotSupported { + flow: "Payment Sync".to_string(), + connector: "Payme".to_string(), + } + .into()) } fn handle_response( &self, - data: &types::PaymentsSyncRouterData, + data: &types::RouterData, res: Response, - ) -> CustomResult { - let response: payme::PaymePaymentsResponse = res + ) -> CustomResult< + types::RouterData, + errors::ConnectorError, + > + where + api::PSync: Clone, + types::PaymentsSyncData: Clone, + types::PaymentsResponseData: Clone, + { + let response: payme::PaymePaySaleResponse = res .response - .parse_struct("payme PaymentsSyncResponse") + .parse_struct("Payme PaymentsResponse") .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: Response, - ) -> CustomResult { - self.build_error_response(res) } } @@ -291,16 +386,22 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}api/capture-sale", self.base_url(connectors))) } fn get_request_body( &self, - _req: &types::PaymentsCaptureRouterData, + req: &types::PaymentsCaptureRouterData, ) -> CustomResult, errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let req_obj = payme::PaymentCaptureRequest::try_from(req)?; + let payme_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(payme_req)) } fn build_request( @@ -326,7 +427,7 @@ impl ConnectorIntegration CustomResult { - let response: payme::PaymePaymentsResponse = res + let response: payme::PaymePaySaleResponse = res .response .parse_struct("Payme PaymentsCaptureResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; @@ -335,7 +436,6 @@ impl ConnectorIntegration for Payme { + fn build_request( + &self, + _req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Void".to_string(), + connector: "Payme".to_string(), + } + .into()) + } } impl ConnectorIntegration for Payme { @@ -367,9 +478,9 @@ impl ConnectorIntegration, - _connectors: &settings::Connectors, + connectors: &settings::Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!("{}api/refund-sale", self.base_url(connectors))) } fn get_request_body( @@ -411,12 +522,14 @@ impl ConnectorIntegration for Payme { - fn get_headers( - &self, - req: &types::RefundSyncRouterData, - connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( + fn build_request( &self, _req: &types::RefundSyncRouterData, _connectors: &settings::Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &types::RefundSyncRouterData, - connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - services::RequestBuilder::new() - .method(services::Method::Get) - .url(&types::RefundSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .body(types::RefundSyncType::get_request_body(self, req)?) - .build(), - )) - } - - fn handle_response( - &self, - data: &types::RefundSyncRouterData, - res: Response, - ) -> CustomResult { - let response: payme::RefundResponse = res - .response - .parse_struct("payme RefundSyncResponse") - .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: Response, - ) -> CustomResult { - self.build_error_response(res) + Err(errors::ConnectorError::FlowNotSupported { + flow: "Refund Sync".to_string(), + connector: "Payme".to_string(), + } + .into()) } } #[async_trait::async_trait] impl api::IncomingWebhook for Payme { - fn get_webhook_object_reference_id( + fn get_webhook_source_verification_algorithm( &self, _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Ok(Box::new(crypto::Md5)) + } + + fn get_webhook_source_verification_signature( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + let resource: payme::WebhookEventDataResourceSignature = request + .body + .parse_struct("WebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(resource.payme_signature.expose().into_bytes()) + } + + fn get_webhook_source_verification_message( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, + _merchant_id: &str, + secret: &[u8], + ) -> CustomResult, errors::ConnectorError> { + let resource: payme::WebhookEventDataResource = + request + .body + .parse_struct("WebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(format!( + "{}{}{}", + String::from_utf8_lossy(secret), + resource.payme_transaction_id, + resource.payme_sale_id + ) + .as_bytes() + .to_vec()) + } + + fn get_webhook_object_reference_id( + &self, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let resource: payme::WebhookEventDataResource = + request + .body + .parse_struct("WebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let id = match resource.notify_type { + transformers::NotifyType::SaleComplete + | transformers::NotifyType::SaleAuthorized + | transformers::NotifyType::SaleFailure => api::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId(resource.payme_sale_id), + ), + transformers::NotifyType::Refund => api::webhooks::ObjectReferenceId::RefundId( + api_models::webhooks::RefundIdType::ConnectorRefundId(resource.payme_sale_id), + ), + transformers::NotifyType::SaleChargeback + | transformers::NotifyType::SaleChargebackRefund => { + api::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::ConnectorTransactionId( + resource.payme_sale_id, + ), + ) + } + }; + Ok(id) } fn get_webhook_event_type( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let resource: payme::WebhookEventDataResourceEvent = request + .body + .parse_struct("WebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + Ok(api::IncomingWebhookEvent::from(resource.notify_type)) + } + + async fn get_webhook_source_verification_merchant_secret( + &self, + db: &dyn StorageInterface, + merchant_id: &str, + ) -> CustomResult, errors::ConnectorError> { + let key = conn_utils::get_webhook_merchant_secret_key(self.id(), merchant_id); + let secret = match db.find_config_by_key(&key).await { + Ok(config) => Some(config), + Err(e) => { + crate::logger::warn!("Unable to fetch merchant webhook secret from DB: {:#?}", e); + None + } + }; + Ok(secret + .map(|conf| conf.config.into_bytes()) + .unwrap_or_default()) } fn get_webhook_resource_object( &self, - _request: &api::IncomingWebhookRequestDetails<'_>, + request: &api::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + let resource: payme::WebhookEventDataResource = + request + .body + .parse_struct("WebhookEvent") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + let sale_response = payme::PaymePaySaleResponse::try_from(resource)?; + + let res_json = serde_json::to_value(sale_response) + .into_report() + .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; + + Ok(res_json) } } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 6a5c9fd4be..e7078dea62 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -1,45 +1,235 @@ -use masking::Secret; +use api_models::payments::PaymentMethodData; +use common_utils::pii; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, Secret}; use serde::{Deserialize, Serialize}; use crate::{ - connector::utils::PaymentsAuthorizeRequestData, + connector::utils::{ + missing_field_err, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, RouterData, + }, core::errors, - types::{self, api, storage::enums}, + types::{self, api, storage::enums, MandateReference}, }; -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct PaymePaymentsRequest { - amount: i64, +#[derive(Debug, Serialize)] +pub struct PayRequest { + buyer_name: Secret, + buyer_email: pii::Email, + payme_sale_id: String, + #[serde(flatten)] card: PaymeCard, } -#[derive(Default, Debug, Serialize, Eq, PartialEq)] -pub struct PaymeCard { - name: Secret, - number: cards::CardNumber, - expiry_month: Secret, - expiry_year: Secret, - cvc: Secret, - complete: bool, +#[derive(Debug, Serialize)] +pub struct MandateRequest { + currency: enums::Currency, + sale_price: i64, + transaction_id: String, + product_name: String, + sale_return_url: String, + seller_payme_id: Secret, + sale_callback_url: String, + buyer_key: Secret, } -impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymePaymentsRequest { +#[derive(Debug, Serialize)] +#[serde(untagged)] +pub enum PaymePaymentRequest { + MandateRequest(MandateRequest), + PayRequest(PayRequest), +} + +#[derive(Debug, Serialize)] +pub struct PaymeCard { + credit_card_cvv: Secret, + credit_card_exp: Secret, + credit_card_number: cards::CardNumber, +} + +#[derive(Debug, Serialize)] +pub struct GenerateSaleRequest { + currency: enums::Currency, + sale_type: SaleType, + sale_price: i64, + transaction_id: String, + product_name: String, + sale_return_url: String, + seller_payme_id: Secret, + sale_callback_url: String, + sale_payment_method: SalePaymentMethod, +} + +#[derive(Debug, Deserialize)] +pub struct GenerateSaleResponse { + payme_sale_id: String, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + Ok(Self { + status: enums::AttemptStatus::from(item.response.sale_status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.payme_sale_id), + redirection_data: None, + mandate_reference: item.response.buyer_key.map(|buyer_key| MandateReference { + connector_mandate_id: Some(buyer_key.expose()), + payment_method_id: None, + }), + connector_metadata: Some( + serde_json::to_value(PaymeMetadata { + payme_transaction_id: item.response.payme_transaction_id, + }) + .into_report() + .change_context(errors::ConnectorError::ResponseHandlingFailed)?, + ), + network_txn_id: None, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum SaleType { + Sale, + Authorize, + Token, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum SalePaymentMethod { + CreditCard, +} + +impl TryFrom<&types::PaymentsInitRouterData> for GenerateSaleRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsInitRouterData) -> Result { + let sale_type = SaleType::try_from(item)?; + let seller_payme_id = PaymeAuthType::try_from(&item.connector_auth_type)?.seller_payme_id; + let order_details = item.request.get_order_details()?; + let product_name = order_details + .first() + .ok_or_else(missing_field_err("order_details"))? + .product_name + .clone(); + Ok(Self { + currency: item.request.currency, + sale_type, + sale_price: item.request.amount, + transaction_id: item.payment_id.clone(), + product_name, + sale_return_url: item.request.get_return_url()?, + seller_payme_id, + sale_callback_url: item.request.get_webhook_url()?, + sale_payment_method: SalePaymentMethod::try_from(&item.request.payment_method_data)?, + }) + } +} + +impl TryFrom<&types::PaymentsInitRouterData> for SaleType { + type Error = error_stack::Report; + fn try_from(value: &types::PaymentsInitRouterData) -> Result { + let sale_type = if value.request.setup_mandate_details.is_some() { + // First mandate + Self::Token + } else { + // Normal payments + match value.request.is_auto_capture()? { + true => Self::Sale, + false => Self::Authorize, + } + }; + Ok(sale_type) + } +} + +impl TryFrom<&PaymentMethodData> for SalePaymentMethod { + type Error = error_stack::Report; + fn try_from(item: &PaymentMethodData) -> Result { + match item { + PaymentMethodData::Card(_) => Ok(Self::CreditCard), + PaymentMethodData::Wallet(_) + | PaymentMethodData::PayLater(_) + | PaymentMethodData::BankRedirect(_) + | PaymentMethodData::BankDebit(_) + | PaymentMethodData::BankTransfer(_) + | PaymentMethodData::Crypto(_) + | PaymentMethodData::MandatePayment + | PaymentMethodData::Reward(_) + | PaymentMethodData::Upi(_) => { + Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) + } + } + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymePaymentRequest { + type Error = error_stack::Report; + fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result { + let payme_request = if value.request.mandate_id.is_some() { + Self::MandateRequest(MandateRequest::try_from(value)?) + } else { + Self::PayRequest(PayRequest::try_from(value)?) + }; + Ok(payme_request) + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for MandateRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let seller_payme_id = PaymeAuthType::try_from(&item.connector_auth_type)?.seller_payme_id; + let order_details = item.request.get_order_details()?; + let product_name = order_details + .first() + .ok_or_else(missing_field_err("order_details"))? + .product_name + .clone(); + Ok(Self { + currency: item.request.currency, + sale_price: item.request.amount, + transaction_id: item.payment_id.clone(), + product_name, + sale_return_url: item.request.get_return_url()?, + seller_payme_id, + sale_callback_url: item.request.get_webhook_url()?, + buyer_key: Secret::new(item.request.get_connector_mandate_id()?), + }) + } +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { match item.request.payment_method_data.clone() { api::PaymentMethodData::Card(req_card) => { let card = PaymeCard { - name: req_card.card_holder_name, - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.request.is_auto_capture()?, + credit_card_cvv: req_card.card_cvc.clone(), + credit_card_exp: req_card + .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()), + credit_card_number: req_card.card_number, }; + let buyer_email = item.request.get_email()?; + let buyer_name = item.get_billing_address()?.get_full_name()?; + let payme_sale_id = item.request.related_transaction_id.clone().ok_or( + errors::ConnectorError::MissingConnectorRelatedTransactionID { + id: "payme_sale_id".to_string(), + }, + )?; Ok(Self { - amount: item.request.amount, card, + buyer_email, + buyer_name, + payme_sale_id, }) } _ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), @@ -47,63 +237,93 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymePaymentsRequest { } } -//TODO: Fill the struct with respective fields // Auth Struct pub struct PaymeAuthType { - pub(super) api_key: Secret, + pub(super) payme_client_key: Secret, + pub(super) seller_payme_id: Secret, } impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType { type Error = error_stack::Report; fn try_from(auth_type: &types::ConnectorAuthType) -> Result { match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: Secret::new(api_key.to_string()), + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + seller_payme_id: Secret::new(api_key.to_string()), + payme_client_key: Secret::new(key1.to_string()), }), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum PaymePaymentStatus { - Succeeded, + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum SaleStatus { + Initial, + Completed, + Refunded, + PartialRefund, + Authorized, + Voided, + PartialVoid, Failed, - #[default] - Processing, + Chargeback, } -impl From for enums::AttemptStatus { - fn from(item: PaymePaymentStatus) -> Self { +impl From for enums::AttemptStatus { + fn from(item: SaleStatus) -> Self { match item { - PaymePaymentStatus::Succeeded => Self::Charged, - PaymePaymentStatus::Failed => Self::Failure, - PaymePaymentStatus::Processing => Self::Authorizing, + SaleStatus::Initial => Self::Authorizing, + SaleStatus::Completed => Self::Charged, + SaleStatus::Refunded | SaleStatus::PartialRefund => Self::AutoRefunded, + SaleStatus::Authorized => Self::Authorized, + SaleStatus::Voided | SaleStatus::PartialVoid => Self::Voided, + SaleStatus::Failed => Self::Failure, + SaleStatus::Chargeback => Self::AutoRefunded, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct PaymePaymentsResponse { - status: PaymePaymentStatus, - id: String, +#[derive(Debug, Serialize, Deserialize)] +pub struct PaymePaySaleResponse { + sale_status: SaleStatus, + payme_sale_id: String, + payme_transaction_id: String, + buyer_key: Option>, } -impl - TryFrom> - for types::RouterData +#[derive(Serialize, Deserialize)] +pub struct PaymeMetadata { + payme_transaction_id: String, +} + +impl + TryFrom< + types::ResponseRouterData< + F, + GenerateSaleResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = error_stack::Report; fn try_from( - item: types::ResponseRouterData, + item: types::ResponseRouterData< + F, + GenerateSaleResponse, + types::PaymentsAuthorizeData, + types::PaymentsResponseData, + >, ) -> Result { Ok(Self { - status: enums::AttemptStatus::from(item.response.status), + status: enums::AttemptStatus::Authorizing, + request: types::PaymentsAuthorizeData { + related_transaction_id: Some(item.response.payme_sale_id.clone()), + ..item.data.request + }, response: Ok(types::PaymentsResponseData::TransactionResponse { - resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + resource_id: types::ResponseId::ConnectorTransactionId(item.response.payme_sale_id), redirection_data: None, mandate_reference: None, connector_metadata: None, @@ -114,87 +334,90 @@ impl } } -//TODO: Fill the struct with respective fields +#[derive(Debug, Serialize)] +pub struct PaymentCaptureRequest { + payme_sale_id: String, + sale_price: i64, +} + +impl TryFrom<&types::PaymentsCaptureRouterData> for PaymentCaptureRequest { + type Error = error_stack::Report; + fn try_from(item: &types::PaymentsCaptureRouterData) -> Result { + Ok(Self { + payme_sale_id: item.request.connector_transaction_id.clone(), + sale_price: item.request.amount_to_capture, + }) + } +} + // REFUND : // Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] +#[derive(Debug, Serialize)] pub struct PaymeRefundRequest { - pub amount: i64, + sale_refund_amount: i64, + payme_sale_id: String, + seller_payme_id: Secret, + payme_client_key: Secret, } impl TryFrom<&types::RefundsRouterData> for PaymeRefundRequest { type Error = error_stack::Report; fn try_from(item: &types::RefundsRouterData) -> Result { + let auth_type = PaymeAuthType::try_from(&item.connector_auth_type)?; Ok(Self { - amount: item.request.refund_amount, + payme_sale_id: item.request.connector_transaction_id.clone(), + seller_payme_id: auth_type.seller_payme_id, + payme_client_key: auth_type.payme_client_key, + sale_refund_amount: item.request.refund_amount, }) } } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping +impl TryFrom for enums::RefundStatus { + type Error = error_stack::Report; + fn try_from(sale_status: SaleStatus) -> Result { + match sale_status { + SaleStatus::Refunded | SaleStatus::PartialRefund => Ok(Self::Success), + SaleStatus::Failed => Ok(Self::Failure), + SaleStatus::Initial + | SaleStatus::Completed + | SaleStatus::Authorized + | SaleStatus::Voided + | SaleStatus::PartialVoid + | SaleStatus::Chargeback => Err(errors::ConnectorError::ResponseHandlingFailed)?, } } } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Deserialize)] pub struct RefundResponse { - id: String, - status: RefundStatus, + sale_status: SaleStatus, } -impl TryFrom> - for types::RefundsRouterData +impl + TryFrom<( + &types::RefundsData, + types::RefundsResponseRouterData, + )> for types::RefundsRouterData { type Error = error_stack::Report; fn try_from( - item: types::RefundsResponseRouterData, + (req, item): ( + &types::RefundsData, + types::RefundsResponseRouterData, + ), ) -> Result { Ok(Self { response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + // Connector doesn't give refund id, So using connector_transaction_id as connector_refund_id. Since refund webhook will also have this id as reference + connector_refund_id: req.connector_transaction_id.clone(), + refund_status: enums::RefundStatus::try_from(item.response.sale_status)?, }), ..item.data }) } } -impl TryFrom> - for types::RefundsRouterData -{ - type Error = error_stack::Report; - fn try_from( - item: types::RefundsResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(types::RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data - }) - } -} - -//TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct PaymeErrorResponse { pub status_code: u16, @@ -202,3 +425,59 @@ pub struct PaymeErrorResponse { pub message: String, pub reason: Option, } + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum NotifyType { + SaleComplete, + SaleAuthorized, + Refund, + SaleFailure, + SaleChargeback, + SaleChargebackRefund, +} + +#[derive(Debug, Deserialize)] +pub struct WebhookEventDataResource { + pub sale_status: SaleStatus, + pub payme_signature: Secret, + pub buyer_key: Option>, + pub notify_type: NotifyType, + pub payme_sale_id: String, + pub payme_transaction_id: String, +} + +#[derive(Debug, Deserialize)] +pub struct WebhookEventDataResourceEvent { + pub notify_type: NotifyType, +} + +#[derive(Debug, Deserialize)] +pub struct WebhookEventDataResourceSignature { + pub payme_signature: Secret, +} + +impl TryFrom for PaymePaySaleResponse { + type Error = error_stack::Report; + fn try_from(value: WebhookEventDataResource) -> Result { + Ok(Self { + sale_status: value.sale_status, + payme_sale_id: value.payme_sale_id, + payme_transaction_id: value.payme_transaction_id, + buyer_key: value.buyer_key, + }) + } +} + +impl From for api::IncomingWebhookEvent { + fn from(value: NotifyType) -> Self { + match value { + NotifyType::SaleComplete => Self::PaymentIntentSuccess, + NotifyType::Refund => Self::RefundSuccess, + NotifyType::SaleFailure => Self::PaymentIntentFailure, + NotifyType::SaleAuthorized + | NotifyType::SaleChargeback + | NotifyType::SaleChargebackRefund => Self::EventNotSupported, + } + } +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 0cb76064ea..48265ceb61 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -205,6 +205,7 @@ pub trait PaymentsAuthorizeRequestData { fn get_router_return_url(&self) -> Result; fn is_wallet(&self) -> bool; fn get_payment_method_type(&self) -> Result; + fn get_connector_mandate_id(&self) -> Result; } impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { @@ -277,6 +278,11 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { .to_owned() .ok_or_else(missing_field_err("payment_method_type")) } + + fn get_connector_mandate_id(&self) -> Result { + self.connector_mandate_id() + .ok_or_else(missing_field_err("connector_mandate_id")) + } } pub trait BrowserInformationData { @@ -643,6 +649,7 @@ impl PhoneDetailsData for api::PhoneDetails { pub trait AddressDetailsData { fn get_first_name(&self) -> Result<&Secret, Error>; fn get_last_name(&self) -> Result<&Secret, Error>; + fn get_full_name(&self) -> Result, Error>; fn get_line1(&self) -> Result<&Secret, Error>; fn get_city(&self) -> Result<&String, Error>; fn get_line2(&self) -> Result<&Secret, Error>; @@ -666,6 +673,13 @@ impl AddressDetailsData for api::AddressDetails { .ok_or_else(missing_field_err("address.last_name")) } + fn get_full_name(&self) -> Result, Error> { + let first_name = self.get_first_name()?.peek().to_owned(); + let last_name = self.get_last_name()?.peek().to_owned(); + let full_name = format!("{} {}", first_name, last_name).trim().to_string(); + Ok(Secret::new(full_name)) + } + fn get_line1(&self) -> Result<&Secret, Error> { self.line1 .as_ref() diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 4589742005..0c5d9ae5ef 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -240,7 +240,7 @@ impl ConnectorData { enums::Connector::Nuvei => Ok(Box::new(&connector::Nuvei)), enums::Connector::Opennode => Ok(Box::new(&connector::Opennode)), // "payeezy" => Ok(Box::new(&connector::Payeezy)), As psync and rsync are not supported by this connector, it is added as template code for future usage - //enums::Connector::Payme => Ok(Box::new(&connector::Payme)), + enums::Connector::Payme => Ok(Box::new(&connector::Payme)), enums::Connector::Payu => Ok(Box::new(&connector::Payu)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index b666f2946c..378170cd1f 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -35,7 +35,7 @@ pub struct ConnectorAuthentication { pub opayo: Option, pub opennode: Option, pub payeezy: Option, - pub payme: Option, + pub payme: Option, pub paypal: Option, pub payu: Option, pub rapyd: Option, diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 0b9edc84dc..9db03cdfff 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -1,9 +1,13 @@ +use std::str::FromStr; + +use api_models::payments::{Address, AddressDetails, OrderDetailsWithAmount}; +use common_utils::pii::Email; use masking::Secret; -use router::types::{self, api, storage::enums}; +use router::types::{self, api, storage::enums, PaymentAddress}; use crate::{ connector_auth, - utils::{self, ConnectorActions}, + utils::{self, ConnectorActions, PaymentAuthorizeType}, }; #[derive(Clone, Copy)] @@ -14,7 +18,7 @@ impl utils::Connector for PaymeTest { use router::connector::Payme; types::api::ConnectorData { connector: Box::new(&Payme), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Payme, get_token: types::api::GetToken::Connector, } } @@ -35,11 +39,52 @@ impl utils::Connector for PaymeTest { static CONNECTOR: PaymeTest = PaymeTest {}; fn get_default_payment_info() -> Option { - None + Some(utils::PaymentInfo { + address: Some(PaymentAddress { + shipping: None, + billing: Some(Address { + address: Some(AddressDetails { + city: None, + country: None, + line1: None, + line2: None, + line3: None, + zip: None, + state: None, + first_name: Some(Secret::new("John".to_string())), + last_name: Some(Secret::new("Doe".to_string())), + }), + phone: None, + }), + }), + auth_type: None, + access_token: None, + connector_meta_data: None, + return_url: None, + }) } fn payment_method_details() -> Option { - None + Some(types::PaymentsAuthorizeData { + order_details: Some(vec![OrderDetailsWithAmount { + product_name: "iphone 13".to_string(), + quantity: 1, + amount: 1000, + }]), + router_return_url: Some("https://hyperswitch.io".to_string()), + webhook_url: Some("https://hyperswitch.io".to_string()), + email: Some(Email::from_str("test@gmail.com").unwrap()), + payment_method_data: types::api::PaymentMethodData::Card(api::Card { + card_number: cards::CardNumber::from_str("4111111111111111").unwrap(), + card_cvc: Secret::new("123".to_string()), + card_exp_month: Secret::new("10".to_string()), + card_exp_year: Secret::new("2025".to_string()), + card_holder_name: Secret::new("John Doe".to_string()), + ..utils::CCardType::default().0 + }), + amount: 1000, + ..PaymentAuthorizeType::default().0 + }) } // Cards Positive Tests @@ -65,6 +110,7 @@ async fn should_capture_authorized_payment() { // Partially captures a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not support partial capture"] async fn should_partially_capture_authorized_payment() { let response = CONNECTOR .authorize_and_capture_payment( @@ -82,6 +128,7 @@ async fn should_partially_capture_authorized_payment() { // Synchronizes a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports sync"] async fn should_sync_authorized_payment() { let authorize_response = CONNECTOR .authorize_payment(payment_method_details(), get_default_payment_info()) @@ -106,6 +153,7 @@ async fn should_sync_authorized_payment() { // Voids a payment using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports void"] async fn should_void_authorized_payment() { let response = CONNECTOR .authorize_and_void_payment( @@ -118,8 +166,11 @@ async fn should_void_authorized_payment() { get_default_payment_info(), ) .await - .expect("Void payment response"); - assert_eq!(response.status, enums::AttemptStatus::Voided); + .unwrap(); + assert_eq!( + response.response.unwrap_err().message, + "Void flow not supported by Payme connector".to_string() + ); } // Refunds a payment using the manual capture flow (Non 3DS). @@ -148,7 +199,6 @@ async fn should_partially_refund_manually_captured_payment() { payment_method_details(), None, Some(types::RefundsData { - refund_amount: 50, ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), @@ -163,6 +213,7 @@ async fn should_partially_refund_manually_captured_payment() { // Synchronizes a refund using the manual capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports sync"] async fn should_sync_manually_captured_refund() { let refund_response = CONNECTOR .capture_payment_and_refund( @@ -200,6 +251,7 @@ async fn should_make_payment() { // Synchronizes a payment using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports sync"] async fn should_sync_auto_captured_payment() { let authorize_response = CONNECTOR .make_payment(payment_method_details(), get_default_payment_info()) @@ -245,7 +297,6 @@ async fn should_partially_refund_succeeded_payment() { .make_payment_and_refund( payment_method_details(), Some(types::RefundsData { - refund_amount: 50, ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), @@ -265,7 +316,7 @@ async fn should_refund_succeeded_payment_multiple_times() { .make_payment_and_multiple_refund( payment_method_details(), Some(types::RefundsData { - refund_amount: 50, + refund_amount: 100, ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), @@ -275,6 +326,7 @@ async fn should_refund_succeeded_payment_multiple_times() { // Synchronizes a refund using the automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports sync"] async fn should_sync_refund() { let refund_response = CONNECTOR .make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) @@ -290,8 +342,8 @@ async fn should_sync_refund() { .await .unwrap(); assert_eq!( - response.response.unwrap().refund_status, - enums::RefundStatus::Success, + response.response.unwrap_err().message, + "Refund Sync flow not supported by Payme connector", ); } @@ -302,10 +354,20 @@ async fn should_fail_payment_for_incorrect_cvc() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + amount: 100, + currency: enums::Currency::ILS, payment_method_data: types::api::PaymentMethodData::Card(api::Card { card_cvc: Secret::new("12345".to_string()), ..utils::CCardType::default().0 }), + order_details: Some(vec![OrderDetailsWithAmount { + product_name: "iphone 13".to_string(), + quantity: 1, + amount: 100, + }]), + router_return_url: Some("https://hyperswitch.io".to_string()), + webhook_url: Some("https://hyperswitch.io".to_string()), + email: Some(Email::from_str("test@gmail.com").unwrap()), ..utils::PaymentAuthorizeType::default().0 }), get_default_payment_info(), @@ -314,7 +376,7 @@ async fn should_fail_payment_for_incorrect_cvc() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card's security code is invalid.".to_string(), + "internal_server_error".to_string(), ); } @@ -324,10 +386,20 @@ async fn should_fail_payment_for_invalid_exp_month() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + amount: 100, + currency: enums::Currency::ILS, payment_method_data: types::api::PaymentMethodData::Card(api::Card { card_exp_month: Secret::new("20".to_string()), ..utils::CCardType::default().0 }), + order_details: Some(vec![OrderDetailsWithAmount { + product_name: "iphone 13".to_string(), + quantity: 1, + amount: 100, + }]), + router_return_url: Some("https://hyperswitch.io".to_string()), + webhook_url: Some("https://hyperswitch.io".to_string()), + email: Some(Email::from_str("test@gmail.com").unwrap()), ..utils::PaymentAuthorizeType::default().0 }), get_default_payment_info(), @@ -336,7 +408,7 @@ async fn should_fail_payment_for_invalid_exp_month() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card's expiration month is invalid.".to_string(), + "internal_server_error".to_string(), ); } @@ -346,10 +418,20 @@ async fn should_fail_payment_for_incorrect_expiry_year() { let response = CONNECTOR .make_payment( Some(types::PaymentsAuthorizeData { + amount: 100, + currency: enums::Currency::ILS, payment_method_data: types::api::PaymentMethodData::Card(api::Card { - card_exp_year: Secret::new("2000".to_string()), + card_exp_year: Secret::new("2012".to_string()), ..utils::CCardType::default().0 }), + order_details: Some(vec![OrderDetailsWithAmount { + product_name: "iphone 13".to_string(), + quantity: 1, + amount: 100, + }]), + router_return_url: Some("https://hyperswitch.io".to_string()), + webhook_url: Some("https://hyperswitch.io".to_string()), + email: Some(Email::from_str("test@gmail.com").unwrap()), ..utils::PaymentAuthorizeType::default().0 }), get_default_payment_info(), @@ -358,12 +440,13 @@ async fn should_fail_payment_for_incorrect_expiry_year() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Your card's expiration year is invalid.".to_string(), + "internal_server_error".to_string(), ); } // Voids a payment using automatic capture flow (Non 3DS). #[actix_web::test] +#[ignore = "Connector does not supports void"] async fn should_fail_void_payment_for_auto_capture() { let authorize_response = CONNECTOR .make_payment(payment_method_details(), get_default_payment_info()) @@ -378,7 +461,7 @@ async fn should_fail_void_payment_for_auto_capture() { .unwrap(); assert_eq!( void_response.response.unwrap_err().message, - "You cannot cancel this PaymentIntent because it has a status of succeeded." + "Void flow not supported by Payme connector" ); } @@ -391,7 +474,7 @@ async fn should_fail_capture_for_invalid_payment() { .unwrap(); assert_eq!( capture_response.response.unwrap_err().message, - String::from("No such payment_intent: '123456789'") + String::from("internal_server_error") ); } @@ -402,7 +485,7 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() { .make_payment_and_refund( payment_method_details(), Some(types::RefundsData { - refund_amount: 150, + refund_amount: 1500, ..utils::PaymentRefundType::default().0 }), get_default_payment_info(), @@ -411,7 +494,7 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() { .unwrap(); assert_eq!( response.response.unwrap_err().message, - "Refund amount (₹1.50) is greater than charge amount (₹1.00)", + "internal_server_error", ); } diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 6b98c7a0f6..483943e11a 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -130,13 +130,15 @@ pypl_pass="" gmail_email="" gmail_pass="" +[payme] +# Open api key +api_key="seller payme id" +key1="payme client key" + [cryptopay] api_key = "api_key" key1 = "key1" -[payme] -api_key="API Key" - [cashtocode] api_key="Classic PMT API Key" key1 = "Evoucher PMT API Key" diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 650f6a4f5d..9b332e14dd 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2947,6 +2947,7 @@ "noon", "nuvei", "opennode", + "payme", "paypal", "payu", "rapyd",