1use std::collections::HashMap;
49
50use chrono::{DateTime, Utc};
51use serde::{Deserialize, Serialize};
52
53use crate::clients::RestClient;
54use crate::rest::{
55 build_path, get_path, ResourceError, ResourceOperation, ResourcePath, ResourceResponse,
56 RestResource,
57};
58use crate::HttpMethod;
59
60use super::common::ChargeCurrency;
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
88pub struct UsageCharge {
89 #[serde(skip_serializing)]
92 pub id: Option<u64>,
93
94 #[serde(skip_serializing_if = "Option::is_none")]
97 pub recurring_application_charge_id: Option<u64>,
98
99 #[serde(skip_serializing_if = "Option::is_none")]
102 pub description: Option<String>,
103
104 #[serde(skip_serializing_if = "Option::is_none")]
107 pub price: Option<String>,
108
109 #[serde(skip_serializing)]
112 pub currency: Option<ChargeCurrency>,
113
114 #[serde(skip_serializing)]
117 pub created_at: Option<DateTime<Utc>>,
118
119 #[serde(skip_serializing)]
122 pub updated_at: Option<DateTime<Utc>>,
123}
124
125impl UsageCharge {
126 pub async fn count_with_parent(
149 client: &RestClient,
150 recurring_application_charge_id: u64,
151 _params: Option<()>,
152 ) -> Result<u64, ResourceError> {
153 let mut ids: HashMap<&str, String> = HashMap::new();
154 ids.insert(
155 "recurring_application_charge_id",
156 recurring_application_charge_id.to_string(),
157 );
158
159 let available_ids: Vec<&str> = ids.keys().copied().collect();
160 let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
161 ResourceError::PathResolutionFailed {
162 resource: Self::NAME,
163 operation: "count",
164 },
165 )?;
166
167 let url = build_path(path.template, &ids);
168 let response = client.get(&url, None).await?;
169
170 if !response.is_ok() {
171 return Err(ResourceError::from_http_response(
172 response.code,
173 &response.body,
174 Self::NAME,
175 None,
176 response.request_id(),
177 ));
178 }
179
180 let count = response
181 .body
182 .get("count")
183 .and_then(serde_json::Value::as_u64)
184 .ok_or_else(|| {
185 ResourceError::Http(crate::clients::HttpError::Response(
186 crate::clients::HttpResponseError {
187 code: response.code,
188 message: "Missing 'count' in response".to_string(),
189 error_reference: response.request_id().map(ToString::to_string),
190 },
191 ))
192 })?;
193
194 Ok(count)
195 }
196
197 pub async fn find_with_parent(
216 client: &RestClient,
217 recurring_application_charge_id: u64,
218 id: u64,
219 _params: Option<UsageChargeFindParams>,
220 ) -> Result<ResourceResponse<Self>, ResourceError> {
221 let mut ids: HashMap<&str, String> = HashMap::new();
222 ids.insert(
223 "recurring_application_charge_id",
224 recurring_application_charge_id.to_string(),
225 );
226 ids.insert("id", id.to_string());
227
228 let available_ids: Vec<&str> = ids.keys().copied().collect();
229 let path = get_path(Self::PATHS, ResourceOperation::Find, &available_ids).ok_or(
230 ResourceError::PathResolutionFailed {
231 resource: Self::NAME,
232 operation: "find",
233 },
234 )?;
235
236 let url = build_path(path.template, &ids);
237 let response = client.get(&url, None).await?;
238
239 if !response.is_ok() {
240 return Err(ResourceError::from_http_response(
241 response.code,
242 &response.body,
243 Self::NAME,
244 Some(&id.to_string()),
245 response.request_id(),
246 ));
247 }
248
249 let key = Self::resource_key();
250 ResourceResponse::from_http_response(response, &key)
251 }
252}
253
254impl RestResource for UsageCharge {
255 type Id = u64;
256 type FindParams = UsageChargeFindParams;
257 type AllParams = UsageChargeListParams;
258 type CountParams = ();
259
260 const NAME: &'static str = "UsageCharge";
261 const PLURAL: &'static str = "usage_charges";
262
263 const PATHS: &'static [ResourcePath] = &[
270 ResourcePath::new(
271 HttpMethod::Get,
272 ResourceOperation::Find,
273 &["recurring_application_charge_id", "id"],
274 "recurring_application_charges/{recurring_application_charge_id}/usage_charges/{id}",
275 ),
276 ResourcePath::new(
277 HttpMethod::Get,
278 ResourceOperation::All,
279 &["recurring_application_charge_id"],
280 "recurring_application_charges/{recurring_application_charge_id}/usage_charges",
281 ),
282 ResourcePath::new(
283 HttpMethod::Post,
284 ResourceOperation::Create,
285 &["recurring_application_charge_id"],
286 "recurring_application_charges/{recurring_application_charge_id}/usage_charges",
287 ),
288 ];
291
292 fn get_id(&self) -> Option<Self::Id> {
293 self.id
294 }
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
299pub struct UsageChargeFindParams {
300 #[serde(skip_serializing_if = "Option::is_none")]
302 pub fields: Option<String>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
307pub struct UsageChargeListParams {
308 #[serde(skip_serializing_if = "Option::is_none")]
310 pub limit: Option<u32>,
311
312 #[serde(skip_serializing_if = "Option::is_none")]
314 pub since_id: Option<u64>,
315
316 #[serde(skip_serializing_if = "Option::is_none")]
318 pub fields: Option<String>,
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use crate::rest::{get_path, ResourceOperation};
325
326 #[test]
327 fn test_usage_charge_serialization() {
328 let charge = UsageCharge {
329 id: Some(12345),
330 recurring_application_charge_id: Some(455696195),
331 description: Some("100 emails sent".to_string()),
332 price: Some("1.00".to_string()),
333 currency: Some(ChargeCurrency::new("USD")),
334 created_at: Some(
335 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
336 .unwrap()
337 .with_timezone(&Utc),
338 ),
339 updated_at: Some(
340 DateTime::parse_from_rfc3339("2024-01-15T10:35:00Z")
341 .unwrap()
342 .with_timezone(&Utc),
343 ),
344 };
345
346 let json = serde_json::to_string(&charge).unwrap();
347 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
348
349 assert_eq!(parsed["recurring_application_charge_id"], 455696195);
351 assert_eq!(parsed["description"], "100 emails sent");
352 assert_eq!(parsed["price"], "1.00");
353
354 assert!(parsed.get("id").is_none());
356 assert!(parsed.get("currency").is_none());
357 assert!(parsed.get("created_at").is_none());
358 assert!(parsed.get("updated_at").is_none());
359 }
360
361 #[test]
362 fn test_usage_charge_deserialization() {
363 let json = r#"{
364 "id": 1034618207,
365 "recurring_application_charge_id": 455696195,
366 "description": "Super Mega Plan 1000 emails",
367 "price": "1.00",
368 "currency": {
369 "currency": "USD"
370 },
371 "created_at": "2024-01-15T10:30:00Z",
372 "updated_at": "2024-01-15T10:35:00Z"
373 }"#;
374
375 let charge: UsageCharge = serde_json::from_str(json).unwrap();
376
377 assert_eq!(charge.id, Some(1034618207));
378 assert_eq!(charge.recurring_application_charge_id, Some(455696195));
379 assert_eq!(
380 charge.description,
381 Some("Super Mega Plan 1000 emails".to_string())
382 );
383 assert_eq!(charge.price, Some("1.00".to_string()));
384 assert_eq!(charge.currency.as_ref().unwrap().code(), Some("USD"));
385 assert!(charge.created_at.is_some());
386 assert!(charge.updated_at.is_some());
387 }
388
389 #[test]
390 fn test_usage_charge_nested_paths() {
391 let find_path = get_path(
395 UsageCharge::PATHS,
396 ResourceOperation::Find,
397 &["recurring_application_charge_id", "id"],
398 );
399 assert!(find_path.is_some());
400 assert_eq!(
401 find_path.unwrap().template,
402 "recurring_application_charges/{recurring_application_charge_id}/usage_charges/{id}"
403 );
404
405 let find_without_parent = get_path(UsageCharge::PATHS, ResourceOperation::Find, &["id"]);
407 assert!(find_without_parent.is_none());
408
409 let all_path = get_path(
411 UsageCharge::PATHS,
412 ResourceOperation::All,
413 &["recurring_application_charge_id"],
414 );
415 assert!(all_path.is_some());
416 assert_eq!(
417 all_path.unwrap().template,
418 "recurring_application_charges/{recurring_application_charge_id}/usage_charges"
419 );
420
421 let all_without_parent = get_path(UsageCharge::PATHS, ResourceOperation::All, &[]);
423 assert!(all_without_parent.is_none());
424
425 let create_path = get_path(
427 UsageCharge::PATHS,
428 ResourceOperation::Create,
429 &["recurring_application_charge_id"],
430 );
431 assert!(create_path.is_some());
432 assert_eq!(
433 create_path.unwrap().template,
434 "recurring_application_charges/{recurring_application_charge_id}/usage_charges"
435 );
436 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
437
438 let update_path = get_path(
440 UsageCharge::PATHS,
441 ResourceOperation::Update,
442 &["recurring_application_charge_id", "id"],
443 );
444 assert!(update_path.is_none());
445
446 let delete_path = get_path(
448 UsageCharge::PATHS,
449 ResourceOperation::Delete,
450 &["recurring_application_charge_id", "id"],
451 );
452 assert!(delete_path.is_none());
453 }
454
455 #[test]
456 fn test_usage_charge_list_params() {
457 let params = UsageChargeListParams {
458 limit: Some(50),
459 since_id: Some(100),
460 fields: Some("id,description,price".to_string()),
461 };
462
463 let json = serde_json::to_value(¶ms).unwrap();
464 assert_eq!(json["limit"], 50);
465 assert_eq!(json["since_id"], 100);
466 assert_eq!(json["fields"], "id,description,price");
467
468 let empty_params = UsageChargeListParams::default();
470 let empty_json = serde_json::to_value(&empty_params).unwrap();
471 assert_eq!(empty_json, serde_json::json!({}));
472 }
473
474 #[test]
475 fn test_usage_charge_constants() {
476 assert_eq!(UsageCharge::NAME, "UsageCharge");
477 assert_eq!(UsageCharge::PLURAL, "usage_charges");
478 }
479
480 #[test]
481 fn test_usage_charge_get_id() {
482 let charge_with_id = UsageCharge {
483 id: Some(12345),
484 ..Default::default()
485 };
486 assert_eq!(charge_with_id.get_id(), Some(12345));
487
488 let charge_without_id = UsageCharge::default();
489 assert_eq!(charge_without_id.get_id(), None);
490 }
491
492 #[test]
493 fn test_usage_charge_with_currency_nested_object() {
494 let json = r#"{
496 "id": 123,
497 "recurring_application_charge_id": 456,
498 "description": "Test charge",
499 "price": "5.00",
500 "currency": {
501 "currency": "EUR"
502 }
503 }"#;
504
505 let charge: UsageCharge = serde_json::from_str(json).unwrap();
506 assert!(charge.currency.is_some());
507 let currency = charge.currency.unwrap();
508 assert_eq!(currency.code(), Some("EUR"));
509 }
510}