feat(connector): [Payme] add Authorize, Sync, Capture, Refund, Refund Sync, Mandate & web hooks support for cards (#1594)

This commit is contained in:
Arjun Karthik
2023-07-07 12:40:02 +05:30
committed by GitHub
parent fc9057ef2c
commit 093cc6a71c
9 changed files with 816 additions and 277 deletions

View File

@ -653,7 +653,7 @@ pub enum Connector {
// Opayo, added as template code for future usage // Opayo, added as template code for future usage
Opennode, Opennode,
// Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage
// Payme, Payme,
Paypal, Paypal,
Payu, Payu,
Rapyd, Rapyd,
@ -758,7 +758,7 @@ pub enum RoutableConnectors {
// Opayo, added as template code for future usage // Opayo, added as template code for future usage
Opennode, Opennode,
// Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage // Payeezy, As psync and rsync are not supported by this connector, it is added as template code for future usage
// Payme, Payme,
Paypal, Paypal,
Payu, Payu,
Rapyd, Rapyd,

View File

@ -2,19 +2,21 @@ mod transformers;
use std::fmt::Debug; use std::fmt::Debug;
use common_utils::{crypto, ext_traits::ByteSliceExt};
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use masking::PeekInterface; use masking::ExposeInterface;
use transformers as payme; use transformers as payme;
use crate::{ use crate::{
configs::settings, configs::settings,
core::errors::{self, CustomResult}, connector::utils as conn_utils,
headers, core::{
services::{ errors::{self, CustomResult},
self, payments,
request::{self, Mask},
ConnectorIntegration,
}, },
db::StorageInterface,
headers, routes,
services::{self, request, ConnectorIntegration},
types::{ types::{
self, self,
api::{self, ConnectorCommon, ConnectorCommonExt}, api::{self, ConnectorCommon, ConnectorCommonExt},
@ -46,7 +48,6 @@ impl
types::PaymentsResponseData, types::PaymentsResponseData,
> for Payme > for Payme
{ {
// Not Implemented (R)
} }
impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Payme impl<Flow, Request, Response> ConnectorCommonExt<Flow, Request, Response> for Payme
@ -55,17 +56,13 @@ where
{ {
fn build_headers( fn build_headers(
&self, &self,
req: &types::RouterData<Flow, Request, Response>, _req: &types::RouterData<Flow, Request, Response>,
_connectors: &settings::Connectors, _connectors: &settings::Connectors,
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> { ) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let mut header = vec![( let header = vec![(
headers::CONTENT_TYPE.to_string(), headers::CONTENT_TYPE.to_string(),
types::PaymentsAuthorizeType::get_content_type(self) Self::get_content_type(self).to_string().into(),
.to_string()
.into(),
)]; )];
let mut api_key = self.get_auth_header(&req.connector_auth_type)?;
header.append(&mut api_key);
Ok(header) Ok(header)
} }
} }
@ -83,18 +80,6 @@ impl ConnectorCommon for Payme {
connectors.payme.base_url.as_ref() 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( fn build_error_response(
&self, &self,
res: Response, res: Response,
@ -116,7 +101,6 @@ impl ConnectorCommon for Payme {
impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData> impl ConnectorIntegration<api::Session, types::PaymentsSessionData, types::PaymentsResponseData>
for Payme for Payme
{ {
//TODO: implement sessions flow
} }
impl ConnectorIntegration<api::AccessTokenAuth, types::AccessTokenRequestData, types::AccessToken> 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> impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::PaymentsResponseData>
for Payme 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( fn get_headers(
&self, &self,
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsAuthorizeRouterData,
@ -146,20 +259,26 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
fn get_url( fn get_url(
&self, &self,
_req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsAuthorizeRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> 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( fn get_request_body(
&self, &self,
req: &types::PaymentsAuthorizeRouterData, req: &types::PaymentsAuthorizeRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> { ) -> 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( let payme_req = types::RequestBody::log_and_get_request_body(
&req_obj, &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)?; .change_context(errors::ConnectorError::RequestEncodingFailed)?;
Ok(Some(payme_req)) Ok(Some(payme_req))
@ -190,7 +309,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: &types::PaymentsAuthorizeRouterData, data: &types::PaymentsAuthorizeRouterData,
res: Response, res: Response,
) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsAuthorizeRouterData, errors::ConnectorError> {
let response: payme::PaymePaymentsResponse = res let response: payme::PaymePaySaleResponse = res
.response .response
.parse_struct("Payme PaymentsAuthorizeResponse") .parse_struct("Payme PaymentsAuthorizeResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -199,7 +318,6 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
data: data.clone(), data: data.clone(),
http_code: res.status_code, http_code: res.status_code,
}) })
.change_context(errors::ConnectorError::ResponseHandlingFailed)
} }
fn get_error_response( fn get_error_response(
@ -213,63 +331,40 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData> impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>
for Payme for Payme
{ {
fn get_headers( fn build_request(
&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(
&self, &self,
_req: &types::PaymentsSyncRouterData, _req: &types::PaymentsSyncRouterData,
_connectors: &settings::Connectors, _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> { ) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some( Err(errors::ConnectorError::FlowNotSupported {
services::RequestBuilder::new() flow: "Payment Sync".to_string(),
.method(services::Method::Get) connector: "Payme".to_string(),
.url(&types::PaymentsSyncType::get_url(self, req, connectors)?) }
.attach_default_headers() .into())
.headers(types::PaymentsSyncType::get_headers(self, req, connectors)?)
.build(),
))
} }
fn handle_response( fn handle_response(
&self, &self,
data: &types::PaymentsSyncRouterData, data: &types::RouterData<api::PSync, types::PaymentsSyncData, types::PaymentsResponseData>,
res: Response, res: Response,
) -> CustomResult<types::PaymentsSyncRouterData, errors::ConnectorError> { ) -> CustomResult<
let response: payme::PaymePaymentsResponse = res 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 .response
.parse_struct("payme PaymentsSyncResponse") .parse_struct("Payme PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData { types::RouterData::try_from(types::ResponseRouterData {
response, response,
data: data.clone(), data: data.clone(),
http_code: res.status_code, 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( fn get_url(
&self, &self,
_req: &types::PaymentsCaptureRouterData, _req: &types::PaymentsCaptureRouterData,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> 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( fn get_request_body(
&self, &self,
_req: &types::PaymentsCaptureRouterData, req: &types::PaymentsCaptureRouterData,
) -> CustomResult<Option<types::RequestBody>, errors::ConnectorError> { ) -> 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( fn build_request(
@ -326,7 +427,7 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
data: &types::PaymentsCaptureRouterData, data: &types::PaymentsCaptureRouterData,
res: Response, res: Response,
) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> { ) -> CustomResult<types::PaymentsCaptureRouterData, errors::ConnectorError> {
let response: payme::PaymePaymentsResponse = res let response: payme::PaymePaySaleResponse = res
.response .response
.parse_struct("Payme PaymentsCaptureResponse") .parse_struct("Payme PaymentsCaptureResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
@ -335,7 +436,6 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
data: data.clone(), data: data.clone(),
http_code: res.status_code, http_code: res.status_code,
}) })
.change_context(errors::ConnectorError::ResponseHandlingFailed)
} }
fn get_error_response( fn get_error_response(
@ -349,6 +449,17 @@ impl ConnectorIntegration<api::Capture, types::PaymentsCaptureData, types::Payme
impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData> impl ConnectorIntegration<api::Void, types::PaymentsCancelData, types::PaymentsResponseData>
for Payme 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 { 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( fn get_url(
&self, &self,
_req: &types::RefundsRouterData<api::Execute>, _req: &types::RefundsRouterData<api::Execute>,
_connectors: &settings::Connectors, connectors: &settings::Connectors,
) -> CustomResult<String, errors::ConnectorError> { ) -> 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( fn get_request_body(
@ -411,12 +522,14 @@ impl ConnectorIntegration<api::Execute, types::RefundsData, types::RefundsRespon
.response .response
.parse_struct("payme RefundResponse") .parse_struct("payme RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?; .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
types::RouterData::try_from(types::ResponseRouterData { types::RouterData::try_from((
response, &data.request,
data: data.clone(), types::ResponseRouterData {
http_code: res.status_code, response,
}) data: data.clone(),
.change_context(errors::ConnectorError::ResponseHandlingFailed) http_code: res.status_code,
},
))
} }
fn get_error_response( 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 { impl ConnectorIntegration<api::RSync, types::RefundsData, types::RefundsResponseData> for Payme {
fn get_headers( fn build_request(
&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(
&self, &self,
_req: &types::RefundSyncRouterData, _req: &types::RefundSyncRouterData,
_connectors: &settings::Connectors, _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> { ) -> CustomResult<Option<services::Request>, errors::ConnectorError> {
Ok(Some( Err(errors::ConnectorError::FlowNotSupported {
services::RequestBuilder::new() flow: "Refund Sync".to_string(),
.method(services::Method::Get) connector: "Payme".to_string(),
.url(&types::RefundSyncType::get_url(self, req, connectors)?) }
.attach_default_headers() .into())
.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<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)
} }
} }
#[async_trait::async_trait] #[async_trait::async_trait]
impl api::IncomingWebhook for Payme { impl api::IncomingWebhook for Payme {
fn get_webhook_object_reference_id( fn get_webhook_source_verification_algorithm(
&self, &self,
_request: &api::IncomingWebhookRequestDetails<'_>, _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> { ) -> 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( fn get_webhook_event_type(
&self, &self,
_request: &api::IncomingWebhookRequestDetails<'_>, request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<api::IncomingWebhookEvent, errors::ConnectorError> { ) -> 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( fn get_webhook_resource_object(
&self, &self,
_request: &api::IncomingWebhookRequestDetails<'_>, request: &api::IncomingWebhookRequestDetails<'_>,
) -> CustomResult<serde_json::Value, errors::ConnectorError> { ) -> 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)
} }
} }

View File

@ -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 serde::{Deserialize, Serialize};
use crate::{ use crate::{
connector::utils::PaymentsAuthorizeRequestData, connector::utils::{
missing_field_err, AddressDetailsData, CardData, PaymentsAuthorizeRequestData, RouterData,
},
core::errors, core::errors,
types::{self, api, storage::enums}, types::{self, api, storage::enums, MandateReference},
}; };
//TODO: Fill the struct with respective fields #[derive(Debug, Serialize)]
#[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct PayRequest {
pub struct PaymePaymentsRequest { buyer_name: Secret<String>,
amount: i64, buyer_email: pii::Email,
payme_sale_id: String,
#[serde(flatten)]
card: PaymeCard, card: PaymeCard,
} }
#[derive(Default, Debug, Serialize, Eq, PartialEq)] #[derive(Debug, Serialize)]
pub struct PaymeCard { pub struct MandateRequest {
name: Secret<String>, currency: enums::Currency,
number: cards::CardNumber, sale_price: i64,
expiry_month: Secret<String>, transaction_id: String,
expiry_year: Secret<String>, product_name: String,
cvc: Secret<String>, sale_return_url: String,
complete: bool, 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>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> { fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data.clone() { match item.request.payment_method_data.clone() {
api::PaymentMethodData::Card(req_card) => { api::PaymentMethodData::Card(req_card) => {
let card = PaymeCard { let card = PaymeCard {
name: req_card.card_holder_name, credit_card_cvv: req_card.card_cvc.clone(),
number: req_card.card_number, credit_card_exp: req_card
expiry_month: req_card.card_exp_month, .get_card_expiry_month_year_2_digit_with_delimiter("".to_string()),
expiry_year: req_card.card_exp_year, credit_card_number: req_card.card_number,
cvc: req_card.card_cvc,
complete: item.request.is_auto_capture()?,
}; };
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 { Ok(Self {
amount: item.request.amount,
card, card,
buyer_email,
buyer_name,
payme_sale_id,
}) })
} }
_ => Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()), _ => 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 // Auth Struct
pub struct PaymeAuthType { 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 { impl TryFrom<&types::ConnectorAuthType> for PaymeAuthType {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> { fn try_from(auth_type: &types::ConnectorAuthType) -> Result<Self, Self::Error> {
match auth_type { match auth_type {
types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self {
api_key: Secret::new(api_key.to_string()), seller_payme_id: Secret::new(api_key.to_string()),
payme_client_key: Secret::new(key1.to_string()),
}), }),
_ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()),
} }
} }
} }
// PaymentsResponse
//TODO: Append the remaining status flags #[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "kebab-case")]
#[serde(rename_all = "lowercase")] pub enum SaleStatus {
pub enum PaymePaymentStatus { Initial,
Succeeded, Completed,
Refunded,
PartialRefund,
Authorized,
Voided,
PartialVoid,
Failed, Failed,
#[default] Chargeback,
Processing,
} }
impl From<PaymePaymentStatus> for enums::AttemptStatus { impl From<SaleStatus> for enums::AttemptStatus {
fn from(item: PaymePaymentStatus) -> Self { fn from(item: SaleStatus) -> Self {
match item { match item {
PaymePaymentStatus::Succeeded => Self::Charged, SaleStatus::Initial => Self::Authorizing,
PaymePaymentStatus::Failed => Self::Failure, SaleStatus::Completed => Self::Charged,
PaymePaymentStatus::Processing => Self::Authorizing, 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(Debug, Serialize, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct PaymePaySaleResponse {
pub struct PaymePaymentsResponse { sale_status: SaleStatus,
status: PaymePaymentStatus, payme_sale_id: String,
id: String, payme_transaction_id: String,
buyer_key: Option<Secret<String>>,
} }
impl<F, T> #[derive(Serialize, Deserialize)]
TryFrom<types::ResponseRouterData<F, PaymePaymentsResponse, T, types::PaymentsResponseData>> pub struct PaymeMetadata {
for types::RouterData<F, T, types::PaymentsResponseData> 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>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from( fn try_from(
item: types::ResponseRouterData<F, PaymePaymentsResponse, T, types::PaymentsResponseData>, item: types::ResponseRouterData<
F,
GenerateSaleResponse,
types::PaymentsAuthorizeData,
types::PaymentsResponseData,
>,
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(Self { 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 { 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, redirection_data: None,
mandate_reference: None, mandate_reference: None,
connector_metadata: 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 : // REFUND :
// Type definition for RefundRequest // Type definition for RefundRequest
#[derive(Default, Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct PaymeRefundRequest { 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 { impl<F> TryFrom<&types::RefundsRouterData<F>> for PaymeRefundRequest {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> { fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
let auth_type = PaymeAuthType::try_from(&item.connector_auth_type)?;
Ok(Self { 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 impl TryFrom<SaleStatus> for enums::RefundStatus {
type Error = error_stack::Report<errors::ConnectorError>;
#[allow(dead_code)] fn try_from(sale_status: SaleStatus) -> Result<Self, Self::Error> {
#[derive(Debug, Serialize, Default, Deserialize, Clone)] match sale_status {
pub enum RefundStatus { SaleStatus::Refunded | SaleStatus::PartialRefund => Ok(Self::Success),
Succeeded, SaleStatus::Failed => Ok(Self::Failure),
Failed, SaleStatus::Initial
#[default] | SaleStatus::Completed
Processing, | SaleStatus::Authorized
} | SaleStatus::Voided
| SaleStatus::PartialVoid
impl From<RefundStatus> for enums::RefundStatus { | SaleStatus::Chargeback => Err(errors::ConnectorError::ResponseHandlingFailed)?,
fn from(item: RefundStatus) -> Self {
match item {
RefundStatus::Succeeded => Self::Success,
RefundStatus::Failed => Self::Failure,
RefundStatus::Processing => Self::Pending,
//TODO: Review mapping
} }
} }
} }
//TODO: Fill the struct with respective fields #[derive(Debug, Deserialize)]
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse { pub struct RefundResponse {
id: String, sale_status: SaleStatus,
status: RefundStatus,
} }
impl TryFrom<types::RefundsResponseRouterData<api::Execute, RefundResponse>> impl
for types::RefundsRouterData<api::Execute> TryFrom<(
&types::RefundsData,
types::RefundsResponseRouterData<api::Execute, RefundResponse>,
)> for types::RefundsRouterData<api::Execute>
{ {
type Error = error_stack::Report<errors::ConnectorError>; type Error = error_stack::Report<errors::ConnectorError>;
fn try_from( fn try_from(
item: types::RefundsResponseRouterData<api::Execute, RefundResponse>, (req, item): (
&types::RefundsData,
types::RefundsResponseRouterData<api::Execute, RefundResponse>,
),
) -> Result<Self, Self::Error> { ) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
response: Ok(types::RefundsResponseData { response: Ok(types::RefundsResponseData {
connector_refund_id: item.response.id.to_string(), // 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
refund_status: enums::RefundStatus::from(item.response.status), connector_refund_id: req.connector_transaction_id.clone(),
refund_status: enums::RefundStatus::try_from(item.response.sale_status)?,
}), }),
..item.data ..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)] #[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct PaymeErrorResponse { pub struct PaymeErrorResponse {
pub status_code: u16, pub status_code: u16,
@ -202,3 +425,59 @@ pub struct PaymeErrorResponse {
pub message: String, pub message: String,
pub reason: Option<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,
}
}
}

View File

@ -205,6 +205,7 @@ pub trait PaymentsAuthorizeRequestData {
fn get_router_return_url(&self) -> Result<String, Error>; fn get_router_return_url(&self) -> Result<String, Error>;
fn is_wallet(&self) -> bool; fn is_wallet(&self) -> bool;
fn get_payment_method_type(&self) -> Result<storage_models::enums::PaymentMethodType, Error>; 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 { impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
@ -277,6 +278,11 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData {
.to_owned() .to_owned()
.ok_or_else(missing_field_err("payment_method_type")) .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 { pub trait BrowserInformationData {
@ -643,6 +649,7 @@ impl PhoneDetailsData for api::PhoneDetails {
pub trait AddressDetailsData { pub trait AddressDetailsData {
fn get_first_name(&self) -> Result<&Secret<String>, Error>; fn get_first_name(&self) -> Result<&Secret<String>, Error>;
fn get_last_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_line1(&self) -> Result<&Secret<String>, Error>;
fn get_city(&self) -> Result<&String, Error>; fn get_city(&self) -> Result<&String, Error>;
fn get_line2(&self) -> Result<&Secret<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")) .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> { fn get_line1(&self) -> Result<&Secret<String>, Error> {
self.line1 self.line1
.as_ref() .as_ref()

View File

@ -240,7 +240,7 @@ impl ConnectorData {
enums::Connector::Nuvei => Ok(Box::new(&connector::Nuvei)), enums::Connector::Nuvei => Ok(Box::new(&connector::Nuvei)),
enums::Connector::Opennode => Ok(Box::new(&connector::Opennode)), 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 // "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::Payu => Ok(Box::new(&connector::Payu)),
enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)), enums::Connector::Rapyd => Ok(Box::new(&connector::Rapyd)),
enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)), enums::Connector::Shift4 => Ok(Box::new(&connector::Shift4)),

View File

@ -35,7 +35,7 @@ pub struct ConnectorAuthentication {
pub opayo: Option<HeaderKey>, pub opayo: Option<HeaderKey>,
pub opennode: Option<HeaderKey>, pub opennode: Option<HeaderKey>,
pub payeezy: Option<SignatureKey>, pub payeezy: Option<SignatureKey>,
pub payme: Option<HeaderKey>, pub payme: Option<BodyKey>,
pub paypal: Option<BodyKey>, pub paypal: Option<BodyKey>,
pub payu: Option<BodyKey>, pub payu: Option<BodyKey>,
pub rapyd: Option<BodyKey>, pub rapyd: Option<BodyKey>,

View File

@ -1,9 +1,13 @@
use std::str::FromStr;
use api_models::payments::{Address, AddressDetails, OrderDetailsWithAmount};
use common_utils::pii::Email;
use masking::Secret; use masking::Secret;
use router::types::{self, api, storage::enums}; use router::types::{self, api, storage::enums, PaymentAddress};
use crate::{ use crate::{
connector_auth, connector_auth,
utils::{self, ConnectorActions}, utils::{self, ConnectorActions, PaymentAuthorizeType},
}; };
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@ -14,7 +18,7 @@ impl utils::Connector for PaymeTest {
use router::connector::Payme; use router::connector::Payme;
types::api::ConnectorData { types::api::ConnectorData {
connector: Box::new(&Payme), connector: Box::new(&Payme),
connector_name: types::Connector::DummyConnector1, connector_name: types::Connector::Payme,
get_token: types::api::GetToken::Connector, get_token: types::api::GetToken::Connector,
} }
} }
@ -35,11 +39,52 @@ impl utils::Connector for PaymeTest {
static CONNECTOR: PaymeTest = PaymeTest {}; static CONNECTOR: PaymeTest = PaymeTest {};
fn get_default_payment_info() -> Option<utils::PaymentInfo> { 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> { 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 // Cards Positive Tests
@ -65,6 +110,7 @@ async fn should_capture_authorized_payment() {
// Partially captures a payment using the manual capture flow (Non 3DS). // Partially captures a payment using the manual capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not support partial capture"]
async fn should_partially_capture_authorized_payment() { async fn should_partially_capture_authorized_payment() {
let response = CONNECTOR let response = CONNECTOR
.authorize_and_capture_payment( .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). // Synchronizes a payment using the manual capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports sync"]
async fn should_sync_authorized_payment() { async fn should_sync_authorized_payment() {
let authorize_response = CONNECTOR let authorize_response = CONNECTOR
.authorize_payment(payment_method_details(), get_default_payment_info()) .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). // Voids a payment using the manual capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports void"]
async fn should_void_authorized_payment() { async fn should_void_authorized_payment() {
let response = CONNECTOR let response = CONNECTOR
.authorize_and_void_payment( .authorize_and_void_payment(
@ -118,8 +166,11 @@ async fn should_void_authorized_payment() {
get_default_payment_info(), get_default_payment_info(),
) )
.await .await
.expect("Void payment response"); .unwrap();
assert_eq!(response.status, enums::AttemptStatus::Voided); 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). // 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(), payment_method_details(),
None, None,
Some(types::RefundsData { Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0 ..utils::PaymentRefundType::default().0
}), }),
get_default_payment_info(), 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). // Synchronizes a refund using the manual capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports sync"]
async fn should_sync_manually_captured_refund() { async fn should_sync_manually_captured_refund() {
let refund_response = CONNECTOR let refund_response = CONNECTOR
.capture_payment_and_refund( .capture_payment_and_refund(
@ -200,6 +251,7 @@ async fn should_make_payment() {
// Synchronizes a payment using the automatic capture flow (Non 3DS). // Synchronizes a payment using the automatic capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports sync"]
async fn should_sync_auto_captured_payment() { async fn should_sync_auto_captured_payment() {
let authorize_response = CONNECTOR let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info()) .make_payment(payment_method_details(), get_default_payment_info())
@ -245,7 +297,6 @@ async fn should_partially_refund_succeeded_payment() {
.make_payment_and_refund( .make_payment_and_refund(
payment_method_details(), payment_method_details(),
Some(types::RefundsData { Some(types::RefundsData {
refund_amount: 50,
..utils::PaymentRefundType::default().0 ..utils::PaymentRefundType::default().0
}), }),
get_default_payment_info(), get_default_payment_info(),
@ -265,7 +316,7 @@ async fn should_refund_succeeded_payment_multiple_times() {
.make_payment_and_multiple_refund( .make_payment_and_multiple_refund(
payment_method_details(), payment_method_details(),
Some(types::RefundsData { Some(types::RefundsData {
refund_amount: 50, refund_amount: 100,
..utils::PaymentRefundType::default().0 ..utils::PaymentRefundType::default().0
}), }),
get_default_payment_info(), 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). // Synchronizes a refund using the automatic capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports sync"]
async fn should_sync_refund() { async fn should_sync_refund() {
let refund_response = CONNECTOR let refund_response = CONNECTOR
.make_payment_and_refund(payment_method_details(), None, get_default_payment_info()) .make_payment_and_refund(payment_method_details(), None, get_default_payment_info())
@ -290,8 +342,8 @@ async fn should_sync_refund() {
.await .await
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap().refund_status, response.response.unwrap_err().message,
enums::RefundStatus::Success, "Refund Sync flow not supported by Payme connector",
); );
} }
@ -302,10 +354,20 @@ async fn should_fail_payment_for_incorrect_cvc() {
let response = CONNECTOR let response = CONNECTOR
.make_payment( .make_payment(
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
amount: 100,
currency: enums::Currency::ILS,
payment_method_data: types::api::PaymentMethodData::Card(api::Card { payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_cvc: Secret::new("12345".to_string()), card_cvc: Secret::new("12345".to_string()),
..utils::CCardType::default().0 ..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 ..utils::PaymentAuthorizeType::default().0
}), }),
get_default_payment_info(), get_default_payment_info(),
@ -314,7 +376,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, 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 let response = CONNECTOR
.make_payment( .make_payment(
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
amount: 100,
currency: enums::Currency::ILS,
payment_method_data: types::api::PaymentMethodData::Card(api::Card { payment_method_data: types::api::PaymentMethodData::Card(api::Card {
card_exp_month: Secret::new("20".to_string()), card_exp_month: Secret::new("20".to_string()),
..utils::CCardType::default().0 ..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 ..utils::PaymentAuthorizeType::default().0
}), }),
get_default_payment_info(), get_default_payment_info(),
@ -336,7 +408,7 @@ async fn should_fail_payment_for_invalid_exp_month() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, 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 let response = CONNECTOR
.make_payment( .make_payment(
Some(types::PaymentsAuthorizeData { Some(types::PaymentsAuthorizeData {
amount: 100,
currency: enums::Currency::ILS,
payment_method_data: types::api::PaymentMethodData::Card(api::Card { 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 ..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 ..utils::PaymentAuthorizeType::default().0
}), }),
get_default_payment_info(), get_default_payment_info(),
@ -358,12 +440,13 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, 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). // Voids a payment using automatic capture flow (Non 3DS).
#[actix_web::test] #[actix_web::test]
#[ignore = "Connector does not supports void"]
async fn should_fail_void_payment_for_auto_capture() { async fn should_fail_void_payment_for_auto_capture() {
let authorize_response = CONNECTOR let authorize_response = CONNECTOR
.make_payment(payment_method_details(), get_default_payment_info()) .make_payment(payment_method_details(), get_default_payment_info())
@ -378,7 +461,7 @@ async fn should_fail_void_payment_for_auto_capture() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
void_response.response.unwrap_err().message, 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(); .unwrap();
assert_eq!( assert_eq!(
capture_response.response.unwrap_err().message, 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( .make_payment_and_refund(
payment_method_details(), payment_method_details(),
Some(types::RefundsData { Some(types::RefundsData {
refund_amount: 150, refund_amount: 1500,
..utils::PaymentRefundType::default().0 ..utils::PaymentRefundType::default().0
}), }),
get_default_payment_info(), get_default_payment_info(),
@ -411,7 +494,7 @@ async fn should_fail_for_refund_amount_higher_than_payment_amount() {
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
response.response.unwrap_err().message, response.response.unwrap_err().message,
"Refund amount (₹1.50) is greater than charge amount (₹1.00)", "internal_server_error",
); );
} }

View File

@ -130,13 +130,15 @@ pypl_pass=""
gmail_email="" gmail_email=""
gmail_pass="" gmail_pass=""
[payme]
# Open api key
api_key="seller payme id"
key1="payme client key"
[cryptopay] [cryptopay]
api_key = "api_key" api_key = "api_key"
key1 = "key1" key1 = "key1"
[payme]
api_key="API Key"
[cashtocode] [cashtocode]
api_key="Classic PMT API Key" api_key="Classic PMT API Key"
key1 = "Evoucher PMT API Key" key1 = "Evoucher PMT API Key"

View File

@ -2947,6 +2947,7 @@
"noon", "noon",
"nuvei", "nuvei",
"opennode", "opennode",
"payme",
"paypal", "paypal",
"payu", "payu",
"rapyd", "rapyd",