Skip to main content

max / makenotwork

4.5 KB · 165 lines History Blame Raw
1 //! Money moving: what a discount is, what a promo code is for, and the state
2 //! machines a transaction and a subscription each walk.
3
4 use super::str_enum::impl_str_enum;
5 use serde::{Deserialize, Serialize};
6
7 // --- Discount codes ---
8
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10 #[serde(rename_all = "lowercase")]
11 pub enum DiscountType {
12 Percentage,
13 Fixed,
14 }
15
16 impl_str_enum!(DiscountType {
17 Percentage => "percentage",
18 Fixed => "fixed",
19 });
20
21 // --- Promo codes ---
22
23 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24 #[serde(rename_all = "snake_case")]
25 pub enum CodePurpose {
26 Discount,
27 FreeAccess,
28 FreeTrial,
29 }
30
31 impl_str_enum!(CodePurpose {
32 Discount => "discount",
33 FreeAccess => "free_access",
34 FreeTrial => "free_trial",
35 });
36
37 // --- Transactions ---
38
39 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
40 #[serde(rename_all = "lowercase")]
41 pub enum TransactionStatus {
42 Pending,
43 Completed,
44 /// In-flight: a refund has been claimed (`completed -> refunding`) and sent to
45 /// Stripe, but the `refund.created` webhook has not yet finalized it. Guards
46 /// against double-submit on shared-cart PaymentIntents.
47 Refunding,
48 Refunded,
49 /// Present in the DB `CHECK` since the initial schema but never written by
50 /// the app (stale pending transactions are deleted, not failed). Kept as a
51 /// variant so the enum can decode any legacy/manual `'failed'` row instead of
52 /// fail-closed-poisoning the whole query, and so the enum-drift test's
53 /// variant set matches the column constraint.
54 Failed,
55 }
56
57 impl_str_enum!(TransactionStatus {
58 Pending => "pending",
59 Completed => "completed",
60 Refunding => "refunding",
61 Refunded => "refunded",
62 Failed => "failed",
63 });
64
65 impl TransactionStatus {
66 /// Badge vocabulary (charter: `docs/design-system.md`). A refund is over
67 /// and needs nobody, so it is neutral rather than red.
68 pub fn badge_status(self) -> crate::types::BadgeStatus {
69 use crate::types::BadgeStatus;
70 match self {
71 Self::Completed => BadgeStatus::Live,
72 Self::Pending | Self::Refunding => BadgeStatus::Pending,
73 Self::Failed => BadgeStatus::Failed,
74 Self::Refunded => BadgeStatus::Ended,
75 }
76 }
77 }
78
79 // --- Subscriptions ---
80
81 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
82 pub enum SubscriptionStatus {
83 #[serde(rename = "active")]
84 Active,
85 #[serde(rename = "trialing")]
86 Trialing,
87 #[serde(rename = "incomplete")]
88 Incomplete,
89 #[serde(rename = "incomplete_expired")]
90 IncompleteExpired,
91 #[serde(rename = "past_due")]
92 PastDue,
93 #[serde(rename = "canceled")]
94 Canceled,
95 #[serde(rename = "unpaid")]
96 Unpaid,
97 }
98
99 impl_str_enum!(SubscriptionStatus {
100 Active => "active",
101 Trialing => "trialing",
102 Incomplete => "incomplete",
103 IncompleteExpired => "incomplete_expired",
104 PastDue => "past_due",
105 Canceled => "canceled",
106 Unpaid => "unpaid",
107 });
108
109 impl SubscriptionStatus {
110 /// Badge vocabulary (charter: `docs/design-system.md`). A trial is live
111 /// because the subscriber has access; a cancellation is over and needs
112 /// nobody, so it is neutral rather than red.
113 pub fn badge_status(self) -> crate::types::BadgeStatus {
114 use crate::types::BadgeStatus;
115 match self {
116 Self::Active | Self::Trialing => BadgeStatus::Live,
117 Self::Incomplete => BadgeStatus::Pending,
118 Self::IncompleteExpired | Self::PastDue | Self::Unpaid => BadgeStatus::Failed,
119 Self::Canceled => BadgeStatus::Ended,
120 }
121 }
122 }
123
124 // --- Project Pricing ---
125
126 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
127 #[serde(rename_all = "snake_case")]
128 pub enum PricingKind {
129 #[default]
130 Free,
131 BuyOnce,
132 Pwyw,
133 Subscription,
134 }
135
136 impl_str_enum!(PricingKind {
137 Free => "free",
138 BuyOnce => "buy_once",
139 Pwyw => "pwyw",
140 Subscription => "subscription",
141 });
142
143 /// Discriminator for checkout session types stored in Stripe metadata.
144 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
145 #[serde(rename_all = "snake_case")]
146 pub enum CheckoutType {
147 Guest,
148 Subscription,
149 Tip,
150 FanPlus,
151 CreatorTier,
152 Cart,
153 SynckitAppSub,
154 }
155
156 impl_str_enum!(CheckoutType {
157 Guest => "guest",
158 Subscription => "subscription",
159 Tip => "tip",
160 FanPlus => "fan_plus",
161 CreatorTier => "creator_tier",
162 Cart => "cart",
163 SynckitAppSub => "synckit_app_sub",
164 });
165