diff --git a/config/Development.toml b/config/Development.toml index 75716e7126..0f5c9f5ab3 100644 --- a/config/Development.toml +++ b/config/Development.toml @@ -61,6 +61,7 @@ cards = [ "dlocal", "fiserv", "globalpay", + "mollie", "multisafepay", "nuvei", "payu", @@ -99,6 +100,7 @@ dlocal.base_url = "https://sandbox.dlocal.com/" fiserv.base_url = "https://cert.api.fiservapps.com/" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" payu.base_url = "https://secure.snd.payu.com/" diff --git a/config/config.example.toml b/config/config.example.toml index 7ef8eca00e..4cd4e75bc9 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -135,6 +135,7 @@ dlocal.base_url = "https://sandbox.dlocal.com/" fiserv.base_url = "https://cert.api.fiservapps.com/" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" payu.base_url = "https://secure.snd.payu.com/" @@ -156,6 +157,7 @@ cards = [ "checkout", "braintree", "cybersource", + "mollie", "shift4", "worldpay", "globalpay", diff --git a/config/docker_compose.toml b/config/docker_compose.toml index c9a962afe4..fdcb63d2a0 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -83,6 +83,7 @@ dlocal.base_url = "https://sandbox.dlocal.com/" fiserv.base_url = "https://cert.api.fiservapps.com/" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" payu.base_url = "https://secure.snd.payu.com/" @@ -109,6 +110,7 @@ cards = [ "dlocal", "fiserv", "globalpay", + "mollie", "multisafepay", "nuvei", "payu", diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 2b876972b9..37b7948327 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -560,6 +560,7 @@ pub enum Connector { Fiserv, Globalpay, Klarna, + Mollie, Multisafepay, Nuvei, Payu, @@ -611,15 +612,16 @@ pub enum RoutableConnectors { Fiserv, Globalpay, Klarna, + Mollie, + Multisafepay, Nuvei, Payu, Rapyd, Shift4, Stripe, + Trustpay, Worldline, Worldpay, - Multisafepay, - Trustpay, } /// Wallets which support obtaining session object diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index ef4ea23865..6886be1f2e 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -442,6 +442,7 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::SuccessfulPaymentNotFound => Self::SuccessfulPaymentNotFound, errors::ApiErrorResponse::AddressNotFound => Self::AddressNotFound, errors::ApiErrorResponse::NotImplemented { .. } => Self::Unauthorized, + errors::ApiErrorResponse::FlowNotSupported { .. } => Self::InternalServerError, errors::ApiErrorResponse::PaymentUnexpectedState { current_flow, field_name, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index c66378d5b5..92891dc1c7 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -252,6 +252,7 @@ pub struct Connectors { pub fiserv: ConnectorParams, pub globalpay: ConnectorParams, pub klarna: ConnectorParams, + pub mollie: ConnectorParams, pub multisafepay: ConnectorParams, pub nuvei: ConnectorParams, pub payu: ConnectorParams, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 67419ed05b..9f5211d053 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -23,11 +23,13 @@ pub mod utils; pub mod worldline; pub mod worldpay; +pub mod mollie; + pub use self::{ aci::Aci, adyen::Adyen, airwallex::Airwallex, applepay::Applepay, authorizedotnet::Authorizedotnet, bambora::Bambora, bluesnap::Bluesnap, braintree::Braintree, checkout::Checkout, cybersource::Cybersource, dlocal::Dlocal, fiserv::Fiserv, - globalpay::Globalpay, klarna::Klarna, multisafepay::Multisafepay, nuvei::Nuvei, payu::Payu, - rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, worldline::Worldline, - worldpay::Worldpay, + globalpay::Globalpay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nuvei::Nuvei, + payu::Payu, rapyd::Rapyd, shift4::Shift4, stripe::Stripe, trustpay::Trustpay, + worldline::Worldline, worldpay::Worldpay, }; diff --git a/crates/router/src/connector/mollie.rs b/crates/router/src/connector/mollie.rs new file mode 100644 index 0000000000..866b89b6c2 --- /dev/null +++ b/crates/router/src/connector/mollie.rs @@ -0,0 +1,508 @@ +mod transformers; + +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use transformers as mollie; + +use crate::{ + configs::settings, + core::{ + errors::{self, CustomResult}, + payments, + }, + headers, + services::{self, ConnectorIntegration}, + types::{ + self, + api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Mollie; + +impl api::Payment for Mollie {} +impl api::PaymentSession for Mollie {} +impl api::ConnectorAccessToken for Mollie {} +impl api::PreVerify for Mollie {} +impl api::PaymentAuthorize for Mollie {} +impl api::PaymentsCompleteAuthorize for Mollie {} +impl api::PaymentSync for Mollie {} +impl api::PaymentCapture for Mollie {} +impl api::PaymentVoid for Mollie {} +impl api::Refund for Mollie {} +impl api::RefundExecute for Mollie {} +impl api::RefundSync for Mollie {} + +impl ConnectorCommonExt for Mollie +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.get_auth_header(&req.connector_auth_type) + } +} + +impl ConnectorCommon for Mollie { + fn id(&self) -> &'static str { + "mollie" + } + + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.mollie.base_url.as_ref() + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult, errors::ConnectorError> { + let auth: mollie::MollieAuthType = auth_type + .try_into() + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + Ok(vec![( + headers::AUTHORIZATION.to_string(), + format!("Bearer {}", auth.api_key), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: mollie::ErrorResponse = res + .response + .parse_struct("MollieErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + message: response.detail, + reason: response.field, + status_code: response.status, + ..Default::default() + }) + } +} + +impl ConnectorIntegration + for Mollie +{ +} + +impl ConnectorIntegration + for Mollie +{ +} + +impl ConnectorIntegration + for Mollie +{ +} + +impl ConnectorIntegration + for Mollie +{ + fn get_headers( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + _req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!("{}payments", self.base_url(connectors))) + } + + fn get_request_body( + &self, + req: &types::PaymentsAuthorizeRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = mollie::MolliePaymentsRequest::try_from(req)?; + let mollie_req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(mollie_req)) + } + + fn build_request( + &self, + req: &types::PaymentsAuthorizeRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::PaymentsAuthorizeType::get_url( + self, req, connectors, + )?) + .headers(types::PaymentsAuthorizeType::get_headers( + self, req, connectors, + )?) + .body(types::PaymentsAuthorizeType::get_request_body(self, req)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsAuthorizeRouterData, + res: Response, + ) -> CustomResult { + let response: mollie::MolliePaymentsResponse = res + .response + .parse_struct("MolliePaymentsResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + api::CompleteAuthorize, + types::CompleteAuthorizeData, + types::PaymentsResponseData, + > for Mollie +{ +} + +impl ConnectorIntegration + for Mollie +{ + fn get_headers( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}payments/{}", + self.base_url(connectors), + req.request + .connector_transaction_id + .get_connector_transaction_id() + .change_context(errors::ConnectorError::RequestEncodingFailed)? + )) + } + + fn build_request( + &self, + req: &types::PaymentsSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) + .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsSyncRouterData, + res: Response, + ) -> CustomResult { + let response: mollie::MolliePaymentsResponse = res + .response + .parse_struct("mollie PaymentsSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration + for Mollie +{ + fn build_request( + &self, + _req: &types::PaymentsCaptureRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::FlowNotSupported { + flow: "Capture".to_string(), + connector: self.id().to_string(), + }) + .into_report() + } +} + +impl ConnectorIntegration + for Mollie +{ + fn get_headers( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}payments/{}", + self.base_url(connectors), + req.request.connector_transaction_id + )) + } + + fn build_request( + &self, + req: &types::PaymentsCancelRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Delete) + .url(&types::PaymentsVoidType::get_url(self, req, connectors)?) + .headers(types::PaymentsVoidType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsCancelRouterData, + res: Response, + ) -> CustomResult { + let response: mollie::MolliePaymentsResponse = res + .response + .parse_struct("MolliePaymentsCancelResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Mollie { + fn get_headers( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}payments/{}/refunds", + self.base_url(connectors), + req.request.connector_transaction_id + )) + } + + fn get_request_body( + &self, + req: &types::RefundsRouterData, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = mollie::MollieRefundRequest::try_from(req)?; + let mollie_req = + utils::Encode::::encode_to_string_of_json(&req_obj) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(mollie_req)) + } + + fn build_request( + &self, + req: &types::RefundsRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = services::RequestBuilder::new() + .method(services::Method::Post) + .url(&types::RefundExecuteType::get_url(self, req, connectors)?) + .headers(types::RefundExecuteType::get_headers( + self, req, connectors, + )?) + .body(types::RefundExecuteType::get_request_body(self, req)?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &types::RefundsRouterData, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: mollie::RefundResponse = res + .response + .parse_struct("MollieRefundResponse") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl ConnectorIntegration for Mollie { + fn get_headers( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_refund_id = req + .request + .connector_refund_id + .clone() + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + Ok(format!( + "{}payments/{}/refunds/{}", + self.base_url(connectors), + req.request.connector_transaction_id, + connector_refund_id + )) + } + + fn build_request( + &self, + req: &types::RefundSyncRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::RefundSyncType::get_url(self, req, connectors)?) + .headers(types::RefundSyncType::get_headers(self, req, connectors)?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::RefundSyncRouterData, + res: Response, + ) -> CustomResult { + let response: mollie::RefundResponse = res + .response + .parse_struct("MollieRefundSyncResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Mollie { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} + +impl services::ConnectorRedirectResponse for Mollie { + fn get_flow_type( + &self, + _query_params: &str, + _json_payload: Option, + _action: services::PaymentAction, + ) -> CustomResult { + Ok(payments::CallConnectorAction::Trigger) + } +} diff --git a/crates/router/src/connector/mollie/transformers.rs b/crates/router/src/connector/mollie/transformers.rs new file mode 100644 index 0000000000..2aa98f4248 --- /dev/null +++ b/crates/router/src/connector/mollie/transformers.rs @@ -0,0 +1,428 @@ +use api_models::payments; +use error_stack::IntoReport; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use storage_models::enums; +use url::Url; + +use crate::{ + connector::utils::{self, AddressDetailsData, RouterData}, + core::errors, + services, types, +}; + +type Error = error_stack::Report; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MolliePaymentsRequest { + amount: Amount, + description: String, + redirect_url: String, + cancel_url: Option, + webhook_url: String, + locale: Option, + #[serde(flatten)] + payment_method_data: PaymentMethodData, + metadata: Option, + sequence_type: SequenceType, + mandate_id: Option, + card_token: Option, +} + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct Amount { + currency: enums::Currency, + value: String, +} + +#[derive(Debug, Serialize)] +#[serde(tag = "method")] +#[serde(rename_all = "lowercase")] +pub enum PaymentMethodData { + Applepay(Box), + Eps, + Giropay, + Ideal(Box), + Paypal(Box), + Sofort, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayMethodData { + apple_pay_payment_token: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct IdealMethodData { + issuer: Option>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaypalMethodData { + billing_address: Option
, + shipping_address: Option
, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SequenceType { + #[default] + Oneoff, + First, + Recurring, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Address { + pub street_and_number: Secret, + pub postal_code: Secret, + pub city: String, + pub region: Option>, + pub country: String, +} + +impl TryFrom<&types::PaymentsAuthorizeRouterData> for MolliePaymentsRequest { + type Error = Error; + fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result { + let amount = Amount { + currency: item.request.currency, + value: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + }; + let description = item.get_description()?; + let redirect_url = item.get_return_url()?; + let payment_method_data = match item.request.capture_method.unwrap_or_default() { + enums::CaptureMethod::Automatic => match item.request.payment_method_data { + api_models::payments::PaymentMethodData::BankRedirect(ref redirect_data) => { + get_payment_method_for_bank_redirect(item, redirect_data) + } + api_models::payments::PaymentMethodData::Wallet(ref wallet_data) => { + get_payment_method_for_wallet(item, wallet_data) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + }, + _ => Err(errors::ConnectorError::FlowNotSupported { + flow: format!( + "{} capture", + item.request.capture_method.unwrap_or_default() + ), + connector: "Mollie".to_string(), + }) + .into_report(), + }?; + Ok(Self { + amount, + description, + redirect_url, + cancel_url: None, + /* webhook_url is a mandatory field. + But we can't support webhook in our core hence keeping it as empty string */ + webhook_url: "".to_string(), + locale: None, + payment_method_data, + metadata: None, + sequence_type: SequenceType::Oneoff, + mandate_id: None, + card_token: None, + }) + } +} + +fn get_payment_method_for_bank_redirect( + _item: &types::PaymentsAuthorizeRouterData, + redirect_data: &api_models::payments::BankRedirectData, +) -> Result { + let payment_method_data = match redirect_data { + api_models::payments::BankRedirectData::Eps { .. } => PaymentMethodData::Eps, + api_models::payments::BankRedirectData::Giropay { .. } => PaymentMethodData::Giropay, + api_models::payments::BankRedirectData::Ideal { .. } => { + PaymentMethodData::Ideal(Box::new(IdealMethodData { + // To do if possible this should be from the payment request + issuer: None, + })) + } + api_models::payments::BankRedirectData::Sofort { .. } => PaymentMethodData::Sofort, + }; + Ok(payment_method_data) +} + +fn get_payment_method_for_wallet( + item: &types::PaymentsAuthorizeRouterData, + wallet_data: &api_models::payments::WalletData, +) -> Result { + match wallet_data { + api_models::payments::WalletData::PaypalRedirect { .. } => { + Ok(PaymentMethodData::Paypal(Box::new(PaypalMethodData { + billing_address: get_billing_details(item)?, + shipping_address: get_shipping_details(item)?, + }))) + } + api_models::payments::WalletData::ApplePay(applepay_wallet_data) => { + Ok(PaymentMethodData::Applepay(Box::new(ApplePayMethodData { + apple_pay_payment_token: applepay_wallet_data.payment_data.to_owned(), + }))) + } + _ => Err(errors::ConnectorError::NotImplemented( + "Payment Method".to_string(), + )) + .into_report(), + } +} + +fn get_shipping_details( + item: &types::PaymentsAuthorizeRouterData, +) -> Result, Error> { + let shipping_address = item + .address + .shipping + .as_ref() + .and_then(|shipping| shipping.address.as_ref()); + get_address_details(shipping_address) +} + +fn get_billing_details( + item: &types::PaymentsAuthorizeRouterData, +) -> Result, Error> { + let billing_address = item + .address + .billing + .as_ref() + .and_then(|billing| billing.address.as_ref()); + get_address_details(billing_address) +} + +fn get_address_details( + address: Option<&payments::AddressDetails>, +) -> Result, Error> { + let address_details = match address { + Some(address) => { + let street_and_number = address.get_combined_address_line()?; + let postal_code = address.get_zip()?.to_owned(); + let city = address.get_city()?.to_owned(); + let region = None; + let country = address.get_country()?.to_owned(); + Some(Address { + street_and_number, + postal_code, + city, + region, + country, + }) + } + None => None, + }; + Ok(address_details) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MolliePaymentsResponse { + pub resource: String, + pub id: String, + pub amount: Amount, + pub description: Option, + pub metadata: Option, + pub status: MolliePaymentStatus, + pub is_cancelable: Option, + pub sequence_type: SequenceType, + pub redirect_url: Option, + pub webhook_url: Option, + #[serde(rename = "_links")] + pub links: Links, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum MolliePaymentStatus { + Open, + Canceled, + #[default] + Pending, + Authorized, + Expired, + Failed, + Paid, +} + +impl From for enums::AttemptStatus { + fn from(item: MolliePaymentStatus) -> Self { + match item { + MolliePaymentStatus::Paid => Self::Charged, + MolliePaymentStatus::Failed => Self::Failure, + MolliePaymentStatus::Pending => Self::Pending, + MolliePaymentStatus::Open => Self::AuthenticationPending, + MolliePaymentStatus::Canceled => Self::Voided, + MolliePaymentStatus::Authorized => Self::Authorized, + MolliePaymentStatus::Expired => Self::Failure, + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Link { + href: Url, + #[serde(rename = "type")] + type_: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Links { + #[serde(rename = "self")] + self_: Option, + checkout: Option, + dashboard: Option, + documentation: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CardDetails { + pub card_number: String, + pub card_holder: String, + pub card_expiry_date: String, + pub card_cvv: String, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct BankDetails { + billing_email: String, +} + +pub struct MollieAuthType { + pub(super) api_key: String, +} + +impl TryFrom<&types::ConnectorAuthType> for MollieAuthType { + type Error = Error; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + if let types::ConnectorAuthType::HeaderKey { api_key } = auth_type { + Ok(Self { + api_key: api_key.to_string(), + }) + } else { + Err(errors::ConnectorError::FailedToObtainAuthType.into()) + } + } +} + +impl + TryFrom> + for types::RouterData +{ + type Error = Error; + fn try_from( + item: types::ResponseRouterData, + ) -> Result { + let url = item + .response + .links + .checkout + .map(|link| services::RedirectForm::from((link.href, services::Method::Get))); + Ok(Self { + status: enums::AttemptStatus::from(item.response.status), + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::ConnectorTransactionId(item.response.id), + redirection_data: url, + mandate_reference: None, + connector_metadata: None, + }), + ..item.data + }) + } +} + +// REFUND : +#[derive(Default, Debug, Serialize)] +pub struct MollieRefundRequest { + amount: Amount, + description: Option, +} + +impl TryFrom<&types::RefundsRouterData> for MollieRefundRequest { + type Error = Error; + fn try_from(item: &types::RefundsRouterData) -> Result { + let amount = Amount { + currency: item.request.currency, + value: utils::to_currency_base_unit(item.request.amount, item.request.currency)?, + }; + Ok(Self { + amount, + description: item.request.reason.to_owned(), + }) + } +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RefundResponse { + resource: String, + id: String, + amount: Amount, + settlement_id: Option, + settlement_amount: Option, + status: MollieRefundStatus, + description: Option, + metadata: serde_json::Value, + payment_id: String, + #[serde(rename = "_links")] + links: Links, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum MollieRefundStatus { + Queued, + #[default] + Pending, + Processing, + Refunded, + Failed, + Canceled, +} + +impl From for enums::RefundStatus { + fn from(item: MollieRefundStatus) -> Self { + match item { + MollieRefundStatus::Queued + | MollieRefundStatus::Pending + | MollieRefundStatus::Processing => Self::Pending, + MollieRefundStatus::Refunded => Self::Success, + MollieRefundStatus::Failed | MollieRefundStatus::Canceled => Self::Failure, + } + } +} + +impl TryFrom> + for types::RefundsRouterData +{ + type Error = Error; + fn try_from( + item: types::RefundsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(types::RefundsResponseData { + connector_refund_id: item.response.id, + refund_status: enums::RefundStatus::from(item.response.status), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize)] +pub struct ErrorResponse { + pub status: u16, + pub title: Option, + pub detail: String, + pub field: Option, + #[serde(rename = "_links")] + pub links: Option, +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 861605a16d..478ee955f0 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -51,6 +51,7 @@ pub trait RouterData { fn get_billing_phone(&self) -> Result<&api::PhoneDetails, Error>; fn get_description(&self) -> Result; fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>; + fn get_shipping_address(&self) -> Result<&api::AddressDetails, Error>; fn get_connector_meta(&self) -> Result; fn get_session_token(&self) -> Result; fn to_connector_meta(&self) -> Result @@ -129,6 +130,14 @@ impl RouterData for types::RouterData Result<&api::AddressDetails, Error> { + self.address + .shipping + .as_ref() + .and_then(|a| a.address.as_ref()) + .ok_or_else(missing_field_err("shipping.address")) + } } pub trait PaymentsAuthorizeRequestData { @@ -306,6 +315,7 @@ pub trait AddressDetailsData { fn get_line2(&self) -> Result<&Secret, Error>; fn get_zip(&self) -> Result<&Secret, Error>; fn get_country(&self) -> Result<&String, Error>; + fn get_combined_address_line(&self) -> Result, Error>; } impl AddressDetailsData for api::AddressDetails { @@ -350,6 +360,14 @@ impl AddressDetailsData for api::AddressDetails { .as_ref() .ok_or_else(missing_field_err("address.country")) } + + fn get_combined_address_line(&self) -> Result, Error> { + Ok(Secret::new(format!( + "{},{}", + self.get_line1()?.peek(), + self.get_line2()?.peek() + ))) + } } pub fn get_header_key_value<'a>( @@ -448,16 +466,16 @@ pub fn to_currency_base_unit( let amount_u32 = u32::try_from(amount) .into_report() .change_context(errors::ConnectorError::RequestEncodingFailed)?; - match currency { - storage_models::enums::Currency::JPY | storage_models::enums::Currency::KRW => { - Ok(amount.to_string()) - } + let amount_f64 = f64::from(amount_u32); + let amount = match currency { + storage_models::enums::Currency::JPY | storage_models::enums::Currency::KRW => amount_f64, storage_models::enums::Currency::BHD | storage_models::enums::Currency::JOD | storage_models::enums::Currency::KWD - | storage_models::enums::Currency::OMR => Ok((f64::from(amount_u32) / 1000.0).to_string()), - _ => Ok((f64::from(amount_u32) / 100.0).to_string()), - } + | storage_models::enums::Currency::OMR => amount_f64 / 1000.00, + _ => amount_f64 / 100.00, + }; + Ok(format!("{:.2}", amount)) } pub fn str_to_f32(value: &str, serializer: S) -> Result diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index add5af5fc5..ea7b85e880 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -263,6 +263,8 @@ pub enum ConnectorError { connector: &'static str, payment_experience: String, }, + #[error("{flow} flow not supported by {connector} connector")] + FlowNotSupported { flow: String, connector: String }, #[error("Missing connector transaction ID")] MissingConnectorTransactionID, #[error("Missing connector refund ID")] diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 5ceed6d8af..f20f5cdd81 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -83,7 +83,8 @@ pub enum ApiErrorResponse { GenericUnauthorized { message: String }, #[error(error_type = ErrorType::InvalidRequestError, code = "IR_19", message = "{message}")] NotSupported { message: String }, - + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_20", message = "{flow} flow not supported by the {connector} connector")] + FlowNotSupported { flow: String, connector: String }, #[error(error_type = ErrorType::ConnectorError, code = "CE_00", message = "{code}: {message}", ignore = "status_code")] ExternalConnectorError { code: String, @@ -239,6 +240,7 @@ impl actix_web::ResponseError for ApiErrorResponse { | Self::ConfigNotFound | Self::AddressNotFound | Self::NotSupported { .. } + | Self::FlowNotSupported { .. } | Self::ApiKeyNotFound => StatusCode::BAD_REQUEST, // 400 Self::DuplicateMerchantAccount | Self::DuplicateMerchantConnectorAccount @@ -428,6 +430,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new("HE", 3, "Payment method type not supported", Some(Extra {reason: Some(message.to_owned()), ..Default::default()}))) } + Self::FlowNotSupported { flow, connector } => { + AER::BadRequest(ApiError::new("IR", 20, format!("{flow} flow not supported"), Some(Extra {connector: Some(connector.to_owned()), ..Default::default()}))) + } } } } diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index a0e951fc3a..3d1c9c5912 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -109,6 +109,9 @@ impl ConnectorErrorExt for error_stack::Report { }, errors::ConnectorError::NotSupported { payment_method, connector, payment_experience } => { errors::ApiErrorResponse::NotSupported { message: format!("Payment method type {payment_method} is not supported by {connector} through payment experience {payment_experience}") } + }, + errors::ConnectorError::FlowNotSupported{ flow, connector } => { + errors::ApiErrorResponse::FlowNotSupported { flow: flow.to_owned(), connector: connector.to_owned() } } _ => errors::ApiErrorResponse::InternalServerError, }; diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c0f1dde3af..468743f119 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -174,6 +174,7 @@ impl ConnectorData { "fiserv" => Ok(Box::new(&connector::Fiserv)), "globalpay" => Ok(Box::new(&connector::Globalpay)), "klarna" => Ok(Box::new(&connector::Klarna)), + "mollie" => Ok(Box::new(&connector::Mollie)), "nuvei" => Ok(Box::new(&connector::Nuvei)), "payu" => Ok(Box::new(&connector::Payu)), "rapyd" => Ok(Box::new(&connector::Rapyd)), diff --git a/crates/router/tests/connectors/connector_auth.rs b/crates/router/tests/connectors/connector_auth.rs index e75e1473da..5dc81cc629 100644 --- a/crates/router/tests/connectors/connector_auth.rs +++ b/crates/router/tests/connectors/connector_auth.rs @@ -14,6 +14,7 @@ pub(crate) struct ConnectorAuthentication { pub dlocal: Option, pub fiserv: Option, pub globalpay: Option, + pub mollie: Option, pub multisafepay: Option, pub nuvei: Option, pub payu: Option, diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 33be93a7ff..62b29ffe98 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -12,6 +12,7 @@ mod cybersource; mod dlocal; mod fiserv; mod globalpay; +mod mollie; mod multisafepay; mod nuvei; mod payu; diff --git a/crates/router/tests/connectors/mollie.rs b/crates/router/tests/connectors/mollie.rs new file mode 100644 index 0000000000..e52736fd5b --- /dev/null +++ b/crates/router/tests/connectors/mollie.rs @@ -0,0 +1,32 @@ +use router::types; + +use crate::{ + connector_auth, + utils::{self, ConnectorActions}, +}; + +#[derive(Clone, Copy)] +struct MollieTest; +impl ConnectorActions for MollieTest {} +impl utils::Connector for MollieTest { + fn get_data(&self) -> types::api::ConnectorData { + use router::connector::Mollie; + types::api::ConnectorData { + connector: Box::new(&Mollie), + connector_name: types::Connector::Mollie, + get_token: types::api::GetToken::Connector, + } + } + + fn get_auth_token(&self) -> types::ConnectorAuthType { + types::ConnectorAuthType::from( + connector_auth::ConnectorAuthentication::new() + .mollie + .expect("Missing connector authentication configuration"), + ) + } + + fn get_name(&self) -> String { + "mollie".to_string() + } +} diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 1b2c705e7d..f12c63851b 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -66,3 +66,6 @@ key1= "key1" api_key = "api_key" key1 = "key1" api_secret = "secret" + +[mollie] +api_key = "API Key" \ No newline at end of file diff --git a/loadtest/config/Development.toml b/loadtest/config/Development.toml index a308557db6..a3a5af7468 100644 --- a/loadtest/config/Development.toml +++ b/loadtest/config/Development.toml @@ -69,6 +69,7 @@ dlocal.base_url = "https://sandbox.dlocal.com/" fiserv.base_url = "https://cert.api.fiservapps.com/" globalpay.base_url = "https://apis.sandbox.globalpay.com/ucp/" klarna.base_url = "https://api-na.playground.klarna.com/" +mollie.base_url = "https://api.mollie.com/v2/" multisafepay.base_url = "https://testapi.multisafepay.com/" nuvei.base_url = "https://ppp-test.nuvei.com/" payu.base_url = "https://secure.snd.payu.com/" @@ -80,6 +81,9 @@ worldpay.base_url = "https://try.access.worldpay.com/" trustpay.base_url = "https://test-tpgw.trustpay.eu/" trustpay.base_url_bank_redirects = "https://aapi.trustpay.eu/" +[connectors.mollie] +base_url = "https://api.mollie.com/v2/" + [connectors.supported] wallets = ["klarna", "braintree", "applepay"] cards = [ @@ -95,6 +99,7 @@ cards = [ "dlocal", "fiserv", "globalpay", + "mollie", "multisafepay", "nuvei", "payu",