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#[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#[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 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 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 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 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}