1use std::collections::HashMap;
50
51use chrono::{DateTime, Utc};
52use serde::{Deserialize, Serialize};
53
54use crate::clients::RestClient;
55use crate::rest::{
56 build_path, get_path, ResourceError, ResourceOperation, ResourcePath, ResourceResponse,
57 RestResource,
58};
59use crate::HttpMethod;
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
86pub struct DiscountCode {
87 #[serde(skip_serializing)]
90 pub id: Option<u64>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
95 pub price_rule_id: Option<u64>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub code: Option<String>,
100
101 #[serde(skip_serializing)]
104 pub usage_count: Option<i32>,
105
106 #[serde(skip_serializing)]
109 pub errors: Option<Vec<DiscountCodeError>>,
110
111 #[serde(skip_serializing)]
114 pub created_at: Option<DateTime<Utc>>,
115
116 #[serde(skip_serializing)]
119 pub updated_at: Option<DateTime<Utc>>,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
124pub struct DiscountCodeError {
125 #[serde(skip_serializing_if = "Option::is_none")]
127 pub code: Option<String>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub message: Option<String>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
136pub struct DiscountCodeBatchResult {
137 pub id: Option<u64>,
139
140 pub price_rule_id: Option<u64>,
142
143 pub started_at: Option<DateTime<Utc>>,
145
146 pub completed_at: Option<DateTime<Utc>>,
148
149 pub created_at: Option<DateTime<Utc>>,
151
152 pub updated_at: Option<DateTime<Utc>>,
154
155 pub status: Option<String>,
157
158 pub codes_count: Option<i32>,
160
161 pub imported_count: Option<i32>,
163
164 pub failed_count: Option<i32>,
166
167 pub logs: Option<Vec<String>>,
169}
170
171impl DiscountCode {
172 pub async fn count_with_parent(
195 client: &RestClient,
196 price_rule_id: u64,
197 params: Option<DiscountCodeCountParams>,
198 ) -> Result<u64, ResourceError> {
199 let mut ids: HashMap<&str, String> = HashMap::new();
200 ids.insert("price_rule_id", price_rule_id.to_string());
201
202 let available_ids: Vec<&str> = ids.keys().copied().collect();
203 let path = get_path(Self::PATHS, ResourceOperation::Count, &available_ids).ok_or(
204 ResourceError::PathResolutionFailed {
205 resource: Self::NAME,
206 operation: "count",
207 },
208 )?;
209
210 let url = build_path(path.template, &ids);
211
212 let query = params
214 .map(|p| {
215 let value = serde_json::to_value(&p).map_err(|e| {
216 ResourceError::Http(crate::clients::HttpError::Response(
217 crate::clients::HttpResponseError {
218 code: 400,
219 message: format!("Failed to serialize params: {e}"),
220 error_reference: None,
221 },
222 ))
223 })?;
224
225 let mut query = HashMap::new();
226 if let serde_json::Value::Object(map) = value {
227 for (key, val) in map {
228 match val {
229 serde_json::Value::String(s) => {
230 query.insert(key, s);
231 }
232 serde_json::Value::Number(n) => {
233 query.insert(key, n.to_string());
234 }
235 serde_json::Value::Bool(b) => {
236 query.insert(key, b.to_string());
237 }
238 _ => {}
239 }
240 }
241 }
242 Ok::<_, ResourceError>(query)
243 })
244 .transpose()?
245 .filter(|q| !q.is_empty());
246
247 let response = client.get(&url, query).await?;
248
249 if !response.is_ok() {
250 return Err(ResourceError::from_http_response(
251 response.code,
252 &response.body,
253 Self::NAME,
254 None,
255 response.request_id(),
256 ));
257 }
258
259 let count = response
260 .body
261 .get("count")
262 .and_then(serde_json::Value::as_u64)
263 .ok_or_else(|| {
264 ResourceError::Http(crate::clients::HttpError::Response(
265 crate::clients::HttpResponseError {
266 code: response.code,
267 message: "Missing 'count' in response".to_string(),
268 error_reference: response.request_id().map(ToString::to_string),
269 },
270 ))
271 })?;
272
273 Ok(count)
274 }
275
276 pub async fn find_with_parent(
295 client: &RestClient,
296 price_rule_id: u64,
297 id: u64,
298 _params: Option<DiscountCodeFindParams>,
299 ) -> Result<ResourceResponse<Self>, ResourceError> {
300 let mut ids: HashMap<&str, String> = HashMap::new();
301 ids.insert("price_rule_id", price_rule_id.to_string());
302 ids.insert("id", id.to_string());
303
304 let available_ids: Vec<&str> = ids.keys().copied().collect();
305 let path = get_path(Self::PATHS, ResourceOperation::Find, &available_ids).ok_or(
306 ResourceError::PathResolutionFailed {
307 resource: Self::NAME,
308 operation: "find",
309 },
310 )?;
311
312 let url = build_path(path.template, &ids);
313 let response = client.get(&url, None).await?;
314
315 if !response.is_ok() {
316 return Err(ResourceError::from_http_response(
317 response.code,
318 &response.body,
319 Self::NAME,
320 Some(&id.to_string()),
321 response.request_id(),
322 ));
323 }
324
325 let key = Self::resource_key();
326 ResourceResponse::from_http_response(response, &key)
327 }
328
329 pub async fn lookup(
354 client: &RestClient,
355 code: &str,
356 ) -> Result<ResourceResponse<Self>, ResourceError> {
357 let url = "discount_codes/lookup";
358 let mut query = HashMap::new();
359 query.insert("code".to_string(), code.to_string());
360
361 let response = client.get(url, Some(query)).await?;
362
363 if !response.is_ok() {
364 return Err(ResourceError::from_http_response(
365 response.code,
366 &response.body,
367 Self::NAME,
368 Some(code),
369 response.request_id(),
370 ));
371 }
372
373 let key = Self::resource_key();
374 ResourceResponse::from_http_response(response, &key)
375 }
376
377 pub async fn batch(
403 client: &RestClient,
404 price_rule_id: u64,
405 codes: Vec<String>,
406 ) -> Result<DiscountCodeBatchResult, ResourceError> {
407 let url = format!("price_rules/{price_rule_id}/batch");
408
409 let discount_codes: Vec<serde_json::Value> = codes
411 .into_iter()
412 .map(|code| serde_json::json!({ "code": code }))
413 .collect();
414
415 let body = serde_json::json!({
416 "discount_codes": discount_codes
417 });
418
419 let response = client.post(&url, body, None).await?;
420
421 if !response.is_ok() {
422 return Err(ResourceError::from_http_response(
423 response.code,
424 &response.body,
425 Self::NAME,
426 None,
427 response.request_id(),
428 ));
429 }
430
431 let result = response.body.get("discount_code_creation").ok_or_else(|| {
433 ResourceError::Http(crate::clients::HttpError::Response(
434 crate::clients::HttpResponseError {
435 code: response.code,
436 message: "Missing 'discount_code_creation' in response".to_string(),
437 error_reference: response.request_id().map(ToString::to_string),
438 },
439 ))
440 })?;
441
442 let batch_result: DiscountCodeBatchResult = serde_json::from_value(result.clone())
443 .map_err(|e| {
444 ResourceError::Http(crate::clients::HttpError::Response(
445 crate::clients::HttpResponseError {
446 code: response.code,
447 message: format!("Failed to parse batch result: {e}"),
448 error_reference: response.request_id().map(ToString::to_string),
449 },
450 ))
451 })?;
452
453 Ok(batch_result)
454 }
455
456 pub async fn batch_status(
475 client: &RestClient,
476 price_rule_id: u64,
477 batch_id: u64,
478 ) -> Result<DiscountCodeBatchResult, ResourceError> {
479 let url = format!("price_rules/{price_rule_id}/batch/{batch_id}");
480
481 let response = client.get(&url, None).await?;
482
483 if !response.is_ok() {
484 return Err(ResourceError::from_http_response(
485 response.code,
486 &response.body,
487 Self::NAME,
488 Some(&batch_id.to_string()),
489 response.request_id(),
490 ));
491 }
492
493 let result = response.body.get("discount_code_creation").ok_or_else(|| {
494 ResourceError::Http(crate::clients::HttpError::Response(
495 crate::clients::HttpResponseError {
496 code: response.code,
497 message: "Missing 'discount_code_creation' in response".to_string(),
498 error_reference: response.request_id().map(ToString::to_string),
499 },
500 ))
501 })?;
502
503 let batch_result: DiscountCodeBatchResult = serde_json::from_value(result.clone())
504 .map_err(|e| {
505 ResourceError::Http(crate::clients::HttpError::Response(
506 crate::clients::HttpResponseError {
507 code: response.code,
508 message: format!("Failed to parse batch result: {e}"),
509 error_reference: response.request_id().map(ToString::to_string),
510 },
511 ))
512 })?;
513
514 Ok(batch_result)
515 }
516
517 pub async fn batch_codes(
538 client: &RestClient,
539 price_rule_id: u64,
540 batch_id: u64,
541 ) -> Result<Vec<Self>, ResourceError> {
542 let url = format!("price_rules/{price_rule_id}/batch/{batch_id}/discount_codes");
543
544 let response = client.get(&url, None).await?;
545
546 if !response.is_ok() {
547 return Err(ResourceError::from_http_response(
548 response.code,
549 &response.body,
550 Self::NAME,
551 Some(&batch_id.to_string()),
552 response.request_id(),
553 ));
554 }
555
556 let codes_value = response.body.get(Self::PLURAL).ok_or_else(|| {
557 ResourceError::Http(crate::clients::HttpError::Response(
558 crate::clients::HttpResponseError {
559 code: response.code,
560 message: format!("Missing '{}' in response", Self::PLURAL),
561 error_reference: response.request_id().map(ToString::to_string),
562 },
563 ))
564 })?;
565
566 let codes: Vec<Self> = serde_json::from_value(codes_value.clone()).map_err(|e| {
567 ResourceError::Http(crate::clients::HttpError::Response(
568 crate::clients::HttpResponseError {
569 code: response.code,
570 message: format!("Failed to parse discount codes: {e}"),
571 error_reference: response.request_id().map(ToString::to_string),
572 },
573 ))
574 })?;
575
576 Ok(codes)
577 }
578}
579
580impl RestResource for DiscountCode {
581 type Id = u64;
582 type FindParams = DiscountCodeFindParams;
583 type AllParams = DiscountCodeListParams;
584 type CountParams = DiscountCodeCountParams;
585
586 const NAME: &'static str = "DiscountCode";
587 const PLURAL: &'static str = "discount_codes";
588
589 const PATHS: &'static [ResourcePath] = &[
594 ResourcePath::new(
595 HttpMethod::Get,
596 ResourceOperation::Find,
597 &["price_rule_id", "id"],
598 "price_rules/{price_rule_id}/discount_codes/{id}",
599 ),
600 ResourcePath::new(
601 HttpMethod::Get,
602 ResourceOperation::All,
603 &["price_rule_id"],
604 "price_rules/{price_rule_id}/discount_codes",
605 ),
606 ResourcePath::new(
607 HttpMethod::Get,
608 ResourceOperation::Count,
609 &["price_rule_id"],
610 "price_rules/{price_rule_id}/discount_codes/count",
611 ),
612 ResourcePath::new(
613 HttpMethod::Post,
614 ResourceOperation::Create,
615 &["price_rule_id"],
616 "price_rules/{price_rule_id}/discount_codes",
617 ),
618 ResourcePath::new(
619 HttpMethod::Put,
620 ResourceOperation::Update,
621 &["price_rule_id", "id"],
622 "price_rules/{price_rule_id}/discount_codes/{id}",
623 ),
624 ResourcePath::new(
625 HttpMethod::Delete,
626 ResourceOperation::Delete,
627 &["price_rule_id", "id"],
628 "price_rules/{price_rule_id}/discount_codes/{id}",
629 ),
630 ];
631
632 fn get_id(&self) -> Option<Self::Id> {
633 self.id
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
639pub struct DiscountCodeFindParams {
640 #[serde(skip_serializing_if = "Option::is_none")]
642 pub fields: Option<String>,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
647pub struct DiscountCodeListParams {
648 #[serde(skip_serializing_if = "Option::is_none")]
650 pub limit: Option<u32>,
651
652 #[serde(skip_serializing_if = "Option::is_none")]
654 pub since_id: Option<u64>,
655
656 #[serde(skip_serializing_if = "Option::is_none")]
658 pub page_info: Option<String>,
659
660 #[serde(skip_serializing_if = "Option::is_none")]
662 pub fields: Option<String>,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
667pub struct DiscountCodeCountParams {
668 #[serde(skip_serializing_if = "Option::is_none")]
670 pub times_used: Option<i32>,
671
672 #[serde(skip_serializing_if = "Option::is_none")]
674 pub times_used_min: Option<i32>,
675
676 #[serde(skip_serializing_if = "Option::is_none")]
678 pub times_used_max: Option<i32>,
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684 use crate::rest::{get_path, ResourceOperation};
685
686 #[test]
687 fn test_discount_code_serialization() {
688 let code = DiscountCode {
689 id: Some(12345),
690 price_rule_id: Some(507328175),
691 code: Some("SUMMER20".to_string()),
692 usage_count: Some(42),
693 errors: None,
694 created_at: Some(
695 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
696 .unwrap()
697 .with_timezone(&Utc),
698 ),
699 updated_at: Some(
700 DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
701 .unwrap()
702 .with_timezone(&Utc),
703 ),
704 };
705
706 let json = serde_json::to_string(&code).unwrap();
707 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
708
709 assert_eq!(parsed["price_rule_id"], 507328175);
711 assert_eq!(parsed["code"], "SUMMER20");
712
713 assert!(parsed.get("id").is_none());
715 assert!(parsed.get("usage_count").is_none());
716 assert!(parsed.get("errors").is_none());
717 assert!(parsed.get("created_at").is_none());
718 assert!(parsed.get("updated_at").is_none());
719 }
720
721 #[test]
722 fn test_discount_code_deserialization() {
723 let json = r#"{
724 "id": 1054381139,
725 "price_rule_id": 507328175,
726 "code": "SUMMERSALE20OFF",
727 "usage_count": 25,
728 "errors": [],
729 "created_at": "2024-01-15T10:30:00Z",
730 "updated_at": "2024-06-20T15:45:00Z"
731 }"#;
732
733 let code: DiscountCode = serde_json::from_str(json).unwrap();
734
735 assert_eq!(code.id, Some(1054381139));
736 assert_eq!(code.price_rule_id, Some(507328175));
737 assert_eq!(code.code, Some("SUMMERSALE20OFF".to_string()));
738 assert_eq!(code.usage_count, Some(25));
739 assert!(code.errors.is_some());
740 assert!(code.errors.unwrap().is_empty());
741 assert!(code.created_at.is_some());
742 assert!(code.updated_at.is_some());
743 }
744
745 #[test]
746 fn test_discount_code_nested_paths() {
747 let find_path = get_path(
751 DiscountCode::PATHS,
752 ResourceOperation::Find,
753 &["price_rule_id", "id"],
754 );
755 assert!(find_path.is_some());
756 assert_eq!(
757 find_path.unwrap().template,
758 "price_rules/{price_rule_id}/discount_codes/{id}"
759 );
760
761 let find_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::Find, &["id"]);
763 assert!(find_without_parent.is_none());
764
765 let all_path = get_path(
767 DiscountCode::PATHS,
768 ResourceOperation::All,
769 &["price_rule_id"],
770 );
771 assert!(all_path.is_some());
772 assert_eq!(
773 all_path.unwrap().template,
774 "price_rules/{price_rule_id}/discount_codes"
775 );
776
777 let all_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::All, &[]);
779 assert!(all_without_parent.is_none());
780
781 let count_path = get_path(
783 DiscountCode::PATHS,
784 ResourceOperation::Count,
785 &["price_rule_id"],
786 );
787 assert!(count_path.is_some());
788 assert_eq!(
789 count_path.unwrap().template,
790 "price_rules/{price_rule_id}/discount_codes/count"
791 );
792
793 let create_path = get_path(
795 DiscountCode::PATHS,
796 ResourceOperation::Create,
797 &["price_rule_id"],
798 );
799 assert!(create_path.is_some());
800 assert_eq!(
801 create_path.unwrap().template,
802 "price_rules/{price_rule_id}/discount_codes"
803 );
804 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
805
806 let update_path = get_path(
808 DiscountCode::PATHS,
809 ResourceOperation::Update,
810 &["price_rule_id", "id"],
811 );
812 assert!(update_path.is_some());
813 assert_eq!(
814 update_path.unwrap().template,
815 "price_rules/{price_rule_id}/discount_codes/{id}"
816 );
817 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
818
819 let delete_path = get_path(
821 DiscountCode::PATHS,
822 ResourceOperation::Delete,
823 &["price_rule_id", "id"],
824 );
825 assert!(delete_path.is_some());
826 assert_eq!(
827 delete_path.unwrap().template,
828 "price_rules/{price_rule_id}/discount_codes/{id}"
829 );
830 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
831 }
832
833 #[test]
834 fn test_discount_code_lookup_is_standalone_path() {
835 }
840
841 #[test]
842 fn test_discount_code_batch_result_deserialization() {
843 let json = r#"{
844 "id": 173232803,
845 "price_rule_id": 507328175,
846 "started_at": "2024-06-15T10:00:00Z",
847 "completed_at": "2024-06-15T10:05:00Z",
848 "created_at": "2024-06-15T09:55:00Z",
849 "updated_at": "2024-06-15T10:05:00Z",
850 "status": "completed",
851 "codes_count": 3,
852 "imported_count": 3,
853 "failed_count": 0,
854 "logs": []
855 }"#;
856
857 let result: DiscountCodeBatchResult = serde_json::from_str(json).unwrap();
858
859 assert_eq!(result.id, Some(173232803));
860 assert_eq!(result.price_rule_id, Some(507328175));
861 assert_eq!(result.status, Some("completed".to_string()));
862 assert_eq!(result.codes_count, Some(3));
863 assert_eq!(result.imported_count, Some(3));
864 assert_eq!(result.failed_count, Some(0));
865 assert!(result.started_at.is_some());
866 assert!(result.completed_at.is_some());
867 }
868
869 #[test]
870 fn test_discount_code_count_params() {
871 let params = DiscountCodeCountParams {
872 times_used: Some(5),
873 times_used_min: Some(1),
874 times_used_max: Some(100),
875 };
876
877 let json = serde_json::to_value(¶ms).unwrap();
878
879 assert_eq!(json["times_used"], 5);
880 assert_eq!(json["times_used_min"], 1);
881 assert_eq!(json["times_used_max"], 100);
882
883 let empty_params = DiscountCodeCountParams::default();
885 let empty_json = serde_json::to_value(&empty_params).unwrap();
886 assert_eq!(empty_json, serde_json::json!({}));
887 }
888
889 #[test]
890 fn test_discount_code_constants() {
891 assert_eq!(DiscountCode::NAME, "DiscountCode");
892 assert_eq!(DiscountCode::PLURAL, "discount_codes");
893 }
894
895 #[test]
896 fn test_discount_code_get_id() {
897 let code_with_id = DiscountCode {
898 id: Some(12345),
899 code: Some("TEST".to_string()),
900 ..Default::default()
901 };
902 assert_eq!(code_with_id.get_id(), Some(12345));
903
904 let code_without_id = DiscountCode::default();
905 assert_eq!(code_without_id.get_id(), None);
906 }
907}