1use 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#[allow(clippy::struct_excessive_bools)]
17#[non_exhaustive]
18#[derive(Deserialize, Debug, Clone, PartialEq)]
19pub struct Transaction {
20 pub account_id: String,
22
23 pub amount: i64,
26
27 pub amount_is_pending: bool,
29
30 pub can_add_to_tab: bool,
32
33 pub can_be_excluded_from_breakdown: bool,
35
36 pub can_be_made_subscription: bool,
38
39 pub can_split_the_bill: bool,
41
42 pub category: String,
44
45 pub created: DateTime<Utc>,
47
48 pub currency: String,
50
51 pub description: String,
53
54 pub id: String,
56
57 pub include_in_spending: bool,
59
60 pub merchant: Option<MerchantInfo>,
63
64 pub metadata: HashMap<String, String>,
66
67 pub notes: String,
69
70 pub decline_reason: Option<DeclineReason>,
72
73 pub is_load: bool,
78
79 #[serde(deserialize_with = "empty_string_as_none")]
83 pub settled: Option<DateTime<Utc>>,
84}
85
86#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
88#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
89#[non_exhaustive]
90pub enum DeclineReason {
91 InsufficientFunds,
93
94 CardInactive,
96
97 CardBlocked,
99
100 InvalidCvc,
102
103 ScaNotAuthenticatedCardNotPresent,
105
106 StrongCustomerAuthenticationRequired,
108
109 AuthenticationRejectedByCardholder,
111
112 Other,
114}
115
116#[derive(Deserialize, Debug, PartialEq, Clone)]
121#[serde(untagged)]
122pub enum MerchantInfo {
123 Id(String),
125
126 Details(Box<Merchant>),
128}
129
130#[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#[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#[derive(Debug, Serialize, Clone)]
172#[serde(untagged)]
173pub enum Since {
174 Timestamp(DateTime<Utc>),
176
177 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 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}