1use anyhow::{Result, anyhow};
2use chrono::Utc;
3use hmac::{Hmac, Mac};
4use serde::Deserialize;
5use serde_json::Value;
6use sha2::Sha256;
7use std::collections::HashMap;
8
9const CREATE_SESSION_URL: &str = "https://api.stripe.com/v1/checkout/sessions";
10
11struct Signature<'r> {
13 t: i64,
14 v1: &'r str,
15}
16
17impl<'r> Signature<'r> {
18 fn parse(raw: &'r str) -> Result<Signature<'r>> {
19 let headers: HashMap<&str, &str> = raw
20 .split(',')
21 .map(|header| {
22 let mut key_and_value = header.split('=');
23 let key = key_and_value.next();
24 let value = key_and_value.next();
25 (key, value)
26 })
27 .filter_map(|(key, value)| match (key, value) {
28 (Some(key), Some(value)) => Some((key, value)),
29 _ => None,
30 })
31 .collect();
32 let t = headers
33 .get("t")
34 .and_then(|t| t.parse::<i64>().ok())
35 .ok_or(anyhow!("Bad signature t"))?;
36 let v1 = headers.get("v1").ok_or(anyhow!("Bad signature v1"))?;
37 Ok(Signature { t, v1 })
38 }
39}
40
41#[derive(Deserialize)]
42struct Event {
43 r#type: String,
44 data: Object,
45}
46
47#[derive(Deserialize)]
48struct Object {
49 object: Value,
50}
51
52pub enum SimpleEvent {
54 PaymentComplete(String, String),
56 SubscriptionCreated(String, String),
58 SubscriptionPaid(String, Vec<String>),
60 SubscriptionDeleted(String, Vec<String>),
62 None(String),
64}
65
66#[derive(Clone, Debug)]
67pub struct Stripe {
68 secret: String,
69 webhook: String,
70 prices: HashMap<String, String>,
71 success_url: Option<String>,
72 cancel_url: Option<String>,
73}
74
75impl Stripe {
76 pub fn init(secret: String, webhook: String) -> Self {
78 Self {
79 secret,
80 webhook,
81 prices: HashMap::new(),
82 success_url: None,
83 cancel_url: None,
84 }
85 }
86
87 pub fn add_payment_price(&mut self, id: &str) {
89 self.prices.insert(id.to_owned(), "payment".to_owned());
90 }
91
92 pub fn add_subscription_price(&mut self, id: &str) {
94 self.prices.insert(id.to_owned(), "subscription".to_owned());
95 }
96
97 pub fn set_urls(&mut self, success: &str, cancel: &str) {
99 self.success_url = Some(success.to_owned());
100 self.cancel_url = Some(cancel.to_owned());
101 }
102
103 pub async fn create_session(
105 &self,
106 uuid: &str,
107 email: Option<String>,
108 customer: Option<String>,
109 price: &str,
110 quantity: i32,
111 ) -> Result<(String, String)> {
112 let mode = self.prices.get(price).ok_or(anyhow!("No price"))?.to_owned();
113
114 let mut params = vec![
115 ("mode", mode),
116 ("line_items[0][price]", price.to_owned()),
117 ("line_items[0][quantity]", quantity.to_string()),
118 ];
119
120 if !uuid.is_empty() {
121 params.push(("client_reference_id", uuid.to_owned()));
122 }
123
124 if let Some(customer) = customer && customer.starts_with("cus_") {
125 params.push(("customer", customer));
126 } else if let Some(email) = email {
127 params.push(("customer_email", email));
128 }
129
130 if let Some(url) = &self.success_url {
131 params.push(("success_url", url.to_owned()));
132 }
133 if let Some(url) = &self.cancel_url {
134 params.push(("cancel_url", url.to_owned()));
135 }
136
137 let client = reqwest::Client::new();
138 let resp = client
139 .post(CREATE_SESSION_URL)
140 .basic_auth(self.secret.clone(), Some(""))
141 .form(¶ms)
142 .send()
143 .await?
144 .json::<Value>()
145 .await?;
146
147 match (resp.get("id"), resp.get("url")) {
148 (Some(id), Some(url)) => {
149 let s_i = id.as_str().unwrap_or_default().to_owned();
150 let s_u = url.as_str().unwrap_or_default().to_owned();
151 Ok((s_i, s_u))
152 }
153 _ => Err(anyhow!("Failed to create session for {uuid}: {}", resp["error"]["message"])),
154 }
155 }
156
157 pub async fn retrieve(&self, _session: &str) -> Result<String> {
159 todo!()
160 }
161
162 pub fn simple_event(&self, body: &str, signature: &str) -> Result<SimpleEvent> {
164 let signature = Signature::parse(&signature)?;
166 let signed_payload = format!("{}.{}", signature.t, body);
167
168 let mut mac = Hmac::<Sha256>::new_from_slice(self.webhook.as_bytes()).map_err(|_| anyhow!("Bad Key"))?;
171 mac.update(signed_payload.as_bytes());
172
173 let sig = hex::decode(signature.v1).map_err(|_| anyhow!("Bad signature hex"))?;
174 mac.verify_slice(sig.as_slice()).map_err(|_| anyhow!("Bad signature verify"))?;
175
176 let current_timestamp = Utc::now().timestamp();
178 if (current_timestamp - signature.t).abs() > 300 {
179 return Err(anyhow!("Bad signature time"));
180 }
181
182 let event: Event = serde_json::from_str(&body)?;
183 let se = match event.r#type.as_str() {
184 "checkout.session.completed" => {
185 let session = event.data.object["id"].as_str().unwrap_or_default().to_owned();
187 let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
188 let paid = event.data.object["payment_status"].as_str().unwrap_or_default();
189 let mode = event.data.object["mode"].as_str().unwrap_or_default();
190 if session.is_empty() || paid.is_empty() || mode.is_empty() {
191 tracing::error!("Stripe checkout.session.completed changed!");
192 }
193 if paid == "paid" {
194 match mode {
195 "payment" => SimpleEvent::PaymentComplete(session, customer),
196 "subscription" => SimpleEvent::SubscriptionCreated(session, customer),
197 _ => SimpleEvent::None(format!("checkout.session.completed payment mode: {mode}")),
198 }
199 } else {
200 SimpleEvent::None(format!("checkout.session.completed payment status: {paid}"))
201 }
202 }
203 "invoice.paid" => {
204 let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
206 let lines = event.data.object["lines"]["data"].as_array();
207
208 let mut prices = vec![];
209 if let Some(lines) = lines {
210 for line in lines {
211 if let Some(p) = line.pointer("/pricing/price_details/price") {
212 prices.push(p.as_str().unwrap_or_default().to_owned());
213 }
214 }
215 }
216
217 if customer.is_empty() {
218 tracing::error!("Stripe invoice.paid changed!");
219 }
220
221 SimpleEvent::SubscriptionPaid(customer, prices)
222 }
223 "invoice.payment_failed" => {
224 let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
226 let lines = event.data.object["lines"]["data"].as_array();
227
228 let mut prices = vec![];
229 if let Some(lines) = lines {
230 for line in lines {
231 if let Some(p) = line.pointer("/pricing/price_details/price") {
232 prices.push(p.as_str().unwrap_or_default().to_owned());
233 }
234 }
235 }
236
237 if customer.is_empty() {
238 tracing::error!("Stripe invoice.payment_failed changed!");
239 }
240
241 SimpleEvent::SubscriptionDeleted(customer, prices)
242 }
243 "customer.subscription.deleted" => {
244 let customer = event.data.object["customer"].as_str().unwrap_or_default().to_owned();
246 let lines = event.data.object["lines"]["data"].as_array();
247
248 let mut prices = vec![];
249 if let Some(lines) = lines {
250 for line in lines {
251 if let Some(p) = line.pointer("/pricing/price_details/price") {
252 prices.push(p.as_str().unwrap_or_default().to_owned());
253 }
254 }
255 }
256
257 if customer.is_empty() {
258 tracing::error!("Stripe customer.subscription.deleted changed!");
259 }
260
261 SimpleEvent::SubscriptionDeleted(customer, prices)
262 }
263 _ => SimpleEvent::None(event.r#type)
264 };
265
266 Ok(se)
267 }
268}