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