versa/protocol/
mod.rs

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  /// EXPERIMENTAL
73  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  /// EXPERIMENTAL, for now, leave as None
116  pub receipt_hash: Option<u64>,
117  /// The latest schema version of the receipt
118  pub schema_version: String,
119  /// Provide as many handles as available to ensure the receipt is routed to the correct receivers
120  pub handles: TransactionHandles,
121  /// The Versa transaction ID, if updating an existing receipt
122  pub transaction_id: Option<String>,
123  /// Client software metadata, such as the client_string
124  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  /// DEPRECATED: use `endpoint_url` instead
133  pub address: String,
134  /// DEPRECATED: use `org_id` instead
135  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  /// DEPRECATED: use `endpoint_url` instead
146  pub address: String,
147  /// DEPRECATED: use `org_id` instead
148  pub client_id: String,
149}
150
151/// A receiver object is hashed by its client_id — the receiver address, secret, and org_id are considered constant and non-configurable.
152/// It is possible for a receiver to change their address and rotate their secret with the registry,
153/// in which case any Receiver records returned by said registry will contain the updated values.
154/// Assembling hash sets of outdated and new receiver objects could result in unexpected behavior—do not do store or cache receivers.
155impl 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  /// DEPRECATED: use `mode` instead
192  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// Receiver structs
231
232#[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  /// The receipt_id that came with the delivery
388  pub receipt_id: String,
389  /// Client software metadata, such as the client_string
390  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  /// This reference was configured by the sender; if the sender deregisters it, receipts matching this handle will no longer be sent to the receiver.
449  Sender,
450  /// This reference was configured by the receiver; the sender cannot deregister it.
451  Receiver,
452  /// This reference was configured by both the sender and the receiver; if the sender deregisters it, receipts matching this handle will still be sent to the receiver, unless it is also disabled by the receiving client.
453  Both,
454}
455
456#[derive(Deserialize, Serialize)]
457pub struct CheckRegistryResponse {
458  pub mode: VersaMode,
459  /// DEPRECATED: use 'mode' instead
460  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}