1use chrono::NaiveDate;
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::PlanId;
6use crate::ynab::client::Client;
7use crate::ynab::common::NO_PARAMS;
8use crate::ynab::errors::Error;
9
10#[derive(Debug, Deserialize)]
13struct TransactionDataEnvelope {
14 data: TransactionData,
15}
16
17#[derive(Debug, Deserialize)]
18struct TransactionData {
19 transaction: Transaction,
20 server_knowledge: i64,
21}
22
23#[derive(Debug, Deserialize)]
24struct TransactionsDataEnvelope {
25 data: TransactionsData,
26}
27
28#[derive(Debug, Deserialize)]
29struct TransactionsData {
30 transactions: Vec<Transaction>,
31 server_knowledge: i64,
32}
33
34#[derive(Debug, Deserialize)]
35struct ScheduledTransactionDataEnvelope {
36 data: ScheduledTransactionData,
37}
38
39#[derive(Debug, Deserialize)]
40struct ScheduledTransactionData {
41 scheduled_transaction: ScheduledTransaction,
42}
43
44#[derive(Debug, Deserialize)]
45struct ScheduledTransactionsDataEnvelope {
46 data: ScheduledTransactionsData,
47}
48
49#[derive(Debug, Deserialize)]
50struct ScheduledTransactionsData {
51 scheduled_transactions: Vec<ScheduledTransaction>,
52 server_knowledge: i64,
53}
54
55#[derive(Debug, Deserialize)]
56struct SaveTransactionsDataEnvelope {
57 data: SaveTransactionsResponse,
58}
59
60#[derive(Debug, Deserialize)]
62pub struct SaveTransactionsResponse {
63 pub transaction_ids: Vec<Uuid>,
64 pub transaction: Option<Transaction>,
65 pub transactions: Option<Vec<Transaction>>,
66 pub duplicate_import_ids: Option<Vec<Uuid>>,
67 pub server_knowledge: i64,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum ClearedStatus {
76 Cleared,
77 Uncleared,
78 Reconciled,
79}
80
81#[derive(Debug, Serialize, Deserialize)]
83#[serde(rename_all = "snake_case")]
84pub enum FlagColor {
85 Red,
86 Orange,
87 Yellow,
88 Green,
89 Blue,
90 Purple,
91}
92
93#[derive(Debug, Serialize, Deserialize)]
95pub enum Frequency {
96 #[serde(rename = "never")]
97 Never,
98 #[serde(rename = "daily")]
99 Daily,
100 #[serde(rename = "weekly")]
101 Weekly,
102 #[serde(rename = "everyOtherWeek")]
103 EveryOtherWeek,
104 #[serde(rename = "twiceAMonth")]
105 TwiceAMonth,
106 #[serde(rename = "every4Weeks")]
107 Every4Weeks,
108 #[serde(rename = "monthly")]
109 Monthly,
110 #[serde(rename = "everyOtherMonth")]
111 EveryOtherMonth,
112 #[serde(rename = "every3Months")]
113 Every3Months,
114 #[serde(rename = "every4Months")]
115 Every4Months,
116 #[serde(rename = "twiceAYear")]
117 TwiceAYear,
118 #[serde(rename = "yearly")]
119 Yearly,
120 #[serde(rename = "everyOtherYear")]
121 EveryOtherYear,
122}
123
124#[derive(Debug, Serialize, Deserialize)]
127pub struct Transaction {
128 pub id: Uuid,
129 pub date: NaiveDate,
130 pub amount: i64,
131 pub memo: Option<String>,
132 pub cleared: ClearedStatus,
133 pub approved: bool,
134 pub flag_color: Option<FlagColor>,
135 pub flag_name: Option<String>,
136 pub account_id: Uuid,
137 pub payee_id: Option<Uuid>,
138 pub account_name: Option<String>,
139 pub payee_name: Option<String>,
140 pub category_id: Option<Uuid>,
141 pub category_name: Option<String>,
142 pub matched_transaction_id: Option<String>,
143 pub import_id: Option<String>,
144 pub import_payee_name: Option<String>,
145 pub import_payee_name_original: Option<String>,
146 pub deleted: bool,
147 #[serde(default)]
148 pub subtransactions: Vec<Subtransaction>,
149}
150
151#[derive(Debug, Serialize, Deserialize)]
153pub struct Subtransaction {
154 pub id: String,
155 pub transaction_id: String,
156 pub amount: i64,
157 pub memo: Option<String>,
158 pub payee_id: Option<Uuid>,
159 pub payee_name: Option<String>,
160 pub category_id: Option<Uuid>,
161 pub category_name: Option<String>,
162 pub transfer_account_id: Option<Uuid>,
163 pub transfer_transaction_id: Option<String>,
164}
165
166#[derive(Debug, Serialize, Deserialize)]
168pub struct ScheduledTransaction {
169 pub id: Uuid,
170 pub date_first: NaiveDate,
171 pub date_next: NaiveDate,
172 pub frequency: Frequency,
173 pub amount: i64,
174 pub memo: Option<String>,
175 pub flag_color: Option<FlagColor>,
176 pub flag_name: Option<String>,
177 pub account_id: Uuid,
178 pub payee_id: Option<Uuid>,
179 pub category_id: Option<Uuid>,
180 pub account_name: String,
181 pub payee_name: Option<String>,
182 pub category_name: Option<String>,
183 pub subtransactions: Vec<ScheduledSubtransaction>,
184 pub transfer_account_id: Option<Uuid>,
185}
186
187#[derive(Debug, Serialize, Deserialize)]
190pub struct ScheduledSubtransaction {
191 pub id: Uuid,
192 pub scheduled_transaction_id: Uuid,
193 pub amount: i64,
194 pub memo: Option<String>,
195 pub payee_id: Option<Uuid>,
196 pub payee_name: Option<String>,
197 pub category_id: Option<Uuid>,
198 pub category_name: Option<String>,
199 pub transfer_account_id: Option<Uuid>,
200 pub deleted: bool,
201}
202
203#[derive(Debug)]
204pub enum TransactionType {
205 Uncategorized,
206 Unapproved,
207}
208
209impl std::fmt::Display for TransactionType {
210 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211 match self {
212 Self::Unapproved => write!(f, "unapproved"),
213 Self::Uncategorized => write!(f, "uncategorized"),
214 }
215 }
216}
217
218#[derive(Debug)]
219enum TransactionScope {
220 All,
221 ByAccount(Uuid),
222 ByCategory(Uuid),
223 ByPayee(Uuid),
224 ByMonth(NaiveDate),
225}
226#[derive(Debug)]
227pub struct GetTransactionsBuilder<'a> {
228 client: &'a Client,
229 scope: TransactionScope,
230 plan_id: PlanId,
231 since_date: Option<NaiveDate>,
232 transaction_type: Option<TransactionType>,
233 last_knowledge_of_server: Option<i64>,
234}
235
236impl<'a> GetTransactionsBuilder<'a> {
237 pub fn with_server_knowledge(mut self, sk: i64) -> Self {
238 self.last_knowledge_of_server = Some(sk);
239 self
240 }
241
242 pub fn since_date(mut self, since_date: NaiveDate) -> Self {
243 self.since_date = Some(since_date);
244 self
245 }
246
247 pub fn transaction_type(mut self, tx_type: TransactionType) -> Self {
248 self.transaction_type = Some(tx_type);
249 self
250 }
251
252 pub async fn send(self) -> Result<(Vec<Transaction>, i64), Error> {
253 let date_str = self.since_date.map(|d| d.to_string());
254 let type_str = self.transaction_type.map(|t| t.to_string());
255 let sk_str = self.last_knowledge_of_server.map(|sk| sk.to_string());
256
257 let mut params: Vec<(&str, &str)> = Vec::new();
258 if let Some(ref s) = date_str {
259 params.push(("since_date", s));
260 }
261 if let Some(ref t) = type_str {
262 params.push(("type", t));
263 }
264 if let Some(ref s) = sk_str {
265 params.push(("last_knowledge_of_server", s));
266 }
267 let url = match self.scope {
268 TransactionScope::All => format!("plans/{}/transactions", self.plan_id),
269 TransactionScope::ByAccount(id) => {
270 format!("plans/{}/accounts/{}/transactions", self.plan_id, id)
271 }
272 TransactionScope::ByCategory(id) => {
273 format!("plans/{}/categories/{}/transactions", self.plan_id, id)
274 }
275 TransactionScope::ByPayee(id) => {
276 format!("plans/{}/payees/{}/transactions", self.plan_id, id)
277 }
278 TransactionScope::ByMonth(month) => {
279 format!("plans/{}/months/{}/transactions", self.plan_id, month)
280 }
281 };
282 let result: TransactionsDataEnvelope = self.client.get(&url, Some(¶ms)).await?;
283 Ok((result.data.transactions, result.data.server_knowledge))
284 }
285}
286
287impl Client {
288 pub fn get_transactions(&self, plan_id: PlanId) -> GetTransactionsBuilder<'_> {
291 GetTransactionsBuilder {
292 client: self,
293 scope: TransactionScope::All,
294 plan_id,
295 since_date: None,
296 transaction_type: None,
297 last_knowledge_of_server: None,
298 }
299 }
300
301 pub async fn get_transaction(
303 &self,
304 plan_id: PlanId,
305 transaction_id: &Uuid,
306 ) -> Result<(Transaction, i64), Error> {
307 let result: TransactionDataEnvelope = self
308 .get(
309 &format!("plans/{}/transactions/{}", plan_id, transaction_id),
310 NO_PARAMS,
311 )
312 .await?;
313 Ok((result.data.transaction, result.data.server_knowledge))
314 }
315
316 pub fn get_transactions_by_account(
318 &self,
319 plan_id: PlanId,
320 account_id: Uuid,
321 ) -> GetTransactionsBuilder<'_> {
322 GetTransactionsBuilder {
323 client: self,
324 scope: TransactionScope::ByAccount(account_id),
325 plan_id,
326 since_date: None,
327 transaction_type: None,
328 last_knowledge_of_server: None,
329 }
330 }
331
332 pub fn get_transactions_by_category(
334 &self,
335 plan_id: PlanId,
336 category_id: Uuid,
337 ) -> GetTransactionsBuilder<'_> {
338 GetTransactionsBuilder {
339 client: self,
340 scope: TransactionScope::ByCategory(category_id),
341 plan_id,
342 since_date: None,
343 transaction_type: None,
344 last_knowledge_of_server: None,
345 }
346 }
347
348 pub fn get_transactions_by_payee(
350 &self,
351 plan_id: PlanId,
352 payee_id: Uuid,
353 ) -> GetTransactionsBuilder<'_> {
354 GetTransactionsBuilder {
355 client: self,
356 scope: TransactionScope::ByPayee(payee_id),
357 plan_id,
358 since_date: None,
359 transaction_type: None,
360 last_knowledge_of_server: None,
361 }
362 }
363
364 pub fn get_transactions_by_month(
366 &self,
367 plan_id: PlanId,
368 month: NaiveDate,
369 ) -> GetTransactionsBuilder<'_> {
370 GetTransactionsBuilder {
371 client: self,
372 scope: TransactionScope::ByMonth(month),
373 plan_id,
374 since_date: None,
375 transaction_type: None,
376 last_knowledge_of_server: None,
377 }
378 }
379}
380
381#[derive(Debug)]
382pub struct GetScheduledTransactionsBuilder<'a> {
383 client: &'a Client,
384 plan_id: PlanId,
385 last_knowledge_of_server: Option<i64>,
386}
387
388impl<'a> GetScheduledTransactionsBuilder<'a> {
389 pub fn with_server_knowledge(mut self, sk: i64) -> Self {
390 self.last_knowledge_of_server = Some(sk);
391 self
392 }
393
394 pub async fn send(self) -> Result<(Vec<ScheduledTransaction>, i64), Error> {
395 let params: Option<&[(&str, &str)]> = if let Some(sk) = self.last_knowledge_of_server {
396 Some(&[("last_knowledge_of_server", &sk.to_string())])
397 } else {
398 None
399 };
400 let result: ScheduledTransactionsDataEnvelope = self
401 .client
402 .get(
403 &format!("plans/{}/scheduled_transactions", self.plan_id),
404 params,
405 )
406 .await?;
407 Ok((
408 result.data.scheduled_transactions,
409 result.data.server_knowledge,
410 ))
411 }
412}
413
414impl Client {
415 pub fn get_scheduled_transactions(
418 &self,
419 plan_id: PlanId,
420 ) -> GetScheduledTransactionsBuilder<'_> {
421 GetScheduledTransactionsBuilder {
422 client: self,
423 plan_id,
424 last_knowledge_of_server: None,
425 }
426 }
427
428 pub async fn get_scheduled_transaction(
430 &self,
431 plan_id: PlanId,
432 transaction_id: Uuid,
433 ) -> Result<ScheduledTransaction, Error> {
434 let result: ScheduledTransactionDataEnvelope = self
435 .get(
436 &format!(
437 "plans/{}/scheduled_transactions/{}",
438 plan_id, transaction_id
439 ),
440 NO_PARAMS,
441 )
442 .await?;
443 Ok(result.data.scheduled_transaction)
444 }
445}
446
447#[derive(Debug, Serialize, Deserialize)]
448struct ImportTransactionsDataEnvelope {
449 data: ImportTransactionsData,
450}
451
452#[derive(Debug, Serialize, Deserialize)]
453struct ImportTransactionsData {
454 transaction_ids: Vec<Uuid>,
455}
456
457#[derive(Debug, Default, Serialize)]
458struct Empty {}
459
460impl Client {
461 pub async fn delete_transaction(
463 &self,
464 plan_id: PlanId,
465 tx_id: Uuid,
466 ) -> Result<(Transaction, i64), Error> {
467 let result: TransactionDataEnvelope = self
468 .delete(&format!("plans/{}/transactions/{}", plan_id, tx_id))
469 .await?;
470 Ok((result.data.transaction, result.data.server_knowledge))
471 }
472
473 pub async fn import_transactions(&self, plan_id: PlanId) -> Result<Vec<Uuid>, Error> {
477 let result: ImportTransactionsDataEnvelope = self
478 .post(
479 &format!("plans/{}/transactions/import", plan_id),
480 Empty::default(),
481 )
482 .await?;
483 Ok(result.data.transaction_ids)
484 }
485}
486
487#[derive(Debug, Serialize)]
489pub struct SaveSubTransaction {
490 pub amount: i64,
491 #[serde(skip_serializing_if = "Option::is_none")]
492 pub payee_id: Option<Uuid>,
493 #[serde(skip_serializing_if = "Option::is_none")]
494 pub payee_name: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none")]
496 pub category_id: Option<Uuid>,
497 #[serde(skip_serializing_if = "Option::is_none")]
498 pub memo: Option<String>,
499}
500
501#[derive(Debug, Serialize)]
503pub struct NewTransaction {
504 pub account_id: Uuid,
505 pub date: NaiveDate,
506 #[serde(skip_serializing_if = "Option::is_none")]
507 pub amount: Option<i64>,
508 #[serde(skip_serializing_if = "Option::is_none")]
509 pub payee_id: Option<Uuid>,
510 #[serde(skip_serializing_if = "Option::is_none")]
511 pub payee_name: Option<String>,
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub category_id: Option<Uuid>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub memo: Option<String>,
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub cleared: Option<ClearedStatus>,
518 #[serde(skip_serializing_if = "Option::is_none")]
519 pub approved: Option<bool>,
520 #[serde(skip_serializing_if = "Option::is_none")]
521 pub flag_color: Option<FlagColor>,
522 #[serde(skip_serializing_if = "Option::is_none")]
523 pub import_id: Option<String>,
524 #[serde(skip_serializing_if = "Option::is_none")]
525 pub subtransactions: Option<Vec<SaveSubTransaction>>,
526}
527
528#[derive(Debug, Serialize)]
530pub struct ExistingTransaction {
531 #[serde(skip_serializing_if = "Option::is_none")]
532 pub account_id: Option<Uuid>,
533 #[serde(skip_serializing_if = "Option::is_none")]
534 pub date: Option<NaiveDate>,
535 #[serde(skip_serializing_if = "Option::is_none")]
536 pub amount: Option<i64>,
537 #[serde(skip_serializing_if = "Option::is_none")]
538 pub payee_id: Option<Uuid>,
539 #[serde(skip_serializing_if = "Option::is_none")]
540 pub payee_name: Option<String>,
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub category_id: Option<Uuid>,
543 #[serde(skip_serializing_if = "Option::is_none")]
544 pub memo: Option<String>,
545 #[serde(skip_serializing_if = "Option::is_none")]
546 pub cleared: Option<ClearedStatus>,
547 #[serde(skip_serializing_if = "Option::is_none")]
548 pub approved: Option<bool>,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 pub flag_color: Option<FlagColor>,
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub subtransactions: Option<Vec<SaveSubTransaction>>,
553}
554
555#[derive(Debug, Serialize)]
558pub struct SaveTransactionWithIdOrImportId {
559 #[serde(skip_serializing_if = "Option::is_none")]
560 pub id: Option<Uuid>,
561 #[serde(skip_serializing_if = "Option::is_none")]
562 pub import_id: Option<Uuid>,
563 #[serde(skip_serializing_if = "Option::is_none")]
564 pub account_id: Option<Uuid>,
565 #[serde(skip_serializing_if = "Option::is_none")]
566 pub date: Option<NaiveDate>,
567 #[serde(skip_serializing_if = "Option::is_none")]
568 pub amount: Option<i64>,
569 #[serde(skip_serializing_if = "Option::is_none")]
570 pub payee_id: Option<Uuid>,
571 #[serde(skip_serializing_if = "Option::is_none")]
572 pub payee_name: Option<String>,
573 #[serde(skip_serializing_if = "Option::is_none")]
574 pub category_id: Option<Uuid>,
575 #[serde(skip_serializing_if = "Option::is_none")]
576 pub memo: Option<String>,
577 #[serde(skip_serializing_if = "Option::is_none")]
578 pub cleared: Option<ClearedStatus>,
579 #[serde(skip_serializing_if = "Option::is_none")]
580 pub approved: Option<bool>,
581 #[serde(skip_serializing_if = "Option::is_none")]
582 pub flag_color: Option<FlagColor>,
583 #[serde(skip_serializing_if = "Option::is_none")]
584 pub subtransactions: Option<Vec<SaveSubTransaction>>,
585}
586
587#[derive(Debug, Serialize)]
588struct PostTransactionsWrapper {
589 transaction: Option<NewTransaction>,
590 transactions: Option<Vec<NewTransaction>>,
591}
592
593#[derive(Debug, Serialize)]
594struct PutTransactionWrapper {
595 transaction: ExistingTransaction,
596}
597
598#[derive(Debug, Serialize)]
599struct PatchTransactionsWrapper {
600 transactions: Vec<SaveTransactionWithIdOrImportId>,
601}
602
603impl Client {
604 pub async fn create_transaction(
606 &self,
607 plan_id: PlanId,
608 transaction: NewTransaction,
609 ) -> Result<SaveTransactionsResponse, Error> {
610 let result: SaveTransactionsDataEnvelope = self
611 .post(
612 &format!("plans/{}/transactions", plan_id),
613 PostTransactionsWrapper {
614 transaction: Some(transaction),
615 transactions: None,
616 },
617 )
618 .await?;
619 Ok(result.data)
620 }
621
622 pub async fn create_transactions(
624 &self,
625 plan_id: PlanId,
626 transactions: Vec<NewTransaction>,
627 ) -> Result<SaveTransactionsResponse, Error> {
628 let result: SaveTransactionsDataEnvelope = self
629 .post(
630 &format!("plans/{}/transactions", plan_id),
631 PostTransactionsWrapper {
632 transaction: None,
633 transactions: Some(transactions),
634 },
635 )
636 .await?;
637 Ok(result.data)
638 }
639
640 pub async fn update_transactions(
642 &self,
643 plan_id: PlanId,
644 transactions: Vec<SaveTransactionWithIdOrImportId>,
645 ) -> Result<SaveTransactionsResponse, Error> {
646 let result: SaveTransactionsDataEnvelope = self
647 .patch(
648 &format!("plans/{}/transactions", plan_id),
649 PatchTransactionsWrapper { transactions },
650 )
651 .await?;
652 Ok(result.data)
653 }
654
655 pub async fn update_transaction(
657 &self,
658 plan_id: PlanId,
659 tx_id: Uuid,
660 transaction: ExistingTransaction,
661 ) -> Result<(Transaction, i64), Error> {
662 let result: TransactionDataEnvelope = self
663 .put(
664 &format!("plans/{}/transactions/{}", plan_id, tx_id),
665 PutTransactionWrapper { transaction },
666 )
667 .await?;
668 Ok((result.data.transaction, result.data.server_knowledge))
669 }
670
671 pub async fn create_scheduled_transaction(
673 &self,
674 plan_id: PlanId,
675 scheduled_transaction: SaveScheduledTransaction,
676 ) -> Result<ScheduledTransaction, Error> {
677 let result: ScheduledTransactionDataEnvelope = self
678 .post(
679 &format!("plans/{}/scheduled_transactions", plan_id),
680 ScheduledTransactionWrapper {
681 scheduled_transaction,
682 },
683 )
684 .await?;
685 Ok(result.data.scheduled_transaction)
686 }
687
688 pub async fn update_scheduled_transaction(
690 &self,
691 plan_id: PlanId,
692 scheduled_transaction_id: Uuid,
693 scheduled_transaction: SaveScheduledTransaction,
694 ) -> Result<ScheduledTransaction, Error> {
695 let result: ScheduledTransactionDataEnvelope = self
696 .put(
697 &format!(
698 "plans/{}/scheduled_transactions/{}",
699 plan_id, scheduled_transaction_id
700 ),
701 ScheduledTransactionWrapper {
702 scheduled_transaction,
703 },
704 )
705 .await?;
706 Ok(result.data.scheduled_transaction)
707 }
708
709 pub async fn delete_scheduled_transaction(
711 &self,
712 plan_id: PlanId,
713 scheduled_transaction_id: Uuid,
714 ) -> Result<ScheduledTransaction, Error> {
715 let result: ScheduledTransactionDataEnvelope = self
716 .delete(&format!(
717 "plans/{}/scheduled_transactions/{}",
718 plan_id, scheduled_transaction_id
719 ))
720 .await?;
721 Ok(result.data.scheduled_transaction)
722 }
723}
724
725#[derive(Debug, Serialize)]
727pub struct SaveScheduledTransaction {
728 pub account_id: Uuid,
729 pub date: NaiveDate,
730 #[serde(skip_serializing_if = "Option::is_none")]
731 pub amount: Option<i64>,
732 #[serde(skip_serializing_if = "Option::is_none")]
733 pub payee_id: Option<Uuid>,
734 #[serde(skip_serializing_if = "Option::is_none")]
735 pub payee_name: Option<String>,
736 #[serde(skip_serializing_if = "Option::is_none")]
737 pub category_id: Option<Uuid>,
738 #[serde(skip_serializing_if = "Option::is_none")]
739 pub memo: Option<String>,
740 #[serde(skip_serializing_if = "Option::is_none")]
741 pub flag_color: Option<FlagColor>,
742 #[serde(skip_serializing_if = "Option::is_none")]
743 pub frequency: Option<Frequency>,
744}
745
746#[derive(Debug, Serialize)]
747struct ScheduledTransactionWrapper {
748 scheduled_transaction: SaveScheduledTransaction,
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::ynab::testutil::{
755 TEST_ID_1, TEST_ID_3, TEST_ID_4, error_body, new_test_client,
756 scheduled_transaction_fixture, transaction_fixture,
757 };
758 use serde_json::json;
759 use uuid::uuid;
760 use wiremock::matchers::{method, path};
761 use wiremock::{Mock, ResponseTemplate};
762
763 fn transactions_list_fixture() -> serde_json::Value {
764 json!({ "data": { "transactions": [transaction_fixture()], "server_knowledge": 10 } })
765 }
766
767 fn transaction_single_fixture() -> serde_json::Value {
768 json!({ "data": { "transaction": transaction_fixture(), "server_knowledge": 10 } })
769 }
770
771 fn save_transactions_fixture() -> serde_json::Value {
772 json!({
773 "data": {
774 "transaction_ids": [TEST_ID_1],
775 "transaction": transaction_fixture(),
776 "transactions": [transaction_fixture()],
777 "duplicate_import_ids": null,
778 "server_knowledge": 10
779 }
780 })
781 }
782
783 fn scheduled_transactions_list_fixture() -> serde_json::Value {
784 json!({
785 "data": {
786 "scheduled_transactions": [scheduled_transaction_fixture()],
787 "server_knowledge": 10
788 }
789 })
790 }
791
792 fn scheduled_transaction_single_fixture() -> serde_json::Value {
793 json!({ "data": { "scheduled_transaction": scheduled_transaction_fixture() } })
794 }
795
796 fn import_transactions_fixture() -> serde_json::Value {
797 json!({ "data": { "transaction_ids": [TEST_ID_1] } })
798 }
799
800 #[tokio::test]
801 async fn get_transactions_returns_transactions() {
802 let (client, server) = new_test_client().await;
803 Mock::given(method("GET"))
804 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
805 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
806 .expect(1)
807 .mount(&server)
808 .await;
809 let (txs, sk) = client
810 .get_transactions(PlanId::Id(uuid!(TEST_ID_1)))
811 .send()
812 .await
813 .unwrap();
814 assert_eq!(txs.len(), 1);
815 assert_eq!(txs[0].id.to_string(), TEST_ID_1);
816 assert_eq!(txs[0].amount, -50000);
817 assert_eq!(sk, 10);
818 }
819
820 #[tokio::test]
821 async fn get_transaction_returns_transaction() {
822 let (client, server) = new_test_client().await;
823 Mock::given(method("GET"))
824 .and(path(format!(
825 "/plans/{}/transactions/{}",
826 TEST_ID_1, TEST_ID_1
827 )))
828 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
829 .expect(1)
830 .mount(&server)
831 .await;
832 let (tx, sk) = client
833 .get_transaction(PlanId::Id(uuid!(TEST_ID_1)), &uuid!(TEST_ID_1))
834 .await
835 .unwrap();
836 assert_eq!(tx.id.to_string(), TEST_ID_1);
837 assert_eq!(tx.amount, -50000);
838 assert_eq!(sk, 10);
839 }
840
841 #[tokio::test]
842 async fn get_transactions_by_account_returns_transactions() {
843 let (client, server) = new_test_client().await;
844 Mock::given(method("GET"))
845 .and(path(format!(
846 "/plans/{}/accounts/{}/transactions",
847 TEST_ID_1, TEST_ID_1
848 )))
849 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
850 .expect(1)
851 .mount(&server)
852 .await;
853 let (txs, _) = client
854 .get_transactions_by_account(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_1))
855 .send()
856 .await
857 .unwrap();
858 assert_eq!(txs.len(), 1);
859 }
860
861 #[tokio::test]
862 async fn get_transactions_by_category_returns_transactions() {
863 let (client, server) = new_test_client().await;
864 Mock::given(method("GET"))
865 .and(path(format!(
866 "/plans/{}/categories/{}/transactions",
867 TEST_ID_1, TEST_ID_1
868 )))
869 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
870 .expect(1)
871 .mount(&server)
872 .await;
873 let (txs, _) = client
874 .get_transactions_by_category(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_1))
875 .send()
876 .await
877 .unwrap();
878 assert_eq!(txs.len(), 1);
879 }
880
881 #[tokio::test]
882 async fn get_transactions_by_payee_returns_transactions() {
883 let (client, server) = new_test_client().await;
884 Mock::given(method("GET"))
885 .and(path(format!(
886 "/plans/{}/payees/{}/transactions",
887 TEST_ID_1, TEST_ID_3
888 )))
889 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
890 .expect(1)
891 .mount(&server)
892 .await;
893 let (txs, _) = client
894 .get_transactions_by_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
895 .send()
896 .await
897 .unwrap();
898 assert_eq!(txs.len(), 1);
899 }
900
901 #[tokio::test]
902 async fn get_transactions_by_month_returns_transactions() {
903 let (client, server) = new_test_client().await;
904 let month = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
905 Mock::given(method("GET"))
906 .and(path(format!(
907 "/plans/{}/months/{}/transactions",
908 TEST_ID_1, month
909 )))
910 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
911 .expect(1)
912 .mount(&server)
913 .await;
914 let (txs, _) = client
915 .get_transactions_by_month(PlanId::Id(uuid!(TEST_ID_1)), month)
916 .send()
917 .await
918 .unwrap();
919 assert_eq!(txs.len(), 1);
920 }
921
922 #[tokio::test]
923 async fn create_transaction_succeeds() {
924 let (client, server) = new_test_client().await;
925 Mock::given(method("POST"))
926 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
927 .respond_with(ResponseTemplate::new(201).set_body_json(save_transactions_fixture()))
928 .expect(1)
929 .mount(&server)
930 .await;
931 let resp = client
932 .create_transaction(
933 PlanId::Id(uuid!(TEST_ID_1)),
934 NewTransaction {
935 account_id: uuid!(TEST_ID_1),
936 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
937 amount: Some(-50000),
938 memo: None,
939 cleared: Some(ClearedStatus::Cleared),
940 approved: Some(true),
941 payee_id: None,
942 payee_name: None,
943 category_id: None,
944 flag_color: None,
945 import_id: None,
946 subtransactions: None,
947 },
948 )
949 .await
950 .unwrap();
951 assert_eq!(resp.transaction_ids, vec![uuid!(TEST_ID_1)]);
952 assert_eq!(resp.transaction.unwrap().amount, -50000);
953 }
954
955 #[tokio::test]
956 async fn create_transactions_succeeds() {
957 let (client, server) = new_test_client().await;
958 Mock::given(method("POST"))
959 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
960 .respond_with(ResponseTemplate::new(201).set_body_json(save_transactions_fixture()))
961 .expect(1)
962 .mount(&server)
963 .await;
964 let resp = client
965 .create_transactions(
966 PlanId::Id(uuid!(TEST_ID_1)),
967 vec![NewTransaction {
968 account_id: uuid!(TEST_ID_1),
969 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
970 amount: Some(-50000),
971 memo: None,
972 cleared: Some(ClearedStatus::Cleared),
973 approved: Some(true),
974 payee_id: None,
975 payee_name: None,
976 category_id: None,
977 flag_color: None,
978 import_id: None,
979 subtransactions: None,
980 }],
981 )
982 .await
983 .unwrap();
984 assert_eq!(resp.transaction_ids, vec![uuid!(TEST_ID_1)]);
985 }
986
987 #[tokio::test]
988 async fn update_transaction_succeeds() {
989 let (client, server) = new_test_client().await;
990 Mock::given(method("PUT"))
991 .and(path(format!(
992 "/plans/{}/transactions/{}",
993 TEST_ID_1, TEST_ID_1
994 )))
995 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
996 .expect(1)
997 .mount(&server)
998 .await;
999 let (tx, sk) = client
1000 .update_transaction(
1001 PlanId::Id(uuid!(TEST_ID_1)),
1002 uuid!(TEST_ID_1),
1003 ExistingTransaction {
1004 amount: Some(-50000),
1005 account_id: None,
1006 date: None,
1007 payee_id: None,
1008 payee_name: None,
1009 category_id: None,
1010 memo: None,
1011 cleared: None,
1012 approved: None,
1013 flag_color: None,
1014 subtransactions: None,
1015 },
1016 )
1017 .await
1018 .unwrap();
1019 assert_eq!(tx.id.to_string(), TEST_ID_1);
1020 assert_eq!(sk, 10);
1021 }
1022
1023 #[tokio::test]
1024 async fn update_transactions_succeeds() {
1025 let (client, server) = new_test_client().await;
1026 Mock::given(method("PATCH"))
1027 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1028 .respond_with(ResponseTemplate::new(200).set_body_json(save_transactions_fixture()))
1029 .expect(1)
1030 .mount(&server)
1031 .await;
1032 let resp = client
1033 .update_transactions(
1034 PlanId::Id(uuid!(TEST_ID_1)),
1035 vec![SaveTransactionWithIdOrImportId {
1036 id: Some(Uuid::from_bytes([0; 16])),
1037 memo: Some("updated".to_string()),
1038 import_id: None,
1039 account_id: None,
1040 date: None,
1041 amount: None,
1042 payee_id: None,
1043 payee_name: None,
1044 category_id: None,
1045 cleared: None,
1046 approved: None,
1047 flag_color: None,
1048 subtransactions: None,
1049 }],
1050 )
1051 .await
1052 .unwrap();
1053 assert_eq!(resp.transaction_ids, vec![uuid!(TEST_ID_1)]);
1054 }
1055
1056 #[tokio::test]
1057 async fn delete_transaction_succeeds() {
1058 let (client, server) = new_test_client().await;
1059 Mock::given(method("DELETE"))
1060 .and(path(format!(
1061 "/plans/{}/transactions/{}",
1062 TEST_ID_1, TEST_ID_1
1063 )))
1064 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
1065 .expect(1)
1066 .mount(&server)
1067 .await;
1068 let (tx, sk) = client
1069 .delete_transaction(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_1))
1070 .await
1071 .unwrap();
1072 assert_eq!(tx.id.to_string(), TEST_ID_1);
1073 assert_eq!(sk, 10);
1074 }
1075
1076 #[tokio::test]
1077 async fn import_transactions_returns_ids() {
1078 let (client, server) = new_test_client().await;
1079 Mock::given(method("POST"))
1080 .and(path(format!("/plans/{}/transactions/import", TEST_ID_1)))
1081 .respond_with(ResponseTemplate::new(200).set_body_json(import_transactions_fixture()))
1082 .expect(1)
1083 .mount(&server)
1084 .await;
1085 let ids = client
1086 .import_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1087 .await
1088 .unwrap();
1089 assert_eq!(ids.len(), 1);
1090 assert_eq!(ids[0].to_string(), TEST_ID_1);
1091 }
1092
1093 #[tokio::test]
1094 async fn get_scheduled_transactions_returns_transactions() {
1095 let (client, server) = new_test_client().await;
1096 Mock::given(method("GET"))
1097 .and(path(format!("/plans/{}/scheduled_transactions", TEST_ID_1)))
1098 .respond_with(
1099 ResponseTemplate::new(200).set_body_json(scheduled_transactions_list_fixture()),
1100 )
1101 .expect(1)
1102 .mount(&server)
1103 .await;
1104 let (txs, sk) = client
1105 .get_scheduled_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1106 .send()
1107 .await
1108 .unwrap();
1109 assert_eq!(txs.len(), 1);
1110 assert_eq!(txs[0].id.to_string(), TEST_ID_4);
1111 assert_eq!(sk, 10);
1112 }
1113
1114 #[tokio::test]
1115 async fn get_scheduled_transaction_returns_transaction() {
1116 let (client, server) = new_test_client().await;
1117 Mock::given(method("GET"))
1118 .and(path(format!(
1119 "/plans/{}/scheduled_transactions/{}",
1120 TEST_ID_1, TEST_ID_4
1121 )))
1122 .respond_with(
1123 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1124 )
1125 .expect(1)
1126 .mount(&server)
1127 .await;
1128 let tx = client
1129 .get_scheduled_transaction(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
1130 .await
1131 .unwrap();
1132 assert_eq!(tx.id.to_string(), TEST_ID_4);
1133 assert!(matches!(tx.frequency, Frequency::Monthly));
1134 }
1135
1136 #[tokio::test]
1137 async fn create_scheduled_transaction_succeeds() {
1138 let (client, server) = new_test_client().await;
1139 Mock::given(method("POST"))
1140 .and(path(format!("/plans/{}/scheduled_transactions", TEST_ID_1)))
1141 .respond_with(
1142 ResponseTemplate::new(201).set_body_json(scheduled_transaction_single_fixture()),
1143 )
1144 .expect(1)
1145 .mount(&server)
1146 .await;
1147 let tx = client
1148 .create_scheduled_transaction(
1149 PlanId::Id(uuid!(TEST_ID_1)),
1150 SaveScheduledTransaction {
1151 account_id: uuid!(TEST_ID_1),
1152 date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1153 amount: Some(-50000),
1154 frequency: Some(Frequency::Monthly),
1155 memo: None,
1156 payee_id: None,
1157 payee_name: None,
1158 category_id: None,
1159 flag_color: None,
1160 },
1161 )
1162 .await
1163 .unwrap();
1164 assert_eq!(tx.id.to_string(), TEST_ID_4);
1165 assert_eq!(tx.amount, -50000);
1166 }
1167
1168 #[tokio::test]
1169 async fn update_scheduled_transaction_succeeds() {
1170 let (client, server) = new_test_client().await;
1171 Mock::given(method("PUT"))
1172 .and(path(format!(
1173 "/plans/{}/scheduled_transactions/{}",
1174 TEST_ID_1, TEST_ID_4
1175 )))
1176 .respond_with(
1177 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1178 )
1179 .expect(1)
1180 .mount(&server)
1181 .await;
1182 let tx = client
1183 .update_scheduled_transaction(
1184 PlanId::Id(uuid!(TEST_ID_1)),
1185 uuid!(TEST_ID_4),
1186 SaveScheduledTransaction {
1187 account_id: uuid!(TEST_ID_1),
1188 date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1189 amount: Some(-50000),
1190 frequency: Some(Frequency::Monthly),
1191 memo: None,
1192 payee_id: None,
1193 payee_name: None,
1194 category_id: None,
1195 flag_color: None,
1196 },
1197 )
1198 .await
1199 .unwrap();
1200 assert_eq!(tx.id.to_string(), TEST_ID_4);
1201 }
1202
1203 #[tokio::test]
1204 async fn delete_scheduled_transaction_succeeds() {
1205 let (client, server) = new_test_client().await;
1206 Mock::given(method("DELETE"))
1207 .and(path(format!(
1208 "/plans/{}/scheduled_transactions/{}",
1209 TEST_ID_1, TEST_ID_4
1210 )))
1211 .respond_with(
1212 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1213 )
1214 .expect(1)
1215 .mount(&server)
1216 .await;
1217 let tx = client
1218 .delete_scheduled_transaction(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
1219 .await
1220 .unwrap();
1221 assert_eq!(tx.id.to_string(), TEST_ID_4);
1222 }
1223
1224 #[tokio::test]
1225 async fn get_transaction_returns_not_found() {
1226 let (client, server) = new_test_client().await;
1227 Mock::given(method("GET"))
1228 .and(path(format!(
1229 "/plans/{}/transactions/{}",
1230 TEST_ID_1, TEST_ID_1
1231 )))
1232 .respond_with(ResponseTemplate::new(404).set_body_json(error_body(
1233 "404",
1234 "not_found",
1235 "Transaction not found",
1236 )))
1237 .mount(&server)
1238 .await;
1239 let err = client
1240 .get_transaction(PlanId::Id(uuid!(TEST_ID_1)), &uuid!(TEST_ID_1))
1241 .await
1242 .unwrap_err();
1243 assert!(matches!(err, Error::NotFound(_)));
1244 }
1245}