1#![doc = include_str!("../README.md")]
2#![deny(unsafe_code)]
3#![deny(clippy::pedantic)]
4#![allow(
5 clippy::struct_excessive_bools,
6 reason = "structs cannot be changed due to serialization"
7)]
8
9use std::collections::HashMap;
10
11use chrono::{DateTime, Utc};
12
13pub use crate::string_boolean::StringBoolean;
14
15#[cfg(feature = "decode_everything")]
16pub type ExtraValues = HashMap<String, serde_json::Value>;
17
18#[cfg(feature = "decode_everything")]
19pub mod has_extra_data;
20#[cfg(feature = "decode_everything")]
21pub use has_extra_data::*;
22
23macro_rules! enum_with_extra {
27 (
28 $(#[$attrs: meta])*
29 untagged $name: ident, $($(#[$variant_attrs: meta])* $variant: ident($variant_inner_ty: ty),)*
30 ) => {
31 $(#[$attrs])*
32 #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
33 #[serde(untagged)]
34 pub enum $name {
35 $($(#[$variant_attrs])* $variant($variant_inner_ty),)*
36
37 #[cfg(feature = "decode_everything")]
39 SomethingElse(crate::ExtraValues),
40 }
41
42 #[cfg(feature = "decode_everything")]
43 impl crate::HasExtraData for $name {
44 fn has_extra_data(&self) -> bool {
45 match self {
46 Self::SomethingElse(_vals) => true,
47 $(Self::$variant(inner) => inner.has_extra_data(),)*
48 }
49 }
50 }
51 };
52 (
53 $case: expr => $(#[$attrs: meta])* $name: ident,
54 $($(#[$variant_attrs: meta])* $variant: ident,)*
55 ) => {
56 $(#[$attrs])*
57 #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
58 #[serde(rename_all = $case)]
59 pub enum $name {
60 $($(#[$variant_attrs])* $variant,)*
61
62 #[cfg(feature = "decode_everything")]
64 #[serde(untagged)]
65 SomethingElse(String),
66 }
67
68 #[cfg(feature = "decode_everything")]
69 impl crate::HasExtraData for $name {
70 fn has_extra_data(&self) -> bool {
71 match self {
72 Self::SomethingElse(_value) => true,
73 _ => false,
74 }
75 }
76 }
77 };
78}
79
80macro_rules! struct_with_extra {
84 (
85 no_extra $(#[$attrs: meta])*
86 $name: ident, $($(#[$field_attrs: meta])* $field_name: ident: $field_typ: ty,)*
87 ) => {
88 $(#[$attrs])*
89 #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
90 pub struct $name {
91 $($(#[$field_attrs])* pub $field_name: $field_typ,)*
92 }
93
94 #[cfg(feature = "decode_everything")]
95 impl crate::HasExtraData for $name {
96 fn has_extra_data(&self) -> bool {
97 false $(|| self.$field_name.has_extra_data())*
98 }
99 }
100 };
101 (
102 $(#[$attrs: meta])*
103 $name: ident, $($(#[$field_attrs: meta])* $field_name: ident: $field_typ: ty,)*
104 ) => {
105 $(#[$attrs])*
106 #[derive(Clone, Debug, PartialEq, serde::Deserialize)]
107 pub struct $name {
108 $($(#[$field_attrs])* pub $field_name: $field_typ,)*
109
110 #[cfg(feature = "decode_everything")]
113 #[cfg_attr(feature = "decode_everything", serde(flatten))]
114 pub extra: crate::ExtraValues,
115 }
116
117 #[cfg(feature = "decode_everything")]
118 impl crate::HasExtraData for $name {
119 fn has_extra_data(&self) -> bool {
120 let this_has_extra = !self.extra.is_empty();
121 this_has_extra
122 $(|| self.$field_name.has_extra_data())*
123 }
124 }
125 };
126}
127
128#[cfg(test)]
129mod tests;
130
131pub mod counterparty;
132pub mod decline;
133pub mod merchant;
134pub mod metadata;
135pub mod string_boolean;
136
137struct_with_extra! {
138 Webhook,
140 r#type: WebhookType,
142 data: WebhookData,
144}
145
146enum_with_extra! {
147 "snake_case" => WebhookType,
148 #[serde(rename = "transaction.created")]
149 TransactionCreated,
150 #[serde(rename = "transaction.updated")]
151 TransactionUpdated,
152}
153
154struct_with_extra! { WebhookData,
155 id: String,
156 created: DateTime<Utc>,
157 description: String,
158 amount: i64,
160 currency: String,
162 is_load: bool,
163 settled: SettledTimestamp,
164 local_amount: i64,
166 local_currency: String,
168 merchant: Option<merchant::Merchant>,
169 merchant_feedback_uri: String,
170 notes: String,
171 metadata: metadata::WebhookMetadata,
172 category: String,
173 updated: DateTime<Utc>,
174 account_id: String,
175 user_id: String,
176 counterparty: counterparty::CounterpartyOrNone,
177 scheme: Scheme,
178 dedupe_id: String,
179 originator: bool,
180 include_in_spending: bool,
181 can_be_excluded_from_breakdown: bool,
182 can_be_made_subscription: bool,
183 can_split_the_bill: bool,
184 can_add_to_tab: bool,
185 can_match_transactions_in_categorization: bool,
186 amount_is_pending: bool,
187 parent_account_id: String,
188 categories: Option<HashMap<String, i64>>,
189 #[serde(flatten)]
190 declined: Option<decline::Decline>,
191 attachments: (),
193 atm_fees_detailed: (),
194 #[allow(clippy::zero_sized_map_values, reason = "this needs refactor when we establish type anyway")]
195 fees: HashMap<(), ()>,
196 international: (),
197 labels: (),
198}
199
200enum_with_extra! {
201 untagged SettledTimestamp,
202 Settled(DateTime<Utc>),
203 NotYetSettled(String),
205}
206
207enum_with_extra! { "snake_case" => Scheme,
208 Mastercard,
209 PayportFasterPayments,
210 UkRetailPot,
211 MonzoFlex,
212 MonzoToMonzo,
213}