From aaac9aa97d1b00d50bec4e02efb0658956463398 Mon Sep 17 00:00:00 2001 From: Anurag Thakur Date: Fri, 25 Oct 2024 15:37:13 +0530 Subject: [PATCH] feat(router): Move organization_id to request header from request body for v2 (#6277) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Narayan Bhat <48803246+Narayanbhat166@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 33 +++++++++------- crates/api_models/src/admin.rs | 15 +++++-- .../common_utils/src/id_type/organization.rs | 9 +++++ crates/openapi/src/openapi_v2.rs | 2 +- crates/openapi/src/routes/merchant_account.rs | 10 +++-- crates/router/src/lib.rs | 1 + crates/router/src/routes/admin.rs | 39 ++++++++++++++++++- crates/router/src/services/authentication.rs | 14 ++++++- .../cypress/fixtures/merchant_account.json | 3 +- cypress-tests-v2/cypress/support/commands.js | 4 +- 10 files changed, 101 insertions(+), 29 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index bd9de3a2962..9a406e9c733 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -453,6 +453,20 @@ "summary": "Merchant Account - Create", "description": "Create a new account for a *merchant* and the *merchant* could be a seller or retailer or client who likes to receive and send payments.\n\nBefore creating the merchant account, it is mandatory to create an organization.", "operationId": "Create a Merchant Account", + "parameters": [ + { + "name": "X-Organization-Id", + "in": "header", + "description": "Organization ID for which the merchant account has to be created.", + "required": true, + "schema": { + "type": "string" + }, + "example": { + "X-Organization-Id": "org_abcdefghijklmnop" + } + } + ], "requestBody": { "content": { "application/json": { @@ -466,8 +480,7 @@ "primary_contact_person": "John Doe", "primary_email": "example@company.com" }, - "merchant_name": "Cloth Store", - "organization_id": "org_abcdefghijklmnop" + "merchant_name": "Cloth Store" } }, "Create a merchant account with metadata": { @@ -476,14 +489,12 @@ "metadata": { "key_1": "John Doe", "key_2": "Trends" - }, - "organization_id": "org_abcdefghijklmnop" + } } }, "Create a merchant account with minimal fields": { "value": { - "merchant_name": "Cloth Store", - "organization_id": "org_abcdefghijklmnop" + "merchant_name": "Cloth Store" } } } @@ -9385,8 +9396,7 @@ "MerchantAccountCreate": { "type": "object", "required": [ - "merchant_name", - "organization_id" + "merchant_name" ], "properties": { "merchant_name": { @@ -9407,13 +9417,6 @@ "type": "object", "description": "Metadata is useful for storing additional, unstructured information about the merchant account.", "nullable": true - }, - "organization_id": { - "type": "string", - "description": "The id of the organization to which the merchant belongs to. Please use the organization endpoint to create an organization", - "example": "org_q98uSGAYbjEwqs0mJwnz", - "maxLength": 64, - "minLength": 1 } }, "additionalProperties": false diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 8f24bdf65bc..c9da386a11d 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -178,7 +178,8 @@ impl MerchantAccountCreate { #[cfg(feature = "v2")] #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[serde(deny_unknown_fields)] -pub struct MerchantAccountCreate { +#[schema(as = MerchantAccountCreate)] +pub struct MerchantAccountCreateWithoutOrgId { /// Name of the Merchant Account, This will be used as a prefix to generate the id #[schema(value_type= String, max_length = 64, example = "NewAge Retailer")] pub merchant_name: Secret, @@ -189,9 +190,17 @@ pub struct MerchantAccountCreate { /// Metadata is useful for storing additional, unstructured information about the merchant account. #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] pub metadata: Option, +} - /// The id of the organization to which the merchant belongs to. Please use the organization endpoint to create an organization - #[schema(value_type = String, max_length = 64, min_length = 1, example = "org_q98uSGAYbjEwqs0mJwnz")] +// In v2 the struct used in the API is MerchantAccountCreateWithoutOrgId +// The following struct is only used internally, so we can reuse the common +// part of `create_merchant_account` without duplicating its code for v2 +#[cfg(feature = "v2")] +#[derive(Clone, Debug, Serialize)] +pub struct MerchantAccountCreate { + pub merchant_name: Secret, + pub merchant_details: Option, + pub metadata: Option, pub organization_id: id_type::OrganizationId, } diff --git a/crates/common_utils/src/id_type/organization.rs b/crates/common_utils/src/id_type/organization.rs index f88a62daa1d..a83f35db1d8 100644 --- a/crates/common_utils/src/id_type/organization.rs +++ b/crates/common_utils/src/id_type/organization.rs @@ -1,3 +1,5 @@ +use crate::errors::{CustomResult, ValidationError}; + crate::id_type!( OrganizationId, "A type for organization_id that can be used for organization ids" @@ -13,3 +15,10 @@ crate::impl_generate_id_id_type!(OrganizationId, "org"); crate::impl_serializable_secret_id_type!(OrganizationId); crate::impl_queryable_id_type!(OrganizationId); crate::impl_to_sql_from_sql_id_type!(OrganizationId); + +impl OrganizationId { + /// Get an organization id from String + pub fn wrap(org_id: String) -> CustomResult { + Self::try_from(std::borrow::Cow::from(org_id)) + } +} diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 0a19c5a63c5..7bbe019dcba 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -154,7 +154,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::organization::OrganizationCreateRequest, api_models::organization::OrganizationUpdateRequest, api_models::organization::OrganizationResponse, - api_models::admin::MerchantAccountCreate, + api_models::admin::MerchantAccountCreateWithoutOrgId, api_models::admin::MerchantAccountUpdate, api_models::admin::MerchantAccountDeleteResponse, api_models::admin::MerchantConnectorDeleteResponse, diff --git a/crates/openapi/src/routes/merchant_account.rs b/crates/openapi/src/routes/merchant_account.rs index b92285245a4..01571da1de9 100644 --- a/crates/openapi/src/routes/merchant_account.rs +++ b/crates/openapi/src/routes/merchant_account.rs @@ -51,6 +51,13 @@ pub async fn merchant_account_create() {} #[utoipa::path( post, path = "/v2/merchant_accounts", + params( + ( + "X-Organization-Id" = String, Header, + description = "Organization ID for which the merchant account has to be created.", + example = json!({"X-Organization-Id": "org_abcdefghijklmnop"}) + ), + ), request_body( content = MerchantAccountCreate, examples( @@ -58,7 +65,6 @@ pub async fn merchant_account_create() {} "Create a merchant account with minimal fields" = ( value = json!({ "merchant_name": "Cloth Store", - "organization_id": "org_abcdefghijklmnop" }) ) ), @@ -66,7 +72,6 @@ pub async fn merchant_account_create() {} "Create a merchant account with merchant details" = ( value = json!({ "merchant_name": "Cloth Store", - "organization_id": "org_abcdefghijklmnop", "merchant_details": { "primary_contact_person": "John Doe", "primary_email": "example@company.com" @@ -78,7 +83,6 @@ pub async fn merchant_account_create() {} "Create a merchant account with metadata" = ( value = json!({ "merchant_name": "Cloth Store", - "organization_id": "org_abcdefghijklmnop", "metadata": { "key_1": "John Doe", "key_2": "Trends" diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 0eff78d38b4..06ab971925b 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -66,6 +66,7 @@ pub mod headers { pub const X_API_VERSION: &str = "X-ApiVersion"; pub const X_FORWARDED_FOR: &str = "X-Forwarded-For"; pub const X_MERCHANT_ID: &str = "X-Merchant-Id"; + pub const X_ORGANIZATION_ID: &str = "X-Organization-Id"; pub const X_LOGIN: &str = "X-Login"; pub const X_TRANS_KEY: &str = "X-Trans-Key"; pub const X_VERSION: &str = "X-Version"; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index b197101d65f..0996838b4cf 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -92,7 +92,7 @@ pub async fn organization_retrieve( .await } -#[cfg(feature = "olap")] +#[cfg(all(feature = "olap", feature = "v1"))] #[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountCreate))] pub async fn merchant_account_create( state: web::Data, @@ -112,6 +112,43 @@ pub async fn merchant_account_create( .await } +#[cfg(all(feature = "olap", feature = "v2"))] +#[instrument(skip_all, fields(flow = ?Flow::MerchantsAccountCreate))] +pub async fn merchant_account_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::MerchantsAccountCreate; + let headers = req.headers(); + + let org_id = match auth::HeaderMapStruct::new(headers).get_organization_id_from_header() { + Ok(org_id) => org_id, + Err(e) => return api::log_and_return_error_response(e), + }; + + // Converting from MerchantAccountCreateWithoutOrgId to MerchantAccountCreate so we can use the existing + // `create_merchant_account` function for v2 as well + let json_payload = json_payload.into_inner(); + let new_request_payload_with_org_id = api_models::admin::MerchantAccountCreate { + merchant_name: json_payload.merchant_name, + merchant_details: json_payload.merchant_details, + metadata: json_payload.metadata, + organization_id: org_id, + }; + + Box::pin(api::server_wrap( + flow, + state, + &req, + new_request_payload_with_org_id, + |state, _, req, _| create_merchant_account(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + /// Merchant Account - Retrieve /// /// Retrieve a merchant account details. diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e6da2b23301..a3cc54de34f 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -931,7 +931,7 @@ where } /// A helper struct to extract headers from the request -struct HeaderMapStruct<'a> { +pub(crate) struct HeaderMapStruct<'a> { headers: &'a HeaderMap, } @@ -981,6 +981,18 @@ impl<'a> HeaderMapStruct<'a> { ) }) } + #[cfg(feature = "v2")] + pub fn get_organization_id_from_header(&self) -> RouterResult { + self.get_mandatory_header_value_by_key(headers::X_ORGANIZATION_ID) + .map(|val| val.to_owned()) + .and_then(|organization_id| { + id_type::OrganizationId::wrap(organization_id).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", headers::X_ORGANIZATION_ID), + }, + ) + }) + } } /// Get the merchant-id from `x-merchant-id` header diff --git a/cypress-tests-v2/cypress/fixtures/merchant_account.json b/cypress-tests-v2/cypress/fixtures/merchant_account.json index ac24e956513..dbe88ae10d4 100644 --- a/cypress-tests-v2/cypress/fixtures/merchant_account.json +++ b/cypress-tests-v2/cypress/fixtures/merchant_account.json @@ -1,7 +1,6 @@ { "ma_create": { - "merchant_name": "Hyperswitch Seller", - "organization_id": "" + "merchant_name": "Hyperswitch Seller" }, "ma_update": { "merchant_name": "Hyperswitch" diff --git a/cypress-tests-v2/cypress/support/commands.js b/cypress-tests-v2/cypress/support/commands.js index c9d1ef3f24f..abdf95194bb 100644 --- a/cypress-tests-v2/cypress/support/commands.js +++ b/cypress-tests-v2/cypress/support/commands.js @@ -173,15 +173,13 @@ Cypress.Commands.add( .replaceAll(" ", "") .toLowerCase(); - // Update request body - merchantAccountCreateBody.organization_id = organization_id; - cy.request({ method: "POST", url: url, headers: { "Content-Type": "application/json", "api-key": api_key, + "X-Organization-Id": organization_id, }, body: merchantAccountCreateBody, failOnStatusCode: false,