Skip to main content

rust_ynab/ynab/
movements.rs

1use chrono::{DateTime, 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)]
11struct MoneyMovementsDataEnvelope {
12    data: MoneyMovementsData,
13}
14
15#[derive(Debug, Deserialize)]
16struct MoneyMovementsData {
17    money_movements: Vec<MoneyMovement>,
18    server_knowledge: i64,
19}
20
21#[derive(Debug, Deserialize)]
22struct MoneyMovementGroupsDataEnvelope {
23    data: MoneyMovementGroupsData,
24}
25
26#[derive(Debug, Deserialize)]
27struct MoneyMovementGroupsData {
28    money_movement_groups: Vec<MoneyMovementGroup>,
29    server_knowledge: i64,
30}
31
32/// A movement of money between categories. Amounts are in milliunits (divide by 1000 for display).
33#[derive(Debug, Serialize, Deserialize)]
34pub struct MoneyMovement {
35    pub id: Uuid,
36    pub month: Option<NaiveDate>,
37    pub moved_at: Option<DateTime<chrono::Utc>>,
38    pub note: Option<String>,
39    pub money_movement_group_id: Option<Uuid>,
40    pub performed_by_user_id: Option<Uuid>,
41    pub from_category_id: Option<Uuid>,
42    pub to_category_id: Option<Uuid>,
43    pub amount: i64,
44}
45
46/// A group of related money movements.
47#[derive(Debug, Serialize, Deserialize)]
48pub struct MoneyMovementGroup {
49    pub id: Uuid,
50    pub group_created_at: DateTime<chrono::Utc>,
51    pub month: NaiveDate,
52    pub note: Option<String>,
53    pub performed_by_user_id: Option<Uuid>,
54}
55
56#[derive(Debug)]
57pub struct GetMoneyMovementsBuilder<'a> {
58    client: &'a Client,
59    plan_id: PlanId,
60    last_knowledge_of_server: Option<i64>,
61}
62
63impl<'a> GetMoneyMovementsBuilder<'a> {
64    pub fn with_server_knowledge(mut self, sk: i64) -> Self {
65        self.last_knowledge_of_server = Some(sk);
66        self
67    }
68
69    pub async fn send(self) -> Result<(Vec<MoneyMovement>, i64), Error> {
70        let params: Option<&[(&str, &str)]> = if let Some(sk) = self.last_knowledge_of_server {
71            Some(&[("last_knowledge_of_server", &sk.to_string())])
72        } else {
73            None
74        };
75        let result: MoneyMovementsDataEnvelope = self
76            .client
77            .get(&format!("plans/{}/money_movements", self.plan_id), params)
78            .await?;
79        Ok((result.data.money_movements, result.data.server_knowledge))
80    }
81}
82
83#[derive(Debug)]
84pub struct GetMoneyMovementGroupsBuilder<'a> {
85    client: &'a Client,
86    plan_id: PlanId,
87    last_knowledge_of_server: Option<i64>,
88}
89
90impl<'a> GetMoneyMovementGroupsBuilder<'a> {
91    pub fn with_server_knowledge(mut self, sk: i64) -> Self {
92        self.last_knowledge_of_server = Some(sk);
93        self
94    }
95
96    pub async fn send(self) -> Result<(Vec<MoneyMovementGroup>, i64), Error> {
97        let params: Option<&[(&str, &str)]> = if let Some(sk) = self.last_knowledge_of_server {
98            Some(&[("last_knowledge_of_server", &sk.to_string())])
99        } else {
100            None
101        };
102        let result: MoneyMovementGroupsDataEnvelope = self
103            .client
104            .get(
105                &format!("plans/{}/money_movement_groups", self.plan_id),
106                params,
107            )
108            .await?;
109        Ok((
110            result.data.money_movement_groups,
111            result.data.server_knowledge,
112        ))
113    }
114}
115
116impl Client {
117    /// Returns all money movements. The second return value is server knowledge for delta requests.
118    pub fn get_money_movements(&self, plan_id: PlanId) -> GetMoneyMovementsBuilder<'_> {
119        GetMoneyMovementsBuilder {
120            client: self,
121            plan_id,
122            last_knowledge_of_server: None,
123        }
124    }
125
126    /// Returns all money movements for a specific month. The second return value is server
127    /// knowledge for delta requests.
128    pub async fn get_money_movements_by_month(
129        &self,
130        plan_id: PlanId,
131        month: NaiveDate,
132    ) -> Result<(Vec<MoneyMovement>, i64), Error> {
133        let result: MoneyMovementsDataEnvelope = self
134            .get(
135                &format!("plans/{}/months/{}/money_movements", plan_id, month),
136                NO_PARAMS,
137            )
138            .await?;
139        Ok((result.data.money_movements, result.data.server_knowledge))
140    }
141
142    /// Returns all money movement groups. The second return value is server knowledge for delta
143    /// requests.
144    pub fn get_money_movement_groups(&self, plan_id: PlanId) -> GetMoneyMovementGroupsBuilder<'_> {
145        GetMoneyMovementGroupsBuilder {
146            client: self,
147            plan_id,
148            last_knowledge_of_server: None,
149        }
150    }
151
152    /// Returns all money movement groups for a specific month. The second return value is server
153    /// knowledge for delta requests.
154    pub async fn get_money_movement_groups_by_month(
155        &self,
156        plan_id: PlanId,
157        month: NaiveDate,
158    ) -> Result<(Vec<MoneyMovementGroup>, i64), Error> {
159        let result: MoneyMovementGroupsDataEnvelope = self
160            .get(
161                &format!("plans/{}/months/{}/money_movement_groups", plan_id, month),
162                NO_PARAMS,
163            )
164            .await?;
165        Ok((
166            result.data.money_movement_groups,
167            result.data.server_knowledge,
168        ))
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::ynab::testutil::{TEST_ID_1, TEST_ID_2, new_test_client};
176    use serde_json::json;
177    use uuid::uuid;
178    use wiremock::matchers::{method, path};
179    use wiremock::{Mock, ResponseTemplate};
180
181    fn movement_fixture() -> serde_json::Value {
182        json!({
183            "id": TEST_ID_1,
184            "month": "2024-01-01",
185            "moved_at": null,
186            "note": null,
187            "money_movement_group_id": null,
188            "performed_by_user_id": null,
189            "from_category_id": TEST_ID_2,
190            "to_category_id": TEST_ID_1,
191            "amount": 10000
192        })
193    }
194
195    fn movement_group_fixture() -> serde_json::Value {
196        json!({
197            "id": TEST_ID_1,
198            "group_created_at": "2024-01-01T00:00:00Z",
199            "month": "2024-01-01",
200            "note": null,
201            "performed_by_user_id": null
202        })
203    }
204
205    fn movements_list_fixture() -> serde_json::Value {
206        json!({ "data": { "money_movements": [movement_fixture()], "server_knowledge": 4 } })
207    }
208
209    fn movement_groups_list_fixture() -> serde_json::Value {
210        json!({ "data": { "money_movement_groups": [movement_group_fixture()], "server_knowledge": 4 } })
211    }
212
213    #[tokio::test]
214    async fn get_money_movements_returns_movements() {
215        let (client, server) = new_test_client().await;
216        Mock::given(method("GET"))
217            .and(path(format!("/plans/{}/money_movements", TEST_ID_1)))
218            .respond_with(ResponseTemplate::new(200).set_body_json(movements_list_fixture()))
219            .expect(1)
220            .mount(&server)
221            .await;
222        let (movements, sk) = client
223            .get_money_movements(PlanId::Id(uuid!(TEST_ID_1)))
224            .send()
225            .await
226            .unwrap();
227        assert_eq!(movements.len(), 1);
228        assert_eq!(movements[0].id.to_string(), TEST_ID_1);
229        assert_eq!(movements[0].amount, 10000);
230        assert_eq!(sk, 4);
231    }
232
233    #[tokio::test]
234    async fn get_money_movements_by_month_returns_movements() {
235        let (client, server) = new_test_client().await;
236        let month = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
237        Mock::given(method("GET"))
238            .and(path(format!(
239                "/plans/{}/months/{}/money_movements",
240                TEST_ID_1, month
241            )))
242            .respond_with(ResponseTemplate::new(200).set_body_json(movements_list_fixture()))
243            .expect(1)
244            .mount(&server)
245            .await;
246        let (movements, sk) = client
247            .get_money_movements_by_month(PlanId::Id(uuid!(TEST_ID_1)), month)
248            .await
249            .unwrap();
250        assert_eq!(movements.len(), 1);
251        assert_eq!(sk, 4);
252    }
253
254    #[tokio::test]
255    async fn get_money_movement_groups_returns_groups() {
256        let (client, server) = new_test_client().await;
257        Mock::given(method("GET"))
258            .and(path(format!("/plans/{}/money_movement_groups", TEST_ID_1)))
259            .respond_with(ResponseTemplate::new(200).set_body_json(movement_groups_list_fixture()))
260            .expect(1)
261            .mount(&server)
262            .await;
263        let (groups, sk) = client
264            .get_money_movement_groups(PlanId::Id(uuid!(TEST_ID_1)))
265            .send()
266            .await
267            .unwrap();
268        assert_eq!(groups.len(), 1);
269        assert_eq!(groups[0].id.to_string(), TEST_ID_1);
270        assert_eq!(sk, 4);
271    }
272
273    #[tokio::test]
274    async fn get_money_movement_groups_by_month_returns_groups() {
275        let (client, server) = new_test_client().await;
276        let month = NaiveDate::from_ymd_opt(2024, 1, 1).unwrap();
277        Mock::given(method("GET"))
278            .and(path(format!(
279                "/plans/{}/months/{}/money_movement_groups",
280                TEST_ID_1, month
281            )))
282            .respond_with(ResponseTemplate::new(200).set_body_json(movement_groups_list_fixture()))
283            .expect(1)
284            .mount(&server)
285            .await;
286        let (groups, sk) = client
287            .get_money_movement_groups_by_month(PlanId::Id(uuid!(TEST_ID_1)), month)
288            .await
289            .unwrap();
290        assert_eq!(groups.len(), 1);
291        assert_eq!(sk, 4);
292    }
293}