diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 29ad9be051..dab1b46adb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -11,6 +11,7 @@ pub mod ephemeral_key; pub mod errors; pub mod files; pub mod mandates; +pub mod organization; pub mod payment_methods; pub mod payments; #[cfg(feature = "payouts")] diff --git a/crates/api_models/src/organization.rs b/crates/api_models/src/organization.rs new file mode 100644 index 0000000000..db4ae21a0d --- /dev/null +++ b/crates/api_models/src/organization.rs @@ -0,0 +1,13 @@ +pub struct OrganizationNew { + pub org_id: String, + pub org_name: Option, +} + +impl OrganizationNew { + pub fn new(org_name: Option) -> Self { + Self { + org_id: common_utils::generate_id_with_default_len("org"), + org_name, + } + } +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 3de35d73f8..5284466780 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -23,6 +23,7 @@ pub mod mandate; pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; +pub mod organization; pub mod payment_attempt; pub mod payment_intent; pub mod payment_link; diff --git a/crates/diesel_models/src/organization.rs b/crates/diesel_models/src/organization.rs new file mode 100644 index 0000000000..2f407b8cbf --- /dev/null +++ b/crates/diesel_models/src/organization.rs @@ -0,0 +1,35 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; + +use crate::schema::organization; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = organization, primary_key(org_id))] +pub struct Organization { + pub org_id: String, + pub org_name: Option, +} + +#[derive(Clone, Debug, Insertable)] +#[diesel(table_name = organization, primary_key(org_id))] +pub struct OrganizationNew { + pub org_id: String, + pub org_name: Option, +} + +#[derive(Clone, Debug, AsChangeset)] +#[diesel(table_name = organization)] +pub struct OrganizationUpdateInternal { + org_name: Option, +} + +pub enum OrganizationUpdate { + Update { org_name: Option }, +} + +impl From for OrganizationUpdateInternal { + fn from(value: OrganizationUpdate) -> Self { + match value { + OrganizationUpdate::Update { org_name } => Self { org_name }, + } + } +} diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ef4ab9f32f..6b705e2987 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -16,6 +16,7 @@ pub mod mandate; pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; +pub mod organization; pub mod payment_attempt; pub mod payment_intent; pub mod payment_link; diff --git a/crates/diesel_models/src/query/organization.rs b/crates/diesel_models/src/query/organization.rs new file mode 100644 index 0000000000..0bea1012c9 --- /dev/null +++ b/crates/diesel_models/src/query/organization.rs @@ -0,0 +1,38 @@ +use diesel::{associations::HasTable, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{ + organization::*, query::generics, schema::organization::dsl, PgPooledConn, StorageResult, +}; + +impl OrganizationNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Organization { + pub async fn find_by_org_id(conn: &PgPooledConn, org_id: String) -> StorageResult { + generics::generic_find_one::<::Table, _, _>(conn, dsl::org_id.eq(org_id)) + .await + } + + pub async fn update_by_org_id( + conn: &PgPooledConn, + org_id: String, + update: OrganizationUpdate, + ) -> StorageResult { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::org_id.eq(org_id), + OrganizationUpdateInternal::from(update), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 6c1b13bacf..926b31c0ce 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -505,6 +505,17 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + organization (org_id) { + #[max_length = 32] + org_id -> Varchar, + org_name -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -879,6 +890,7 @@ diesel::allow_tables_to_appear_in_same_query!( merchant_account, merchant_connector_account, merchant_key_store, + organization, payment_attempt, payment_intent, payment_link, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 11cdc49dd6..2e10aeaaf2 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -26,13 +26,11 @@ use crate::{ types::{self as domain_types, AsyncLift}, }, storage::{self, enums::MerchantStorageScheme}, - transformers::ForeignTryFrom, + transformers::{ForeignFrom, ForeignTryFrom}, }, utils::{self, OptionExt}, }; -const DEFAULT_ORG_ID: &str = "org_abcdefghijklmn"; - #[inline] pub fn create_merchant_publishable_key() -> String { format!( @@ -146,6 +144,24 @@ pub async fn create_merchant_account( }) .transpose()?; + let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { + db.find_organization_by_org_id(organization_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "organization with the given id does not exist".to_string(), + })?; + organization_id.to_string() + } else { + let new_organization = api_models::organization::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + let organization = db + .insert_organization(db_organization) + .await + .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error when creating organization")?; + organization.org_id + }; + let mut merchant_account = async { Ok(domain::MerchantAccount { merchant_id: req.merchant_id, @@ -177,7 +193,7 @@ pub async fn create_merchant_account( intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), payout_routing_algorithm: req.payout_routing_algorithm, id: None, - organization_id: req.organization_id.unwrap_or(DEFAULT_ORG_ID.to_string()), + organization_id, is_recon_enabled: false, default_profile: None, recon_status: diesel_models::enums::ReconStatus::NotRequested, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 4270d2ab64..f5647b3d17 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -17,6 +17,7 @@ pub mod mandate; pub mod merchant_account; pub mod merchant_connector_account; pub mod merchant_key_store; +pub mod organization; pub mod payment_link; pub mod payment_method; pub mod payout_attempt; @@ -75,6 +76,7 @@ pub trait StorageInterface: + payment_link::PaymentLinkInterface + RedisConnInterface + business_profile::BusinessProfileInterface + + organization::OrganizationInterface + 'static { fn get_scheduler_db(&self) -> Box; diff --git a/crates/router/src/db/organization.rs b/crates/router/src/db/organization.rs new file mode 100644 index 0000000000..ddb8d9f9d9 --- /dev/null +++ b/crates/router/src/db/organization.rs @@ -0,0 +1,131 @@ +use common_utils::errors::CustomResult; +use diesel_models::organization as storage; +use error_stack::IntoReport; + +use crate::{connection, core::errors, services::Store}; + +#[async_trait::async_trait] +pub trait OrganizationInterface { + async fn insert_organization( + &self, + organization: storage::OrganizationNew, + ) -> CustomResult; + + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult; + + async fn update_organization_by_org_id( + &self, + user_id: &str, + update: storage::OrganizationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl OrganizationInterface for Store { + async fn insert_organization( + &self, + organization: storage::OrganizationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + organization + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::Organization::find_by_org_id(&conn, org_id.to_string()) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_organization_by_org_id( + &self, + org_id: &str, + update: storage::OrganizationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + + storage::Organization::update_by_org_id(&conn, org_id.to_string(), update) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl OrganizationInterface for super::MockDb { + async fn insert_organization( + &self, + organization: storage::OrganizationNew, + ) -> CustomResult { + let mut organizations = self.organizations.lock().await; + + if organizations + .iter() + .any(|org| org.org_id == organization.org_id) + { + Err(errors::StorageError::DuplicateValue { + entity: "org_id", + key: None, + })? + } + let org = storage::Organization { + org_id: organization.org_id.clone(), + org_name: organization.org_name, + }; + organizations.push(org.clone()); + Ok(org) + } + + async fn find_organization_by_org_id( + &self, + org_id: &str, + ) -> CustomResult { + let organizations = self.organizations.lock().await; + + organizations + .iter() + .find(|org| org.org_id == org_id) + .cloned() + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No organization available for org_id = {org_id}" + )) + .into(), + ) + } + + async fn update_organization_by_org_id( + &self, + org_id: &str, + update: storage::OrganizationUpdate, + ) -> CustomResult { + let mut organizations = self.organizations.lock().await; + + organizations + .iter_mut() + .find(|org| org.org_id == org_id) + .map(|org| match &update { + storage::OrganizationUpdate::Update { org_name } => storage::Organization { + org_name: org_name.clone(), + ..org.to_owned() + }, + }) + .ok_or( + errors::StorageError::ValueNotFound(format!( + "No organization available for org_id = {org_id}" + )) + .into(), + ) + } +} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 31ec31cfa9..d38497c710 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -861,3 +861,14 @@ impl From for payments::AddressDetails { } } } + +impl ForeignFrom + for diesel_models::organization::OrganizationNew +{ + fn foreign_from(item: api_models::organization::OrganizationNew) -> Self { + Self { + org_id: item.org_id, + org_name: item.org_name, + } + } +} diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 8674f76156..76bdb1160c 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -41,6 +41,7 @@ pub struct MockDb { pub business_profiles: Arc>>, pub reverse_lookups: Arc>>, pub payment_link: Arc>>, + pub organizations: Arc>>, } impl MockDb { @@ -74,6 +75,7 @@ impl MockDb { business_profiles: Default::default(), reverse_lookups: Default::default(), payment_link: Default::default(), + organizations: Default::default(), }) } } diff --git a/migrations/2023-10-23-101023_add_organization_table/down.sql b/migrations/2023-10-23-101023_add_organization_table/down.sql new file mode 100644 index 0000000000..9f267438f5 --- /dev/null +++ b/migrations/2023-10-23-101023_add_organization_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS ORGANIZATION; diff --git a/migrations/2023-10-23-101023_add_organization_table/up.sql b/migrations/2023-10-23-101023_add_organization_table/up.sql new file mode 100644 index 0000000000..6e8cf3cf54 --- /dev/null +++ b/migrations/2023-10-23-101023_add_organization_table/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS ORGANIZATION ( + org_id VARCHAR(32) PRIMARY KEY NOT NULL, + org_name TEXT +); diff --git a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js index 9c5a8900dc..41eecccf83 100644 --- a/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js +++ b/postman/collection-dir/stripe/MerchantAccounts/Merchant Account - Create/event.test.js @@ -67,3 +67,11 @@ if (jsonData?.merchant_id) { "INFO - Unable to assign variable {{organization_id}}, as jsonData.organization_id is undefined.", ); } + +// Response body should have "mandate_id" +pm.test( + "[POST]::/accounts - Organization id is generated", + function () { + pm.expect(typeof jsonData.organization_id !== "undefined").to.be.true; + }, +);