monzo/endpoints/
transactions.rs

1//! Retrieve and manipulate transactions
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::endpoints::utils::empty_string_as_none;
9
10mod list;
11pub use list::Request as List;
12mod get;
13pub use get::Request as Get;
14
15/// A Monzo transaction
16#[allow(clippy::struct_excessive_bools)]
17#[non_exhaustive]
18#[derive(Deserialize, Debug, Clone, PartialEq)]
19pub struct Transaction {
20    /// The unique ID of the account associated with the transaction
21    pub account_id: String,
22
23    /// The amount of the transaction, in the smallest unit of currency (ie.
24    /// 'pence' or 'cents')
25    pub amount: i64,
26
27    /// Whether the transaction is pending, or complete
28    pub amount_is_pending: bool,
29
30    /// Whether the transaction can be added to a tab
31    pub can_add_to_tab: bool,
32
33    /// Whether the transaction can be excluded from breakdown
34    pub can_be_excluded_from_breakdown: bool,
35
36    /// Whether the transaction can be made into a recurring subscription
37    pub can_be_made_subscription: bool,
38
39    /// Whether the transaction can be split
40    pub can_split_the_bill: bool,
41
42    /// The transaction category
43    pub category: String,
44
45    /// The timestamp when the transaction was created
46    pub created: DateTime<Utc>,
47
48    /// The three-letter currency string for the transaction
49    pub currency: String,
50
51    /// The transaction description
52    pub description: String,
53
54    /// The unique transaction ID
55    pub id: String,
56
57    /// Whether transaction is included in spending
58    pub include_in_spending: bool,
59
60    /// This can be either None, the merchant ID, or an object containing the
61    /// merchant details
62    pub merchant: Option<MerchantInfo>,
63
64    /// Any custom metadata which has been added to the transaction
65    pub metadata: HashMap<String, String>,
66
67    /// User-added transaction notes
68    pub notes: String,
69
70    /// If the transaction was declined, this enum will encode the reason
71    pub decline_reason: Option<DeclineReason>,
72
73    /// Top-ups to an account are represented as transactions with a positive
74    /// amount and `is_load` = true. Other transactions such as refunds,
75    /// reversals or chargebacks may have a positive amount but `is_load` =
76    /// false
77    pub is_load: bool,
78
79    /// The timestamp at which the transaction was settled
80    ///
81    /// This is `None` if the transaction is authorised, but not yet complete.
82    #[serde(deserialize_with = "empty_string_as_none")]
83    pub settled: Option<DateTime<Utc>>,
84}
85
86/// The set of reasons for which a monzo transaction may be declined
87#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
88#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
89#[non_exhaustive]
90pub enum DeclineReason {
91    /// Not enough funds in account to complete transaction
92    InsufficientFunds,
93
94    /// Monzo card is not active
95    CardInactive,
96
97    /// The monzo card has been blocked
98    CardBlocked,
99
100    /// Incorrect CVC code used
101    InvalidCvc,
102
103    /// Strong Customer Authentication blocking 'not present' transaction
104    ScaNotAuthenticatedCardNotPresent,
105
106    /// Requires SCA
107    StrongCustomerAuthenticationRequired,
108
109    /// Transaction declined by the cardholder
110    AuthenticationRejectedByCardholder,
111
112    /// All other errors
113    Other,
114}
115
116/// Merchant information which might be returned in transactions data.
117///
118/// An id or a struct may be returned depending on whether the 'expand merchant'
119/// flag is set in the transactions request.
120#[derive(Deserialize, Debug, PartialEq, Clone)]
121#[serde(untagged)]
122pub enum MerchantInfo {
123    /// A unique ID associated with a merchant
124    Id(String),
125
126    /// Extra merchant information which may optionally be requested
127    Details(Box<Merchant>),
128}
129
130/// Merchant details
131#[derive(Deserialize, Debug, PartialEq, Clone)]
132#[allow(missing_docs)]
133pub struct Merchant {
134    pub address: Address,
135    pub created: DateTime<Utc>,
136    pub group_id: String,
137    pub id: String,
138    pub logo: String,
139    pub emoji: String,
140    pub name: String,
141    pub category: String,
142}
143
144/// Address details
145#[derive(Deserialize, Debug, PartialEq, Clone)]
146#[allow(missing_docs)]
147pub struct Address {
148    pub address: String,
149    pub city: String,
150    pub country: String,
151    pub latitude: f32,
152    pub longitude: f32,
153    pub postcode: String,
154    pub region: String,
155}
156
157#[derive(Serialize, Default, Debug)]
158struct Pagination {
159    #[serde(skip_serializing_if = "Option::is_none")]
160    limit: Option<u16>,
161
162    #[serde(skip_serializing_if = "Option::is_none")]
163    since: Option<Since>,
164
165    #[serde(skip_serializing_if = "Option::is_none")]
166    before: Option<DateTime<Utc>>,
167}
168
169/// The 'since' parameter of a pagination request can be either a timestamp or
170/// an object id
171#[derive(Debug, Serialize, Clone)]
172#[serde(untagged)]
173pub enum Since {
174    /// A timestamp
175    Timestamp(DateTime<Utc>),
176
177    /// An id of an object
178    ObjectId(String),
179}
180
181#[cfg(test)]
182mod tests {
183    #![allow(clippy::too_many_lines, clippy::non_ascii_literal)]
184    use super::Transaction;
185
186    #[test]
187    fn deserialise_expanded_transaction() {
188        let raw = r##"
189        {
190          "id": "tx_0000A1aBC2Dbc34Ede5fEH",
191          "created": "2021-06-29T13:10:09.992Z",
192          "description": "Online Subscription",
193          "amount": -5000,
194          "fees": {},
195          "currency": "GBP",
196          "merchant": {
197            "id": "merch_000000abcABCDEFGdHIeJ0",
198            "group_id": "grp_000000abc1ABde2fChDE34",
199            "created": "2016-01-08T00:20:13.969Z",
200            "name": "Online Service",
201            "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
202            "emoji": "💻",
203            "category": "entertainment",
204            "online": true,
205            "atm": false,
206            "address": {
207              "short_formatted": "Somewhere in the world",
208              "formatted": "world",
209              "address": "",
210              "city": "",
211              "region": "",
212              "country": "GLO",
213              "postcode": "",
214              "latitude": 50.99999999999999,
215              "longitude": 5.111111111111111,
216              "zoom_level": 5,
217              "approximate": true
218            },
219            "updated": "2021-06-17T14:21:38.608Z",
220            "metadata": {
221              "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
222              "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
223              "provider": "user",
224              "provider_id": "",
225              "suggested_tags": "#subscription #personal",
226              "twitter_id": "ServiceUk",
227              "website": "service.co.uk"
228            },
229            "disable_feedback": false
230          },
231          "notes": "Subscription to online service",
232          "metadata": {
233            "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
234            "mastercard_approval_type": "full",
235            "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
236            "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
237            "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
238            "mcc": "1234"
239          },
240          "labels": null,
241          "attachments": null,
242          "international": null,
243          "category": "bills",
244          "categories": {
245            "bills": -5000
246          },
247          "is_load": false,
248          "settled": "2021-06-30T00:46:44.233Z",
249          "local_amount": -3900,
250          "local_currency": "GBP",
251          "updated": "2021-06-30T00:46:44.589Z",
252          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
253          "user_id": "user_000000abcABCDEFGdHIeJ",
254          "counterparty": {},
255          "scheme": "mastercard",
256          "dedupe_id": "mclifecycle",
257          "originator": false,
258          "include_in_spending": true,
259          "can_be_excluded_from_breakdown": true,
260          "can_be_made_subscription": true,
261          "can_split_the_bill": true,
262          "can_add_to_tab": true,
263          "amount_is_pending": false,
264          "atm_fees_detailed": null
265        }
266        "##;
267
268        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
269    }
270
271    #[test]
272    fn deserialise_declined_transaction() {
273        let raw = r##"
274        {
275          "id": "tx_0000A1aBC2Dbc34Ede5fEH",
276          "created": "2021-06-29T13:10:09.992Z",
277          "description": "Online Subscription",
278          "amount": -5000,
279          "fees": {},
280          "currency": "GBP",
281          "merchant": {
282            "id": "merch_000000abcABCDEFGdHIeJ0",
283            "group_id": "grp_000000abc1ABde2fChDE34",
284            "created": "2016-01-08T00:20:13.969Z",
285            "name": "Online Service",
286            "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
287            "emoji": "💻",
288            "category": "entertainment",
289            "online": true,
290            "atm": false,
291            "address": {
292              "short_formatted": "Somewhere in the world",
293              "formatted": "world",
294              "address": "",
295              "city": "",
296              "region": "",
297              "country": "GLO",
298              "postcode": "",
299              "latitude": 50.99999999999999,
300              "longitude": 5.111111111111111,
301              "zoom_level": 5,
302              "approximate": true
303            },
304            "updated": "2021-06-17T14:21:38.608Z",
305            "metadata": {
306              "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
307              "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
308              "provider": "user",
309              "provider_id": "",
310              "suggested_tags": "#subscription #personal",
311              "twitter_id": "ServiceUk",
312              "website": "service.co.uk"
313            },
314            "disable_feedback": false
315          },
316          "notes": "Subscription to online service",
317          "metadata": {
318            "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
319            "mastercard_approval_type": "full",
320            "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
321            "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
322            "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
323            "mcc": "1234"
324          },
325          "labels": null,
326          "attachments": null,
327          "international": null,
328          "category": "bills",
329          "categories": {
330            "bills": -5000
331          },
332          "is_load": false,
333          "settled": "2021-06-30T00:46:44.233Z",
334          "decline_reason": "SCA_NOT_AUTHENTICATED_CARD_NOT_PRESENT",
335          "local_amount": -3900,
336          "local_currency": "GBP",
337          "updated": "2021-06-30T00:46:44.589Z",
338          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
339          "user_id": "user_000000abcABCDEFGdHIeJ",
340          "counterparty": {},
341          "scheme": "mastercard",
342          "dedupe_id": "mclifecycle",
343          "originator": false,
344          "include_in_spending": true,
345          "can_be_excluded_from_breakdown": true,
346          "can_be_made_subscription": true,
347          "can_split_the_bill": true,
348          "can_add_to_tab": true,
349          "amount_is_pending": false,
350          "atm_fees_detailed": null
351        }
352        "##;
353
354        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
355
356        let new = raw.replace(
357            "SCA_NOT_AUTHENTICATED_CARD_NOT_PRESENT",
358            "AUTHENTICATION_REJECTED_BY_CARDHOLDER",
359        );
360        serde_json::from_str::<Transaction>(&new).expect("couldn't decode Transaction from json");
361    }
362
363    #[test]
364    // Tests for null merchant
365    fn deserialise_topup_transaction() {
366        let raw = r#"
367        {
368          "id": "tx_0000A1aBC2Dbc34Ede5fEF",
369          "created": "2021-07-01T00:21:30.935Z",
370          "description": "USER",
371          "amount": 2000,
372          "fees": {},
373          "currency": "GBP",
374          "merchant": null,
375          "notes": "USER",
376          "metadata": {
377            "faster_payment": "true",
378            "fps_fpid": "FP123456789123456789123456789123456",
379            "fps_payment_id": "FP123456789123456789123456789123456",
380            "insertion": "entryset_0000A1aBC2Dbc34Ede5fEF",
381            "notes": "USER",
382            "trn": "FP12345678912345"
383          },
384          "labels": null,
385          "attachments": null,
386          "international": null,
387          "category": "general",
388          "categories": null,
389          "is_load": false,
390          "settled": "2021-07-01T06:00:00Z",
391          "local_amount": 2000,
392          "local_currency": "GBP",
393          "updated": "2021-07-01T00:21:31.022Z",
394          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
395          "user_id": "",
396          "counterparty": {
397            "account_number": "12345678",
398            "name": "John Smith",
399            "sort_code": "987654",
400            "user_id": "anonuser_1234567a89b123456cd7e8"
401          },
402          "scheme": "payport_faster_pajments",
403          "dedupe_id": "com.monzo.fps:1234:FP123456789123456789123456789123456:INBOUND",
404          "originator": false,
405          "include_in_spending": false,
406          "can_be_excluded_from_breakdown": false,
407          "can_be_made_subscription": false,
408          "can_split_the_bill": false,
409          "can_add_to_tab": false,
410          "amount_is_pending": false,
411          "atm_fees_detailed": null
412        }
413        "#;
414
415        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
416    }
417
418    #[test]
419    fn deserialise_list() {
420        use serde::Deserialize;
421        #[derive(Deserialize)]
422        struct Response {
423            #[allow(dead_code)]
424            transactions: Vec<Transaction>,
425        }
426
427        let raw = r##"
428        {
429          "transactions": [
430            {
431              "id": "tx_0000A1aBC2Dbc34Ede5fEH",
432              "created": "2021-06-29T13:10:09.992Z",
433              "description": "Online Subscription",
434              "amount": -3900,
435              "fees": {},
436              "currency": "GBP",
437              "merchant": {
438                "id": "merch_000000abcABCDEFGdHIeJ0",
439                "group_id": "grp_000000abc1ABde2fChDE34",
440                "created": "2016-01-08T00:20:13.969Z",
441                "name": "Online Service",
442                "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
443                "emoji": "💻",
444                "category": "entertainment",
445                "online": true,
446                "atm": false,
447                "address": {
448                  "short_formatted": "Somewhere in the world",
449                  "formatted": "world",
450                  "address": "",
451                  "city": "",
452                  "region": "",
453                  "country": "GLO",
454                  "postcode": "",
455                  "latitude": 50.99999999999999,
456                  "longitude": 5.111111111111111,
457                  "zoom_level": 5,
458                  "approximate": true
459                },
460                "updated": "2021-06-17T14:21:38.608Z",
461                "metadata": {
462                  "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
463                  "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
464                  "provider": "user",
465                  "provider_id": "",
466                  "suggested_tags": "#subscription #personal",
467                  "twitter_id": "ServiceUk",
468                  "website": "service.co.uk"
469                },
470                "disable_feedback": false
471              },
472              "notes": "Subscription to online service",
473              "metadata": {
474                "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
475                "mastercard_approval_type": "full",
476                "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
477                "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
478                "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
479                "mcc": "1234"
480              },
481              "labels": null,
482              "attachments": null,
483              "international": null,
484              "category": "bills",
485              "categories": {
486                "bills": -3900
487              },
488              "is_load": false,
489              "settled": "2021-06-30T00:46:44.233Z",
490              "local_amount": -3900,
491              "local_currency": "GBP",
492              "updated": "2021-06-30T00:46:44.589Z",
493              "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
494              "user_id": "user_000000abcABCDEFGdHIeJ",
495              "counterparty": {},
496              "scheme": "mastercard",
497              "dedupe_id": "mclifecycle",
498              "originator": false,
499              "include_in_spending": true,
500              "can_be_excluded_from_breakdown": true,
501              "can_be_made_subscription": true,
502              "can_split_the_bill": true,
503              "can_add_to_tab": true,
504              "amount_is_pending": false,
505              "atm_fees_detailed": null
506            },
507            {
508              "id": "tx_0000A1aBC2Dbc34Ede5fEF",
509              "created": "2021-07-01T00:21:30.935Z",
510              "description": "USER",
511              "amount": 2000,
512              "fees": {},
513              "currency": "GBP",
514              "merchant": null,
515              "notes": "USER",
516              "metadata": {
517                "faster_payment": "true",
518                "fps_fpid": "FP123456789123456789123456789123456",
519                "fps_payment_id": "FP123456789123456789123456789123456",
520                "insertion": "entryset_0000A1aBC2Dbc34Ede5fEF",
521                "notes": "USER",
522                "trn": "FP12345678912345"
523              },
524              "labels": null,
525              "attachments": null,
526              "international": null,
527              "category": "general",
528              "categories": null,
529              "is_load": false,
530              "settled": "2021-07-01T06:00:00Z",
531              "local_amount": 2000,
532              "local_currency": "GBP",
533              "updated": "2021-07-01T00:21:31.022Z",
534              "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
535              "user_id": "",
536              "counterparty": {
537                "account_number": "12345678",
538                "name": "John Smith",
539                "sort_code": "987654",
540                "user_id": "anonuser_1234567a89b123456cd7e8"
541              },
542              "scheme": "payport_faster_pajments",
543              "dedupe_id": "com.monzo.fps:1234:FP123456789123456789123456789123456:INBOUND",
544              "originator": false,
545              "include_in_spending": false,
546              "can_be_excluded_from_breakdown": false,
547              "can_be_made_subscription": false,
548              "can_split_the_bill": false,
549              "can_add_to_tab": false,
550              "amount_is_pending": false,
551              "atm_fees_detailed": null
552            }
553          ]
554        }
555        "##;
556
557        serde_json::from_str::<Response>(raw).expect("couldn't decode Transaction from json");
558    }
559}