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