1use chrono::{DateTime, NaiveDate, Utc};
52use serde::{Deserialize, Serialize};
53
54use crate::clients::RestClient;
55use crate::rest::{ResourceError, ResourceOperation, ResourcePath, RestResource};
56use crate::HttpMethod;
57
58#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
103pub struct GiftCard {
104 #[serde(skip_serializing)]
107 pub id: Option<u64>,
108
109 #[serde(skip_serializing)]
111 pub balance: Option<String>,
112
113 #[serde(skip_serializing)]
116 pub disabled_at: Option<DateTime<Utc>>,
117
118 #[serde(skip_serializing)]
120 pub line_item_id: Option<u64>,
121
122 #[serde(skip_serializing)]
124 pub api_client_id: Option<u64>,
125
126 #[serde(skip_serializing)]
128 pub user_id: Option<u64>,
129
130 #[serde(skip_serializing)]
133 pub last_characters: Option<String>,
134
135 #[serde(skip_serializing)]
137 pub order_id: Option<u64>,
138
139 #[serde(skip_serializing)]
141 pub created_at: Option<DateTime<Utc>>,
142
143 #[serde(skip_serializing)]
145 pub updated_at: Option<DateTime<Utc>>,
146
147 #[serde(skip_serializing)]
149 pub admin_graphql_api_id: Option<String>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
163 pub code: Option<String>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
170 pub initial_value: Option<String>,
171
172 #[serde(skip_serializing_if = "Option::is_none")]
174 pub currency: Option<String>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
180 pub customer_id: Option<u64>,
181
182 #[serde(skip_serializing_if = "Option::is_none")]
186 pub note: Option<String>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
192 pub expires_on: Option<NaiveDate>,
193
194 #[serde(skip_serializing_if = "Option::is_none")]
198 pub template_suffix: Option<String>,
199}
200
201impl GiftCard {
202 pub fn is_enabled(&self) -> bool {
206 self.disabled_at.is_none()
207 }
208
209 pub fn is_disabled(&self) -> bool {
213 self.disabled_at.is_some()
214 }
215}
216
217impl RestResource for GiftCard {
218 type Id = u64;
219 type FindParams = GiftCardFindParams;
220 type AllParams = GiftCardListParams;
221 type CountParams = GiftCardCountParams;
222
223 const NAME: &'static str = "GiftCard";
224 const PLURAL: &'static str = "gift_cards";
225
226 const PATHS: &'static [ResourcePath] = &[
231 ResourcePath::new(
232 HttpMethod::Get,
233 ResourceOperation::Find,
234 &["id"],
235 "gift_cards/{id}",
236 ),
237 ResourcePath::new(HttpMethod::Get, ResourceOperation::All, &[], "gift_cards"),
238 ResourcePath::new(
239 HttpMethod::Get,
240 ResourceOperation::Count,
241 &[],
242 "gift_cards/count",
243 ),
244 ResourcePath::new(
245 HttpMethod::Post,
246 ResourceOperation::Create,
247 &[],
248 "gift_cards",
249 ),
250 ResourcePath::new(
251 HttpMethod::Put,
252 ResourceOperation::Update,
253 &["id"],
254 "gift_cards/{id}",
255 ),
256 ];
258
259 fn get_id(&self) -> Option<Self::Id> {
260 self.id
261 }
262}
263
264impl GiftCard {
265 pub async fn disable(&self, client: &RestClient) -> Result<Self, ResourceError> {
292 let id = self.get_id().ok_or(ResourceError::PathResolutionFailed {
293 resource: Self::NAME,
294 operation: "disable",
295 })?;
296
297 let path = format!("gift_cards/{id}/disable");
298 let body = serde_json::json!({});
299
300 let response = client.post(&path, body, None).await?;
301
302 if !response.is_ok() {
303 return Err(ResourceError::from_http_response(
304 response.code,
305 &response.body,
306 Self::NAME,
307 Some(&id.to_string()),
308 response.request_id(),
309 ));
310 }
311
312 let gift_card: Self = response
314 .body
315 .get("gift_card")
316 .ok_or_else(|| {
317 ResourceError::Http(crate::clients::HttpError::Response(
318 crate::clients::HttpResponseError {
319 code: response.code,
320 message: "Missing 'gift_card' in response".to_string(),
321 error_reference: response.request_id().map(ToString::to_string),
322 },
323 ))
324 })
325 .and_then(|v| {
326 serde_json::from_value(v.clone()).map_err(|e| {
327 ResourceError::Http(crate::clients::HttpError::Response(
328 crate::clients::HttpResponseError {
329 code: response.code,
330 message: format!("Failed to deserialize gift_card: {e}"),
331 error_reference: response.request_id().map(ToString::to_string),
332 },
333 ))
334 })
335 })?;
336
337 Ok(gift_card)
338 }
339
340 pub async fn search(client: &RestClient, query: &str) -> Result<Vec<Self>, ResourceError> {
363 let path = format!("gift_cards/search");
364
365 let mut query_params = std::collections::HashMap::new();
366 query_params.insert("query".to_string(), query.to_string());
367
368 let response = client.get(&path, Some(query_params)).await?;
369
370 if !response.is_ok() {
371 return Err(ResourceError::from_http_response(
372 response.code,
373 &response.body,
374 Self::NAME,
375 None,
376 response.request_id(),
377 ));
378 }
379
380 let gift_cards: Vec<Self> = response
382 .body
383 .get("gift_cards")
384 .ok_or_else(|| {
385 ResourceError::Http(crate::clients::HttpError::Response(
386 crate::clients::HttpResponseError {
387 code: response.code,
388 message: "Missing 'gift_cards' in response".to_string(),
389 error_reference: response.request_id().map(ToString::to_string),
390 },
391 ))
392 })
393 .and_then(|v| {
394 serde_json::from_value(v.clone()).map_err(|e| {
395 ResourceError::Http(crate::clients::HttpError::Response(
396 crate::clients::HttpResponseError {
397 code: response.code,
398 message: format!("Failed to deserialize gift_cards: {e}"),
399 error_reference: response.request_id().map(ToString::to_string),
400 },
401 ))
402 })
403 })?;
404
405 Ok(gift_cards)
406 }
407}
408
409#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
411pub struct GiftCardFindParams {
412 #[serde(skip_serializing_if = "Option::is_none")]
414 pub fields: Option<String>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
421pub struct GiftCardListParams {
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub limit: Option<u32>,
425
426 #[serde(skip_serializing_if = "Option::is_none")]
428 pub since_id: Option<u64>,
429
430 #[serde(skip_serializing_if = "Option::is_none")]
432 pub status: Option<String>,
433
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub fields: Option<String>,
437
438 #[serde(skip_serializing_if = "Option::is_none")]
440 pub page_info: Option<String>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
445pub struct GiftCardCountParams {
446 #[serde(skip_serializing_if = "Option::is_none")]
448 pub status: Option<String>,
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use crate::rest::{get_path, ResourceOperation};
455
456 #[test]
457 fn test_gift_card_struct_serialization() {
458 let gift_card = GiftCard {
459 id: Some(123456789),
460 balance: Some("75.00".to_string()),
461 disabled_at: None,
462 line_item_id: Some(111),
463 api_client_id: Some(222),
464 user_id: Some(333),
465 last_characters: Some("abc1".to_string()),
466 order_id: Some(444),
467 created_at: Some(
468 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
469 .unwrap()
470 .with_timezone(&Utc),
471 ),
472 updated_at: Some(
473 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
474 .unwrap()
475 .with_timezone(&Utc),
476 ),
477 admin_graphql_api_id: Some("gid://shopify/GiftCard/123456789".to_string()),
478 code: Some("GIFT1234ABCD5678".to_string()),
479 initial_value: Some("100.00".to_string()),
480 currency: Some("USD".to_string()),
481 customer_id: Some(789012),
482 note: Some("Employee reward".to_string()),
483 expires_on: Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap()),
484 template_suffix: Some("premium".to_string()),
485 };
486
487 let json = serde_json::to_string(&gift_card).unwrap();
488 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
489
490 assert_eq!(parsed["code"], "GIFT1234ABCD5678");
492 assert_eq!(parsed["initial_value"], "100.00");
493 assert_eq!(parsed["currency"], "USD");
494 assert_eq!(parsed["customer_id"], 789012);
495 assert_eq!(parsed["note"], "Employee reward");
496 assert_eq!(parsed["expires_on"], "2025-12-31");
497 assert_eq!(parsed["template_suffix"], "premium");
498
499 assert!(parsed.get("id").is_none());
501 assert!(parsed.get("balance").is_none());
502 assert!(parsed.get("disabled_at").is_none());
503 assert!(parsed.get("line_item_id").is_none());
504 assert!(parsed.get("api_client_id").is_none());
505 assert!(parsed.get("user_id").is_none());
506 assert!(parsed.get("last_characters").is_none());
507 assert!(parsed.get("order_id").is_none());
508 assert!(parsed.get("created_at").is_none());
509 assert!(parsed.get("updated_at").is_none());
510 assert!(parsed.get("admin_graphql_api_id").is_none());
511 }
512
513 #[test]
514 fn test_gift_card_deserialization_from_api_response() {
515 let json_str = r#"{
516 "id": 1035197676,
517 "balance": "100.00",
518 "created_at": "2024-01-15T10:30:00Z",
519 "updated_at": "2024-01-15T10:30:00Z",
520 "currency": "USD",
521 "initial_value": "100.00",
522 "disabled_at": null,
523 "line_item_id": 466157049,
524 "api_client_id": 755357713,
525 "user_id": null,
526 "customer_id": 207119551,
527 "note": "Birthday gift for John",
528 "expires_on": "2025-12-31",
529 "template_suffix": null,
530 "last_characters": "0e0e",
531 "order_id": 450789469,
532 "admin_graphql_api_id": "gid://shopify/GiftCard/1035197676"
533 }"#;
534
535 let gift_card: GiftCard = serde_json::from_str(json_str).unwrap();
536
537 assert_eq!(gift_card.id, Some(1035197676));
538 assert_eq!(gift_card.balance.as_deref(), Some("100.00"));
539 assert_eq!(gift_card.currency.as_deref(), Some("USD"));
540 assert_eq!(gift_card.initial_value.as_deref(), Some("100.00"));
541 assert_eq!(gift_card.disabled_at, None);
542 assert_eq!(gift_card.line_item_id, Some(466157049));
543 assert_eq!(gift_card.api_client_id, Some(755357713));
544 assert_eq!(gift_card.user_id, None);
545 assert_eq!(gift_card.customer_id, Some(207119551));
546 assert_eq!(gift_card.note.as_deref(), Some("Birthday gift for John"));
547 assert_eq!(
548 gift_card.expires_on,
549 Some(NaiveDate::from_ymd_opt(2025, 12, 31).unwrap())
550 );
551 assert_eq!(gift_card.template_suffix, None);
552 assert_eq!(gift_card.last_characters.as_deref(), Some("0e0e"));
553 assert_eq!(gift_card.order_id, Some(450789469));
554 assert!(gift_card.created_at.is_some());
555 assert!(gift_card.updated_at.is_some());
556
557 assert_eq!(gift_card.code, None);
559 }
560
561 #[test]
562 fn test_gift_card_path_constants() {
563 let find_path = get_path(GiftCard::PATHS, ResourceOperation::Find, &["id"]);
565 assert!(find_path.is_some());
566 assert_eq!(find_path.unwrap().template, "gift_cards/{id}");
567 assert_eq!(find_path.unwrap().http_method, HttpMethod::Get);
568
569 let all_path = get_path(GiftCard::PATHS, ResourceOperation::All, &[]);
571 assert!(all_path.is_some());
572 assert_eq!(all_path.unwrap().template, "gift_cards");
573
574 let count_path = get_path(GiftCard::PATHS, ResourceOperation::Count, &[]);
576 assert!(count_path.is_some());
577 assert_eq!(count_path.unwrap().template, "gift_cards/count");
578
579 let create_path = get_path(GiftCard::PATHS, ResourceOperation::Create, &[]);
581 assert!(create_path.is_some());
582 assert_eq!(create_path.unwrap().template, "gift_cards");
583 assert_eq!(create_path.unwrap().http_method, HttpMethod::Post);
584
585 let update_path = get_path(GiftCard::PATHS, ResourceOperation::Update, &["id"]);
587 assert!(update_path.is_some());
588 assert_eq!(update_path.unwrap().template, "gift_cards/{id}");
589 assert_eq!(update_path.unwrap().http_method, HttpMethod::Put);
590
591 let delete_path = get_path(GiftCard::PATHS, ResourceOperation::Delete, &["id"]);
593 assert!(delete_path.is_none());
594
595 assert_eq!(GiftCard::NAME, "GiftCard");
597 assert_eq!(GiftCard::PLURAL, "gift_cards");
598 }
599
600 #[test]
601 fn test_disable_method_signature() {
602 fn _assert_disable_signature<F, Fut>(f: F)
604 where
605 F: Fn(&GiftCard, &RestClient) -> Fut,
606 Fut: std::future::Future<Output = Result<GiftCard, ResourceError>>,
607 {
608 let _ = f;
609 }
610
611 let gift_card_without_id = GiftCard::default();
613 assert!(gift_card_without_id.get_id().is_none());
614 }
615
616 #[test]
617 fn test_search_method_signature() {
618 fn _assert_search_signature<F, Fut>(f: F)
620 where
621 F: Fn(&RestClient, &str) -> Fut,
622 Fut: std::future::Future<Output = Result<Vec<GiftCard>, ResourceError>>,
623 {
624 let _ = f;
625 }
626 }
627
628 #[test]
629 fn test_gift_card_is_enabled_disabled() {
630 let enabled_gift_card = GiftCard {
631 id: Some(123),
632 balance: Some("100.00".to_string()),
633 disabled_at: None,
634 ..Default::default()
635 };
636
637 assert!(enabled_gift_card.is_enabled());
638 assert!(!enabled_gift_card.is_disabled());
639
640 let disabled_gift_card = GiftCard {
641 id: Some(456),
642 balance: Some("0.00".to_string()),
643 disabled_at: Some(
644 DateTime::parse_from_rfc3339("2024-01-15T10:30:00Z")
645 .unwrap()
646 .with_timezone(&Utc),
647 ),
648 ..Default::default()
649 };
650
651 assert!(!disabled_gift_card.is_enabled());
652 assert!(disabled_gift_card.is_disabled());
653 }
654
655 #[test]
656 fn test_gift_card_get_id_returns_correct_value() {
657 let gift_card_with_id = GiftCard {
658 id: Some(1035197676),
659 balance: Some("100.00".to_string()),
660 ..Default::default()
661 };
662 assert_eq!(gift_card_with_id.get_id(), Some(1035197676));
663
664 let gift_card_without_id = GiftCard {
665 id: None,
666 initial_value: Some("50.00".to_string()),
667 ..Default::default()
668 };
669 assert_eq!(gift_card_without_id.get_id(), None);
670 }
671
672 #[test]
673 fn test_gift_card_list_params_serialization() {
674 let params = GiftCardListParams {
675 limit: Some(50),
676 since_id: Some(12345),
677 status: Some("enabled".to_string()),
678 fields: Some("id,balance,last_characters".to_string()),
679 page_info: None,
680 };
681
682 let json = serde_json::to_value(¶ms).unwrap();
683
684 assert_eq!(json["limit"], 50);
685 assert_eq!(json["since_id"], 12345);
686 assert_eq!(json["status"], "enabled");
687 assert_eq!(json["fields"], "id,balance,last_characters");
688 assert!(json.get("page_info").is_none());
689
690 let empty_params = GiftCardListParams::default();
692 let empty_json = serde_json::to_value(&empty_params).unwrap();
693 assert_eq!(empty_json, serde_json::json!({}));
694 }
695
696 #[test]
697 fn test_gift_card_count_params_serialization() {
698 let params = GiftCardCountParams {
699 status: Some("disabled".to_string()),
700 };
701
702 let json = serde_json::to_value(¶ms).unwrap();
703 assert_eq!(json["status"], "disabled");
704
705 let empty_params = GiftCardCountParams::default();
707 let empty_json = serde_json::to_value(&empty_params).unwrap();
708 assert_eq!(empty_json, serde_json::json!({}));
709 }
710
711 #[test]
712 fn test_gift_card_updatable_fields() {
713 let update_gift_card = GiftCard {
715 expires_on: Some(NaiveDate::from_ymd_opt(2026, 6, 30).unwrap()),
716 note: Some("Updated note".to_string()),
717 template_suffix: Some("custom".to_string()),
718 customer_id: Some(999999),
719 ..Default::default()
720 };
721
722 let json = serde_json::to_value(&update_gift_card).unwrap();
723
724 assert_eq!(json["expires_on"], "2026-06-30");
726 assert_eq!(json["note"], "Updated note");
727 assert_eq!(json["template_suffix"], "custom");
728 assert_eq!(json["customer_id"], 999999);
729 }
730
731 #[test]
732 fn test_gift_card_code_is_write_only() {
733 let create_gift_card = GiftCard {
735 initial_value: Some("100.00".to_string()),
736 code: Some("MYGIFTCODE1234".to_string()),
737 ..Default::default()
738 };
739
740 let json = serde_json::to_value(&create_gift_card).unwrap();
741 assert_eq!(json["code"], "MYGIFTCODE1234");
742 assert_eq!(json["initial_value"], "100.00");
743
744 let api_response = r#"{
746 "id": 123,
747 "balance": "100.00",
748 "initial_value": "100.00",
749 "last_characters": "1234"
750 }"#;
751
752 let gift_card: GiftCard = serde_json::from_str(api_response).unwrap();
753 assert_eq!(gift_card.code, None);
754 assert_eq!(gift_card.last_characters.as_deref(), Some("1234"));
755 }
756}