use serde::{Deserialize, Serialize};
use std::fmt;
use webhook::TransactionEvent;
pub mod customer_registration;
pub mod event;
pub mod misuse;
pub mod webhook;
use std::hash::{Hash, Hasher};
#[cfg(feature = "diesel")]
use std::io::Write;
#[cfg(feature = "diesel")]
use diesel::deserialize::{self, FromSql, FromSqlRow};
#[cfg(feature = "diesel")]
use diesel::prelude::*;
#[cfg(feature = "diesel")]
use diesel::serialize::{self, IsNull, Output, ToSql};
#[cfg(feature = "diesel")]
use diesel::{expression::AsExpression, sql_types::Text};
#[cfg(feature = "diesel")]
use diesel::mysql::{Mysql, MysqlValue};
use crate::protocol::customer_registration::HandleType;
use crate::protocol::event::{EventType, InitialEventType, UpdateEventType};
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiverCategory {
Expense,
Firehose,
DutyOfCare,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ReceiverFilter {
Category(Vec<ReceiverCategory>),
Region(Vec<String>),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PublicOrg {
pub id: String,
pub name: String,
pub legal_name: Option<String>,
pub slug: String,
pub website: String,
pub logo: Option<String>,
pub brand_color: Option<String>,
pub stock_symbol: Option<String>,
pub twitter: Option<String>,
pub isin: Option<String>,
pub lei: Option<String>,
pub naics: Option<String>,
pub vat_number: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Handling {
pub sender_registered: bool,
pub receiver_registered: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReceiverQueryResult {
pub org: PublicOrg,
pub category: ReceiverCategory,
pub handling: Option<Handling>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ReceiverQueryRequest {
pub filters: Option<Vec<ReceiverFilter>>,
pub handles: Option<TransactionHandles>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReceiverQueryResponse {
pub mode: VersaMode,
pub receivers: Vec<ReceiverQueryResult>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(
feature = "diesel",
derive(diesel::expression::AsExpression, FromSqlRow)
)]
#[cfg_attr(feature = "diesel", diesel(sql_type = Text))]
pub enum VersaMode {
Prod,
Test,
}
pub type VersaEnv = VersaMode;
impl fmt::Display for VersaMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let state = match self {
VersaMode::Prod => "prod",
VersaMode::Test => "test",
};
write!(f, "{}", state)
}
}
#[cfg(feature = "diesel")]
impl ToSql<Text, Mysql> for VersaMode {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Mysql>) -> diesel::serialize::Result {
out.write_all(&*self.to_string().as_bytes())?;
Ok(IsNull::No)
}
}
#[cfg(feature = "diesel")]
impl FromSql<Text, Mysql> for VersaMode {
fn from_sql(bytes: MysqlValue<'_>) -> diesel::deserialize::Result<Self> {
match bytes.as_bytes() {
b"prod" => Ok(VersaMode::Prod),
b"test" => Ok(VersaMode::Test),
_ => Err("Unrecognized VersaMode enum variant".into()),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TransactionHandles {
pub customer_email: Option<String>,
pub customer_email_domain: Option<String>,
pub merchant_group_code: Option<String>,
pub merchant_user_code: Option<String>,
pub versa_client_ids: Option<Vec<String>>,
pub versa_org_ids: Option<Vec<String>>,
}
impl TransactionHandles {
pub fn new() -> Self {
Self {
customer_email: None,
customer_email_domain: None,
merchant_group_code: None,
merchant_user_code: None,
versa_client_ids: None,
versa_org_ids: None,
}
}
pub fn with_customer_email(mut self, email_address: String) -> Self {
self.customer_email = Some(email_address);
self
}
pub fn with_customer_email_domain(mut self, domain: String) -> Self {
self.customer_email_domain = Some(domain);
self
}
pub fn with_merchant_group_code(mut self, group_code: String) -> Self {
self.merchant_group_code = Some(group_code);
self
}
pub fn with_merchant_user_code(mut self, user_code: String) -> Self {
self.merchant_user_code = Some(user_code);
self
}
#[deprecated(since = "0.28.0", note = "please use `with_versa_org_ids` instead")]
pub fn with_versa_client_ids(mut self, ids: Vec<String>) -> Self {
self.versa_client_ids = Some(ids);
self
}
pub fn with_versa_org_ids(mut self, ids: Vec<String>) -> Self {
self.versa_org_ids = Some(ids);
self
}
pub fn iter(&self) -> TransactionHandlesIter<'_> {
TransactionHandlesIter {
handles: self,
position: 0,
}
}
}
pub struct TransactionHandlesIter<'a> {
handles: &'a TransactionHandles,
position: usize,
}
impl<'a> Iterator for TransactionHandlesIter<'a> {
type Item = (&'static HandleType, &'a str);
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.position {
0 => {
self.position += 1;
if let Some(ref value) = self.handles.customer_email {
return Some((&HandleType::CustomerEmail, value));
}
}
1 => {
self.position += 1;
if let Some(ref value) = self.handles.customer_email_domain {
return Some((&HandleType::CustomerEmailDomain, value));
}
}
2 => {
self.position += 1;
if let Some(ref value) = self.handles.merchant_group_code {
return Some((&HandleType::MerchantGroupCode, value));
}
}
3 => {
self.position += 1;
if let Some(ref value) = self.handles.merchant_user_code {
return Some((&HandleType::MerchantUserCode, value));
}
}
_ => return None,
}
}
}
}
impl<'a> IntoIterator for &'a TransactionHandles {
type Item = (&'static HandleType, &'a str);
type IntoIter = TransactionHandlesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ClientMetadata {
pub client_string: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct EventRegistrationRequest {
pub schema_version: String,
pub handles: Option<TransactionHandles>,
pub transaction_id: Option<String>,
pub client_metadata: Option<ClientMetadata>,
pub event_type: Option<EventType>,
pub transaction_event_filter: Option<TransactionEvent>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct InitialEventRegistrationRequest {
pub schema_version: String,
pub handles: TransactionHandles,
pub client_metadata: Option<ClientMetadata>,
pub event_type: InitialEventType,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct UpdateEventRegistrationRequest {
pub schema_version: String,
pub transaction_id: String,
pub client_metadata: Option<ClientMetadata>,
pub event_type: UpdateEventType,
}
pub type ReceiptRegistrationRequest = EventRegistrationRequest;
#[derive(Debug, Deserialize, Serialize)]
pub struct Receiver {
pub org_id: String,
pub secret: String,
pub endpoint_url: String,
pub address: String,
pub client_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ReceiverInstruction {
pub endpoint_url: String,
pub event_id: String,
pub event_type: EventType,
pub org_id: String,
pub org_name: String,
pub secret: String,
pub address: String,
pub client_id: String,
}
impl Hash for Receiver {
fn hash<H: Hasher>(&self, state: &mut H) {
self.org_id.hash(state);
self.endpoint_url.hash(state);
}
}
impl PartialEq for Receiver {
fn eq(&self, other: &Self) -> bool {
self.org_id == other.org_id && self.endpoint_url == other.endpoint_url
}
}
impl Eq for Receiver {}
impl Hash for ReceiverInstruction {
fn hash<H: Hasher>(&self, state: &mut H) {
self.org_id.hash(state);
self.endpoint_url.hash(state);
self.event_id.hash(state);
}
}
impl PartialEq for ReceiverInstruction {
fn eq(&self, other: &Self) -> bool {
self.event_id == other.event_id
}
}
impl Eq for ReceiverInstruction {}
#[derive(Debug, Deserialize, Serialize)]
pub struct ReceiptRegistrationResponse {
pub mode: VersaMode,
pub receipt_id: String,
pub transaction_id: String,
pub receivers: Vec<ReceiverInstruction>,
pub encryption_key: String,
pub env: VersaMode,
}
#[derive(Clone, Debug)]
pub struct ReceiptRegistrationSummary {
pub mode: VersaMode,
pub receipt_id: String,
pub transaction_id: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct EventRegistrationResponse {
pub mode: VersaMode,
pub event_id: String,
pub receipt_id: String,
pub transaction_id: String,
pub receivers: Vec<ReceiverInstruction>,
pub encryption_key: String,
pub env: VersaMode,
}
#[derive(Clone, Debug)]
pub struct EventRegistrationSummary {
pub mode: VersaMode,
pub event_id: String,
pub receipt_id: String,
pub transaction_id: String,
}
#[derive(Clone, Debug)]
pub struct EncryptionKey(pub String);
impl From<ReceiptRegistrationSummary> for EventRegistrationSummary {
fn from(summary: ReceiptRegistrationSummary) -> Self {
EventRegistrationSummary {
mode: summary.mode,
event_id: summary.receipt_id.clone(),
receipt_id: summary.receipt_id,
transaction_id: summary.transaction_id,
}
}
}
impl EventRegistrationResponse {
pub fn ready_for_delivery(
self,
) -> (
EncryptionKey,
EventRegistrationSummary,
Vec<ReceiverInstruction>,
) {
let EventRegistrationResponse {
mode,
event_id,
receipt_id,
transaction_id,
receivers,
encryption_key,
env: _env,
} = self;
let summary = EventRegistrationSummary {
mode,
event_id,
receipt_id,
transaction_id,
};
(EncryptionKey(encryption_key), summary, receivers)
}
}
impl ReceiptRegistrationResponse {
pub fn ready_for_delivery(
self,
) -> (
EncryptionKey,
ReceiptRegistrationSummary,
Vec<ReceiverInstruction>,
) {
let ReceiptRegistrationResponse {
mode,
receipt_id,
transaction_id,
receivers,
encryption_key,
env: _env,
} = self;
let summary = ReceiptRegistrationSummary {
mode,
receipt_id,
transaction_id,
};
(EncryptionKey(encryption_key), summary, receivers)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Address {
pub street_address: Option<String>,
pub city: Option<String>,
pub region: Option<String>,
pub country: String,
pub postal_code: Option<String>,
pub tz: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Sender {
pub org_id: String,
pub name: String,
pub website: String,
pub brand_color: Option<String>,
pub legal_name: Option<String>,
pub logo: Option<String>,
pub vat_number: Option<String>,
pub address: Option<Address>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Checkout {
pub key: String,
pub receipt_id: String,
pub schema_version: String,
pub transaction_id: String,
pub sender: Option<Sender>,
pub handles: TransactionHandles,
pub registered_at: i64,
pub transaction_event_index: u8,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Envelope {
pub encrypted: String,
pub nonce: String,
}
#[derive(Deserialize, Serialize)]
pub struct ReceiverPayload {
pub sender_client_id: String,
pub receipt_id: String,
pub envelope: Envelope,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct CheckoutRequest {
pub receipt_id: String,
pub client_metadata: Option<ClientMetadata>,
}
#[cfg(test)]
mod tests {
use crate::protocol::{customer_registration::HandleType, event::InitialEventType};
#[test]
fn test_versa_env_display() {
use super::VersaMode;
assert_eq!(VersaMode::Prod.to_string(), "prod");
assert_eq!(VersaMode::Test.to_string(), "test");
}
#[test]
fn test_hash_sets_of_receivers() {
use super::Receiver;
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Receiver {
address: "foobar".to_string(),
endpoint_url: "https://example.com".to_string(),
client_id: "versa_cid_xyz".to_string(),
org_id: "org_aaa".to_string(),
secret: "flargh".to_string(),
});
assert_eq!(set.len(), 1);
set.insert(Receiver {
address: "bazbat".to_string(),
endpoint_url: "https://example.com".to_string(),
client_id: "versa_cid_abc".to_string(),
org_id: "org_aaa".to_string(),
secret: "blargh".to_string(),
});
assert_eq!(set.len(), 1);
assert!(set.into_iter().next().unwrap().org_id == "org_aaa");
}
#[test]
fn test_transaction_handles_iter() {
use super::TransactionHandles;
let handles = TransactionHandles::new()
.with_customer_email("test@example.com".to_string())
.with_merchant_group_code("GROUP123".to_string())
.with_versa_org_ids(vec!["org1".to_string(), "org2".to_string()]);
let items: Vec<_> = handles.iter().collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (&HandleType::CustomerEmail, "test@example.com"));
assert_eq!(items[1], (&HandleType::MerchantGroupCode, "GROUP123"));
}
#[test]
fn test_transaction_handles_into_iter() {
use super::TransactionHandles;
let handles = TransactionHandles::new()
.with_customer_email_domain("example.com".to_string())
.with_merchant_user_code("USER456".to_string());
let items: Vec<_> = (&handles).into_iter().collect();
assert_eq!(items.len(), 2);
assert_eq!(items[0], (&HandleType::CustomerEmailDomain, "example.com"));
assert_eq!(items[1], (&HandleType::MerchantUserCode, "USER456"));
}
#[test]
fn test_initial_event_type_serializes_to_valid_event_type() {
use super::event::{EventType, InitialEventType};
use strum::IntoEnumIterator;
for update_event in InitialEventType::iter() {
let serialized = serde_json::to_string(&update_event).unwrap();
let deserialized_result: Result<EventType, _> = serde_json::from_str(&serialized);
assert!(
deserialized_result.is_ok(),
"InitialEventType::{:?} failed to deserialize into EventType",
update_event
);
}
}
#[test]
fn test_update_event_type_serializes_to_valid_event_type() {
use super::event::{EventType, UpdateEventType};
use strum::IntoEnumIterator;
for update_event in UpdateEventType::iter() {
let serialized = serde_json::to_string(&update_event).unwrap();
let deserialized_result: Result<EventType, _> = serde_json::from_str(&serialized);
assert!(
deserialized_result.is_ok(),
"UpdateEventType::{:?} failed to deserialize into EventType",
update_event
);
}
}
#[test]
fn test_initial_event_type_from_conversion() {
use super::event::{EventType, InitialEventType};
use strum::IntoEnumIterator;
for initial_event in InitialEventType::iter() {
let event_type: EventType = initial_event.clone().into();
let initial_serialized = serde_json::to_string(&initial_event).unwrap();
let event_serialized = serde_json::to_string(&event_type).unwrap();
assert_eq!(
initial_serialized, event_serialized,
"InitialEventType::{:?} conversion to EventType changed serialization",
initial_event
);
}
}
#[test]
fn test_update_event_type_from_conversion() {
use super::event::{EventType, UpdateEventType};
use strum::IntoEnumIterator;
for update_event in UpdateEventType::iter() {
let event_type: EventType = update_event.clone().into();
let update_serialized = serde_json::to_string(&update_event).unwrap();
let event_serialized = serde_json::to_string(&event_type).unwrap();
assert_eq!(
update_serialized, event_serialized,
"UpdateEventType::{:?} conversion to EventType changed serialization",
update_event
);
}
}
#[test]
fn test_event_registration_request_variants_serialize_correctly() {
use super::event::UpdateEventType;
use super::webhook::TransactionEvent;
use super::{
EventRegistrationRequest, InitialEventRegistrationRequest, TransactionHandles,
UpdateEventRegistrationRequest,
};
let initial_request = InitialEventRegistrationRequest {
schema_version: "2.2.0".to_string(),
handles: TransactionHandles::new().with_customer_email("test@example.com".to_string()),
client_metadata: None,
event_type: InitialEventType::Receipt,
};
let serialized = serde_json::to_string(&initial_request).unwrap();
let deserialized: EventRegistrationRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.schema_version, "2.2.0");
assert!(deserialized.handles.is_some());
assert!(deserialized.transaction_id.is_none());
let update_request = UpdateEventRegistrationRequest {
schema_version: "2.2.0".to_string(),
transaction_id: "txn_123".to_string(),
client_metadata: None,
event_type: UpdateEventType::TransitRouteStatusUpdated,
};
let serialized = serde_json::to_string(&update_request).unwrap();
let deserialized: EventRegistrationRequest = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized.schema_version, "2.2.0");
assert_eq!(deserialized.transaction_id, Some("txn_123".to_string()));
}
#[test]
fn test_receipt_registration_summary_to_event_registration_summary() {
use super::{EventRegistrationSummary, ReceiptRegistrationSummary, VersaMode};
let receipt_summary = ReceiptRegistrationSummary {
mode: VersaMode::Test,
receipt_id: "rcpt_123".to_string(),
transaction_id: "txn_456".to_string(),
};
let event_summary: EventRegistrationSummary = receipt_summary.clone().into();
assert_eq!(event_summary.mode, VersaMode::Test);
assert_eq!(event_summary.receipt_id, "rcpt_123");
assert_eq!(event_summary.event_id, "rcpt_123");
assert_eq!(event_summary.transaction_id, "txn_456");
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Org {
pub id: String,
pub name: String,
pub slug: String,
pub website: String,
pub logo: Option<String>,
pub brand_color: Option<String>,
pub stock_symbol: Option<String>,
pub twitter: Option<String>,
pub isin: Option<String>,
pub lei: Option<String>,
pub naics: Option<String>,
pub vat_number: Option<String>,
pub created: i64,
}
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CustomerRefManagedBy {
Sender,
Receiver,
Both,
}
#[deprecated(
since = "1.2.0",
note = "The CheckRegistryResponse struct is deprecated and will be removed in a future version"
)]
#[derive(Deserialize, Serialize)]
pub struct CheckRegistryResponse {
pub mode: VersaMode,
pub env: VersaMode,
pub receivers: Vec<ReceiverInfo>,
}
#[deprecated(
since = "1.2.0",
note = "The ReceiverInfo struct is deprecated and will be removed in a future version; use ReceiverQueryResult instead"
)]
#[derive(Debug, Deserialize, Serialize)]
pub struct ReceiverInfo {
pub client_id: String,
pub receiver: Option<Org>,
pub managed_by: CustomerRefManagedBy,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct AssetRegistrationResponse {
pub asset_id: String,
}