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    /// Transaction declined because verifcation failed
113    AuthenticationVerificationFailed,
114
115    /// All other errors
116    Other,
117}
118
119/// Merchant information which might be returned in transactions data.
120///
121/// An id or a struct may be returned depending on whether the 'expand merchant'
122/// flag is set in the transactions request.
123#[derive(Deserialize, Debug, PartialEq, Clone)]
124#[serde(untagged)]
125pub enum MerchantInfo {
126    /// A unique ID associated with a merchant
127    Id(String),
128
129    /// Extra merchant information which may optionally be requested
130    Details(Box<Merchant>),
131}
132
133/// Merchant details
134#[derive(Deserialize, Debug, PartialEq, Clone)]
135#[allow(missing_docs)]
136pub struct Merchant {
137    pub address: Address,
138    pub created: DateTime<Utc>,
139    pub group_id: String,
140    pub id: String,
141    pub logo: String,
142    pub emoji: String,
143    pub name: String,
144    pub category: String,
145}
146
147/// Address details
148#[derive(Deserialize, Debug, PartialEq, Clone)]
149#[allow(missing_docs)]
150pub struct Address {
151    pub address: String,
152    pub city: String,
153    pub country: String,
154    pub latitude: f32,
155    pub longitude: f32,
156    pub postcode: String,
157    pub region: String,
158}
159
160#[derive(Serialize, Default, Debug)]
161struct Pagination {
162    #[serde(skip_serializing_if = "Option::is_none")]
163    limit: Option<u16>,
164
165    #[serde(skip_serializing_if = "Option::is_none")]
166    since: Option<Since>,
167
168    #[serde(skip_serializing_if = "Option::is_none")]
169    before: Option<DateTime<Utc>>,
170}
171
172/// The 'since' parameter of a pagination request can be either a timestamp or
173/// an object id
174#[derive(Debug, Serialize, Clone)]
175#[serde(untagged)]
176pub enum Since {
177    /// A timestamp
178    Timestamp(DateTime<Utc>),
179
180    /// An id of an object
181    ObjectId(String),
182}
183
184#[cfg(test)]
185mod tests {
186    #![allow(clippy::too_many_lines, clippy::non_ascii_literal)]
187    use super::Transaction;
188
189    #[test]
190    fn deserialise_expanded_transaction() {
191        let raw = r##"
192        {
193          "id": "tx_0000A1aBC2Dbc34Ede5fEH",
194          "created": "2021-06-29T13:10:09.992Z",
195          "description": "Online Subscription",
196          "amount": -5000,
197          "fees": {},
198          "currency": "GBP",
199          "merchant": {
200            "id": "merch_000000abcABCDEFGdHIeJ0",
201            "group_id": "grp_000000abc1ABde2fChDE34",
202            "created": "2016-01-08T00:20:13.969Z",
203            "name": "Online Service",
204            "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
205            "emoji": "💻",
206            "category": "entertainment",
207            "online": true,
208            "atm": false,
209            "address": {
210              "short_formatted": "Somewhere in the world",
211              "formatted": "world",
212              "address": "",
213              "city": "",
214              "region": "",
215              "country": "GLO",
216              "postcode": "",
217              "latitude": 50.99999999999999,
218              "longitude": 5.111111111111111,
219              "zoom_level": 5,
220              "approximate": true
221            },
222            "updated": "2021-06-17T14:21:38.608Z",
223            "metadata": {
224              "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
225              "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
226              "provider": "user",
227              "provider_id": "",
228              "suggested_tags": "#subscription #personal",
229              "twitter_id": "ServiceUk",
230              "website": "service.co.uk"
231            },
232            "disable_feedback": false
233          },
234          "notes": "Subscription to online service",
235          "metadata": {
236            "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
237            "mastercard_approval_type": "full",
238            "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
239            "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
240            "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
241            "mcc": "1234"
242          },
243          "labels": null,
244          "attachments": null,
245          "international": null,
246          "category": "bills",
247          "categories": {
248            "bills": -5000
249          },
250          "is_load": false,
251          "settled": "2021-06-30T00:46:44.233Z",
252          "local_amount": -3900,
253          "local_currency": "GBP",
254          "updated": "2021-06-30T00:46:44.589Z",
255          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
256          "user_id": "user_000000abcABCDEFGdHIeJ",
257          "counterparty": {},
258          "scheme": "mastercard",
259          "dedupe_id": "mclifecycle",
260          "originator": false,
261          "include_in_spending": true,
262          "can_be_excluded_from_breakdown": true,
263          "can_be_made_subscription": true,
264          "can_split_the_bill": true,
265          "can_add_to_tab": true,
266          "amount_is_pending": false,
267          "atm_fees_detailed": null
268        }
269        "##;
270
271        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
272    }
273
274    #[test]
275    fn deserialise_declined_transaction() {
276        let raw = r##"
277        {
278          "id": "tx_0000A1aBC2Dbc34Ede5fEH",
279          "created": "2021-06-29T13:10:09.992Z",
280          "description": "Online Subscription",
281          "amount": -5000,
282          "fees": {},
283          "currency": "GBP",
284          "merchant": {
285            "id": "merch_000000abcABCDEFGdHIeJ0",
286            "group_id": "grp_000000abc1ABde2fChDE34",
287            "created": "2016-01-08T00:20:13.969Z",
288            "name": "Online Service",
289            "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
290            "emoji": "💻",
291            "category": "entertainment",
292            "online": true,
293            "atm": false,
294            "address": {
295              "short_formatted": "Somewhere in the world",
296              "formatted": "world",
297              "address": "",
298              "city": "",
299              "region": "",
300              "country": "GLO",
301              "postcode": "",
302              "latitude": 50.99999999999999,
303              "longitude": 5.111111111111111,
304              "zoom_level": 5,
305              "approximate": true
306            },
307            "updated": "2021-06-17T14:21:38.608Z",
308            "metadata": {
309              "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
310              "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
311              "provider": "user",
312              "provider_id": "",
313              "suggested_tags": "#subscription #personal",
314              "twitter_id": "ServiceUk",
315              "website": "service.co.uk"
316            },
317            "disable_feedback": false
318          },
319          "notes": "Subscription to online service",
320          "metadata": {
321            "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
322            "mastercard_approval_type": "full",
323            "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
324            "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
325            "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
326            "mcc": "1234"
327          },
328          "labels": null,
329          "attachments": null,
330          "international": null,
331          "category": "bills",
332          "categories": {
333            "bills": -5000
334          },
335          "is_load": false,
336          "settled": "2021-06-30T00:46:44.233Z",
337          "decline_reason": "SCA_NOT_AUTHENTICATED_CARD_NOT_PRESENT",
338          "local_amount": -3900,
339          "local_currency": "GBP",
340          "updated": "2021-06-30T00:46:44.589Z",
341          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
342          "user_id": "user_000000abcABCDEFGdHIeJ",
343          "counterparty": {},
344          "scheme": "mastercard",
345          "dedupe_id": "mclifecycle",
346          "originator": false,
347          "include_in_spending": true,
348          "can_be_excluded_from_breakdown": true,
349          "can_be_made_subscription": true,
350          "can_split_the_bill": true,
351          "can_add_to_tab": true,
352          "amount_is_pending": false,
353          "atm_fees_detailed": null
354        }
355        "##;
356
357        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
358
359        let new = raw.replace(
360            "SCA_NOT_AUTHENTICATED_CARD_NOT_PRESENT",
361            "AUTHENTICATION_REJECTED_BY_CARDHOLDER",
362        );
363        serde_json::from_str::<Transaction>(&new).expect("couldn't decode Transaction from json");
364
365        let new = raw.replace(
366            "AUTHENTICATION_REJECTED_BY_CARDHOLDER",
367            "AUTHENTICATION_VERIFICATION_FAILED",
368        );
369        serde_json::from_str::<Transaction>(&new).expect("couldn't decode Transaction from json");
370    }
371
372    #[test]
373    // Tests for null merchant
374    fn deserialise_topup_transaction() {
375        let raw = r#"
376        {
377          "id": "tx_0000A1aBC2Dbc34Ede5fEF",
378          "created": "2021-07-01T00:21:30.935Z",
379          "description": "USER",
380          "amount": 2000,
381          "fees": {},
382          "currency": "GBP",
383          "merchant": null,
384          "notes": "USER",
385          "metadata": {
386            "faster_payment": "true",
387            "fps_fpid": "FP123456789123456789123456789123456",
388            "fps_payment_id": "FP123456789123456789123456789123456",
389            "insertion": "entryset_0000A1aBC2Dbc34Ede5fEF",
390            "notes": "USER",
391            "trn": "FP12345678912345"
392          },
393          "labels": null,
394          "attachments": null,
395          "international": null,
396          "category": "general",
397          "categories": null,
398          "is_load": false,
399          "settled": "2021-07-01T06:00:00Z",
400          "local_amount": 2000,
401          "local_currency": "GBP",
402          "updated": "2021-07-01T00:21:31.022Z",
403          "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
404          "user_id": "",
405          "counterparty": {
406            "account_number": "12345678",
407            "name": "John Smith",
408            "sort_code": "987654",
409            "user_id": "anonuser_1234567a89b123456cd7e8"
410          },
411          "scheme": "payport_faster_pajments",
412          "dedupe_id": "com.monzo.fps:1234:FP123456789123456789123456789123456:INBOUND",
413          "originator": false,
414          "include_in_spending": false,
415          "can_be_excluded_from_breakdown": false,
416          "can_be_made_subscription": false,
417          "can_split_the_bill": false,
418          "can_add_to_tab": false,
419          "amount_is_pending": false,
420          "atm_fees_detailed": null
421        }
422        "#;
423
424        serde_json::from_str::<Transaction>(raw).expect("couldn't decode Transaction from json");
425    }
426
427    #[test]
428    fn deserialise_list() {
429        use serde::Deserialize;
430        #[derive(Deserialize)]
431        struct Response {
432            #[allow(dead_code)]
433            transactions: Vec<Transaction>,
434        }
435
436        let raw = r##"
437        {
438          "transactions": [
439            {
440              "id": "tx_0000A1aBC2Dbc34Ede5fEH",
441              "created": "2021-06-29T13:10:09.992Z",
442              "description": "Online Subscription",
443              "amount": -3900,
444              "fees": {},
445              "currency": "GBP",
446              "merchant": {
447                "id": "merch_000000abcABCDEFGdHIeJ0",
448                "group_id": "grp_000000abc1ABde2fChDE34",
449                "created": "2016-01-08T00:20:13.969Z",
450                "name": "Online Service",
451                "logo": "https://mondo-logo-cache.appspot.com/twitter/ServiceUk/?size=large",
452                "emoji": "💻",
453                "category": "entertainment",
454                "online": true,
455                "atm": false,
456                "address": {
457                  "short_formatted": "Somewhere in the world",
458                  "formatted": "world",
459                  "address": "",
460                  "city": "",
461                  "region": "",
462                  "country": "GLO",
463                  "postcode": "",
464                  "latitude": 50.99999999999999,
465                  "longitude": 5.111111111111111,
466                  "zoom_level": 5,
467                  "approximate": true
468                },
469                "updated": "2021-06-17T14:21:38.608Z",
470                "metadata": {
471                  "created_for_merchant": "merch_000000abcABCDEFGdHIeJ0",
472                  "created_for_transaction": "tx_0000A1aBC2Dbc34Ede5fEH",
473                  "provider": "user",
474                  "provider_id": "",
475                  "suggested_tags": "#subscription #personal",
476                  "twitter_id": "ServiceUk",
477                  "website": "service.co.uk"
478                },
479                "disable_feedback": false
480              },
481              "notes": "Subscription to online service",
482              "metadata": {
483                "ledger_insertion_id": "entryset_0000A2bBcDEF3HdIJK4LMe",
484                "mastercard_approval_type": "full",
485                "mastercard_auth_message_id": "mcauthmsg_0000A2bBcDEF3HdIJK4LMe",
486                "mastercard_card_id": "mccard_0000A2bBcDEF3HdIJK4LMe",
487                "mastercard_lifecycle_id": "mclifecycle_0000A2bBcDEF3HdIJK4LMe",
488                "mcc": "1234"
489              },
490              "labels": null,
491              "attachments": null,
492              "international": null,
493              "category": "bills",
494              "categories": {
495                "bills": -3900
496              },
497              "is_load": false,
498              "settled": "2021-06-30T00:46:44.233Z",
499              "local_amount": -3900,
500              "local_currency": "GBP",
501              "updated": "2021-06-30T00:46:44.589Z",
502              "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
503              "user_id": "user_000000abcABCDEFGdHIeJ",
504              "counterparty": {},
505              "scheme": "mastercard",
506              "dedupe_id": "mclifecycle",
507              "originator": false,
508              "include_in_spending": true,
509              "can_be_excluded_from_breakdown": true,
510              "can_be_made_subscription": true,
511              "can_split_the_bill": true,
512              "can_add_to_tab": true,
513              "amount_is_pending": false,
514              "atm_fees_detailed": null
515            },
516            {
517              "id": "tx_0000A1aBC2Dbc34Ede5fEF",
518              "created": "2021-07-01T00:21:30.935Z",
519              "description": "USER",
520              "amount": 2000,
521              "fees": {},
522              "currency": "GBP",
523              "merchant": null,
524              "notes": "USER",
525              "metadata": {
526                "faster_payment": "true",
527                "fps_fpid": "FP123456789123456789123456789123456",
528                "fps_payment_id": "FP123456789123456789123456789123456",
529                "insertion": "entryset_0000A1aBC2Dbc34Ede5fEF",
530                "notes": "USER",
531                "trn": "FP12345678912345"
532              },
533              "labels": null,
534              "attachments": null,
535              "international": null,
536              "category": "general",
537              "categories": null,
538              "is_load": false,
539              "settled": "2021-07-01T06:00:00Z",
540              "local_amount": 2000,
541              "local_currency": "GBP",
542              "updated": "2021-07-01T00:21:31.022Z",
543              "account_id": "acc_99999aAbBc0DEFH1I2JdKL",
544              "user_id": "",
545              "counterparty": {
546                "account_number": "12345678",
547                "name": "John Smith",
548                "sort_code": "987654",
549                "user_id": "anonuser_1234567a89b123456cd7e8"
550              },
551              "scheme": "payport_faster_pajments",
552              "dedupe_id": "com.monzo.fps:1234:FP123456789123456789123456789123456:INBOUND",
553              "originator": false,
554              "include_in_spending": false,
555              "can_be_excluded_from_breakdown": false,
556              "can_be_made_subscription": false,
557              "can_split_the_bill": false,
558              "can_add_to_tab": false,
559              "amount_is_pending": false,
560              "atm_fees_detailed": null
561            }
562          ]
563        }
564        "##;
565
566        serde_json::from_str::<Response>(raw).expect("couldn't decode Transaction from json");
567    }
568}