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