1use std::sync::Arc;
2
3use crate::error::{RovenueError, RovenueResult};
4use crate::transport::http_client::HttpClient;
5use crate::transport::types::HttpPostRequest;
6
7use super::{ClaimInstallParams, FunnelClaimResult};
8
9pub struct FunnelClient {
10 http: Arc<HttpClient>,
11}
12
13impl FunnelClient {
14 pub fn new(http: Arc<HttpClient>) -> Self {
15 Self { http }
16 }
17
18 pub fn claim_funnel_token(
21 &self,
22 token: &str,
23 anon_id: &str,
24 ) -> RovenueResult<FunnelClaimResult> {
25 let body = serde_json::json!({ "token": token, "anon_id": anon_id });
26 let (status, parsed) = self.http.post_json_status(
27 HttpPostRequest::new("/v1/subscribers/claim-funnel-token"),
28 &body,
29 )?;
30 match status {
31 200 => {
32 let data = parsed
33 .and_then(|v| v.get("data").cloned())
34 .ok_or(RovenueError::Internal())?;
35 let subscriber_id = data
36 .get("subscriber_id")
37 .and_then(|v| v.as_str())
38 .ok_or(RovenueError::Internal())?
39 .to_string();
40 let funnel_answers_json = data
41 .get("funnel_answers")
42 .cloned()
43 .unwrap_or(serde_json::json!({}))
44 .to_string();
45 Ok(FunnelClaimResult {
46 subscriber_id,
47 funnel_answers_json,
48 })
49 }
50 404 => Err(RovenueError::FunnelTokenNotFound()),
51 410 => Err(RovenueError::FunnelTokenExpired()),
52 409 => Err(RovenueError::FunnelTokenAlreadyClaimed()),
53 401 => Err(RovenueError::InvalidApiKey()),
54 _ => Err(RovenueError::ServerError()),
55 }
56 }
57
58 pub fn claim_install(
60 &self,
61 params: &ClaimInstallParams,
62 install_id: &str,
63 ) -> RovenueResult<Option<String>> {
64 let mut body = serde_json::json!({
65 "platform": params.platform,
66 "locale": params.locale,
67 "timezone": params.timezone,
68 "screen_dims": params.screen_dims,
69 "install_id": install_id,
70 });
71 if let Some(dm) = ¶ms.device_model {
72 body["device_model"] = serde_json::json!(dm);
73 }
74 if let Some(ir) = ¶ms.install_referrer {
75 body["install_referrer"] = serde_json::json!(ir);
76 }
77 let (status, parsed) = self
78 .http
79 .post_json_status(HttpPostRequest::new("/v1/sdk/claim-install"), &body)?;
80 match status {
81 200 => {
82 let token = parsed.and_then(|v| {
83 v.get("data")
84 .and_then(|d| d.get("token"))
85 .and_then(|t| t.as_str())
86 .map(str::to_string)
87 });
88 match token {
89 Some(t) => Ok(Some(t)),
90 None => Err(RovenueError::Internal()),
91 }
92 }
93 404 => Ok(None),
94 401 => Err(RovenueError::InvalidApiKey()),
95 _ => Err(RovenueError::ServerError()),
96 }
97 }
98
99 pub fn claim_via_email(&self, email: &str, install_id: &str) -> RovenueResult<()> {
101 let body = serde_json::json!({ "email": email, "install_id": install_id });
102 let (status, _) = self
103 .http
104 .post_json_status(HttpPostRequest::new("/v1/sdk/claim-via-email"), &body)?;
105 match status {
106 202 | 200 => Ok(()),
107 401 => Err(RovenueError::InvalidApiKey()),
108 _ => Err(RovenueError::ServerError()),
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::error::ErrorKind;
117
118 fn client(url: &str) -> FunnelClient {
119 FunnelClient::new(Arc::new(
120 HttpClient::new(url.to_string(), "pk_test".into()).with_max_attempts(1),
121 ))
122 }
123
124 #[test]
125 fn claim_funnel_token_parses_200() {
126 let mut server = mockito::Server::new();
127 let m = server.mock("POST", "/v1/subscribers/claim-funnel-token")
128 .with_status(200).with_header("content-type", "application/json")
129 .with_body(r#"{"data":{"subscriber_id":"sub_1","entitlements":[],"funnel_answers":{"q1":"yes"}}}"#)
130 .create();
131 let r = client(&server.url())
132 .claim_funnel_token("tok", "rov_x")
133 .expect("ok");
134 assert_eq!(r.subscriber_id, "sub_1");
135 assert_eq!(r.funnel_answers_json, r#"{"q1":"yes"}"#);
136 m.assert();
137 }
138
139 #[test]
140 fn claim_funnel_token_maps_status_errors() {
141 type StatusCheck = fn(RovenueError) -> bool;
142 let cases: &[(u16, StatusCheck)] = &[
143 (404, |e: RovenueError| {
144 e.kind == ErrorKind::FunnelTokenNotFound
145 }),
146 (410, |e: RovenueError| {
147 e.kind == ErrorKind::FunnelTokenExpired
148 }),
149 (409, |e: RovenueError| {
150 e.kind == ErrorKind::FunnelTokenAlreadyClaimed
151 }),
152 ];
153 for (code, check_fn) in cases {
154 let mut server = mockito::Server::new();
155 let _m = server
156 .mock("POST", "/v1/subscribers/claim-funnel-token")
157 .with_status((*code).into())
158 .create();
159 let err = client(&server.url())
160 .claim_funnel_token("tok", "rov_x")
161 .unwrap_err();
162 assert!(check_fn(err), "status {code}");
163 }
164 }
165
166 #[test]
167 fn claim_install_returns_token_or_none() {
168 let mut server = mockito::Server::new();
169 let m_ok = server
170 .mock("POST", "/v1/sdk/claim-install")
171 .with_status(200)
172 .with_header("content-type", "application/json")
173 .with_body(r#"{"data":{"token":"recovered_tok"}}"#)
174 .create();
175 let params = ClaimInstallParams {
176 platform: "android".into(),
177 locale: "en-US".into(),
178 timezone: "UTC".into(),
179 screen_dims: "390x844".into(),
180 device_model: None,
181 install_referrer: Some("rovenue_funnel_token=recovered_tok".into()),
182 };
183 assert_eq!(
184 client(&server.url())
185 .claim_install(¶ms, "inst_1")
186 .unwrap(),
187 Some("recovered_tok".into())
188 );
189 m_ok.assert();
190
191 let mut server2 = mockito::Server::new();
192 let _m404 = server2
193 .mock("POST", "/v1/sdk/claim-install")
194 .with_status(404)
195 .create();
196 assert_eq!(
197 client(&server2.url())
198 .claim_install(¶ms, "inst_1")
199 .unwrap(),
200 None
201 );
202 }
203
204 #[test]
205 fn claim_install_malformed_200_returns_internal_error() {
206 let mut server = mockito::Server::new();
207 let _m = server
208 .mock("POST", "/v1/sdk/claim-install")
209 .with_status(200)
210 .with_header("content-type", "application/json")
211 .with_body(r#"{"data":{}}"#)
212 .create();
213 let params = ClaimInstallParams {
214 platform: "ios".into(),
215 locale: "en-US".into(),
216 timezone: "UTC".into(),
217 screen_dims: "390x844".into(),
218 device_model: None,
219 install_referrer: None,
220 };
221 let err = client(&server.url())
222 .claim_install(¶ms, "inst_m")
223 .unwrap_err();
224 assert!(
225 err.kind == ErrorKind::Internal,
226 "malformed 200 must be Internal error"
227 );
228 }
229
230 #[test]
231 fn claim_via_email_accepts_202() {
232 let mut server = mockito::Server::new();
233 let m = server
234 .mock("POST", "/v1/sdk/claim-via-email")
235 .with_status(202)
236 .create();
237 client(&server.url())
238 .claim_via_email("a@b.com", "inst_1")
239 .expect("202 ok");
240 m.assert();
241 }
242}