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