rovenue/receipts/
client.rs

1use std::sync::Arc;
2
3use crate::error::{RovenueError, RovenueResult};
4use crate::placements::CorePresentedContext;
5use crate::transport::api::ApiEnvelope;
6use crate::transport::http_client::HttpClient;
7use crate::transport::types::HttpPostRequest;
8
9use super::types::{PresentedContextWire, ReceiptBody, ReceiptPostOutcome, ReceiptResponse};
10
11pub struct ReceiptClient {
12    http: Arc<HttpClient>,
13}
14
15impl ReceiptClient {
16    pub fn new(http: Arc<HttpClient>) -> Self {
17        Self { http }
18    }
19
20    #[allow(clippy::too_many_arguments)]
21    pub fn post_apple(
22        &self,
23        receipt: &str,
24        app_user_id: &str,
25        product_id: &str,
26        idempotency_key: &str,
27        app_account_token: Option<&str>,
28        presented_context: Option<&CorePresentedContext>,
29    ) -> RovenueResult<ReceiptPostOutcome> {
30        self.post(
31            "/v1/receipts/apple",
32            receipt,
33            app_user_id,
34            product_id,
35            idempotency_key,
36            app_account_token,
37            None,
38            None,
39            presented_context,
40        )
41    }
42
43    #[allow(clippy::too_many_arguments)]
44    pub fn post_google(
45        &self,
46        receipt: &str,
47        app_user_id: &str,
48        product_id: &str,
49        idempotency_key: &str,
50        obfuscated_account_id: Option<&str>,
51        obfuscated_profile_id: Option<&str>,
52        presented_context: Option<&CorePresentedContext>,
53    ) -> RovenueResult<ReceiptPostOutcome> {
54        self.post(
55            "/v1/receipts/google",
56            receipt,
57            app_user_id,
58            product_id,
59            idempotency_key,
60            None,
61            obfuscated_account_id,
62            obfuscated_profile_id,
63            presented_context,
64        )
65    }
66
67    #[allow(clippy::too_many_arguments)]
68    fn post(
69        &self,
70        path: &str,
71        receipt: &str,
72        app_user_id: &str,
73        product_id: &str,
74        idempotency_key: &str,
75        app_account_token: Option<&str>,
76        obfuscated_account_id: Option<&str>,
77        obfuscated_profile_id: Option<&str>,
78        presented_context: Option<&CorePresentedContext>,
79    ) -> RovenueResult<ReceiptPostOutcome> {
80        let presented_context_wire = presented_context.map(|c| PresentedContextWire {
81            placement_id: &c.placement_id,
82            funnel_id: &c.funnel_id,
83            variant_id: c.variant_id.as_deref(),
84            experiment_key: c.experiment_key.as_deref(),
85        });
86        let body = ReceiptBody {
87            receipt,
88            app_user_id,
89            product_id,
90            app_account_token,
91            obfuscated_account_id,
92            obfuscated_profile_id,
93            presented_context: presented_context_wire,
94        };
95        let resp = self
96            .http
97            .post_json::<ReceiptBody<'_>, ApiEnvelope<ReceiptResponse>>(
98                HttpPostRequest::new(path)
99                    .user_scope(app_user_id)
100                    .idempotency_key(idempotency_key),
101                &body,
102            )?;
103        let data = resp.body.ok_or(RovenueError::Internal())?.data;
104        Ok(ReceiptPostOutcome {
105            subscriber_id: data.subscriber.id,
106            app_user_id: data.subscriber.app_user_id,
107            virtual_currencies: data.virtual_currency_balances,
108            access: data.access,
109            product_type: data.product.map(|p| p.product_type),
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use std::sync::Arc;
118    use std::time::Duration;
119
120    #[test]
121    fn post_apple_parses_access_map() {
122        let mut server = mockito::Server::new();
123        let _m = server
124            .mock("POST", "/v1/receipts/apple")
125            .with_status(200)
126            .with_header("content-type", "application/json")
127            .with_body(
128                r#"{"data":{"subscriber":{"id":"sub_1","appUserId":"u1"},
129                    "virtualCurrencyBalances":{"gems":42},
130                    "access":{"pro":{"isActive":true,"expiresDate":null,
131                              "store":"APP_STORE","productIdentifier":"pro_monthly"}}}}"#,
132            )
133            .create();
134
135        let http = Arc::new(
136            HttpClient::new(server.url(), "pk_test".into())
137                .with_max_attempts(1)
138                .with_request_timeout(Duration::from_millis(500)),
139        );
140        let client = ReceiptClient::new(http);
141        let outcome = client
142            .post_apple("rcpt", "u1", "pro_monthly", "idem_rcpt_x", None, None)
143            .expect("post ok");
144        assert_eq!(outcome.subscriber_id, "sub_1");
145        let access = outcome.access.expect("access present");
146        assert!(access.get("pro").unwrap().is_active);
147    }
148}