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