diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 68ee6cb48b..b61f3dcb1c 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -71,7 +71,7 @@ pub struct MerchantAccountCreate { /// Refers to the hash key used for calculating the signature for webhooks and redirect response. If the value is not provided, a default value is used. pub payment_response_hash_key: Option, - /// A boolean value to indicate if redirect to merchant with http post needs to be enabled + /// A boolean value to indicate if redirect to merchant with http post needs to be enabled. #[schema(default = false, example = true)] pub redirect_to_merchant_with_http_post: Option, @@ -79,7 +79,8 @@ pub struct MerchantAccountCreate { #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, - /// API key that will be used for server side API access + /// API key that will be used for client side API access. A publishable key has to be always paired with a `client_secret`. + /// A `client_secret` can be obtained by creating a payment with `confirm` set to false #[schema(example = "AH3423bkjbkjdsfbkj")] pub publishable_key: Option, @@ -195,7 +196,7 @@ pub struct MerchantAccountResponse { #[schema(value_type = Option,example = "NewAge Retailer")] pub merchant_name: OptionalEncryptableName, - /// The URL to redirect after the completion of the operation + /// The URL to redirect after completion of the payment #[schema(max_length = 255, example = "https://www.example.com/success")] pub return_url: Option, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index ee9f91fc47..d28e988a03 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -129,10 +129,10 @@ pub enum StripeErrorCode { #[error(error_type = StripeErrorType::InvalidRequestError, code = "token_already_used", message = "duplicate merchant account")] DuplicateMerchantAccount, - #[error(error_type = StripeErrorType::InvalidRequestError, code = "token_already_used", message = "The merchant connector account with the specified profile_id '{profile_id}' and connector_name '{connector_name}' already exists in our records")] + #[error(error_type = StripeErrorType::InvalidRequestError, code = "token_already_used", message = "The merchant connector account with the specified profile_id '{profile_id}' and connector_label '{connector_label}' already exists in our records")] DuplicateMerchantConnectorAccount { profile_id: String, - connector_name: String, + connector_label: String, }, #[error(error_type = StripeErrorType::InvalidRequestError, code = "token_already_used", message = "duplicate payment method")] @@ -523,10 +523,10 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::DuplicateMerchantAccount => Self::DuplicateMerchantAccount, errors::ApiErrorResponse::DuplicateMerchantConnectorAccount { profile_id, - connector_name, + connector_label, } => Self::DuplicateMerchantConnectorAccount { profile_id, - connector_name, + connector_label, }, errors::ApiErrorResponse::DuplicatePaymentMethod => Self::DuplicatePaymentMethod, errors::ApiErrorResponse::PaymentBlockedError { diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 4f0ad73380..f45ad5ee4e 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -923,7 +923,7 @@ pub async fn create_payment_connector( disabled, metadata: req.metadata, frm_configs, - connector_label: Some(connector_label), + connector_label: Some(connector_label.clone()), business_country: req.business_country, business_label: req.business_label.clone(), business_sub_label: req.business_sub_label.clone(), @@ -960,7 +960,7 @@ pub async fn create_payment_connector( .to_duplicate_response( errors::ApiErrorResponse::DuplicateMerchantConnectorAccount { profile_id: profile_id.clone(), - connector_name: req.connector_name.to_string(), + connector_label, }, )?; @@ -1277,7 +1277,7 @@ pub async fn update_payment_connector( .change_context( errors::ApiErrorResponse::DuplicateMerchantConnectorAccount { profile_id, - connector_name: request_connector_label.unwrap_or_default(), + connector_label: request_connector_label.unwrap_or_default(), }, ) .attach_printable_lazy(|| { diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index 780859d986..f9ed61e0f1 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -133,10 +133,10 @@ pub enum ApiErrorResponse { DuplicateMandate, #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The merchant account with the specified details already exists in our records")] DuplicateMerchantAccount, - #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The merchant connector account with the specified profile_id '{profile_id}' and connector_name '{connector_name}' already exists in our records")] + #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The merchant connector account with the specified profile_id '{profile_id}' and connector_label '{connector_label}' already exists in our records")] DuplicateMerchantConnectorAccount { profile_id: String, - connector_name: String, + connector_label: String, }, #[error(error_type = ErrorType::DuplicateRequest, code = "HE_01", message = "The payment method with the specified details already exists in our records")] DuplicatePaymentMethod, diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index d0ca83c84f..ee63074544 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -102,7 +102,7 @@ impl ErrorSwitch for ApiErrorRespon connector, reason, status_code, - } => AER::ConnectorError(ApiError::new("CE", 0, format!("{code}: {message}"), Some(Extra {connector: Some(connector.clone()), reason: reason.clone(), ..Default::default()})), StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)), + } => AER::ConnectorError(ApiError::new("CE", 0, format!("{code}: {message}"), Some(Extra {connector: Some(connector.clone()), reason: reason.to_owned().map(Into::into), ..Default::default()})), StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)), Self::PaymentAuthorizationFailed { data } => { AER::BadRequest(ApiError::new("CE", 1, "Payment failed during authorization with connector. Retry payment", Some(Extra { data: data.clone(), ..Default::default()}))) } @@ -133,8 +133,8 @@ impl ErrorSwitch for ApiErrorRespon Self::DuplicateRefundRequest => AER::BadRequest(ApiError::new("HE", 1, "Duplicate refund request. Refund already attempted with the refund ID", None)), Self::DuplicateMandate => AER::BadRequest(ApiError::new("HE", 1, "Duplicate mandate request. Mandate already attempted with the Mandate ID", None)), Self::DuplicateMerchantAccount => AER::BadRequest(ApiError::new("HE", 1, "The merchant account with the specified details already exists in our records", None)), - Self::DuplicateMerchantConnectorAccount { profile_id, connector_name } => { - AER::BadRequest(ApiError::new("HE", 1, format!("The merchant connector account with the specified profile_id '{profile_id}' and connector_name '{connector_name}' already exists in our records"), None)) + Self::DuplicateMerchantConnectorAccount { profile_id, connector_label: connector_name } => { + AER::BadRequest(ApiError::new("HE", 1, format!("The merchant connector account with the specified profile_id '{profile_id}' and connector_label '{connector_name}' already exists in our records"), None)) } Self::DuplicatePaymentMethod => AER::BadRequest(ApiError::new("HE", 1, "The payment method with the specified details already exists in our records", None)), Self::DuplicatePayment { payment_id } => { @@ -187,7 +187,7 @@ impl ErrorSwitch for ApiErrorRespon AER::BadRequest(ApiError::new("HE", 3, format!("This refund is not possible through Hyperswitch. Please raise the refund through {connector} dashboard"), None)) } Self::MandateValidationFailed { reason } => { - AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.clone()), ..Default::default() }))) + AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.to_owned()), ..Default::default() }))) } Self::PaymentNotSucceeded => AER::BadRequest(ApiError::new("HE", 3, "The payment has not succeeded yet. Please pass a successful payment to initiate refund", None)), Self::PaymentBlockedError { diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 74a382e63d..635ba91716 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -779,7 +779,7 @@ pub fn validate_mandate( let req: api::MandateValidationFields = req.into(); match req.validate_and_get_mandate_type().change_context( errors::ApiErrorResponse::MandateValidationFailed { - reason: "Expected one out of mandate_id and mandate_data but got both".to_string(), + reason: "Expected one out of mandate_id and mandate_data but got both".into(), }, )? { Some(api::MandateTransactionType::NewMandateTransaction) => { @@ -950,7 +950,7 @@ pub fn verify_mandate_details( .unwrap_or(true), || { Err(report!(errors::ApiErrorResponse::MandateValidationFailed { - reason: "request amount is greater than mandate amount".to_string() + reason: "request amount is greater than mandate amount".into() })) }, ), @@ -963,7 +963,7 @@ pub fn verify_mandate_details( .unwrap_or(false), || { Err(report!(errors::ApiErrorResponse::MandateValidationFailed { - reason: "request amount is greater than mandate amount".to_string() + reason: "request amount is greater than mandate amount".into() })) }, ), @@ -975,7 +975,7 @@ pub fn verify_mandate_details( .unwrap_or(false), || { Err(report!(errors::ApiErrorResponse::MandateValidationFailed { - reason: "cross currency mandates not supported".to_string() + reason: "cross currency mandates not supported".into() })) }, ) diff --git a/migrations/2023-12-06-060216_change_primary_key_for_mca/down.sql b/migrations/2023-12-06-060216_change_primary_key_for_mca/down.sql new file mode 100644 index 0000000000..64117bcfbe --- /dev/null +++ b/migrations/2023-12-06-060216_change_primary_key_for_mca/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +CREATE UNIQUE INDEX IF NOT EXISTS "merchant_connector_account_profile_id_connector_id_index" ON merchant_connector_account (profile_id, connector_name); + +ALTER TABLE merchant_connector_account DROP CONSTRAINT IF EXISTS "merchant_connector_account_profile_id_connector_label_key"; diff --git a/migrations/2023-12-06-060216_change_primary_key_for_mca/up.sql b/migrations/2023-12-06-060216_change_primary_key_for_mca/up.sql new file mode 100644 index 0000000000..4aec49ab8f --- /dev/null +++ b/migrations/2023-12-06-060216_change_primary_key_for_mca/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE merchant_connector_account +ADD UNIQUE (profile_id, connector_label); + +DROP INDEX IF EXISTS "merchant_connector_account_profile_id_connector_id_index"; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index eda3747b94..9e64f3e9d0 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9392,7 +9392,7 @@ }, "redirect_to_merchant_with_http_post": { "type": "boolean", - "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled", + "description": "A boolean value to indicate if redirect to merchant with http post needs to be enabled.", "default": false, "example": true, "nullable": true @@ -9404,7 +9404,7 @@ }, "publishable_key": { "type": "string", - "description": "API key that will be used for server side API access", + "description": "API key that will be used for client side API access. A publishable key has to be always paired with a `client_secret`.\nA `client_secret` can be obtained by creating a payment with `confirm` set to false", "example": "AH3423bkjbkjdsfbkj", "nullable": true }, @@ -9480,7 +9480,7 @@ }, "return_url": { "type": "string", - "description": "The URL to redirect after the completion of the operation", + "description": "The URL to redirect after completion of the payment", "example": "https://www.example.com/success", "nullable": true, "maxLength": 255 diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/.event.meta.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/.event.meta.json new file mode 100644 index 0000000000..0731450e6b --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/event.test.js b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/event.test.js new file mode 100644 index 0000000000..9db900b7ac --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/accounts/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is the same as that is passed in the request", + function () { + pm.expect(jsonData.connector_label).to.eql("first_stripe") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/request.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/request.json new file mode 100644 index 0000000000..f6d2739fc4 --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/request.json @@ -0,0 +1,123 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "stripe", + "business_country": "US", + "business_label": "default", + "connector_label": "first_stripe", + "connector_account_details": { + "auth_type": "HeaderKey", + "api_key": "{{connector_api_key}}" + }, + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/response.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/response.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create First Connector/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/.event.meta.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/.event.meta.json new file mode 100644 index 0000000000..0731450e6b --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/event.test.js b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/event.test.js new file mode 100644 index 0000000000..c40597293d --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/event.test.js @@ -0,0 +1,47 @@ +// Validate status 2xx +pm.test( + "[POST]::/accounts/:account_id/connectors - Status code is 2xx", + function () { + pm.response.to.be.success; + }, +); + +// Validate if response header has matching content-type +pm.test( + "[POST]::/accounts/:account_id/connectors - Content-Type is application/json", + function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); + }, +); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) { } + +// pm.collectionVariables - Set merchant_connector_id as variable for jsonData.merchant_connector_id +if (jsonData?.merchant_connector_id) { + pm.collectionVariables.set( + "merchant_connector_id", + jsonData.merchant_connector_id, + ); + console.log( + "- use {{merchant_connector_id}} as collection variable for value", + jsonData.merchant_connector_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{merchant_connector_id}}, as jsonData.merchant_connector_id is undefined.", + ); +} + +// Validate if the connector label is the one that is passed in the request +pm.test( + "[POST]::/accounts/:account_id/connectors - connector_label is the same as that is passed in the request", + function () { + pm.expect(jsonData.connector_label).to.eql("second_stripe") + }, +); \ No newline at end of file diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/request.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/request.json new file mode 100644 index 0000000000..2de362fc3e --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/request.json @@ -0,0 +1,123 @@ +{ + "auth": { + "type": "apikey", + "apikey": [ + { + "key": "value", + "value": "{{admin_api_key}}", + "type": "string" + }, + { + "key": "key", + "value": "api-key", + "type": "string" + }, + { + "key": "in", + "value": "header", + "type": "string" + } + ] + }, + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "connector_type": "fiz_operations", + "connector_name": "stripe", + "business_country": "US", + "business_label": "default", + "connector_label": "second_stripe", + "connector_account_details": { + "auth_type": "HeaderKey", + "api_key": "{{connector_api_key}}" + }, + "test_mode": false, + "disabled": false, + "payment_methods_enabled": [ + { + "payment_method": "card", + "payment_method_types": [ + { + "payment_method_type": "credit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "debit", + "card_networks": ["Visa", "Mastercard"], + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, + { + "payment_method": "pay_later", + "payment_method_types": [ + { + "payment_method_type": "klarna", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "affirm", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + }, + { + "payment_method_type": "afterpay_clearpay", + "payment_experience": "redirect_to_url", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + } + ], + "metadata": { + "city": "NY", + "unit": "245" + } + } + }, + "url": { + "raw": "{{baseUrl}}/account/:account_id/connectors", + "host": ["{{baseUrl}}"], + "path": ["account", ":account_id", "connectors"], + "variable": [ + { + "key": "account_id", + "value": "{{merchant_id}}", + "description": "(Required) The unique identifier for the merchant account" + } + ] + }, + "description": "Create a new Payment Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialised services like Fraud / Accounting etc." +} diff --git a/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/response.json b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/response.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/postman/collection-dir/stripe/PaymentConnectors/Payment Connector - Create Multiple/Create Second Connector/response.json @@ -0,0 +1 @@ +[]