rovenue/events/
client.rs

1use std::sync::Arc;
2
3use crate::error::RovenueResult;
4use crate::transport::http_client::HttpClient;
5use crate::transport::types::HttpPostRequest;
6
7use super::EventEnvelope;
8
9/// Thin client for `POST /v1/events`. The route returns an empty 202; transport
10/// errors (network failure, 5xx after retries) propagate to the caller.
11pub struct EventsClient {
12    http: Arc<HttpClient>,
13}
14
15impl EventsClient {
16    pub fn new(http: Arc<HttpClient>) -> Self {
17        Self { http }
18    }
19
20    /// POST the serialized envelope to `/v1/events`. The optional `scope`
21    /// travels in the `X-Rovenue-App-User-Id` header so the server can resolve
22    /// the current user; the canonical subscriber identity is carried in the
23    /// envelope body (`subscriberId`).
24    pub fn post(&self, envelope: &EventEnvelope, scope: Option<&str>) -> RovenueResult<()> {
25        let mut req = HttpPostRequest::new("/v1/events").expect_empty_body();
26        if let Some(s) = scope {
27            req = req.user_scope(s);
28        }
29        self.http
30            .post_json::<EventEnvelope, serde_json::Value>(req, envelope)?;
31        Ok(())
32    }
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::events::IdentityContext;
39
40    fn envelope() -> EventEnvelope {
41        EventEnvelope {
42            version: None,
43            event_id: None,
44            event_type: "purchase".into(),
45            occurred_at: "2026-06-20T10:00:00Z".into(),
46            subscriber_id: Some("user_42".into()),
47            product_id: None,
48            amount: Some("9.99".into()),
49            currency: Some("USD".into()),
50            event_source_url: None,
51            identity_context: Some(IdentityContext {
52                email: Some("a@b.com".into()),
53                ..Default::default()
54            }),
55            funnel_context: None,
56        }
57    }
58
59    #[test]
60    fn post_sends_camelcase_envelope_and_omits_none() {
61        let mut server = mockito::Server::new();
62        let m = server
63            .mock("POST", "/v1/events")
64            .match_header("X-Rovenue-App-User-Id", "user_42")
65            .match_body(mockito::Matcher::JsonString(
66                r#"{"eventType":"purchase","occurredAt":"2026-06-20T10:00:00Z","subscriberId":"user_42","amount":"9.99","currency":"USD","identityContext":{"email":"a@b.com"}}"#.into(),
67            ))
68            .with_status(202)
69            .create();
70
71        let http = Arc::new(HttpClient::new(server.url(), "pk_test".into()).with_max_attempts(1));
72        EventsClient::new(http)
73            .post(&envelope(), Some("user_42"))
74            .expect("post ok");
75
76        m.assert();
77    }
78
79    #[test]
80    fn post_omits_scope_header_when_none() {
81        let mut server = mockito::Server::new();
82        let m = server
83            .mock("POST", "/v1/events")
84            .match_header("X-Rovenue-App-User-Id", mockito::Matcher::Missing)
85            .with_status(202)
86            .create();
87
88        let http = Arc::new(HttpClient::new(server.url(), "pk_test".into()).with_max_attempts(1));
89        EventsClient::new(http)
90            .post(&envelope(), None)
91            .expect("post ok");
92
93        m.assert();
94    }
95}