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> {
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 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 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 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 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 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}