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 AuthenticationVerificationFailed,
114
115 Other,
117}
118
119#[derive(Deserialize, Debug, PartialEq, Clone)]
124#[serde(untagged)]
125pub enum MerchantInfo {
126 Id(String),
128
129 Details(Box<Merchant>),
131}
132
133#[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#[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#[derive(Debug, Serialize, Clone)]
175#[serde(untagged)]
176pub enum Since {
177 Timestamp(DateTime<Utc>),
179
180 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 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}