1use chrono::NaiveDate;
2use futures::stream::{self, StreamExt};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use crate::PlanId;
7use crate::ynab::client::Client;
8use crate::ynab::common::NO_PARAMS;
9use crate::ynab::errors::Error;
10
11#[derive(Debug, Deserialize)]
14struct TransactionDataEnvelope {
15 data: TransactionData,
16}
17
18#[derive(Debug, Deserialize)]
19struct TransactionData {
20 transaction: Transaction,
21 server_knowledge: i64,
22}
23
24#[derive(Debug, Deserialize)]
25struct TransactionsDataEnvelope {
26 data: TransactionsData,
27}
28
29#[derive(Debug, Deserialize)]
30struct TransactionsData {
31 transactions: Vec<Transaction>,
32 server_knowledge: i64,
33}
34
35#[derive(Debug, Deserialize)]
36struct HybridTransactionsDataEnvelope {
37 data: HybridTransactionsData,
38}
39
40#[derive(Debug, Deserialize)]
41struct HybridTransactionsData {
42 transactions: Vec<HybridTransaction>,
43 server_knowledge: i64,
44}
45
46#[derive(Debug, Deserialize)]
47struct ScheduledTransactionDataEnvelope {
48 data: ScheduledTransactionData,
49}
50
51#[derive(Debug, Deserialize)]
52struct ScheduledTransactionData {
53 scheduled_transaction: ScheduledTransaction,
54}
55
56#[derive(Debug, Deserialize)]
57struct ScheduledTransactionsDataEnvelope {
58 data: ScheduledTransactionsData,
59}
60
61#[derive(Debug, Deserialize)]
62struct ScheduledTransactionsData {
63 scheduled_transactions: Vec<ScheduledTransaction>,
64 server_knowledge: i64,
65}
66
67#[derive(Debug, Deserialize)]
68struct SaveTransactionsDataEnvelope {
69 data: SaveTransactionsResponse,
70}
71
72#[derive(Debug, Deserialize)]
73struct SaveTransactionDataEnvelope {
74 data: SaveTransactionResponse,
75}
76
77#[derive(Debug, Clone, PartialEq, Deserialize)]
79pub struct SaveTransactionsResponse {
80 #[serde(default)]
81 pub transactions: Vec<Transaction>,
82
83 pub transaction_ids: Vec<String>,
84 pub duplicate_import_ids: Option<Vec<String>>,
85 pub server_knowledge: i64,
86}
87
88#[derive(Debug, Clone, PartialEq, Deserialize)]
90pub struct SaveTransactionResponse {
91 pub transaction: Transaction,
92
93 pub transaction_ids: Vec<String>,
94 pub duplicate_import_ids: Option<Vec<String>>,
95 pub server_knowledge: i64,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[non_exhaustive]
103#[serde(rename_all = "snake_case")]
104pub enum ClearedStatus {
105 Cleared,
106 Uncleared,
107 Reconciled,
108 #[serde(other)]
109 Other,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
114#[non_exhaustive]
115#[serde(rename_all = "snake_case")]
116pub enum FlagColor {
117 Red,
118 Orange,
119 Yellow,
120 Green,
121 Blue,
122 Purple,
123 #[serde(rename = "")]
124 None,
125 #[serde(other)]
126 Other,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
131#[non_exhaustive]
132#[serde(rename_all = "camelCase")]
133pub enum Frequency {
134 Never,
135 Daily,
136 Weekly,
137 EveryOtherWeek,
138 TwiceAMonth,
139 Every4Weeks,
140 Monthly,
141 EveryOtherMonth,
142 Every3Months,
143 Every4Months,
144 TwiceAYear,
145 Yearly,
146 EveryOtherYear,
147 #[serde(other)]
148 Other,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
160pub struct Transaction {
161 pub id: String,
162 pub account_name: String,
163 pub date: NaiveDate,
164 pub amount: i64,
165 pub memo: Option<String>,
166 pub cleared: ClearedStatus,
167 pub approved: bool,
168 pub flag_color: Option<FlagColor>,
169 pub flag_name: Option<String>,
170 pub account_id: Uuid,
171 pub payee_id: Option<Uuid>,
172 pub payee_name: Option<String>,
173 pub category_id: Option<Uuid>,
174 pub category_name: Option<String>,
175 pub matched_transaction_id: Option<String>,
176 pub import_id: Option<String>,
177 pub import_payee_name: Option<String>,
178 pub import_payee_name_original: Option<String>,
179 pub transfer_account_id: Option<Uuid>,
180 pub transfer_transaction_id: Option<String>,
181 pub debt_transaction_type: Option<DebtTransactionType>,
182 pub deleted: bool,
183 #[serde(default)]
184 pub subtransactions: Vec<Subtransaction>,
185}
186
187#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
189pub struct Subtransaction {
190 pub id: String,
191 pub transaction_id: String,
192 pub amount: i64,
193 pub memo: Option<String>,
194 pub payee_id: Option<Uuid>,
195 pub payee_name: Option<String>,
196 pub category_id: Option<Uuid>,
197 pub category_name: Option<String>,
198 pub transfer_account_id: Option<Uuid>,
199 pub transfer_transaction_id: Option<String>,
200 #[serde(default)]
201 pub deleted: bool,
202}
203
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
209pub struct ScheduledTransaction {
210 pub id: Uuid,
211 pub date_first: NaiveDate,
212 pub date_next: NaiveDate,
213 pub frequency: Frequency,
214 pub amount: i64,
215 pub memo: Option<String>,
216 pub flag_color: Option<FlagColor>,
217 pub flag_name: Option<String>,
218 pub account_id: Uuid,
219 pub payee_id: Option<Uuid>,
220 pub category_id: Option<Uuid>,
221 pub account_name: String,
222 pub payee_name: Option<String>,
223 pub category_name: Option<String>,
224 pub subtransactions: Vec<ScheduledSubtransaction>,
225 pub transfer_account_id: Option<Uuid>,
226 #[serde(default)]
227 pub deleted: bool,
228}
229
230#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
233pub struct ScheduledSubtransaction {
234 pub id: Uuid,
235 pub scheduled_transaction_id: Uuid,
236 pub amount: i64,
237 pub memo: Option<String>,
238 pub payee_id: Option<Uuid>,
239 pub payee_name: Option<String>,
240 pub category_id: Option<Uuid>,
241 pub category_name: Option<String>,
242 pub transfer_account_id: Option<Uuid>,
243 pub deleted: bool,
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
247#[serde(rename_all = "snake_case")]
248#[non_exhaustive]
249pub enum HybridTransactionType {
250 Transaction,
251 Subtransaction,
252 #[serde(other)]
253 Other,
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct HybridTransaction {
258 #[serde(rename = "type")]
259 pub ttype: HybridTransactionType,
260 pub id: String,
261 pub date: NaiveDate,
262 pub amount: i64,
263 pub memo: Option<String>,
264 pub cleared: ClearedStatus,
265 pub approved: bool,
266 pub account_id: Uuid,
267 pub account_name: String,
268 pub category_name: String,
269 pub parent_transaction_id: Option<String>,
270 pub flag_color: Option<FlagColor>,
271 pub flag_name: Option<String>,
272 pub payee_id: Option<Uuid>,
273 pub payee_name: Option<String>,
274 pub category_id: Option<Uuid>,
275 pub matched_transaction_id: Option<String>,
276 pub import_id: Option<String>,
277 pub import_payee_name: Option<String>,
278 pub import_payee_name_original: Option<String>,
279 pub transfer_account_id: Option<Uuid>,
280 pub transfer_transaction_id: Option<String>,
281 pub debt_transaction_type: Option<DebtTransactionType>,
282 pub deleted: bool,
283}
284
285#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
289pub struct TransactionSummary {
290 pub id: String,
291 pub date: NaiveDate,
292 pub amount: i64,
293 pub memo: Option<String>,
294 pub cleared: ClearedStatus,
295 pub approved: bool,
296 pub flag_color: Option<FlagColor>,
297 pub flag_name: Option<String>,
298 pub account_id: Uuid,
299 pub payee_id: Option<Uuid>,
300 pub category_id: Option<Uuid>,
301 pub matched_transaction_id: Option<String>,
302 pub import_id: Option<String>,
303 pub import_payee_name: Option<String>,
304 pub import_payee_name_original: Option<String>,
305 pub transfer_account_id: Option<Uuid>,
306 pub transfer_transaction_id: Option<String>,
307 pub debt_transaction_type: Option<DebtTransactionType>,
308 pub deleted: bool,
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
315pub struct ScheduledTransactionSummary {
316 pub id: Uuid,
317 pub date_first: NaiveDate,
318 pub date_next: NaiveDate,
319 pub frequency: Frequency,
320 pub amount: i64,
321 pub memo: Option<String>,
322 pub flag_color: Option<FlagColor>,
323 pub flag_name: Option<String>,
324 pub account_id: Uuid,
325 pub payee_id: Option<Uuid>,
326 pub category_id: Option<Uuid>,
327 pub transfer_account_id: Option<Uuid>,
328 pub deleted: bool,
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
334#[non_exhaustive]
335#[serde(rename_all = "camelCase")]
336pub enum DebtTransactionType {
337 Payment,
338 Refund,
339 Fee,
340 Interest,
341 Escrow,
342 BalanceAdjustment,
343 Credit,
344 Charge,
345 #[serde(other)]
346 Other,
347}
348
349#[derive(Debug, Clone, PartialEq, Eq, Hash)]
352pub enum TransactionType {
353 Uncategorized,
354 Unapproved,
355}
356
357impl std::fmt::Display for TransactionType {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 match self {
360 Self::Unapproved => write!(f, "unapproved"),
361 Self::Uncategorized => write!(f, "uncategorized"),
362 }
363 }
364}
365
366#[derive(Debug, Clone)]
367enum TransactionScope {
368 All,
369 ByAccount(Uuid),
370 ByMonth(NaiveDate),
371}
372
373#[derive(Debug, Clone)]
374enum HybridTransactionScope {
375 ByCategory(Uuid),
376 ByPayee(Uuid),
377}
378
379#[derive(Debug, Clone)]
380pub struct GetHybridTransactionsBuilder<'a> {
381 client: &'a Client,
382 scope: HybridTransactionScope,
383 plan_id: PlanId,
384 since_date: Option<NaiveDate>,
385 transaction_type: Option<TransactionType>,
386 last_knowledge_of_server: Option<i64>,
387}
388
389impl<'a> GetHybridTransactionsBuilder<'a> {
390 pub fn with_server_knowledge(mut self, sk: i64) -> Self {
391 self.last_knowledge_of_server = Some(sk);
392 self
393 }
394
395 pub fn since_date(mut self, since_date: NaiveDate) -> Self {
396 self.since_date = Some(since_date);
397 self
398 }
399
400 pub fn transaction_type(mut self, tx_type: TransactionType) -> Self {
401 self.transaction_type = Some(tx_type);
402 self
403 }
404
405 pub async fn send(self) -> Result<(Vec<HybridTransaction>, i64), Error> {
407 let date_str = self.since_date.map(|d| d.to_string());
408 let type_str = self.transaction_type.map(|t| t.to_string());
409 let sk_str = self.last_knowledge_of_server.map(|sk| sk.to_string());
410
411 let mut params: Vec<(&str, &str)> = Vec::new();
412 if let Some(ref s) = date_str {
413 params.push(("since_date", s));
414 }
415 if let Some(ref t) = type_str {
416 params.push(("type", t));
417 }
418 if let Some(ref s) = sk_str {
419 params.push(("last_knowledge_of_server", s));
420 }
421 let url = match self.scope {
422 HybridTransactionScope::ByCategory(id) => {
423 format!("plans/{}/categories/{}/transactions", self.plan_id, id)
424 }
425 HybridTransactionScope::ByPayee(id) => {
426 format!("plans/{}/payees/{}/transactions", self.plan_id, id)
427 }
428 };
429 let result: HybridTransactionsDataEnvelope = self.client.get(&url, Some(¶ms)).await?;
430 Ok((result.data.transactions, result.data.server_knowledge))
431 }
432}
433
434#[derive(Debug, Clone)]
435pub struct GetTransactionsBuilder<'a> {
436 client: &'a Client,
437 scope: TransactionScope,
438 plan_id: PlanId,
439 since_date: Option<NaiveDate>,
440 transaction_type: Option<TransactionType>,
441 last_knowledge_of_server: Option<i64>,
442}
443
444impl<'a> GetTransactionsBuilder<'a> {
445 pub fn with_server_knowledge(mut self, sk: i64) -> Self {
446 self.last_knowledge_of_server = Some(sk);
447 self
448 }
449
450 pub fn since_date(mut self, since_date: NaiveDate) -> Self {
451 self.since_date = Some(since_date);
452 self
453 }
454
455 pub fn transaction_type(mut self, tx_type: TransactionType) -> Self {
456 self.transaction_type = Some(tx_type);
457 self
458 }
459
460 pub async fn send(self) -> Result<(Vec<Transaction>, i64), Error> {
462 let date_str = self.since_date.map(|d| d.to_string());
463 let type_str = self.transaction_type.map(|t| t.to_string());
464 let sk_str = self.last_knowledge_of_server.map(|sk| sk.to_string());
465
466 let mut params: Vec<(&str, &str)> = Vec::new();
467 if let Some(ref s) = date_str {
468 params.push(("since_date", s));
469 }
470 if let Some(ref t) = type_str {
471 params.push(("type", t));
472 }
473 if let Some(ref s) = sk_str {
474 params.push(("last_knowledge_of_server", s));
475 }
476 let url = match self.scope {
477 TransactionScope::All => format!("plans/{}/transactions", self.plan_id),
478 TransactionScope::ByAccount(id) => {
479 format!("plans/{}/accounts/{}/transactions", self.plan_id, id)
480 }
481 TransactionScope::ByMonth(month) => {
482 format!("plans/{}/months/{}/transactions", self.plan_id, month)
483 }
484 };
485 let result: TransactionsDataEnvelope = self.client.get(&url, Some(¶ms)).await?;
486 Ok((result.data.transactions, result.data.server_knowledge))
487 }
488}
489
490impl Client {
491 pub fn get_transactions(&self, plan_id: PlanId) -> GetTransactionsBuilder<'_> {
515 GetTransactionsBuilder {
516 client: self,
517 scope: TransactionScope::All,
518 plan_id,
519 since_date: None,
520 transaction_type: None,
521 last_knowledge_of_server: None,
522 }
523 }
524
525 pub async fn get_transaction(
527 &self,
528 plan_id: PlanId,
529 transaction_id: &str,
530 ) -> Result<(Transaction, i64), Error> {
531 let result: TransactionDataEnvelope = self
532 .get(
533 &format!("plans/{}/transactions/{}", plan_id, transaction_id),
534 NO_PARAMS,
535 )
536 .await?;
537 Ok((result.data.transaction, result.data.server_knowledge))
538 }
539
540 pub fn get_transactions_by_account(
542 &self,
543 plan_id: PlanId,
544 account_id: Uuid,
545 ) -> GetTransactionsBuilder<'_> {
546 GetTransactionsBuilder {
547 client: self,
548 scope: TransactionScope::ByAccount(account_id),
549 plan_id,
550 since_date: None,
551 transaction_type: None,
552 last_knowledge_of_server: None,
553 }
554 }
555
556 pub fn get_transactions_by_category(
558 &self,
559 plan_id: PlanId,
560 category_id: Uuid,
561 ) -> GetHybridTransactionsBuilder<'_> {
562 GetHybridTransactionsBuilder {
563 client: self,
564 scope: HybridTransactionScope::ByCategory(category_id),
565 plan_id,
566 since_date: None,
567 transaction_type: None,
568 last_knowledge_of_server: None,
569 }
570 }
571
572 pub fn get_transactions_by_payee(
574 &self,
575 plan_id: PlanId,
576 payee_id: Uuid,
577 ) -> GetHybridTransactionsBuilder<'_> {
578 GetHybridTransactionsBuilder {
579 client: self,
580 scope: HybridTransactionScope::ByPayee(payee_id),
581 plan_id,
582 since_date: None,
583 transaction_type: None,
584 last_knowledge_of_server: None,
585 }
586 }
587
588 pub fn get_transactions_by_month(
590 &self,
591 plan_id: PlanId,
592 month: NaiveDate,
593 ) -> GetTransactionsBuilder<'_> {
594 GetTransactionsBuilder {
595 client: self,
596 scope: TransactionScope::ByMonth(month),
597 plan_id,
598 since_date: None,
599 transaction_type: None,
600 last_knowledge_of_server: None,
601 }
602 }
603}
604
605#[derive(Debug, Clone)]
606pub struct GetScheduledTransactionsBuilder<'a> {
607 client: &'a Client,
608 plan_id: PlanId,
609 last_knowledge_of_server: Option<i64>,
610}
611
612impl<'a> GetScheduledTransactionsBuilder<'a> {
613 pub fn with_server_knowledge(mut self, sk: i64) -> Self {
614 self.last_knowledge_of_server = Some(sk);
615 self
616 }
617
618 pub async fn send(self) -> Result<(Vec<ScheduledTransaction>, i64), Error> {
620 let params: Option<&[(&str, &str)]> = if let Some(sk) = self.last_knowledge_of_server {
621 Some(&[("last_knowledge_of_server", &sk.to_string())])
622 } else {
623 None
624 };
625 let result: ScheduledTransactionsDataEnvelope = self
626 .client
627 .get(
628 &format!("plans/{}/scheduled_transactions", self.plan_id),
629 params,
630 )
631 .await?;
632 Ok((
633 result.data.scheduled_transactions,
634 result.data.server_knowledge,
635 ))
636 }
637}
638
639impl Client {
640 pub fn get_scheduled_transactions(
643 &self,
644 plan_id: PlanId,
645 ) -> GetScheduledTransactionsBuilder<'_> {
646 GetScheduledTransactionsBuilder {
647 client: self,
648 plan_id,
649 last_knowledge_of_server: None,
650 }
651 }
652
653 pub async fn get_scheduled_transaction(
655 &self,
656 plan_id: PlanId,
657 transaction_id: Uuid,
658 ) -> Result<ScheduledTransaction, Error> {
659 let result: ScheduledTransactionDataEnvelope = self
660 .get(
661 &format!(
662 "plans/{}/scheduled_transactions/{}",
663 plan_id, transaction_id
664 ),
665 NO_PARAMS,
666 )
667 .await?;
668 Ok(result.data.scheduled_transaction)
669 }
670}
671
672#[derive(Debug, Serialize, Deserialize)]
673struct ImportTransactionsDataEnvelope {
674 data: ImportTransactionsData,
675}
676
677#[derive(Debug, Serialize, Deserialize)]
678struct ImportTransactionsData {
679 transaction_ids: Vec<String>,
680}
681
682#[derive(Debug, Default, Serialize)]
683struct Empty {}
684
685impl Client {
686 pub async fn delete_transaction(
688 &self,
689 plan_id: PlanId,
690 tx_id: &str,
691 ) -> Result<(Transaction, i64), Error> {
692 let result: TransactionDataEnvelope = self
693 .delete(&format!("plans/{}/transactions/{}", plan_id, tx_id))
694 .await?;
695 Ok((result.data.transaction, result.data.server_knowledge))
696 }
697
698 pub async fn delete_transactions_bulk(
701 &self,
702 plan_id: PlanId,
703 tx_ids: &[&str],
704 concurrency: usize,
705 ) -> Vec<Result<(Transaction, i64), Error>> {
706 stream::iter(tx_ids)
707 .map(|tid| self.delete_transaction(plan_id, tid))
708 .buffered(concurrency)
709 .collect()
710 .await
711 }
712
713 pub async fn import_transactions(&self, plan_id: PlanId) -> Result<Vec<String>, Error> {
717 let result: ImportTransactionsDataEnvelope = self
718 .post(
719 &format!("plans/{}/transactions/import", plan_id),
720 Empty::default(),
721 )
722 .await?;
723 Ok(result.data.transaction_ids)
724 }
725}
726
727#[derive(Debug, Clone, PartialEq, Serialize)]
729pub struct SaveSubTransaction {
730 pub amount: i64,
731 #[serde(skip_serializing_if = "Option::is_none")]
732 pub payee_id: Option<Uuid>,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub payee_name: Option<String>,
735 #[serde(skip_serializing_if = "Option::is_none")]
736 pub category_id: Option<Uuid>,
737 #[serde(skip_serializing_if = "Option::is_none")]
738 pub memo: Option<String>,
739}
740
741#[derive(Debug, Clone, PartialEq, Serialize)]
743pub struct NewTransaction {
744 pub account_id: Uuid,
745 pub date: NaiveDate,
746 pub amount: i64,
747 #[serde(skip_serializing_if = "Option::is_none")]
748 pub payee_id: Option<Uuid>,
749 #[serde(skip_serializing_if = "Option::is_none")]
750 pub payee_name: Option<String>,
751 #[serde(skip_serializing_if = "Option::is_none")]
752 pub category_id: Option<Uuid>,
753 #[serde(skip_serializing_if = "Option::is_none")]
754 pub memo: Option<String>,
755 #[serde(skip_serializing_if = "Option::is_none")]
756 pub cleared: Option<ClearedStatus>,
757 #[serde(skip_serializing_if = "Option::is_none")]
758 pub approved: Option<bool>,
759 #[serde(skip_serializing_if = "Option::is_none")]
760 pub flag_color: Option<FlagColor>,
761 #[serde(skip_serializing_if = "Option::is_none")]
762 pub import_id: Option<String>,
763 #[serde(skip_serializing_if = "Option::is_none")]
764 pub subtransactions: Option<Vec<SaveSubTransaction>>,
765}
766
767#[derive(Debug, Clone, PartialEq, Serialize)]
769pub struct ExistingTransaction {
770 #[serde(skip_serializing_if = "Option::is_none")]
771 pub account_id: Option<Uuid>,
772 #[serde(skip_serializing_if = "Option::is_none")]
773 pub date: Option<NaiveDate>,
774 #[serde(skip_serializing_if = "Option::is_none")]
775 pub amount: Option<i64>,
776 #[serde(skip_serializing_if = "Option::is_none")]
777 pub payee_id: Option<Uuid>,
778 #[serde(skip_serializing_if = "Option::is_none")]
779 pub payee_name: Option<String>,
780 #[serde(skip_serializing_if = "Option::is_none")]
781 pub category_id: Option<Uuid>,
782 #[serde(skip_serializing_if = "Option::is_none")]
783 pub memo: Option<String>,
784 #[serde(skip_serializing_if = "Option::is_none")]
785 pub cleared: Option<ClearedStatus>,
786 #[serde(skip_serializing_if = "Option::is_none")]
787 pub approved: Option<bool>,
788 #[serde(skip_serializing_if = "Option::is_none")]
789 pub flag_color: Option<FlagColor>,
790 #[serde(skip_serializing_if = "Option::is_none")]
791 pub subtransactions: Option<Vec<SaveSubTransaction>>,
792}
793
794#[derive(Debug, Clone, PartialEq, Serialize)]
797pub struct SaveTransactionWithIdOrImportId {
798 #[serde(skip_serializing_if = "Option::is_none")]
799 pub id: Option<String>,
800 #[serde(skip_serializing_if = "Option::is_none")]
801 pub import_id: Option<String>,
802 #[serde(skip_serializing_if = "Option::is_none")]
803 pub account_id: Option<Uuid>,
804 #[serde(skip_serializing_if = "Option::is_none")]
805 pub date: Option<NaiveDate>,
806 #[serde(skip_serializing_if = "Option::is_none")]
807 pub amount: Option<i64>,
808 #[serde(skip_serializing_if = "Option::is_none")]
809 pub payee_id: Option<Uuid>,
810 #[serde(skip_serializing_if = "Option::is_none")]
811 pub payee_name: Option<String>,
812 #[serde(skip_serializing_if = "Option::is_none")]
813 pub category_id: Option<Uuid>,
814 #[serde(skip_serializing_if = "Option::is_none")]
815 pub memo: Option<String>,
816 #[serde(skip_serializing_if = "Option::is_none")]
817 pub cleared: Option<ClearedStatus>,
818 #[serde(skip_serializing_if = "Option::is_none")]
819 pub approved: Option<bool>,
820 #[serde(skip_serializing_if = "Option::is_none")]
821 pub flag_color: Option<FlagColor>,
822 #[serde(skip_serializing_if = "Option::is_none")]
823 pub subtransactions: Option<Vec<SaveSubTransaction>>,
824}
825
826#[derive(Debug, Serialize)]
827struct PostTransactionsWrapper {
828 transactions: Vec<NewTransaction>,
829}
830
831#[derive(Debug, Serialize)]
832struct PostTransactionWrapper {
833 transaction: NewTransaction,
834}
835
836#[derive(Debug, Serialize)]
837struct PutTransactionWrapper {
838 transaction: ExistingTransaction,
839}
840
841#[derive(Debug, Serialize)]
842struct PatchTransactionsWrapper {
843 transactions: Vec<SaveTransactionWithIdOrImportId>,
844}
845
846impl Client {
847 pub async fn create_transaction(
875 &self,
876 plan_id: PlanId,
877 transaction: NewTransaction,
878 ) -> Result<SaveTransactionResponse, Error> {
879 let result: SaveTransactionDataEnvelope = self
880 .post(
881 &format!("plans/{}/transactions", plan_id),
882 PostTransactionWrapper { transaction },
883 )
884 .await?;
885 Ok(result.data)
886 }
887
888 pub async fn create_transactions(
890 &self,
891 plan_id: PlanId,
892 transactions: Vec<NewTransaction>,
893 ) -> Result<SaveTransactionsResponse, Error> {
894 let result: SaveTransactionsDataEnvelope = self
895 .post(
896 &format!("plans/{}/transactions", plan_id),
897 PostTransactionsWrapper { transactions },
898 )
899 .await?;
900 Ok(result.data)
901 }
902
903 pub async fn update_transactions(
905 &self,
906 plan_id: PlanId,
907 transactions: Vec<SaveTransactionWithIdOrImportId>,
908 ) -> Result<SaveTransactionsResponse, Error> {
909 let result: SaveTransactionsDataEnvelope = self
910 .patch(
911 &format!("plans/{}/transactions", plan_id),
912 PatchTransactionsWrapper { transactions },
913 )
914 .await?;
915 Ok(result.data)
916 }
917
918 pub async fn update_transaction(
920 &self,
921 plan_id: PlanId,
922 tx_id: &str,
923 transaction: ExistingTransaction,
924 ) -> Result<(Transaction, i64), Error> {
925 let result: TransactionDataEnvelope = self
926 .put(
927 &format!("plans/{}/transactions/{}", plan_id, tx_id),
928 PutTransactionWrapper { transaction },
929 )
930 .await?;
931 Ok((result.data.transaction, result.data.server_knowledge))
932 }
933
934 pub async fn create_scheduled_transaction(
936 &self,
937 plan_id: PlanId,
938 scheduled_transaction: SaveScheduledTransaction,
939 ) -> Result<ScheduledTransaction, Error> {
940 let result: ScheduledTransactionDataEnvelope = self
941 .post(
942 &format!("plans/{}/scheduled_transactions", plan_id),
943 ScheduledTransactionWrapper {
944 scheduled_transaction,
945 },
946 )
947 .await?;
948 Ok(result.data.scheduled_transaction)
949 }
950
951 pub async fn update_scheduled_transaction(
953 &self,
954 plan_id: PlanId,
955 scheduled_transaction_id: Uuid,
956 scheduled_transaction: SaveScheduledTransaction,
957 ) -> Result<ScheduledTransaction, Error> {
958 let result: ScheduledTransactionDataEnvelope = self
959 .put(
960 &format!(
961 "plans/{}/scheduled_transactions/{}",
962 plan_id, scheduled_transaction_id
963 ),
964 ScheduledTransactionWrapper {
965 scheduled_transaction,
966 },
967 )
968 .await?;
969 Ok(result.data.scheduled_transaction)
970 }
971
972 pub async fn delete_scheduled_transaction(
974 &self,
975 plan_id: PlanId,
976 scheduled_transaction_id: Uuid,
977 ) -> Result<ScheduledTransaction, Error> {
978 let result: ScheduledTransactionDataEnvelope = self
979 .delete(&format!(
980 "plans/{}/scheduled_transactions/{}",
981 plan_id, scheduled_transaction_id
982 ))
983 .await?;
984 Ok(result.data.scheduled_transaction)
985 }
986}
987
988#[derive(Debug, Clone, PartialEq, Serialize)]
990pub struct SaveScheduledTransaction {
991 pub account_id: Uuid,
992 pub date: NaiveDate,
993 #[serde(skip_serializing_if = "Option::is_none")]
994 pub amount: Option<i64>,
995 #[serde(skip_serializing_if = "Option::is_none")]
996 pub payee_id: Option<Uuid>,
997 #[serde(skip_serializing_if = "Option::is_none")]
998 pub payee_name: Option<String>,
999 #[serde(skip_serializing_if = "Option::is_none")]
1000 pub category_id: Option<Uuid>,
1001 #[serde(skip_serializing_if = "Option::is_none")]
1002 pub memo: Option<String>,
1003 #[serde(skip_serializing_if = "Option::is_none")]
1004 pub flag_color: Option<FlagColor>,
1005 #[serde(skip_serializing_if = "Option::is_none")]
1006 pub frequency: Option<Frequency>,
1007}
1008
1009#[derive(Debug, Serialize)]
1010struct ScheduledTransactionWrapper {
1011 scheduled_transaction: SaveScheduledTransaction,
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017 use crate::ynab::testutil::{
1018 TEST_ID_1, TEST_ID_2, TEST_ID_3, TEST_ID_4, TEST_ID_5, error_body,
1019 hybrid_transaction_fixture, new_test_client, scheduled_transaction_fixture,
1020 transaction_fixture,
1021 };
1022 use serde_json::json;
1023 use uuid::uuid;
1024 use wiremock::matchers::{method, path, query_param};
1025 use wiremock::{Mock, ResponseTemplate};
1026
1027 fn transactions_list_fixture() -> serde_json::Value {
1028 json!({ "data": { "transactions": [transaction_fixture()], "server_knowledge": 10 } })
1029 }
1030
1031 fn hybrid_transactions_list_fixture() -> serde_json::Value {
1032 json!({ "data": { "transactions": [hybrid_transaction_fixture()], "server_knowledge": 10 } })
1033 }
1034
1035 fn transaction_single_fixture() -> serde_json::Value {
1036 json!({ "data": { "transaction": transaction_fixture(), "server_knowledge": 10 } })
1037 }
1038
1039 fn transaction_single_fixture_with_id(id: &str) -> serde_json::Value {
1040 let mut tx = transaction_fixture();
1041 tx["id"] = json!(id);
1042 json!({ "data": { "transaction": tx, "server_knowledge": 10 } })
1043 }
1044
1045 fn save_transactions_fixture() -> serde_json::Value {
1046 json!({
1047 "data": {
1048 "transaction_ids": [TEST_ID_1],
1049 "transaction": transaction_fixture(),
1050 "transactions": [transaction_fixture()],
1051 "duplicate_import_ids": null,
1052 "server_knowledge": 10
1053 }
1054 })
1055 }
1056
1057 fn scheduled_transactions_list_fixture() -> serde_json::Value {
1058 json!({
1059 "data": {
1060 "scheduled_transactions": [scheduled_transaction_fixture()],
1061 "server_knowledge": 10
1062 }
1063 })
1064 }
1065
1066 fn scheduled_transaction_single_fixture() -> serde_json::Value {
1067 json!({ "data": { "scheduled_transaction": scheduled_transaction_fixture() } })
1068 }
1069
1070 fn import_transactions_fixture() -> serde_json::Value {
1071 json!({ "data": { "transaction_ids": [TEST_ID_1] } })
1072 }
1073
1074 #[tokio::test]
1075 async fn get_transactions_returns_transactions() {
1076 let (client, server) = new_test_client().await;
1077 Mock::given(method("GET"))
1078 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1079 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
1080 .expect(1)
1081 .mount(&server)
1082 .await;
1083 let (txs, sk) = client
1084 .get_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1085 .send()
1086 .await
1087 .unwrap();
1088 assert_eq!(txs.len(), 1);
1089 assert_eq!(txs[0].id, TEST_ID_1);
1090 assert_eq!(txs[0].amount, -50000);
1091 assert_eq!(sk, 10);
1092 }
1093
1094 #[tokio::test]
1095 async fn get_transaction_returns_transaction() {
1096 let (client, server) = new_test_client().await;
1097 Mock::given(method("GET"))
1098 .and(path(format!(
1099 "/plans/{}/transactions/{}",
1100 TEST_ID_1, TEST_ID_1
1101 )))
1102 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
1103 .expect(1)
1104 .mount(&server)
1105 .await;
1106 let (tx, sk) = client
1107 .get_transaction(PlanId::Id(uuid!(TEST_ID_1)), TEST_ID_1)
1108 .await
1109 .unwrap();
1110 assert_eq!(tx.id, TEST_ID_1);
1111 assert_eq!(tx.amount, -50000);
1112 assert_eq!(sk, 10);
1113 }
1114
1115 #[tokio::test]
1116 async fn get_transactions_by_account_returns_transactions() {
1117 let (client, server) = new_test_client().await;
1118 Mock::given(method("GET"))
1119 .and(path(format!(
1120 "/plans/{}/accounts/{}/transactions",
1121 TEST_ID_1, TEST_ID_1
1122 )))
1123 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
1124 .expect(1)
1125 .mount(&server)
1126 .await;
1127 let (txs, _) = client
1128 .get_transactions_by_account(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_1))
1129 .send()
1130 .await
1131 .unwrap();
1132 assert_eq!(txs.len(), 1);
1133 }
1134
1135 #[tokio::test]
1136 async fn get_transactions_by_category_returns_transactions() {
1137 let (client, server) = new_test_client().await;
1138 Mock::given(method("GET"))
1139 .and(path(format!(
1140 "/plans/{}/categories/{}/transactions",
1141 TEST_ID_1, TEST_ID_1
1142 )))
1143 .respond_with(
1144 ResponseTemplate::new(200).set_body_json(hybrid_transactions_list_fixture()),
1145 )
1146 .expect(1)
1147 .mount(&server)
1148 .await;
1149 let (txs, _) = client
1150 .get_transactions_by_category(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_1))
1151 .send()
1152 .await
1153 .unwrap();
1154 assert_eq!(txs.len(), 1);
1155 }
1156
1157 #[tokio::test]
1158 async fn get_transactions_by_payee_returns_transactions() {
1159 let (client, server) = new_test_client().await;
1160 Mock::given(method("GET"))
1161 .and(path(format!(
1162 "/plans/{}/payees/{}/transactions",
1163 TEST_ID_1, TEST_ID_3
1164 )))
1165 .respond_with(
1166 ResponseTemplate::new(200).set_body_json(hybrid_transactions_list_fixture()),
1167 )
1168 .expect(1)
1169 .mount(&server)
1170 .await;
1171 let (txs, _) = client
1172 .get_transactions_by_payee(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_3))
1173 .send()
1174 .await
1175 .unwrap();
1176 assert_eq!(txs.len(), 1);
1177 }
1178
1179 #[tokio::test]
1180 async fn get_transactions_by_month_returns_transactions() {
1181 let (client, server) = new_test_client().await;
1182 let month = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
1183 Mock::given(method("GET"))
1184 .and(path(format!(
1185 "/plans/{}/months/{}/transactions",
1186 TEST_ID_1, month
1187 )))
1188 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
1189 .expect(1)
1190 .mount(&server)
1191 .await;
1192 let (txs, _) = client
1193 .get_transactions_by_month(PlanId::Id(uuid!(TEST_ID_1)), month)
1194 .send()
1195 .await
1196 .unwrap();
1197 assert_eq!(txs.len(), 1);
1198 }
1199
1200 #[tokio::test]
1201 async fn get_transactions_sends_filter_query_params() {
1202 let (client, server) = new_test_client().await;
1203 Mock::given(method("GET"))
1204 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1205 .and(query_param("since_date", "2024-01-01"))
1206 .and(query_param("type", "unapproved"))
1207 .respond_with(ResponseTemplate::new(200).set_body_json(transactions_list_fixture()))
1208 .expect(1)
1209 .mount(&server)
1210 .await;
1211 let (txs, _) = client
1212 .get_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1213 .since_date(NaiveDate::from_ymd_opt(2024, 1, 1).unwrap())
1214 .transaction_type(TransactionType::Unapproved)
1215 .send()
1216 .await
1217 .unwrap();
1218 assert_eq!(txs.len(), 1);
1219 }
1220
1221 #[tokio::test]
1222 async fn create_transaction_succeeds() {
1223 let (client, server) = new_test_client().await;
1224 Mock::given(method("POST"))
1225 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1226 .respond_with(ResponseTemplate::new(201).set_body_json(save_transactions_fixture()))
1227 .expect(1)
1228 .mount(&server)
1229 .await;
1230 let resp = client
1231 .create_transaction(
1232 PlanId::Id(uuid!(TEST_ID_1)),
1233 NewTransaction {
1234 account_id: uuid!(TEST_ID_1),
1235 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1236 amount: -50000,
1237 memo: None,
1238 cleared: Some(ClearedStatus::Cleared),
1239 approved: Some(true),
1240 payee_id: None,
1241 payee_name: None,
1242 category_id: None,
1243 flag_color: None,
1244 import_id: None,
1245 subtransactions: None,
1246 },
1247 )
1248 .await
1249 .unwrap();
1250 assert_eq!(resp.transaction_ids, vec![TEST_ID_1]);
1251 assert_eq!(resp.transaction.amount, -50000);
1252 }
1253
1254 #[tokio::test]
1255 async fn create_transactions_succeeds() {
1256 let (client, server) = new_test_client().await;
1257 Mock::given(method("POST"))
1258 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1259 .respond_with(ResponseTemplate::new(201).set_body_json(save_transactions_fixture()))
1260 .expect(1)
1261 .mount(&server)
1262 .await;
1263 let resp = client
1264 .create_transactions(
1265 PlanId::Id(uuid!(TEST_ID_1)),
1266 vec![NewTransaction {
1267 account_id: uuid!(TEST_ID_1),
1268 date: NaiveDate::from_ymd_opt(2024, 1, 15).unwrap(),
1269 amount: -50000,
1270 memo: None,
1271 cleared: Some(ClearedStatus::Cleared),
1272 approved: Some(true),
1273 payee_id: None,
1274 payee_name: None,
1275 category_id: None,
1276 flag_color: None,
1277 import_id: None,
1278 subtransactions: None,
1279 }],
1280 )
1281 .await
1282 .unwrap();
1283 assert_eq!(resp.transaction_ids, vec![TEST_ID_1]);
1284 }
1285
1286 #[tokio::test]
1287 async fn update_transaction_succeeds() {
1288 let (client, server) = new_test_client().await;
1289 Mock::given(method("PUT"))
1290 .and(path(format!(
1291 "/plans/{}/transactions/{}",
1292 TEST_ID_1, TEST_ID_1
1293 )))
1294 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
1295 .expect(1)
1296 .mount(&server)
1297 .await;
1298 let (tx, sk) = client
1299 .update_transaction(
1300 PlanId::Id(uuid!(TEST_ID_1)),
1301 TEST_ID_1,
1302 ExistingTransaction {
1303 amount: Some(-50000),
1304 account_id: None,
1305 date: None,
1306 payee_id: None,
1307 payee_name: None,
1308 category_id: None,
1309 memo: None,
1310 cleared: None,
1311 approved: None,
1312 flag_color: None,
1313 subtransactions: None,
1314 },
1315 )
1316 .await
1317 .unwrap();
1318 assert_eq!(tx.id, TEST_ID_1);
1319 assert_eq!(sk, 10);
1320 }
1321
1322 #[tokio::test]
1323 async fn update_transactions_succeeds() {
1324 let (client, server) = new_test_client().await;
1325 Mock::given(method("PATCH"))
1326 .and(path(format!("/plans/{}/transactions", TEST_ID_1)))
1327 .respond_with(ResponseTemplate::new(200).set_body_json(save_transactions_fixture()))
1328 .expect(1)
1329 .mount(&server)
1330 .await;
1331 let resp = client
1332 .update_transactions(
1333 PlanId::Id(uuid!(TEST_ID_1)),
1334 vec![SaveTransactionWithIdOrImportId {
1335 id: Some(String::new()),
1336 memo: Some("updated".to_string()),
1337 import_id: None,
1338 account_id: None,
1339 date: None,
1340 amount: None,
1341 payee_id: None,
1342 payee_name: None,
1343 category_id: None,
1344 cleared: None,
1345 approved: None,
1346 flag_color: None,
1347 subtransactions: None,
1348 }],
1349 )
1350 .await
1351 .unwrap();
1352 assert_eq!(resp.transaction_ids, vec![TEST_ID_1]);
1353 }
1354
1355 #[tokio::test]
1356 async fn delete_transaction_succeeds() {
1357 let (client, server) = new_test_client().await;
1358 Mock::given(method("DELETE"))
1359 .and(path(format!(
1360 "/plans/{}/transactions/{}",
1361 TEST_ID_1, TEST_ID_1
1362 )))
1363 .respond_with(ResponseTemplate::new(200).set_body_json(transaction_single_fixture()))
1364 .expect(1)
1365 .mount(&server)
1366 .await;
1367 let (tx, sk) = client
1368 .delete_transaction(PlanId::Id(uuid!(TEST_ID_1)), TEST_ID_1)
1369 .await
1370 .unwrap();
1371 assert_eq!(tx.id, TEST_ID_1);
1372 assert_eq!(sk, 10);
1373 }
1374
1375 #[tokio::test]
1376 async fn delete_transactions_bulk_returns_results_in_order() {
1377 let (client, server) = new_test_client().await;
1378 let plan_id = PlanId::Id(uuid!(TEST_ID_5));
1379 let tx_ids = [TEST_ID_1, TEST_ID_2, TEST_ID_4];
1380
1381 for id in tx_ids {
1382 Mock::given(method("DELETE"))
1383 .and(path(format!("/plans/{}/transactions/{}", TEST_ID_5, id)))
1384 .respond_with(
1385 ResponseTemplate::new(200)
1386 .set_body_json(transaction_single_fixture_with_id(id)),
1387 )
1388 .expect(1)
1389 .mount(&server)
1390 .await;
1391 }
1392
1393 let results = client.delete_transactions_bulk(plan_id, &tx_ids, 2).await;
1394
1395 assert_eq!(results.len(), 3);
1396 for (result, expected_id) in results.into_iter().zip(tx_ids) {
1397 let (tx, sk) = result.unwrap();
1398 assert_eq!(tx.id, expected_id);
1399 assert_eq!(sk, 10);
1400 }
1401 }
1402
1403 #[tokio::test]
1404 async fn delete_transactions_bulk_preserves_order_on_partial_failure() {
1405 let (client, server) = new_test_client().await;
1406 let plan_id = PlanId::Id(uuid!(TEST_ID_5));
1407 let tx_ids = [TEST_ID_1, TEST_ID_2];
1408
1409 Mock::given(method("DELETE"))
1410 .and(path(format!(
1411 "/plans/{}/transactions/{}",
1412 TEST_ID_5, TEST_ID_1
1413 )))
1414 .respond_with(
1415 ResponseTemplate::new(200)
1416 .set_body_json(transaction_single_fixture_with_id(TEST_ID_1)),
1417 )
1418 .expect(1)
1419 .mount(&server)
1420 .await;
1421 Mock::given(method("DELETE"))
1422 .and(path(format!(
1423 "/plans/{}/transactions/{}",
1424 TEST_ID_5, TEST_ID_2
1425 )))
1426 .respond_with(ResponseTemplate::new(404).set_body_json(error_body(
1427 "404",
1428 "not_found",
1429 "Transaction not found",
1430 )))
1431 .expect(1)
1432 .mount(&server)
1433 .await;
1434
1435 let results = client.delete_transactions_bulk(plan_id, &tx_ids, 2).await;
1436
1437 assert_eq!(results.len(), 2);
1438 let (tx, _) = results[0].as_ref().unwrap();
1439 assert_eq!(tx.id, TEST_ID_1);
1440 assert!(matches!(results[1], Err(Error::NotFound(_))));
1441 }
1442
1443 #[tokio::test]
1444 async fn import_transactions_returns_ids() {
1445 let (client, server) = new_test_client().await;
1446 Mock::given(method("POST"))
1447 .and(path(format!("/plans/{}/transactions/import", TEST_ID_1)))
1448 .respond_with(ResponseTemplate::new(200).set_body_json(import_transactions_fixture()))
1449 .expect(1)
1450 .mount(&server)
1451 .await;
1452 let ids = client
1453 .import_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1454 .await
1455 .unwrap();
1456 assert_eq!(ids.len(), 1);
1457 assert_eq!(ids[0].to_string(), TEST_ID_1);
1458 }
1459
1460 #[tokio::test]
1461 async fn get_scheduled_transactions_returns_transactions() {
1462 let (client, server) = new_test_client().await;
1463 Mock::given(method("GET"))
1464 .and(path(format!("/plans/{}/scheduled_transactions", TEST_ID_1)))
1465 .respond_with(
1466 ResponseTemplate::new(200).set_body_json(scheduled_transactions_list_fixture()),
1467 )
1468 .expect(1)
1469 .mount(&server)
1470 .await;
1471 let (txs, sk) = client
1472 .get_scheduled_transactions(PlanId::Id(uuid!(TEST_ID_1)))
1473 .send()
1474 .await
1475 .unwrap();
1476 assert_eq!(txs.len(), 1);
1477 assert_eq!(txs[0].id.to_string(), TEST_ID_4);
1478 assert_eq!(sk, 10);
1479 }
1480
1481 #[tokio::test]
1482 async fn get_scheduled_transaction_returns_transaction() {
1483 let (client, server) = new_test_client().await;
1484 Mock::given(method("GET"))
1485 .and(path(format!(
1486 "/plans/{}/scheduled_transactions/{}",
1487 TEST_ID_1, TEST_ID_4
1488 )))
1489 .respond_with(
1490 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1491 )
1492 .expect(1)
1493 .mount(&server)
1494 .await;
1495 let tx = client
1496 .get_scheduled_transaction(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
1497 .await
1498 .unwrap();
1499 assert_eq!(tx.id.to_string(), TEST_ID_4);
1500 assert!(matches!(tx.frequency, Frequency::Monthly));
1501 }
1502
1503 #[tokio::test]
1504 async fn create_scheduled_transaction_succeeds() {
1505 let (client, server) = new_test_client().await;
1506 Mock::given(method("POST"))
1507 .and(path(format!("/plans/{}/scheduled_transactions", TEST_ID_1)))
1508 .respond_with(
1509 ResponseTemplate::new(201).set_body_json(scheduled_transaction_single_fixture()),
1510 )
1511 .expect(1)
1512 .mount(&server)
1513 .await;
1514 let tx = client
1515 .create_scheduled_transaction(
1516 PlanId::Id(uuid!(TEST_ID_1)),
1517 SaveScheduledTransaction {
1518 account_id: uuid!(TEST_ID_1),
1519 date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1520 amount: Some(-50000),
1521 frequency: Some(Frequency::Monthly),
1522 memo: None,
1523 payee_id: None,
1524 payee_name: None,
1525 category_id: None,
1526 flag_color: None,
1527 },
1528 )
1529 .await
1530 .unwrap();
1531 assert_eq!(tx.id.to_string(), TEST_ID_4);
1532 assert_eq!(tx.amount, -50000);
1533 }
1534
1535 #[tokio::test]
1536 async fn update_scheduled_transaction_succeeds() {
1537 let (client, server) = new_test_client().await;
1538 Mock::given(method("PUT"))
1539 .and(path(format!(
1540 "/plans/{}/scheduled_transactions/{}",
1541 TEST_ID_1, TEST_ID_4
1542 )))
1543 .respond_with(
1544 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1545 )
1546 .expect(1)
1547 .mount(&server)
1548 .await;
1549 let tx = client
1550 .update_scheduled_transaction(
1551 PlanId::Id(uuid!(TEST_ID_1)),
1552 uuid!(TEST_ID_4),
1553 SaveScheduledTransaction {
1554 account_id: uuid!(TEST_ID_1),
1555 date: NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(),
1556 amount: Some(-50000),
1557 frequency: Some(Frequency::Monthly),
1558 memo: None,
1559 payee_id: None,
1560 payee_name: None,
1561 category_id: None,
1562 flag_color: None,
1563 },
1564 )
1565 .await
1566 .unwrap();
1567 assert_eq!(tx.id.to_string(), TEST_ID_4);
1568 }
1569
1570 #[tokio::test]
1571 async fn delete_scheduled_transaction_succeeds() {
1572 let (client, server) = new_test_client().await;
1573 Mock::given(method("DELETE"))
1574 .and(path(format!(
1575 "/plans/{}/scheduled_transactions/{}",
1576 TEST_ID_1, TEST_ID_4
1577 )))
1578 .respond_with(
1579 ResponseTemplate::new(200).set_body_json(scheduled_transaction_single_fixture()),
1580 )
1581 .expect(1)
1582 .mount(&server)
1583 .await;
1584 let tx = client
1585 .delete_scheduled_transaction(PlanId::Id(uuid!(TEST_ID_1)), uuid!(TEST_ID_4))
1586 .await
1587 .unwrap();
1588 assert_eq!(tx.id.to_string(), TEST_ID_4);
1589 }
1590
1591 #[tokio::test]
1592 async fn get_transaction_returns_not_found() {
1593 let (client, server) = new_test_client().await;
1594 Mock::given(method("GET"))
1595 .and(path(format!(
1596 "/plans/{}/transactions/{}",
1597 TEST_ID_1, TEST_ID_1
1598 )))
1599 .respond_with(ResponseTemplate::new(404).set_body_json(error_body(
1600 "404",
1601 "not_found",
1602 "Transaction not found",
1603 )))
1604 .mount(&server)
1605 .await;
1606 let err = client
1607 .get_transaction(PlanId::Id(uuid!(TEST_ID_1)), TEST_ID_1)
1608 .await
1609 .unwrap_err();
1610 assert!(matches!(err, Error::NotFound(_)));
1611 }
1612}