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