mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-02 12:06:56 +08:00
feat(organization): add organization table (#2669)
This commit is contained in:
@ -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")]
|
||||
|
||||
13
crates/api_models/src/organization.rs
Normal file
13
crates/api_models/src/organization.rs
Normal file
@ -0,0 +1,13 @@
|
||||
pub struct OrganizationNew {
|
||||
pub org_id: String,
|
||||
pub org_name: Option<String>,
|
||||
}
|
||||
|
||||
impl OrganizationNew {
|
||||
pub fn new(org_name: Option<String>) -> Self {
|
||||
Self {
|
||||
org_id: common_utils::generate_id_with_default_len("org"),
|
||||
org_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
35
crates/diesel_models/src/organization.rs
Normal file
35
crates/diesel_models/src/organization.rs
Normal file
@ -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<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Insertable)]
|
||||
#[diesel(table_name = organization, primary_key(org_id))]
|
||||
pub struct OrganizationNew {
|
||||
pub org_id: String,
|
||||
pub org_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, AsChangeset)]
|
||||
#[diesel(table_name = organization)]
|
||||
pub struct OrganizationUpdateInternal {
|
||||
org_name: Option<String>,
|
||||
}
|
||||
|
||||
pub enum OrganizationUpdate {
|
||||
Update { org_name: Option<String> },
|
||||
}
|
||||
|
||||
impl From<OrganizationUpdate> for OrganizationUpdateInternal {
|
||||
fn from(value: OrganizationUpdate) -> Self {
|
||||
match value {
|
||||
OrganizationUpdate::Update { org_name } => Self { org_name },
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
38
crates/diesel_models/src/query/organization.rs
Normal file
38
crates/diesel_models/src/query/organization.rs
Normal file
@ -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<Organization> {
|
||||
generics::generic_insert(conn, self).await
|
||||
}
|
||||
}
|
||||
|
||||
impl Organization {
|
||||
pub async fn find_by_org_id(conn: &PgPooledConn, org_id: String) -> StorageResult<Self> {
|
||||
generics::generic_find_one::<<Self as HasTable>::Table, _, _>(conn, dsl::org_id.eq(org_id))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn update_by_org_id(
|
||||
conn: &PgPooledConn,
|
||||
org_id: String,
|
||||
update: OrganizationUpdate,
|
||||
) -> StorageResult<Self> {
|
||||
generics::generic_update_with_unique_predicate_get_result::<
|
||||
<Self as HasTable>::Table,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
>(
|
||||
conn,
|
||||
dsl::org_id.eq(org_id),
|
||||
OrganizationUpdateInternal::from(update),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@ -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<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<dyn scheduler::SchedulerInterface>;
|
||||
|
||||
131
crates/router/src/db/organization.rs
Normal file
131
crates/router/src/db/organization.rs
Normal file
@ -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<storage::Organization, errors::StorageError>;
|
||||
|
||||
async fn find_organization_by_org_id(
|
||||
&self,
|
||||
org_id: &str,
|
||||
) -> CustomResult<storage::Organization, errors::StorageError>;
|
||||
|
||||
async fn update_organization_by_org_id(
|
||||
&self,
|
||||
user_id: &str,
|
||||
update: storage::OrganizationUpdate,
|
||||
) -> CustomResult<storage::Organization, errors::StorageError>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OrganizationInterface for Store {
|
||||
async fn insert_organization(
|
||||
&self,
|
||||
organization: storage::OrganizationNew,
|
||||
) -> CustomResult<storage::Organization, errors::StorageError> {
|
||||
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<storage::Organization, errors::StorageError> {
|
||||
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<storage::Organization, errors::StorageError> {
|
||||
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<storage::Organization, errors::StorageError> {
|
||||
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<storage::Organization, errors::StorageError> {
|
||||
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<storage::Organization, errors::StorageError> {
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -861,3 +861,14 @@ impl From<domain::Address> for payments::AddressDetails {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<api_models::organization::OrganizationNew>
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ pub struct MockDb {
|
||||
pub business_profiles: Arc<Mutex<Vec<crate::store::business_profile::BusinessProfile>>>,
|
||||
pub reverse_lookups: Arc<Mutex<Vec<store::ReverseLookup>>>,
|
||||
pub payment_link: Arc<Mutex<Vec<store::payment_link::PaymentLink>>>,
|
||||
pub organizations: Arc<Mutex<Vec<store::organization::Organization>>>,
|
||||
}
|
||||
|
||||
impl MockDb {
|
||||
@ -74,6 +75,7 @@ impl MockDb {
|
||||
business_profiles: Default::default(),
|
||||
reverse_lookups: Default::default(),
|
||||
payment_link: Default::default(),
|
||||
organizations: Default::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE IF EXISTS ORGANIZATION;
|
||||
@ -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
|
||||
);
|
||||
@ -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;
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user