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
433 .body
434 .get("discount_code_creation")
435 .ok_or_else(|| {
436 ResourceError::Http(crate::clients::HttpError::Response(
437 crate::clients::HttpResponseError {
438 code: response.code,
439 message: "Missing 'discount_code_creation' in response".to_string(),
440 error_reference: response.request_id().map(ToString::to_string),
441 },
442 ))
443 })?;
444
445 let batch_result: DiscountCodeBatchResult =
446 serde_json::from_value(result.clone()).map_err(|e| {
447 ResourceError::Http(crate::clients::HttpError::Response(
448 crate::clients::HttpResponseError {
449 code: response.code,
450 message: format!("Failed to parse batch result: {e}"),
451 error_reference: response.request_id().map(ToString::to_string),
452 },
453 ))
454 })?;
455
456 Ok(batch_result)
457 }
458
459 pub async fn batch_status(
478 client: &RestClient,
479 price_rule_id: u64,
480 batch_id: u64,
481 ) -> Result<DiscountCodeBatchResult, ResourceError> {
482 let url = format!("price_rules/{price_rule_id}/batch/{batch_id}");
483
484 let response = client.get(&url, None).await?;
485
486 if !response.is_ok() {
487 return Err(ResourceError::from_http_response(
488 response.code,
489 &response.body,
490 Self::NAME,
491 Some(&batch_id.to_string()),
492 response.request_id(),
493 ));
494 }
495
496 let result = response
497 .body
498 .get("discount_code_creation")
499 .ok_or_else(|| {
500 ResourceError::Http(crate::clients::HttpError::Response(
501 crate::clients::HttpResponseError {
502 code: response.code,
503 message: "Missing 'discount_code_creation' in response".to_string(),
504 error_reference: response.request_id().map(ToString::to_string),
505 },
506 ))
507 })?;
508
509 let batch_result: DiscountCodeBatchResult =
510 serde_json::from_value(result.clone()).map_err(|e| {
511 ResourceError::Http(crate::clients::HttpError::Response(
512 crate::clients::HttpResponseError {
513 code: response.code,
514 message: format!("Failed to parse batch result: {e}"),
515 error_reference: response.request_id().map(ToString::to_string),
516 },
517 ))
518 })?;
519
520 Ok(batch_result)
521 }
522
523 pub async fn batch_codes(
544 client: &RestClient,
545 price_rule_id: u64,
546 batch_id: u64,
547 ) -> Result<Vec<Self>, ResourceError> {
548 let url = format!("price_rules/{price_rule_id}/batch/{batch_id}/discount_codes");
549
550 let response = client.get(&url, None).await?;
551
552 if !response.is_ok() {
553 return Err(ResourceError::from_http_response(
554 response.code,
555 &response.body,
556 Self::NAME,
557 Some(&batch_id.to_string()),
558 response.request_id(),
559 ));
560 }
561
562 let codes_value = response
563 .body
564 .get(Self::PLURAL)
565 .ok_or_else(|| {
566 ResourceError::Http(crate::clients::HttpError::Response(
567 crate::clients::HttpResponseError {
568 code: response.code,
569 message: format!("Missing '{}' in response", Self::PLURAL),
570 error_reference: response.request_id().map(ToString::to_string),
571 },
572 ))
573 })?;
574
575 let codes: Vec<Self> = serde_json::from_value(codes_value.clone()).map_err(|e| {
576 ResourceError::Http(crate::clients::HttpError::Response(
577 crate::clients::HttpResponseError {
578 code: response.code,
579 message: format!("Failed to parse discount codes: {e}"),
580 error_reference: response.request_id().map(ToString::to_string),
581 },
582 ))
583 })?;
584
585 Ok(codes)
586 }
587}
588
589impl RestResource for DiscountCode {
590 type Id = u64;
591 type FindParams = DiscountCodeFindParams;
592 type AllParams = DiscountCodeListParams;
593 type CountParams = DiscountCodeCountParams;
594
595 const NAME: &'static str = "DiscountCode";
596 const PLURAL: &'static str = "discount_codes";
597
598 const PATHS: &'static [ResourcePath] = &[
603 ResourcePath::new(
604 HttpMethod::Get,
605 ResourceOperation::Find,
606 &["price_rule_id", "id"],
607 "price_rules/{price_rule_id}/discount_codes/{id}",
608 ),
609 ResourcePath::new(
610 HttpMethod::Get,
611 ResourceOperation::All,
612 &["price_rule_id"],
613 "price_rules/{price_rule_id}/discount_codes",
614 ),
615 ResourcePath::new(
616 HttpMethod::Get,
617 ResourceOperation::Count,
618 &["price_rule_id"],
619 "price_rules/{price_rule_id}/discount_codes/count",
620 ),
621 ResourcePath::new(
622 HttpMethod::Post,
623 ResourceOperation::Create,
624 &["price_rule_id"],
625 "price_rules/{price_rule_id}/discount_codes",
626 ),
627 ResourcePath::new(
628 HttpMethod::Put,
629 ResourceOperation::Update,
630 &["price_rule_id", "id"],
631 "price_rules/{price_rule_id}/discount_codes/{id}",
632 ),
633 ResourcePath::new(
634 HttpMethod::Delete,
635 ResourceOperation::Delete,
636 &["price_rule_id", "id"],
637 "price_rules/{price_rule_id}/discount_codes/{id}",
638 ),
639 ];
640
641 fn get_id(&self) -> Option<Self::Id> {
642 self.id
643 }
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
648pub struct DiscountCodeFindParams {
649 #[serde(skip_serializing_if = "Option::is_none")]
651 pub fields: Option<String>,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
656pub struct DiscountCodeListParams {
657 #[serde(skip_serializing_if = "Option::is_none")]
659 pub limit: Option<u32>,
660
661 #[serde(skip_serializing_if = "Option::is_none")]
663 pub since_id: Option<u64>,
664
665 #[serde(skip_serializing_if = "Option::is_none")]
667 pub page_info: Option<String>,
668
669 #[serde(skip_serializing_if = "Option::is_none")]
671 pub fields: Option<String>,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
676pub struct DiscountCodeCountParams {
677 #[serde(skip_serializing_if = "Option::is_none")]
679 pub times_used: Option<i32>,
680
681 #[serde(skip_serializing_if = "Option::is_none")]
683 pub times_used_min: Option<i32>,
684
685 #[serde(skip_serializing_if = "Option::is_none")]
687 pub times_used_max: Option<i32>,
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use crate::rest::{get_path, ResourceOperation};
694
695 #[test]
696 fn test_discount_code_serialization() {
697 let code = DiscountCode {
698 id: Some(12345),
699 price_rule_id: Some(507328175),
700 code: Some("SUMMER20".to_string()),
701 usage_count: Some(42),
702 errors: None,
703 created_at: Some(
704 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
705 .unwrap()
706 .with_timezone(&Utc),
707 ),
708 updated_at: Some(
709 DateTime::parse_from_rfc3339("2024-06-20T15:45:00Z")
710 .unwrap()
711 .with_timezone(&Utc),
712 ),
713 };
714
715 let json = serde_json::to_string(&code).unwrap();
716 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
717
718 assert_eq!(parsed["price_rule_id"], 507328175);
720 assert_eq!(parsed["code"], "SUMMER20");
721
722 assert!(parsed.get("id").is_none());
724 assert!(parsed.get("usage_count").is_none());
725 assert!(parsed.get("errors").is_none());
726 assert!(parsed.get("created_at").is_none());
727 assert!(parsed.get("updated_at").is_none());
728 }
729
730 #[test]
731 fn test_discount_code_deserialization() {
732 let json = r#"{
733 "id": 1054381139,
734 "price_rule_id": 507328175,
735 "code": "SUMMERSALE20OFF",
736 "usage_count": 25,
737 "errors": [],
738 "created_at": "2024-01-15T10:30:00Z",
739 "updated_at": "2024-06-20T15:45:00Z"
740 }"#;
741
742 let code: DiscountCode = serde_json::from_str(json).unwrap();
743
744 assert_eq!(code.id, Some(1054381139));
745 assert_eq!(code.price_rule_id, Some(507328175));
746 assert_eq!(code.code, Some("SUMMERSALE20OFF".to_string()));
747 assert_eq!(code.usage_count, Some(25));
748 assert!(code.errors.is_some());
749 assert!(code.errors.unwrap().is_empty());
750 assert!(code.created_at.is_some());
751 assert!(code.updated_at.is_some());
752 }
753
754 #[test]
755 fn test_discount_code_nested_paths() {
756 let find_path = get_path(
760 DiscountCode::PATHS,
761 ResourceOperation::Find,
762 &["price_rule_id", "id"],
763 );
764 assert!(find_path.is_some());
765 assert_eq!(
766 find_path.unwrap().template,
767 "price_rules/{price_rule_id}/discount_codes/{id}"
768 );
769
770 let find_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::Find, &["id"]);
772 assert!(find_without_parent.is_none());
773
774 let all_path = get_path(
776 DiscountCode::PATHS,
777 ResourceOperation::All,
778 &["price_rule_id"],
779 );
780 assert!(all_path.is_some());
781 assert_eq!(
782 all_path.unwrap().template,
783 "price_rules/{price_rule_id}/discount_codes"
784 );
785
786 let all_without_parent = get_path(DiscountCode::PATHS, ResourceOperation::All, &[]);
788 assert!(all_without_parent.is_none());
789
790 let count_path = get_path(
792 DiscountCode::PATHS,
793 ResourceOperation::Count,
794 &["price_rule_id"],
795 );
796 assert!(count_path.is_some());
797 assert_eq!(
798 count_path.unwrap().template,
799 "price_rules/{price_rule_id}/discount_codes/count"
800 );
801
802 let create_path = get_path(
804 DiscountCode::PATHS,
805 ResourceOperation::Create,
806 &["price_rule_id"],
807 );
808 assert!(create_path.is_some());
809 assert_eq!(
810 create_path.unwrap().template,
811 "price_rules/{price_rule_id}/discount_codes"
812 );
813 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
814
815 let update_path = get_path(
817 DiscountCode::PATHS,
818 ResourceOperation::Update,
819 &["price_rule_id", "id"],
820 );
821 assert!(update_path.is_some());
822 assert_eq!(
823 update_path.unwrap().template,
824 "price_rules/{price_rule_id}/discount_codes/{id}"
825 );
826 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
827
828 let delete_path = get_path(
830 DiscountCode::PATHS,
831 ResourceOperation::Delete,
832 &["price_rule_id", "id"],
833 );
834 assert!(delete_path.is_some());
835 assert_eq!(
836 delete_path.unwrap().template,
837 "price_rules/{price_rule_id}/discount_codes/{id}"
838 );
839 assert_eq!(delete_path.unwrap().http_method, HttpMethod::Delete);
840 }
841
842 #[test]
843 fn test_discount_code_lookup_is_standalone_path() {
844 }
849
850 #[test]
851 fn test_discount_code_batch_result_deserialization() {
852 let json = r#"{
853 "id": 173232803,
854 "price_rule_id": 507328175,
855 "started_at": "2024-06-15T10:00:00Z",
856 "completed_at": "2024-06-15T10:05:00Z",
857 "created_at": "2024-06-15T09:55:00Z",
858 "updated_at": "2024-06-15T10:05:00Z",
859 "status": "completed",
860 "codes_count": 3,
861 "imported_count": 3,
862 "failed_count": 0,
863 "logs": []
864 }"#;
865
866 let result: DiscountCodeBatchResult = serde_json::from_str(json).unwrap();
867
868 assert_eq!(result.id, Some(173232803));
869 assert_eq!(result.price_rule_id, Some(507328175));
870 assert_eq!(result.status, Some("completed".to_string()));
871 assert_eq!(result.codes_count, Some(3));
872 assert_eq!(result.imported_count, Some(3));
873 assert_eq!(result.failed_count, Some(0));
874 assert!(result.started_at.is_some());
875 assert!(result.completed_at.is_some());
876 }
877
878 #[test]
879 fn test_discount_code_count_params() {
880 let params = DiscountCodeCountParams {
881 times_used: Some(5),
882 times_used_min: Some(1),
883 times_used_max: Some(100),
884 };
885
886 let json = serde_json::to_value(¶ms).unwrap();
887
888 assert_eq!(json["times_used"], 5);
889 assert_eq!(json["times_used_min"], 1);
890 assert_eq!(json["times_used_max"], 100);
891
892 let empty_params = DiscountCodeCountParams::default();
894 let empty_json = serde_json::to_value(&empty_params).unwrap();
895 assert_eq!(empty_json, serde_json::json!({}));
896 }
897
898 #[test]
899 fn test_discount_code_constants() {
900 assert_eq!(DiscountCode::NAME, "DiscountCode");
901 assert_eq!(DiscountCode::PLURAL, "discount_codes");
902 }
903
904 #[test]
905 fn test_discount_code_get_id() {
906 let code_with_id = DiscountCode {
907 id: Some(12345),
908 code: Some("TEST".to_string()),
909 ..Default::default()
910 };
911 assert_eq!(code_with_id.get_id(), Some(12345));
912
913 let code_without_id = DiscountCode::default();
914 assert_eq!(code_without_id.get_id(), None);
915 }
916}