Skip to main content

max / makenotwork

Extract checkout-metadata helpers into their own module payments/checkout.rs had grown to ~1.3K lines, half of which was the metadata struct + line-item construction for each checkout variant. Move that half into payments/checkout_metadata.rs. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author: Max J. <87768334+MaxJMath@users.noreply.github.com> · 2026-05-15 16:03 UTC
Commit: 9e8b24228be8467087f99cac7906b9fae7a6badd
Parent: a63168b
3 files changed, +502 insertions, -497 deletions
@@ -583,695 +583,3 @@ pub struct AppSyncCheckoutParams<'a> {
583 583 pub success_url: &'a str,
584 584 pub cancel_url: &'a str,
585 585 }
586 -
587 - // ── Metadata types ──
588 -
589 - /// Parsed metadata from a checkout session
590 - #[derive(Debug)]
591 - pub struct CheckoutMetadata {
592 - /// UUID of the user making the purchase.
593 - pub buyer_id: UserId,
594 - /// UUID of the creator receiving payment.
595 - pub seller_id: UserId,
596 - /// UUID of the item being purchased (`None` for project-level purchases).
597 - pub item_id: Option<ItemId>,
598 - /// UUID of the promo code used, if any.
599 - pub promo_code_id: Option<PromoCodeId>,
600 - }
601 -
602 - impl CheckoutMetadata {
603 - /// Extract metadata from a checkout session
604 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
605 - let metadata = session.metadata.as_ref()
606 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
607 -
608 - let buyer_id: UserId = metadata.get("buyer_id")
609 - .ok_or_else(|| AppError::BadRequest("Missing buyer_id in metadata".to_string()))?
610 - .parse::<uuid::Uuid>()
611 - .map(UserId::from)
612 - .map_err(|_| AppError::BadRequest("Invalid buyer_id format".to_string()))?;
613 -
614 - let seller_id: UserId = metadata.get("seller_id")
615 - .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
616 - .parse::<uuid::Uuid>()
617 - .map(UserId::from)
618 - .map_err(|_| AppError::BadRequest("Invalid seller_id format".to_string()))?;
619 -
620 - let item_id: Option<ItemId> = metadata.get("item_id")
621 - .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ItemId::from));
622 -
623 - let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
624 - .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
625 -
626 - Ok(CheckoutMetadata {
627 - buyer_id,
628 - seller_id,
629 - item_id,
630 - promo_code_id,
631 - })
632 - }
633 - }
634 -
635 - /// Parsed metadata from a subscription checkout session
636 - #[derive(Debug)]
637 - pub struct SubscriptionCheckoutMetadata {
638 - pub subscriber_id: UserId,
639 - pub project_id: ProjectId,
640 - pub tier_id: SubscriptionTierId,
641 - pub promo_code_id: Option<PromoCodeId>,
642 - }
643 -
644 - impl SubscriptionCheckoutMetadata {
645 - /// Extract subscription metadata from a checkout session
646 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
647 - let metadata = session.metadata.as_ref()
648 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
649 -
650 - let subscriber_id: UserId = metadata.get("subscriber_id")
651 - .ok_or_else(|| AppError::BadRequest("Missing subscriber_id in metadata".to_string()))?
652 - .parse::<uuid::Uuid>()
653 - .map(UserId::from)
654 - .map_err(|_| AppError::BadRequest("Invalid subscriber_id format".to_string()))?;
655 -
656 - let project_id: ProjectId = metadata.get("project_id")
657 - .ok_or_else(|| AppError::BadRequest("Missing project_id in metadata".to_string()))?
658 - .parse::<uuid::Uuid>()
659 - .map(ProjectId::from)
660 - .map_err(|_| AppError::BadRequest("Invalid project_id format".to_string()))?;
661 -
662 - let tier_id: SubscriptionTierId = metadata.get("tier_id")
663 - .ok_or_else(|| AppError::BadRequest("Missing tier_id in metadata".to_string()))?
664 - .parse::<uuid::Uuid>()
665 - .map(SubscriptionTierId::from)
666 - .map_err(|_| AppError::BadRequest("Invalid tier_id format".to_string()))?;
667 -
668 - let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
669 - .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
670 -
671 - Ok(SubscriptionCheckoutMetadata {
672 - subscriber_id,
673 - project_id,
674 - tier_id,
675 - promo_code_id,
676 - })
677 - }
678 - }
679 -
680 - /// Parsed metadata from a Fan+ checkout session.
681 - #[derive(Debug)]
682 - pub struct FanPlusCheckoutMetadata {
683 - pub user_id: UserId,
684 - }
685 -
686 - impl FanPlusCheckoutMetadata {
687 - /// Extract Fan+ metadata from a checkout session.
688 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
689 - let metadata = session.metadata.as_ref()
690 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
691 -
692 - let user_id: UserId = metadata.get("user_id")
693 - .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
694 - .parse::<uuid::Uuid>()
695 - .map(UserId::from)
696 - .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
697 -
698 - Ok(FanPlusCheckoutMetadata { user_id })
699 - }
700 - }
701 -
702 - /// Parsed metadata from a creator tier checkout session.
703 - #[derive(Debug)]
704 - pub struct CreatorTierCheckoutMetadata {
705 - pub user_id: UserId,
706 - pub tier: String,
707 - }
708 -
709 - impl CreatorTierCheckoutMetadata {
710 - /// Extract creator tier metadata from a checkout session.
711 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
712 - let metadata = session.metadata.as_ref()
713 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
714 -
715 - let user_id: UserId = metadata.get("user_id")
716 - .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
717 - .parse::<uuid::Uuid>()
718 - .map(UserId::from)
719 - .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
720 -
721 - let tier = metadata.get("tier")
722 - .ok_or_else(|| AppError::BadRequest("Missing tier in metadata".to_string()))?
723 - .clone();
724 -
725 - Ok(CreatorTierCheckoutMetadata { user_id, tier })
726 - }
727 - }
728 -
729 - /// Parsed metadata from a tip checkout session.
730 - #[derive(Debug)]
731 - pub struct TipCheckoutMetadata {
732 - pub tipper_id: UserId,
733 - pub recipient_id: UserId,
734 - pub project_id: Option<ProjectId>,
735 - pub message: Option<String>,
736 - }
737 -
738 - impl TipCheckoutMetadata {
739 - /// Extract tip metadata from a checkout session.
740 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
741 - let metadata = session.metadata.as_ref()
742 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
743 -
744 - let tipper_id: UserId = metadata.get("tipper_id")
745 - .ok_or_else(|| AppError::BadRequest("Missing tipper_id in metadata".to_string()))?
746 - .parse::<uuid::Uuid>()
747 - .map(UserId::from)
748 - .map_err(|_| AppError::BadRequest("Invalid tipper_id format".to_string()))?;
749 -
750 - let recipient_id: UserId = metadata.get("recipient_id")
751 - .ok_or_else(|| AppError::BadRequest("Missing recipient_id in metadata".to_string()))?
752 - .parse::<uuid::Uuid>()
753 - .map(UserId::from)
754 - .map_err(|_| AppError::BadRequest("Invalid recipient_id format".to_string()))?;
755 -
756 - let project_id: Option<ProjectId> = metadata.get("project_id")
757 - .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ProjectId::from));
758 -
759 - let message = metadata.get("message").cloned();
760 -
761 - Ok(TipCheckoutMetadata {
762 - tipper_id,
763 - recipient_id,
764 - project_id,
765 - message,
766 - })
767 - }
768 - }
769 -
770 - /// Parsed metadata from an app sync checkout session.
771 - #[derive(Debug)]
772 - pub struct AppSyncCheckoutMetadata {
773 - pub user_id: UserId,
774 - pub app_id: SyncAppId,
775 - pub tier: String,
776 - pub app_name: String,
777 - }
778 -
779 - impl AppSyncCheckoutMetadata {
780 - /// Extract app sync metadata from a checkout session.
781 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
782 - let metadata = session.metadata.as_ref()
783 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
784 -
785 - let user_id: UserId = metadata.get("user_id")
786 - .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
787 - .parse::<uuid::Uuid>()
788 - .map(UserId::from)
789 - .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
790 -
791 - let app_id: SyncAppId = metadata.get("app_id")
792 - .ok_or_else(|| AppError::BadRequest("Missing app_id in metadata".to_string()))?
793 - .parse::<uuid::Uuid>()
794 - .map(SyncAppId::from)
795 - .map_err(|_| AppError::BadRequest("Invalid app_id format".to_string()))?;
796 -
797 - let tier = metadata.get("tier")
798 - .ok_or_else(|| AppError::BadRequest("Missing tier in metadata".to_string()))?
799 - .clone();
800 -
801 - let app_name = metadata.get("app_name")
802 - .ok_or_else(|| AppError::BadRequest("Missing app_name in metadata".to_string()))?
803 - .clone();
804 -
805 - Ok(AppSyncCheckoutMetadata { user_id, app_id, tier, app_name })
806 - }
807 - }
808 -
809 - /// Extract the checkout type from a Stripe session's metadata.
810 - pub fn get_checkout_type(session: &CheckoutSession) -> Option<CheckoutType> {
811 - session.metadata.as_ref()
812 - .and_then(|m| m.get("checkout_type"))
813 - .and_then(|t| t.parse().ok())
814 - }
815 -
816 - /// Check if a checkout session is for a tip.
817 - pub fn is_tip_checkout(session: &CheckoutSession) -> bool {
818 - get_checkout_type(session) == Some(CheckoutType::Tip)
819 - }
820 -
821 - /// Check if a checkout session is for a Fan+ subscription.
822 - pub fn is_fan_plus_checkout(session: &CheckoutSession) -> bool {
823 - get_checkout_type(session) == Some(CheckoutType::FanPlus)
824 - }
825 -
826 - /// Check if a checkout session is for a creator tier subscription.
827 - pub fn is_creator_tier_checkout(session: &CheckoutSession) -> bool {
828 - get_checkout_type(session) == Some(CheckoutType::CreatorTier)
829 - }
830 -
831 - /// Check if a checkout session is for a subscription (vs one-time purchase)
832 - pub fn is_subscription_checkout(session: &CheckoutSession) -> bool {
833 - get_checkout_type(session) == Some(CheckoutType::Subscription)
834 - }
835 -
836 - /// Check if a checkout session is a guest checkout (no MNW account).
837 - pub fn is_guest_checkout(session: &CheckoutSession) -> bool {
838 - get_checkout_type(session) == Some(CheckoutType::Guest)
839 - }
840 -
841 - /// Check if a checkout session is for an app sync subscription.
842 - pub fn is_app_sync_checkout(session: &CheckoutSession) -> bool {
843 - get_checkout_type(session) == Some(CheckoutType::AppSync)
844 - }
845 -
846 - /// Check if a checkout session is a cart (multi-item) checkout.
847 - pub fn is_cart_checkout(session: &CheckoutSession) -> bool {
848 - get_checkout_type(session) == Some(CheckoutType::Cart)
849 - }
850 -
851 - /// Parsed metadata from a cart checkout session.
852 - #[derive(Debug)]
853 - pub struct CartCheckoutMetadata {
854 - pub buyer_id: UserId,
855 - pub seller_id: UserId,
856 - }
857 -
858 - impl CartCheckoutMetadata {
859 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
860 - let meta = session.metadata.as_ref().ok_or(AppError::BadRequest(
861 - "Missing checkout metadata".to_string(),
862 - ))?;
863 -
864 - let buyer_id: UserId = meta
865 - .get("buyer_id")
866 - .ok_or_else(|| AppError::BadRequest("Missing buyer_id in metadata".to_string()))?
867 - .parse()
868 - .map_err(|_| AppError::BadRequest("Invalid buyer_id".to_string()))?;
869 -
870 - let seller_id: UserId = meta
871 - .get("seller_id")
872 - .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
873 - .parse()
874 - .map_err(|_| AppError::BadRequest("Invalid seller_id".to_string()))?;
875 -
876 - Ok(Self { buyer_id, seller_id })
877 - }
878 - }
879 -
880 - /// Parsed metadata from a guest checkout session.
881 - #[derive(Debug)]
882 - pub struct GuestCheckoutMetadata {
883 - pub seller_id: UserId,
884 - pub item_id: ItemId,
885 - pub promo_code_id: Option<PromoCodeId>,
886 - }
887 -
888 - impl GuestCheckoutMetadata {
889 - pub fn from_session(session: &CheckoutSession) -> Result<Self> {
890 - let metadata = session.metadata.as_ref()
891 - .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
892 -
893 - let seller_id: UserId = metadata.get("seller_id")
894 - .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
895 - .parse::<uuid::Uuid>()
896 - .map(UserId::from)
897 - .map_err(|_| AppError::BadRequest("Invalid seller_id format".to_string()))?;
898 -
899 - let item_id: ItemId = metadata.get("item_id")
900 - .ok_or_else(|| AppError::BadRequest("Missing item_id in metadata".to_string()))?
901 - .parse::<uuid::Uuid>()
902 - .map(ItemId::from)
903 - .map_err(|_| AppError::BadRequest("Invalid item_id format".to_string()))?;
904 -
905 - let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
906 - .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
907 -
908 - Ok(GuestCheckoutMetadata { seller_id, item_id, promo_code_id })
909 - }
910 - }
911 -
912 - #[cfg(test)]
913 - mod tests {
914 - use super::*;
915 - use std::collections::HashMap;
916 -
917 - #[allow(clippy::field_reassign_with_default)]
918 - fn session_with_metadata(metadata: Option<HashMap<String, String>>) -> CheckoutSession {
919 - let mut session = CheckoutSession::default();
920 - session.metadata = metadata;
921 - session
922 - }
923 -
924 - // --- CheckoutMetadata::from_session ---
925 -
926 - #[test]
927 - fn from_session_valid_metadata() {
928 - let buyer = UserId::new();
929 - let seller = UserId::new();
930 - let item = ItemId::new();
931 - let mut meta = HashMap::new();
932 - meta.insert("buyer_id".to_string(), buyer.to_string());
933 - meta.insert("seller_id".to_string(), seller.to_string());
934 - meta.insert("item_id".to_string(), item.to_string());
935 -
936 - let session = session_with_metadata(Some(meta));
937 - let result = CheckoutMetadata::from_session(&session).unwrap();
938 - assert_eq!(result.buyer_id, buyer);
939 - assert_eq!(result.seller_id, seller);
940 - assert_eq!(result.item_id, Some(item));
941 - }
942 -
943 - #[test]
944 - fn from_session_missing_metadata() {
945 - let session = session_with_metadata(None);
946 - assert!(CheckoutMetadata::from_session(&session).is_err());
947 - }
948 -
949 - #[test]
950 - fn from_session_missing_buyer_id() {
951 - let mut meta = HashMap::new();
952 - meta.insert("seller_id".to_string(), UserId::new().to_string());
953 - meta.insert("item_id".to_string(), ItemId::new().to_string());
954 - let session = session_with_metadata(Some(meta));
955 - assert!(CheckoutMetadata::from_session(&session).is_err());
956 - }
957 -
958 - #[test]
959 - fn from_session_missing_seller_id() {
960 - let mut meta = HashMap::new();
961 - meta.insert("buyer_id".to_string(), UserId::new().to_string());
962 - meta.insert("item_id".to_string(), ItemId::new().to_string());
963 - let session = session_with_metadata(Some(meta));
964 - assert!(CheckoutMetadata::from_session(&session).is_err());
965 - }
966 -
967 - #[test]
968 - fn from_session_invalid_uuid_format() {
969 - let mut meta = HashMap::new();
970 - meta.insert("buyer_id".to_string(), "not-a-uuid".to_string());
971 - meta.insert("seller_id".to_string(), UserId::new().to_string());
972 - meta.insert("item_id".to_string(), ItemId::new().to_string());
973 - let session = session_with_metadata(Some(meta));
974 - assert!(CheckoutMetadata::from_session(&session).is_err());
975 - }
976 -
977 - #[test]
978 - fn from_session_empty_metadata() {
979 - let session = session_with_metadata(Some(HashMap::new()));
980 - assert!(CheckoutMetadata::from_session(&session).is_err());
981 - }
982 -
983 - // --- SubscriptionCheckoutMetadata ---
984 -
985 - #[test]
986 - fn sub_metadata_valid() {
987 - let sub_id = UserId::new();
988 - let proj_id = ProjectId::new();
989 - let tier_id = SubscriptionTierId::new();
990 - let mut meta = HashMap::new();
991 - meta.insert("subscriber_id".to_string(), sub_id.to_string());
992 - meta.insert("project_id".to_string(), proj_id.to_string());
993 - meta.insert("tier_id".to_string(), tier_id.to_string());
994 - meta.insert("checkout_type".to_string(), "subscription".to_string());
995 -
996 - let session = session_with_metadata(Some(meta));
997 - let result = SubscriptionCheckoutMetadata::from_session(&session).unwrap();
998 - assert_eq!(result.subscriber_id, sub_id);
999 - assert_eq!(result.project_id, proj_id);
1000 - assert_eq!(result.tier_id, tier_id);
1001 - }
1002 -
1003 - #[test]
1004 - fn sub_metadata_missing_tier_id() {
1005 - let mut meta = HashMap::new();
1006 - meta.insert("subscriber_id".to_string(), UserId::new().to_string());
1007 - meta.insert("project_id".to_string(), ProjectId::new().to_string());
1008 - let session = session_with_metadata(Some(meta));
1009 - assert!(SubscriptionCheckoutMetadata::from_session(&session).is_err());
1010 - }
1011 -
1012 - #[test]
1013 - fn sub_metadata_missing_all() {
1014 - let session = session_with_metadata(None);
1015 - assert!(SubscriptionCheckoutMetadata::from_session(&session).is_err());
1016 - }
1017 -
1018 - // --- is_*_checkout ---
1019 -
1020 - #[test]
1021 - fn is_subscription_checkout_true() {
1022 - let mut meta = HashMap::new();
1023 - meta.insert("checkout_type".to_string(), "subscription".to_string());
1024 - let session = session_with_metadata(Some(meta));
1025 - assert!(is_subscription_checkout(&session));
1026 - }
1027 -
1028 - #[test]
1029 - fn is_subscription_checkout_false_no_metadata() {
1030 - let session = session_with_metadata(None);
1031 - assert!(!is_subscription_checkout(&session));
1032 - }
1033 -
1034 - #[test]
1035 - fn is_subscription_checkout_false_purchase() {
1036 - let mut meta = HashMap::new();
1037 - meta.insert("buyer_id".to_string(), UserId::new().to_string());
1038 - let session = session_with_metadata(Some(meta));
1039 - assert!(!is_subscription_checkout(&session));
1040 - }
1041 -
1042 - // --- CartCheckoutMetadata ---
1043 -
1044 - #[test]
1045 - fn cart_metadata_valid() {
1046 - let buyer = UserId::new();
1047 - let seller = UserId::new();
1048 - let mut meta = HashMap::new();
1049 - meta.insert("checkout_type".to_string(), "cart".to_string());
1050 - meta.insert("buyer_id".to_string(), buyer.to_string());
1051 - meta.insert("seller_id".to_string(), seller.to_string());
1052 -
1053 - let session = session_with_metadata(Some(meta));
1054 - let result = CartCheckoutMetadata::from_session(&session).unwrap();
1055 - assert_eq!(result.buyer_id, buyer);
1056 - assert_eq!(result.seller_id, seller);
1057 - }
1058 -
1059 - #[test]
1060 - fn cart_metadata_missing_buyer_id() {
1061 - let mut meta = HashMap::new();
1062 - meta.insert("checkout_type".to_string(), "cart".to_string());
1063 - meta.insert("seller_id".to_string(), UserId::new().to_string());
1064 - let session = session_with_metadata(Some(meta));
1065 - assert!(CartCheckoutMetadata::from_session(&session).is_err());
1066 - }
1067 -
1068 - #[test]
1069 - fn cart_metadata_missing_seller_id() {
1070 - let mut meta = HashMap::new();
1071 - meta.insert("checkout_type".to_string(), "cart".to_string());
1072 - meta.insert("buyer_id".to_string(), UserId::new().to_string());
1073 - let session = session_with_metadata(Some(meta));
1074 - assert!(CartCheckoutMetadata::from_session(&session).is_err());
1075 - }
1076 -
1077 - #[test]
1078 - fn cart_metadata_invalid_uuid() {
1079 - let mut meta = HashMap::new();
1080 - meta.insert("checkout_type".to_string(), "cart".to_string());
1081 - meta.insert("buyer_id".to_string(), "not-a-uuid".to_string());
1082 - meta.insert("seller_id".to_string(), UserId::new().to_string());
Lines truncated
@@ -0,0 +1,701 @@
1 + //! Parsed metadata types for Stripe Checkout sessions, plus type-discriminator helpers.
2 + //!
3 + //! Each variant (`CheckoutMetadata`, `SubscriptionCheckoutMetadata`, etc.) corresponds
4 + //! to one of the `CheckoutType` flavors set in the session's `metadata.checkout_type`
5 + //! field at creation time. The `from_session` constructors parse the relevant fields
6 + //! back out at webhook time.
7 +
8 + use stripe::CheckoutSession;
9 +
10 + use crate::db::{CheckoutType, ItemId, ProjectId, PromoCodeId, SubscriptionTierId, SyncAppId, UserId};
11 + use crate::error::{AppError, Result};
12 +
13 + /// Parsed metadata from a checkout session
14 + #[derive(Debug)]
15 + pub struct CheckoutMetadata {
16 + /// UUID of the user making the purchase.
17 + pub buyer_id: UserId,
18 + /// UUID of the creator receiving payment.
19 + pub seller_id: UserId,
20 + /// UUID of the item being purchased (`None` for project-level purchases).
21 + pub item_id: Option<ItemId>,
22 + /// UUID of the promo code used, if any.
23 + pub promo_code_id: Option<PromoCodeId>,
24 + }
25 +
26 + impl CheckoutMetadata {
27 + /// Extract metadata from a checkout session
28 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
29 + let metadata = session.metadata.as_ref()
30 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
31 +
32 + let buyer_id: UserId = metadata.get("buyer_id")
33 + .ok_or_else(|| AppError::BadRequest("Missing buyer_id in metadata".to_string()))?
34 + .parse::<uuid::Uuid>()
35 + .map(UserId::from)
36 + .map_err(|_| AppError::BadRequest("Invalid buyer_id format".to_string()))?;
37 +
38 + let seller_id: UserId = metadata.get("seller_id")
39 + .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
40 + .parse::<uuid::Uuid>()
41 + .map(UserId::from)
42 + .map_err(|_| AppError::BadRequest("Invalid seller_id format".to_string()))?;
43 +
44 + let item_id: Option<ItemId> = metadata.get("item_id")
45 + .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ItemId::from));
46 +
47 + let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
48 + .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
49 +
50 + Ok(CheckoutMetadata {
51 + buyer_id,
52 + seller_id,
53 + item_id,
54 + promo_code_id,
55 + })
56 + }
57 + }
58 +
59 + /// Parsed metadata from a subscription checkout session
60 + #[derive(Debug)]
61 + pub struct SubscriptionCheckoutMetadata {
62 + pub subscriber_id: UserId,
63 + pub project_id: ProjectId,
64 + pub tier_id: SubscriptionTierId,
65 + pub promo_code_id: Option<PromoCodeId>,
66 + }
67 +
68 + impl SubscriptionCheckoutMetadata {
69 + /// Extract subscription metadata from a checkout session
70 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
71 + let metadata = session.metadata.as_ref()
72 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
73 +
74 + let subscriber_id: UserId = metadata.get("subscriber_id")
75 + .ok_or_else(|| AppError::BadRequest("Missing subscriber_id in metadata".to_string()))?
76 + .parse::<uuid::Uuid>()
77 + .map(UserId::from)
78 + .map_err(|_| AppError::BadRequest("Invalid subscriber_id format".to_string()))?;
79 +
80 + let project_id: ProjectId = metadata.get("project_id")
81 + .ok_or_else(|| AppError::BadRequest("Missing project_id in metadata".to_string()))?
82 + .parse::<uuid::Uuid>()
83 + .map(ProjectId::from)
84 + .map_err(|_| AppError::BadRequest("Invalid project_id format".to_string()))?;
85 +
86 + let tier_id: SubscriptionTierId = metadata.get("tier_id")
87 + .ok_or_else(|| AppError::BadRequest("Missing tier_id in metadata".to_string()))?
88 + .parse::<uuid::Uuid>()
89 + .map(SubscriptionTierId::from)
90 + .map_err(|_| AppError::BadRequest("Invalid tier_id format".to_string()))?;
91 +
92 + let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
93 + .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
94 +
95 + Ok(SubscriptionCheckoutMetadata {
96 + subscriber_id,
97 + project_id,
98 + tier_id,
99 + promo_code_id,
100 + })
101 + }
102 + }
103 +
104 + /// Parsed metadata from a Fan+ checkout session.
105 + #[derive(Debug)]
106 + pub struct FanPlusCheckoutMetadata {
107 + pub user_id: UserId,
108 + }
109 +
110 + impl FanPlusCheckoutMetadata {
111 + /// Extract Fan+ metadata from a checkout session.
112 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
113 + let metadata = session.metadata.as_ref()
114 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
115 +
116 + let user_id: UserId = metadata.get("user_id")
117 + .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
118 + .parse::<uuid::Uuid>()
119 + .map(UserId::from)
120 + .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
121 +
122 + Ok(FanPlusCheckoutMetadata { user_id })
123 + }
124 + }
125 +
126 + /// Parsed metadata from a creator tier checkout session.
127 + #[derive(Debug)]
128 + pub struct CreatorTierCheckoutMetadata {
129 + pub user_id: UserId,
130 + pub tier: String,
131 + }
132 +
133 + impl CreatorTierCheckoutMetadata {
134 + /// Extract creator tier metadata from a checkout session.
135 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
136 + let metadata = session.metadata.as_ref()
137 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
138 +
139 + let user_id: UserId = metadata.get("user_id")
140 + .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
141 + .parse::<uuid::Uuid>()
142 + .map(UserId::from)
143 + .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
144 +
145 + let tier = metadata.get("tier")
146 + .ok_or_else(|| AppError::BadRequest("Missing tier in metadata".to_string()))?
147 + .clone();
148 +
149 + Ok(CreatorTierCheckoutMetadata { user_id, tier })
150 + }
151 + }
152 +
153 + /// Parsed metadata from a tip checkout session.
154 + #[derive(Debug)]
155 + pub struct TipCheckoutMetadata {
156 + pub tipper_id: UserId,
157 + pub recipient_id: UserId,
158 + pub project_id: Option<ProjectId>,
159 + pub message: Option<String>,
160 + }
161 +
162 + impl TipCheckoutMetadata {
163 + /// Extract tip metadata from a checkout session.
164 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
165 + let metadata = session.metadata.as_ref()
166 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
167 +
168 + let tipper_id: UserId = metadata.get("tipper_id")
169 + .ok_or_else(|| AppError::BadRequest("Missing tipper_id in metadata".to_string()))?
170 + .parse::<uuid::Uuid>()
171 + .map(UserId::from)
172 + .map_err(|_| AppError::BadRequest("Invalid tipper_id format".to_string()))?;
173 +
174 + let recipient_id: UserId = metadata.get("recipient_id")
175 + .ok_or_else(|| AppError::BadRequest("Missing recipient_id in metadata".to_string()))?
176 + .parse::<uuid::Uuid>()
177 + .map(UserId::from)
178 + .map_err(|_| AppError::BadRequest("Invalid recipient_id format".to_string()))?;
179 +
180 + let project_id: Option<ProjectId> = metadata.get("project_id")
181 + .and_then(|v| v.parse::<uuid::Uuid>().ok().map(ProjectId::from));
182 +
183 + let message = metadata.get("message").cloned();
184 +
185 + Ok(TipCheckoutMetadata {
186 + tipper_id,
187 + recipient_id,
188 + project_id,
189 + message,
190 + })
191 + }
192 + }
193 +
194 + /// Parsed metadata from an app sync checkout session.
195 + #[derive(Debug)]
196 + pub struct AppSyncCheckoutMetadata {
197 + pub user_id: UserId,
198 + pub app_id: SyncAppId,
199 + pub tier: String,
200 + pub app_name: String,
201 + }
202 +
203 + impl AppSyncCheckoutMetadata {
204 + /// Extract app sync metadata from a checkout session.
205 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
206 + let metadata = session.metadata.as_ref()
207 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
208 +
209 + let user_id: UserId = metadata.get("user_id")
210 + .ok_or_else(|| AppError::BadRequest("Missing user_id in metadata".to_string()))?
211 + .parse::<uuid::Uuid>()
212 + .map(UserId::from)
213 + .map_err(|_| AppError::BadRequest("Invalid user_id format".to_string()))?;
214 +
215 + let app_id: SyncAppId = metadata.get("app_id")
216 + .ok_or_else(|| AppError::BadRequest("Missing app_id in metadata".to_string()))?
217 + .parse::<uuid::Uuid>()
218 + .map(SyncAppId::from)
219 + .map_err(|_| AppError::BadRequest("Invalid app_id format".to_string()))?;
220 +
221 + let tier = metadata.get("tier")
222 + .ok_or_else(|| AppError::BadRequest("Missing tier in metadata".to_string()))?
223 + .clone();
224 +
225 + let app_name = metadata.get("app_name")
226 + .ok_or_else(|| AppError::BadRequest("Missing app_name in metadata".to_string()))?
227 + .clone();
228 +
229 + Ok(AppSyncCheckoutMetadata { user_id, app_id, tier, app_name })
230 + }
231 + }
232 +
233 + /// Extract the checkout type from a Stripe session's metadata.
234 + pub fn get_checkout_type(session: &CheckoutSession) -> Option<CheckoutType> {
235 + session.metadata.as_ref()
236 + .and_then(|m| m.get("checkout_type"))
237 + .and_then(|t| t.parse().ok())
238 + }
239 +
240 + /// Check if a checkout session is for a tip.
241 + pub fn is_tip_checkout(session: &CheckoutSession) -> bool {
242 + get_checkout_type(session) == Some(CheckoutType::Tip)
243 + }
244 +
245 + /// Check if a checkout session is for a Fan+ subscription.
246 + pub fn is_fan_plus_checkout(session: &CheckoutSession) -> bool {
247 + get_checkout_type(session) == Some(CheckoutType::FanPlus)
248 + }
249 +
250 + /// Check if a checkout session is for a creator tier subscription.
251 + pub fn is_creator_tier_checkout(session: &CheckoutSession) -> bool {
252 + get_checkout_type(session) == Some(CheckoutType::CreatorTier)
253 + }
254 +
255 + /// Check if a checkout session is for a subscription (vs one-time purchase)
256 + pub fn is_subscription_checkout(session: &CheckoutSession) -> bool {
257 + get_checkout_type(session) == Some(CheckoutType::Subscription)
258 + }
259 +
260 + /// Check if a checkout session is a guest checkout (no MNW account).
261 + pub fn is_guest_checkout(session: &CheckoutSession) -> bool {
262 + get_checkout_type(session) == Some(CheckoutType::Guest)
263 + }
264 +
265 + /// Check if a checkout session is for an app sync subscription.
266 + pub fn is_app_sync_checkout(session: &CheckoutSession) -> bool {
267 + get_checkout_type(session) == Some(CheckoutType::AppSync)
268 + }
269 +
270 + /// Check if a checkout session is a cart (multi-item) checkout.
271 + pub fn is_cart_checkout(session: &CheckoutSession) -> bool {
272 + get_checkout_type(session) == Some(CheckoutType::Cart)
273 + }
274 +
275 + /// Parsed metadata from a cart checkout session.
276 + #[derive(Debug)]
277 + pub struct CartCheckoutMetadata {
278 + pub buyer_id: UserId,
279 + pub seller_id: UserId,
280 + }
281 +
282 + impl CartCheckoutMetadata {
283 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
284 + let meta = session.metadata.as_ref().ok_or(AppError::BadRequest(
285 + "Missing checkout metadata".to_string(),
286 + ))?;
287 +
288 + let buyer_id: UserId = meta
289 + .get("buyer_id")
290 + .ok_or_else(|| AppError::BadRequest("Missing buyer_id in metadata".to_string()))?
291 + .parse()
292 + .map_err(|_| AppError::BadRequest("Invalid buyer_id".to_string()))?;
293 +
294 + let seller_id: UserId = meta
295 + .get("seller_id")
296 + .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
297 + .parse()
298 + .map_err(|_| AppError::BadRequest("Invalid seller_id".to_string()))?;
299 +
300 + Ok(Self { buyer_id, seller_id })
301 + }
302 + }
303 +
304 + /// Parsed metadata from a guest checkout session.
305 + #[derive(Debug)]
306 + pub struct GuestCheckoutMetadata {
307 + pub seller_id: UserId,
308 + pub item_id: ItemId,
309 + pub promo_code_id: Option<PromoCodeId>,
310 + }
311 +
312 + impl GuestCheckoutMetadata {
313 + pub fn from_session(session: &CheckoutSession) -> Result<Self> {
314 + let metadata = session.metadata.as_ref()
315 + .ok_or_else(|| AppError::BadRequest("Missing session metadata".to_string()))?;
316 +
317 + let seller_id: UserId = metadata.get("seller_id")
318 + .ok_or_else(|| AppError::BadRequest("Missing seller_id in metadata".to_string()))?
319 + .parse::<uuid::Uuid>()
320 + .map(UserId::from)
321 + .map_err(|_| AppError::BadRequest("Invalid seller_id format".to_string()))?;
322 +
323 + let item_id: ItemId = metadata.get("item_id")
324 + .ok_or_else(|| AppError::BadRequest("Missing item_id in metadata".to_string()))?
325 + .parse::<uuid::Uuid>()
326 + .map(ItemId::from)
327 + .map_err(|_| AppError::BadRequest("Invalid item_id format".to_string()))?;
328 +
329 + let promo_code_id: Option<PromoCodeId> = metadata.get("promo_code_id")
330 + .and_then(|v| v.parse::<uuid::Uuid>().ok().map(PromoCodeId::from));
331 +
332 + Ok(GuestCheckoutMetadata { seller_id, item_id, promo_code_id })
333 + }
334 + }
335 +
336 + #[cfg(test)]
337 + mod tests {
338 + use super::*;
339 + use std::collections::HashMap;
340 +
341 + #[allow(clippy::field_reassign_with_default)]
342 + fn session_with_metadata(metadata: Option<HashMap<String, String>>) -> CheckoutSession {
343 + let mut session = CheckoutSession::default();
344 + session.metadata = metadata;
345 + session
346 + }
347 +
348 + // --- CheckoutMetadata::from_session ---
349 +
350 + #[test]
351 + fn from_session_valid_metadata() {
352 + let buyer = UserId::new();
353 + let seller = UserId::new();
354 + let item = ItemId::new();
355 + let mut meta = HashMap::new();
356 + meta.insert("buyer_id".to_string(), buyer.to_string());
357 + meta.insert("seller_id".to_string(), seller.to_string());
358 + meta.insert("item_id".to_string(), item.to_string());
359 +
360 + let session = session_with_metadata(Some(meta));
361 + let result = CheckoutMetadata::from_session(&session).unwrap();
362 + assert_eq!(result.buyer_id, buyer);
363 + assert_eq!(result.seller_id, seller);
364 + assert_eq!(result.item_id, Some(item));
365 + }
366 +
367 + #[test]
368 + fn from_session_missing_metadata() {
369 + let session = session_with_metadata(None);
370 + assert!(CheckoutMetadata::from_session(&session).is_err());
371 + }
372 +
373 + #[test]
374 + fn from_session_missing_buyer_id() {
375 + let mut meta = HashMap::new();
376 + meta.insert("seller_id".to_string(), UserId::new().to_string());
377 + meta.insert("item_id".to_string(), ItemId::new().to_string());
378 + let session = session_with_metadata(Some(meta));
379 + assert!(CheckoutMetadata::from_session(&session).is_err());
380 + }
381 +
382 + #[test]
383 + fn from_session_missing_seller_id() {
384 + let mut meta = HashMap::new();
385 + meta.insert("buyer_id".to_string(), UserId::new().to_string());
386 + meta.insert("item_id".to_string(), ItemId::new().to_string());
387 + let session = session_with_metadata(Some(meta));
388 + assert!(CheckoutMetadata::from_session(&session).is_err());
389 + }
390 +
391 + #[test]
392 + fn from_session_invalid_uuid_format() {
393 + let mut meta = HashMap::new();
394 + meta.insert("buyer_id".to_string(), "not-a-uuid".to_string());
395 + meta.insert("seller_id".to_string(), UserId::new().to_string());
396 + meta.insert("item_id".to_string(), ItemId::new().to_string());
397 + let session = session_with_metadata(Some(meta));
398 + assert!(CheckoutMetadata::from_session(&session).is_err());
399 + }
400 +
401 + #[test]
402 + fn from_session_empty_metadata() {
403 + let session = session_with_metadata(Some(HashMap::new()));
404 + assert!(CheckoutMetadata::from_session(&session).is_err());
405 + }
406 +
407 + // --- SubscriptionCheckoutMetadata ---
408 +
409 + #[test]
410 + fn sub_metadata_valid() {
411 + let sub_id = UserId::new();
412 + let proj_id = ProjectId::new();
413 + let tier_id = SubscriptionTierId::new();
414 + let mut meta = HashMap::new();
415 + meta.insert("subscriber_id".to_string(), sub_id.to_string());
416 + meta.insert("project_id".to_string(), proj_id.to_string());
417 + meta.insert("tier_id".to_string(), tier_id.to_string());
418 + meta.insert("checkout_type".to_string(), "subscription".to_string());
419 +
420 + let session = session_with_metadata(Some(meta));
421 + let result = SubscriptionCheckoutMetadata::from_session(&session).unwrap();
422 + assert_eq!(result.subscriber_id, sub_id);
423 + assert_eq!(result.project_id, proj_id);
424 + assert_eq!(result.tier_id, tier_id);
425 + }
426 +
427 + #[test]
428 + fn sub_metadata_missing_tier_id() {
429 + let mut meta = HashMap::new();
430 + meta.insert("subscriber_id".to_string(), UserId::new().to_string());
431 + meta.insert("project_id".to_string(), ProjectId::new().to_string());
432 + let session = session_with_metadata(Some(meta));
433 + assert!(SubscriptionCheckoutMetadata::from_session(&session).is_err());
434 + }
435 +
436 + #[test]
437 + fn sub_metadata_missing_all() {
438 + let session = session_with_metadata(None);
439 + assert!(SubscriptionCheckoutMetadata::from_session(&session).is_err());
440 + }
441 +
442 + // --- is_*_checkout ---
443 +
444 + #[test]
445 + fn is_subscription_checkout_true() {
446 + let mut meta = HashMap::new();
447 + meta.insert("checkout_type".to_string(), "subscription".to_string());
448 + let session = session_with_metadata(Some(meta));
449 + assert!(is_subscription_checkout(&session));
450 + }
451 +
452 + #[test]
453 + fn is_subscription_checkout_false_no_metadata() {
454 + let session = session_with_metadata(None);
455 + assert!(!is_subscription_checkout(&session));
456 + }
457 +
458 + #[test]
459 + fn is_subscription_checkout_false_purchase() {
460 + let mut meta = HashMap::new();
461 + meta.insert("buyer_id".to_string(), UserId::new().to_string());
462 + let session = session_with_metadata(Some(meta));
463 + assert!(!is_subscription_checkout(&session));
464 + }
465 +
466 + // --- CartCheckoutMetadata ---
467 +
468 + #[test]
469 + fn cart_metadata_valid() {
470 + let buyer = UserId::new();
471 + let seller = UserId::new();
472 + let mut meta = HashMap::new();
473 + meta.insert("checkout_type".to_string(), "cart".to_string());
474 + meta.insert("buyer_id".to_string(), buyer.to_string());
475 + meta.insert("seller_id".to_string(), seller.to_string());
476 +
477 + let session = session_with_metadata(Some(meta));
478 + let result = CartCheckoutMetadata::from_session(&session).unwrap();
479 + assert_eq!(result.buyer_id, buyer);
480 + assert_eq!(result.seller_id, seller);
481 + }
482 +
483 + #[test]
484 + fn cart_metadata_missing_buyer_id() {
485 + let mut meta = HashMap::new();
486 + meta.insert("checkout_type".to_string(), "cart".to_string());
487 + meta.insert("seller_id".to_string(), UserId::new().to_string());
488 + let session = session_with_metadata(Some(meta));
489 + assert!(CartCheckoutMetadata::from_session(&session).is_err());
490 + }
491 +
492 + #[test]
493 + fn cart_metadata_missing_seller_id() {
494 + let mut meta = HashMap::new();
495 + meta.insert("checkout_type".to_string(), "cart".to_string());
496 + meta.insert("buyer_id".to_string(), UserId::new().to_string());
497 + let session = session_with_metadata(Some(meta));
498 + assert!(CartCheckoutMetadata::from_session(&session).is_err());
499 + }
500 +
Lines truncated
@@ -15,10 +15,12 @@
15 15 //! - Subscription product and price creation on connected accounts
16 16
17 17 mod checkout;
18 + mod checkout_metadata;
18 19 mod connect;
19 20 mod webhooks;
20 21
21 22 pub use checkout::*;
23 + pub use checkout_metadata::*;
22 24 pub use webhooks::*;
23 25
24 26 use stripe::Client;