mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(connector): [Payme] add Authorize, Sync, Capture, Refund, Refund Sync, Mandate & web hooks support for cards (#1594)
This commit is contained in:
@ -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,
|
||||
|
||||
@ -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<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Payme
|
||||
@ -55,17 +56,13 @@ where
|
||||
{
|
||||
fn build_headers(
|
||||
&self,
|
||||
req: &types::RouterData<Flow, Request, Response>,
|
||||
_req: &types::RouterData<Flow, Request, Response>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<Vec<(String, request::Maskable<String>)>, 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<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
|
||||
for Payme
|
||||
{
|
||||
//TODO: implement sessions flow
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken>
|
||||
@ -129,9 +113,138 @@ impl ConnectorIntegration<api::Verify, types::VerifyRequestData, types::Payments
|
||||
{
|
||||
}
|
||||
|
||||
impl
|
||||
ConnectorIntegration<
|
||||
api::InitPayment,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
> for Payme
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsInitRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
|
||||
Ok(format!("{}api/generate-sale", self.base_url(connectors)))
|
||||
}
|
||||
|
||||
fn get_request_body(
|
||||
&self,
|
||||
req: &types::PaymentsInitRouterData,
|
||||
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> {
|
||||
let req_obj = payme::GenerateSaleRequest::try_from(req)?;
|
||||
let payme_req = types::RequestBody::log_and_get_request_body(
|
||||
&req_obj,
|
||||
utils::Encode::<payme::GenerateSaleRequest>::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<Option<services::Request>, 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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
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<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsAuthorizeRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
req: &types::PaymentsAuthorizeRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
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<Option<types::RequestBody>, 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::<payme::PaymePaymentsRequest>::encode_to_string_of_json,
|
||||
utils::Encode::<payme::PayRequest>::encode_to_string_of_json,
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(payme_req))
|
||||
@ -190,7 +309,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
data: &types::PaymentsAuthorizeRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
|
||||
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<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -213,63 +331,40 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
|
||||
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
|
||||
for Payme
|
||||
{
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::PaymentsSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> {
|
||||
let response: payme::PaymePaymentsResponse = res
|
||||
) -> CustomResult<
|
||||
types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
|
||||
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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,16 +386,22 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::PaymentsCaptureRouterData,
|
||||
_connectors: &settings::Connectors,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
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<Option<types::RequestBody>, 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::<payme::PaymentCaptureRequest>::encode_to_string_of_json,
|
||||
)
|
||||
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
|
||||
Ok(Some(payme_req))
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
@ -326,7 +427,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
data: &types::PaymentsCaptureRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
|
||||
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<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -349,6 +449,17 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
|
||||
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
|
||||
for Payme
|
||||
{
|
||||
fn build_request(
|
||||
&self,
|
||||
_req: &types::RouterData<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::FlowNotSupported {
|
||||
flow: "Void".to_string(),
|
||||
connector: "Payme".to_string(),
|
||||
}
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsResponseData> for Payme {
|
||||
@ -367,9 +478,9 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
|
||||
fn get_url(
|
||||
&self,
|
||||
_req: &types::RefundsRouterData<api::Execute>,
|
||||
_connectors: &settings::Connectors,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<String, errors::ConnectorError> {
|
||||
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<api::Execute, types::RefundsData, types::RefundsRespon
|
||||
.response
|
||||
.parse_struct("payme RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
types::RouterData::try_from((
|
||||
&data.request,
|
||||
types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -428,87 +541,134 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
|
||||
}
|
||||
|
||||
impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Payme {
|
||||
fn get_headers(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Vec<(String, request::Maskable<String>)>, 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<String, errors::ConnectorError> {
|
||||
Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: &types::RefundSyncRouterData,
|
||||
connectors: &settings::Connectors,
|
||||
) -> CustomResult<Option<services::Request>, 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(),
|
||||
))
|
||||
Err(errors::ConnectorError::FlowNotSupported {
|
||||
flow: "Refund Sync".to_string(),
|
||||
connector: "Payme".to_string(),
|
||||
}
|
||||
|
||||
fn handle_response(
|
||||
&self,
|
||||
data: &types::RefundSyncRouterData,
|
||||
res: Response,
|
||||
) -> CustomResult<types::RefundSyncRouterData, errors::ConnectorError> {
|
||||
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<ErrorResponse, errors::ConnectorError> {
|
||||
self.build_error_response(res)
|
||||
.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<Box<dyn crypto::VerifySignature + Send>, errors::ConnectorError> {
|
||||
Ok(Box::new(crypto::Md5))
|
||||
}
|
||||
|
||||
fn get_webhook_source_verification_signature(
|
||||
&self,
|
||||
request: &api::IncomingWebhookRequestDetails<'_>,
|
||||
) -> CustomResult<Vec<u8>, 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<Vec<u8>, 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<api::webhooks::ObjectReferenceId, errors::ConnectorError> {
|
||||
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<api::IncomingWebhookEvent, errors::ConnectorError> {
|
||||
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<Vec<u8>, 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<serde_json::Value, errors::ConnectorError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<String>,
|
||||
buyer_email: pii::Email,
|
||||
payme_sale_id: String,
|
||||
#[serde(flatten)]
|
||||
card: PaymeCard,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Eq, PartialEq)]
|
||||
pub struct PaymeCard {
|
||||
name: Secret<String>,
|
||||
number: cards::CardNumber,
|
||||
expiry_month: Secret<String>,
|
||||
expiry_year: Secret<String>,
|
||||
cvc: Secret<String>,
|
||||
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<String>,
|
||||
sale_callback_url: String,
|
||||
buyer_key: Secret<String>,
|
||||
}
|
||||
|
||||
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<String>,
|
||||
credit_card_exp: Secret<String>,
|
||||
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<String>,
|
||||
sale_callback_url: String,
|
||||
sale_payment_method: SalePaymentMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GenerateSaleResponse {
|
||||
payme_sale_id: String,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, PaymePaySaleResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaymePaySaleResponse, T, types::PaymentsResponseData>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsInitRouterData) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(value: &types::PaymentsInitRouterData) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(item: &PaymentMethodData) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(value: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
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<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
|
||||
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<String>,
|
||||
pub(super) payme_client_key: Secret<String>,
|
||||
pub(super) seller_payme_id: Secret<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
|
||||
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<PaymePaymentStatus> for enums::AttemptStatus {
|
||||
fn from(item: PaymePaymentStatus) -> Self {
|
||||
impl From<SaleStatus> 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<Secret<String>>,
|
||||
}
|
||||
|
||||
impl<F, T>
|
||||
TryFrom<types::ResponseRouterData<F, PaymePaymentsResponse, T, types::PaymentsResponseData>>
|
||||
for types::RouterData<F, T, types::PaymentsResponseData>
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PaymeMetadata {
|
||||
payme_transaction_id: String,
|
||||
}
|
||||
|
||||
impl<F>
|
||||
TryFrom<
|
||||
types::ResponseRouterData<
|
||||
F,
|
||||
GenerateSaleResponse,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
> for types::RouterData<F, types::PaymentsAuthorizeData, types::PaymentsResponseData>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::ResponseRouterData<F, PaymePaymentsResponse, T, types::PaymentsResponseData>,
|
||||
item: types::ResponseRouterData<
|
||||
F,
|
||||
GenerateSaleResponse,
|
||||
types::PaymentsAuthorizeData,
|
||||
types::PaymentsResponseData,
|
||||
>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<F, T>
|
||||
}
|
||||
}
|
||||
|
||||
//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<errors::ConnectorError>;
|
||||
fn try_from(item: &types::PaymentsCaptureRouterData) -> Result<Self, Self::Error> {
|
||||
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<String>,
|
||||
payme_client_key: Secret<String>,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for PaymeRefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
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<RefundStatus> 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<SaleStatus> for enums::RefundStatus {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(sale_status: SaleStatus) -> Result<Self, Self::Error> {
|
||||
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<types::RefundsResponseRouterData<api::Execute, RefundResponse>>
|
||||
for types::RefundsRouterData<api::Execute>
|
||||
impl
|
||||
TryFrom<(
|
||||
&types::RefundsData,
|
||||
types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
)> for types::RefundsRouterData<api::Execute>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
(req, item): (
|
||||
&types::RefundsData,
|
||||
types::RefundsResponseRouterData<api::Execute, RefundResponse>,
|
||||
),
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<types::RefundsResponseRouterData<api::RSync, RefundResponse>>
|
||||
for types::RefundsRouterData<api::RSync>
|
||||
{
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(
|
||||
item: types::RefundsResponseRouterData<api::RSync, RefundResponse>,
|
||||
) -> Result<Self, Self::Error> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub buyer_key: Option<Secret<String>>,
|
||||
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<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<WebhookEventDataResource> for PaymePaySaleResponse {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(value: WebhookEventDataResource) -> Result<Self, Self::Error> {
|
||||
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<NotifyType> 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,6 +205,7 @@ pub trait PaymentsAuthorizeRequestData {
|
||||
fn get_router_return_url(&self) -> Result<String, Error>;
|
||||
fn is_wallet(&self) -> bool;
|
||||
fn get_payment_method_type(&self) -> Result<storage_models::enums::PaymentMethodType, Error>;
|
||||
fn get_connector_mandate_id(&self) -> Result<String, Error>;
|
||||
}
|
||||
|
||||
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<String, Error> {
|
||||
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<String>, Error>;
|
||||
fn get_last_name(&self) -> Result<&Secret<String>, Error>;
|
||||
fn get_full_name(&self) -> Result<Secret<String>, Error>;
|
||||
fn get_line1(&self) -> Result<&Secret<String>, Error>;
|
||||
fn get_city(&self) -> Result<&String, Error>;
|
||||
fn get_line2(&self) -> Result<&Secret<String>, 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<Secret<String>, 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<String>, Error> {
|
||||
self.line1
|
||||
.as_ref()
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -35,7 +35,7 @@ pub struct ConnectorAuthentication {
|
||||
pub opayo: Option<HeaderKey>,
|
||||
pub opennode: Option<HeaderKey>,
|
||||
pub payeezy: Option<SignatureKey>,
|
||||
pub payme: Option<HeaderKey>,
|
||||
pub payme: Option<BodyKey>,
|
||||
pub paypal: Option<BodyKey>,
|
||||
pub payu: Option<BodyKey>,
|
||||
pub rapyd: Option<BodyKey>,
|
||||
|
||||
@ -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<utils::PaymentInfo> {
|
||||
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<types::PaymentsAuthorizeData> {
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -2947,6 +2947,7 @@
|
||||
"noon",
|
||||
"nuvei",
|
||||
"opennode",
|
||||
"payme",
|
||||
"paypal",
|
||||
"payu",
|
||||
"rapyd",
|
||||
|
||||
Reference in New Issue
Block a user