1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5pub mod customer_registration;
6pub mod misuse;
7
8use std::hash::{Hash, Hasher};
9
10#[cfg(feature = "diesel")]
11use std::io::Write;
12
13#[cfg(feature = "diesel")]
14use diesel::deserialize::{self, FromSql, FromSqlRow};
15#[cfg(feature = "diesel")]
16use diesel::prelude::*;
17#[cfg(feature = "diesel")]
18use diesel::serialize::{self, IsNull, Output, ToSql};
19#[cfg(feature = "diesel")]
20use diesel::{expression::AsExpression, sql_types::Text};
21
22#[cfg(feature = "diesel")]
23use diesel::mysql::{Mysql, MysqlValue};
24
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27#[cfg_attr(
28 feature = "diesel",
29 derive(diesel::expression::AsExpression, FromSqlRow)
30)]
31#[cfg_attr(feature = "diesel", diesel(sql_type = Text))]
32pub enum VersaMode {
33 Prod,
34 Test,
35}
36
37pub type VersaEnv = VersaMode;
38
39impl fmt::Display for VersaMode {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 let state = match self {
42 VersaMode::Prod => "prod",
43 VersaMode::Test => "test",
44 };
45 write!(f, "{}", state)
46 }
47}
48
49#[cfg(feature = "diesel")]
50impl ToSql<Text, Mysql> for VersaMode {
51 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> diesel::serialize::Result {
52 out.write_all(&*self.to_string().as_bytes())?;
53 Ok(IsNull::No)
54 }
55}
56
57#[cfg(feature = "diesel")]
58impl FromSql<Text, Mysql> for VersaMode {
59 fn from_sql(bytes: MysqlValue<'_>) -> diesel::deserialize::Result<Self> {
60 match bytes.as_bytes() {
61 b"prod" => Ok(VersaMode::Prod),
62 b"test" => Ok(VersaMode::Test),
63 _ => Err("Unrecognized VersaMode enum variant".into()),
64 }
65 }
66}
67
68#[derive(Clone, Debug, Deserialize, Serialize)]
69pub struct TransactionHandles {
70 pub customer_email: Option<String>,
71 pub customer_email_domain: Option<String>,
72 pub versa_client_ids: Option<Vec<String>>,
74 pub versa_org_ids: Option<Vec<String>>,
75}
76
77impl TransactionHandles {
78 pub fn new() -> Self {
79 Self {
80 customer_email: None,
81 customer_email_domain: None,
82 versa_client_ids: None,
83 versa_org_ids: None,
84 }
85 }
86
87 pub fn with_customer_email(mut self, email_address: String) -> Self {
88 self.customer_email = Some(email_address);
89 self
90 }
91
92 pub fn with_customer_email_domain(mut self, domain: String) -> Self {
93 self.customer_email_domain = Some(domain);
94 self
95 }
96
97 #[deprecated(since = "0.28.0", note = "please use `with_versa_org_ids` instead")]
98 pub fn with_versa_client_ids(mut self, ids: Vec<String>) -> Self {
99 self.versa_client_ids = Some(ids);
100 self
101 }
102
103 pub fn with_versa_org_ids(mut self, ids: Vec<String>) -> Self {
104 self.versa_org_ids = Some(ids);
105 self
106 }
107}
108#[derive(Debug, Deserialize, Serialize)]
109pub struct ClientMetadata {
110 pub client_string: Option<String>,
111}
112
113#[derive(Debug, Deserialize, Serialize)]
114pub struct ReceiptRegistrationRequest {
115 pub receipt_hash: Option<u64>,
117 pub schema_version: String,
119 pub handles: TransactionHandles,
121 pub transaction_id: Option<String>,
123 pub client_metadata: Option<ClientMetadata>,
125}
126
127#[derive(Debug, Deserialize, Serialize)]
128pub struct Receiver {
129 pub org_id: String,
130 pub secret: String,
131 pub endpoint_url: String,
132 pub address: String,
134 pub client_id: String,
136}
137
138#[derive(Debug, Deserialize, Serialize)]
139pub struct ReceiverInstruction {
140 pub endpoint_url: String,
141 pub event_id: String,
142 pub event_type: WebhookEventType,
143 pub org_id: String,
144 pub secret: String,
145 pub address: String,
147 pub client_id: String,
149}
150
151impl Hash for Receiver {
156 fn hash<H: Hasher>(&self, state: &mut H) {
157 self.org_id.hash(state);
158 self.endpoint_url.hash(state);
159 }
160}
161
162impl PartialEq for Receiver {
163 fn eq(&self, other: &Self) -> bool {
164 self.org_id == other.org_id && self.endpoint_url == other.endpoint_url
165 }
166}
167impl Eq for Receiver {}
168
169impl Hash for ReceiverInstruction {
170 fn hash<H: Hasher>(&self, state: &mut H) {
171 self.org_id.hash(state);
172 self.endpoint_url.hash(state);
173 self.event_id.hash(state);
174 }
175}
176
177impl PartialEq for ReceiverInstruction {
178 fn eq(&self, other: &Self) -> bool {
179 self.event_id == other.event_id
180 }
181}
182impl Eq for ReceiverInstruction {}
183
184#[derive(Debug, Deserialize, Serialize)]
185pub struct ReceiptRegistrationResponse {
186 pub mode: VersaMode,
187 pub receipt_id: String,
188 pub transaction_id: String,
189 pub receivers: Vec<ReceiverInstruction>,
190 pub encryption_key: String,
191 pub env: VersaMode,
193}
194
195#[derive(Clone, Debug)]
196pub struct ReceiptRegistrationSummary {
197 pub mode: VersaMode,
198 pub receipt_id: String,
199 pub transaction_id: String,
200}
201
202#[derive(Clone, Debug)]
203pub struct EncryptionKey(pub String);
204
205impl ReceiptRegistrationResponse {
206 pub fn ready_for_delivery(
207 self,
208 ) -> (
209 EncryptionKey,
210 ReceiptRegistrationSummary,
211 Vec<ReceiverInstruction>,
212 ) {
213 let ReceiptRegistrationResponse {
214 mode,
215 receipt_id,
216 transaction_id,
217 receivers,
218 encryption_key,
219 env: _env,
220 } = self;
221 let summary = ReceiptRegistrationSummary {
222 mode,
223 receipt_id,
224 transaction_id,
225 };
226 (EncryptionKey(encryption_key), summary, receivers)
227 }
228}
229
230#[derive(Clone, Debug, Deserialize, Serialize)]
233pub struct Address {
234 pub street_address: Option<String>,
235 pub city: Option<String>,
236 pub region: Option<String>,
237 pub country: String,
238 pub postal_code: Option<String>,
239 pub tz: Option<String>,
240}
241
242#[derive(Clone, Debug, Deserialize, Serialize)]
243pub struct Sender {
244 pub org_id: String,
245 pub name: String,
246 pub website: String,
247 pub brand_color: Option<String>,
248 pub legal_name: Option<String>,
249 pub logo: Option<String>,
250 pub vat_number: Option<String>,
251 pub address: Option<Address>,
252}
253
254#[derive(Clone, Debug, Deserialize, Serialize)]
255pub struct Checkout {
256 pub key: String,
257 pub receipt_id: String,
258 pub receipt_hash: String,
259 pub schema_version: String,
260 pub transaction_id: String,
261 pub sender: Option<Sender>,
262 pub handles: TransactionHandles,
263 pub registered_at: i64,
264 pub transaction_event_index: u8,
265}
266
267#[derive(Debug, Deserialize, Serialize)]
268pub struct Envelope {
269 pub encrypted: String,
270 pub nonce: String,
271}
272
273#[derive(Deserialize, Serialize)]
274pub struct ReceiverPayload {
275 pub sender_client_id: String,
276 pub receipt_id: String,
277 pub envelope: Envelope,
278}
279
280#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
281pub enum TransactionEvent {
282 #[serde(rename = "itinerary")]
283 Itinerary,
284 #[serde(rename = "receipt")]
285 Receipt,
286}
287
288impl fmt::Display for TransactionEvent {
289 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
290 let serialized = serde_json::to_string(self).unwrap().replace("\"", "");
291 write!(f, "{}", serialized)
292 }
293}
294
295impl FromStr for TransactionEvent {
296 type Err = String;
297
298 fn from_str(s: &str) -> Result<Self, Self::Err> {
299 match s {
300 "itinerary" => Ok(TransactionEvent::Itinerary),
301 "receipt" => Ok(TransactionEvent::Receipt),
302 _ => Err("Unrecognized TransactionEvent enum variant".to_string()),
303 }
304 }
305}
306
307#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Hash)]
308#[serde(rename_all = "snake_case")]
309#[cfg_attr(
310 feature = "diesel",
311 derive(diesel::expression::AsExpression, diesel::deserialize::FromSqlRow)
312)]
313#[cfg_attr(feature = "diesel", diesel(sql_type = Text))]
314pub enum WebhookEventType {
315 #[serde(rename = "customer.deregistered_by_sender")]
316 CustomerDeregisteredBySender,
317 #[serde(rename = "customer.registered_by_sender")]
318 CustomerRegisteredBySender,
319 #[serde(rename = "itinerary")]
320 Itinerary,
321 #[serde(rename = "itinerary.decrypted")]
322 ItineraryDecrypted,
323 #[serde(rename = "receipt")]
324 Receipt,
325 #[serde(rename = "receipt.decrypted")]
326 ReceiptDecrypted,
327}
328
329impl fmt::Display for WebhookEventType {
330 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331 let serialized = serde_json::to_string(self).unwrap().replace("\"", "");
332 write!(f, "{}", serialized)
333 }
334}
335
336impl FromStr for WebhookEventType {
337 type Err = String;
338
339 fn from_str(s: &str) -> Result<Self, Self::Err> {
340 match s {
341 "customer.deregistered_by_sender" => Ok(WebhookEventType::CustomerDeregisteredBySender),
342 "customer.registered_by_sender" => Ok(WebhookEventType::CustomerRegisteredBySender),
343 "itinerary" => Ok(WebhookEventType::Itinerary),
344 "itinerary.decrypted" => Ok(WebhookEventType::ItineraryDecrypted),
345 "receipt" => Ok(WebhookEventType::Receipt),
346 "receipt.decrypted" => Ok(WebhookEventType::ReceiptDecrypted),
347 _ => Err("Unrecognized WebhookEventType enum variant".to_string()),
348 }
349 }
350}
351
352#[cfg(feature = "diesel")]
353impl ToSql<Text, Mysql> for WebhookEventType {
354 fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> serialize::Result {
355 out.write_all(&*self.to_string().as_bytes())?;
356 Ok(IsNull::No)
357 }
358}
359
360#[cfg(feature = "diesel")]
361impl FromSql<Text, Mysql> for WebhookEventType {
362 fn from_sql(bytes: MysqlValue<'_>) -> deserialize::Result<Self> {
363 match bytes.as_bytes() {
364 b"customer.deregistered_by_sender" => Ok(WebhookEventType::CustomerDeregisteredBySender),
365 b"customer.registered_by_sender" => Ok(WebhookEventType::CustomerRegisteredBySender),
366 b"itinerary" => Ok(WebhookEventType::Itinerary),
367 b"itinerary.decrypted" => Ok(WebhookEventType::ItineraryDecrypted),
368 b"receipt" => Ok(WebhookEventType::Receipt),
369 b"receipt.decrypted" => Ok(WebhookEventType::ReceiptDecrypted),
370 _ => Err("Unrecognized misuse_code enum variant".into()),
371 }
372 }
373}
374
375#[derive(Deserialize, Serialize)]
376pub struct WebhookEvent<T> {
377 pub event: WebhookEventType,
378 pub event_id: Option<String>,
379 pub event_at: Option<i64>,
380 pub delivery_id: Option<String>,
381 pub delivery_at: Option<i64>,
382 pub data: T,
383}
384
385#[derive(Serialize)]
386pub struct CheckoutRequest {
387 pub receipt_id: String,
389 pub client_metadata: Option<ClientMetadata>,
391}
392
393#[cfg(test)]
394mod tests {
395
396 #[test]
397 fn test_versa_env_display() {
398 use super::VersaMode;
399 assert_eq!(VersaMode::Prod.to_string(), "prod");
400 assert_eq!(VersaMode::Test.to_string(), "test");
401 }
402
403 #[test]
404 fn test_hash_sets_of_receivers() {
405 use super::Receiver;
406 use std::collections::HashSet;
407 let mut set = HashSet::new();
408 set.insert(Receiver {
409 address: "foobar".to_string(),
410 endpoint_url: "https://example.com".to_string(),
411 client_id: "versa_cid_xyz".to_string(),
412 org_id: "org_aaa".to_string(),
413 secret: "flargh".to_string(),
414 });
415 assert_eq!(set.len(), 1);
416 set.insert(Receiver {
417 address: "bazbat".to_string(),
418 endpoint_url: "https://example.com".to_string(),
419 client_id: "versa_cid_abc".to_string(),
420 org_id: "org_aaa".to_string(),
421 secret: "blargh".to_string(),
422 });
423 assert_eq!(set.len(), 1);
424 assert!(set.into_iter().next().unwrap().org_id == "org_aaa");
425 }
426}
427
428#[derive(Debug, Serialize, Deserialize)]
429pub struct Org {
430 pub id: String,
431 pub name: String,
432 pub slug: String,
433 pub website: String,
434 pub logo: Option<String>,
435 pub brand_color: Option<String>,
436 pub stock_symbol: Option<String>,
437 pub twitter: Option<String>,
438 pub isin: Option<String>,
439 pub lei: Option<String>,
440 pub naics: Option<String>,
441 pub vat_number: Option<String>,
442 pub created: i64,
443}
444
445#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
446#[serde(rename_all = "snake_case")]
447pub enum CustomerReferenceManagedBy {
448 Sender,
450 Receiver,
452 Both,
454}
455
456#[derive(Deserialize, Serialize)]
457pub struct CheckRegistryResponse {
458 pub mode: VersaMode,
459 pub env: VersaMode,
461 pub receivers: Vec<ReceiverInfo>,
462}
463
464#[derive(Debug, Deserialize, Serialize)]
465pub struct ReceiverInfo {
466 pub client_id: String,
467 pub receiver: Option<Org>,
468 pub managed_by: CustomerReferenceManagedBy,
469}