Skip to content

Commit 67f38f8

Browse files
chore(users): add hubspot tracking to prod intent (#7798)
Co-authored-by: Gnanasundari24 <[email protected]>
1 parent 1dabfe3 commit 67f38f8

File tree

36 files changed

+923
-420
lines changed

36 files changed

+923
-420
lines changed

Cargo.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/config.example.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,13 @@ encryption_manager = "aws_kms" # Encryption manager client to be used
970970
key_id = "kms_key_id" # The AWS key ID used by the KMS SDK for decrypting data.
971971
region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
972972

973+
[crm]
974+
crm_manager = "hubspot_proxy" # Crm manager client to be used
975+
976+
[crm.hubspot_proxy]
977+
form_id="hubspot_proxy_form_id" # Form ID for Hubspot integration
978+
request_url="hubspot_proxy_request_url" # Request URL for Hubspot API
979+
973980
[opensearch]
974981
host = "https://localhost:9200"
975982
enabled = false

config/deployments/env_specific.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,14 @@ encryption_manager = "aws_kms" # Encryption manager client to be used
303303
key_id = "kms_key_id" # The AWS key ID used by the KMS SDK for decrypting data.
304304
region = "kms_region" # The AWS region used by the KMS SDK for decrypting data.
305305

306+
[crm]
307+
crm_manager = "hubspot_proxy" # Crm manager client to be used
308+
309+
[crm.hubspot_proxy]
310+
form_id="" # Form ID for Hubspot integration
311+
request_url="" # Request URL for Hubspot API
312+
313+
306314
[multitenancy]
307315
enabled = false
308316
global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default"}

crates/api_models/src/user/dashboard_metadata.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,13 @@ pub struct ProdIntent {
9999
pub business_type: Option<String>,
100100
pub business_identifier: Option<String>,
101101
pub business_website: Option<String>,
102-
pub poc_name: Option<String>,
103-
pub poc_contact: Option<String>,
102+
pub poc_name: Option<Secret<String>>,
103+
pub poc_contact: Option<Secret<String>>,
104104
pub comments: Option<String>,
105105
pub is_completed: bool,
106106
#[serde(default)]
107107
pub product_type: MerchantProductType,
108+
pub business_country_name: Option<String>,
108109
}
109110

110111
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]

crates/common_utils/src/consts.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,9 @@ pub const DEFAULT_CARD_TESTING_GUARD_EXPIRY_IN_SECS: i32 = 3600;
176176

177177
/// SOAP 1.1 Envelope Namespace
178178
pub const SOAP_ENV_NAMESPACE: &str = "http://schemas.xmlsoap.org/soap/envelope/";
179+
180+
/// The tag name used for identifying the host in metrics.
181+
pub const METRICS_HOST_TAG_NAME: &str = "host";
182+
183+
/// API client request timeout (in seconds)
184+
pub const REQUEST_TIME_OUT: u64 = 30;

crates/external_services/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ tonic-reflection = { version = "0.12.2", optional = true }
4141
tonic-types = { version = "0.12.2", optional = true }
4242
hyper-util = { version = "0.1.9", optional = true }
4343
http-body-util = { version = "0.1.2", optional = true }
44+
reqwest = { version = "0.11.27", features = ["rustls-tls"] }
45+
http = "0.2.12"
46+
url = { version = "2.5.0", features = ["serde"] }
47+
quick-xml = { version = "0.31.0", features = ["serialize"] }
4448

4549

4650
# First party crates

crates/external_services/src/crm.rs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use std::sync::Arc;
2+
3+
use common_utils::{
4+
errors::CustomResult,
5+
ext_traits::ConfigExt,
6+
request::{Method, Request, RequestBuilder, RequestContent},
7+
};
8+
use error_stack::ResultExt;
9+
use http::header;
10+
use hyperswitch_interfaces::{
11+
crm::{CrmInterface, CrmPayload},
12+
errors::HttpClientError,
13+
types::Proxy,
14+
};
15+
use reqwest;
16+
use router_env::logger;
17+
18+
use crate::{http_client, hubspot_proxy::HubspotRequest};
19+
20+
/// Hubspot Crm configuration
21+
#[derive(Debug, Clone, serde::Deserialize)]
22+
pub struct HubspotProxyConfig {
23+
/// The ID of the Hubspot form to be submitted.
24+
pub form_id: String,
25+
26+
/// The URL to which the Hubspot form data will be sent.
27+
pub request_url: String,
28+
}
29+
30+
impl HubspotProxyConfig {
31+
/// Validates Hubspot configuration
32+
pub(super) fn validate(&self) -> Result<(), InvalidCrmConfig> {
33+
use common_utils::fp_utils::when;
34+
35+
when(self.request_url.is_default_or_empty(), || {
36+
Err(InvalidCrmConfig("request url must not be empty"))
37+
})?;
38+
39+
when(self.form_id.is_default_or_empty(), || {
40+
Err(InvalidCrmConfig("form_id must not be empty"))
41+
})
42+
}
43+
}
44+
45+
/// Error thrown when the crm config is invalid
46+
#[derive(Debug, Clone)]
47+
pub struct InvalidCrmConfig(pub &'static str);
48+
49+
impl std::error::Error for InvalidCrmConfig {}
50+
51+
impl std::fmt::Display for InvalidCrmConfig {
52+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53+
write!(f, "crm: {}", self.0)
54+
}
55+
}
56+
57+
#[derive(Debug, Clone, Copy)]
58+
/// NoCrm struct
59+
pub struct NoCrm;
60+
61+
/// Enum representing different Crm configurations
62+
#[derive(Debug, Clone, Default, serde::Deserialize)]
63+
#[serde(tag = "crm_manager")]
64+
#[serde(rename_all = "snake_case")]
65+
pub enum CrmManagerConfig {
66+
/// Hubspot Crm configuration
67+
HubspotProxy {
68+
/// Hubspot Crm configuration
69+
hubspot_proxy: HubspotProxyConfig,
70+
},
71+
72+
/// No Crm configuration
73+
#[default]
74+
NoCrm,
75+
}
76+
77+
impl CrmManagerConfig {
78+
/// Verifies that the client configuration is usable
79+
pub fn validate(&self) -> Result<(), InvalidCrmConfig> {
80+
match self {
81+
Self::HubspotProxy { hubspot_proxy } => hubspot_proxy.validate(),
82+
Self::NoCrm => Ok(()),
83+
}
84+
}
85+
86+
/// Retrieves the appropriate Crm client based on the configuration.
87+
pub async fn get_crm_client(&self) -> Arc<dyn CrmInterface> {
88+
match self {
89+
Self::HubspotProxy { hubspot_proxy } => Arc::new(hubspot_proxy.clone()),
90+
Self::NoCrm => Arc::new(NoCrm),
91+
}
92+
}
93+
}
94+
95+
#[async_trait::async_trait]
96+
impl CrmInterface for NoCrm {
97+
async fn make_body(&self, _details: CrmPayload) -> RequestContent {
98+
RequestContent::Json(Box::new(()))
99+
}
100+
101+
async fn make_request(&self, _body: RequestContent, _origin_base_url: String) -> Request {
102+
RequestBuilder::default().build()
103+
}
104+
105+
async fn send_request(
106+
&self,
107+
_proxy: &Proxy,
108+
_request: Request,
109+
) -> CustomResult<reqwest::Response, HttpClientError> {
110+
logger::info!("No CRM configured!");
111+
Err(HttpClientError::UnexpectedState).attach_printable("No CRM configured!")
112+
}
113+
}
114+
115+
#[async_trait::async_trait]
116+
impl CrmInterface for HubspotProxyConfig {
117+
async fn make_body(&self, details: CrmPayload) -> RequestContent {
118+
RequestContent::FormUrlEncoded(Box::new(HubspotRequest::new(
119+
details.business_country_name.unwrap_or_default(),
120+
self.form_id.clone(),
121+
details.poc_name.unwrap_or_default(),
122+
details.poc_email.clone().unwrap_or_default(),
123+
details.legal_business_name.unwrap_or_default(),
124+
details.business_website.unwrap_or_default(),
125+
)))
126+
}
127+
128+
async fn make_request(&self, body: RequestContent, origin_base_url: String) -> Request {
129+
RequestBuilder::new()
130+
.method(Method::Post)
131+
.url(self.request_url.as_str())
132+
.set_body(body)
133+
.attach_default_headers()
134+
.headers(vec![(
135+
header::ORIGIN.to_string(),
136+
format!("{origin_base_url}/dashboard").into(),
137+
)])
138+
.build()
139+
}
140+
141+
async fn send_request(
142+
&self,
143+
proxy: &Proxy,
144+
request: Request,
145+
) -> CustomResult<reqwest::Response, HttpClientError> {
146+
http_client::send_request(proxy, request, None).await
147+
}
148+
}

0 commit comments

Comments
 (0)