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