Skip to main content

max / makenotwork

10.9 KB · 274 lines History Blame Raw
1 //! Promo code CRUD, validation, free trial, expiry, and project scope tests.
2
3 use crate::harness::TestHarness;
4 use serde_json::Value;
5
6 /// Creator setup: signup, grant, re-login, create project + item, publish both.
7 /// Returns (user_id, project_id, item_id).
8 async fn setup_creator_with_item(h: &mut TestHarness) -> (makenotwork::db::UserId, String, String) {
9 let setup = h.create_creator_with_item("dcseller", "digital", 1000).await;
10 h.publish_project_and_item(&setup.project_id, &setup.item_id).await;
11 (setup.user_id, setup.project_id, setup.item_id)
12 }
13
14 #[tokio::test]
15 async fn discount_code_crud_lifecycle() {
16 let mut h = TestHarness::new().await;
17 let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
18
19 // Create a percentage discount promo code
20 let resp = h.client.post_form(
21 "/api/promo-codes",
22 "code=SUMMER50&code_purpose=discount&discount_type=percentage&discount_value=50",
23 ).await;
24 assert!(resp.status.is_success(), "Create promo code failed: {} {}", resp.status, resp.text);
25 let code: Value = resp.json();
26 assert_eq!(code["code"].as_str().unwrap(), "SUMMER50");
27 let code_id = code["id"].as_str().expect("promo code should have id");
28
29 // List codes
30 let resp = h.client.get("/api/promo-codes").await;
31 assert!(resp.status.is_success(), "List promo codes failed: {} {}", resp.status, resp.text);
32 let list: Value = resp.json();
33 let data = list["data"].as_array().expect("data should be array");
34 assert_eq!(data.len(), 1);
35 assert_eq!(data[0]["code"].as_str().unwrap(), "SUMMER50");
36
37 // Delete code
38 let resp = h.client.delete(&format!("/api/promo-codes/{}", code_id)).await;
39 assert_eq!(resp.status, 204, "Delete should return 204 No Content");
40
41 // List again — should be empty
42 let resp = h.client.get("/api/promo-codes").await;
43 let list: Value = resp.json();
44 let data = list["data"].as_array().unwrap();
45 assert!(data.is_empty(), "Promo codes list should be empty after delete");
46 }
47
48 #[tokio::test]
49 async fn discount_code_item_scoped() {
50 let mut h = TestHarness::new().await;
51 let (_user_id, _project_id, item_id) = setup_creator_with_item(&mut h).await;
52
53 // Create code scoped to the item
54 let resp = h.client.post_form(
55 "/api/promo-codes",
56 &format!("code=ITEM10&code_purpose=discount&discount_type=fixed&discount_value=500&item_id={}", item_id),
57 ).await;
58 assert!(resp.status.is_success(), "Create item-scoped code failed: {} {}", resp.status, resp.text);
59 let code: Value = resp.json();
60 assert_eq!(code["code"].as_str().unwrap(), "ITEM10");
61 }
62
63 #[tokio::test]
64 async fn discount_code_validation_errors() {
65 let mut h = TestHarness::new().await;
66 let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
67
68 // Empty code
69 let resp = h.client.post_form(
70 "/api/promo-codes",
71 "code=&code_purpose=discount&discount_type=percentage&discount_value=50",
72 ).await;
73 assert_eq!(resp.status, 400, "Empty code should return 400");
74
75 // Percentage 0
76 let resp = h.client.post_form(
77 "/api/promo-codes",
78 "code=BAD1&code_purpose=discount&discount_type=percentage&discount_value=0",
79 ).await;
80 assert_eq!(resp.status, 400, "Percentage 0 should return 400");
81
82 // Percentage 101
83 let resp = h.client.post_form(
84 "/api/promo-codes",
85 "code=BAD2&code_purpose=discount&discount_type=percentage&discount_value=101",
86 ).await;
87 assert_eq!(resp.status, 400, "Percentage 101 should return 400");
88
89 // Fixed 0
90 let resp = h.client.post_form(
91 "/api/promo-codes",
92 "code=BAD3&code_purpose=discount&discount_type=fixed&discount_value=0",
93 ).await;
94 assert_eq!(resp.status, 400, "Fixed 0 should return 400");
95 }
96
97 #[tokio::test]
98 async fn discount_code_delete_other_users_code() {
99 let mut h = TestHarness::new().await;
100
101 // Seller A: create a promo code
102 let seller_a = h.signup("dcseller_a", "dcseller_a@test.com", "password123").await;
103 h.grant_creator(seller_a).await;
104 h.client.post_form("/logout", "").await;
105 h.login("dcseller_a", "password123").await;
106
107 let resp = h.client.post_form(
108 "/api/promo-codes",
109 "code=PRIVCODE&code_purpose=discount&discount_type=percentage&discount_value=25",
110 ).await;
111 assert!(resp.status.is_success(), "Seller A create failed: {}", resp.text);
112 let code: Value = resp.json();
113 let code_id = code["id"].as_str().unwrap();
114
115 // Switch to Seller B
116 h.client.post_form("/logout", "").await;
117 let seller_b = h.signup("dcseller_b", "dcseller_b@test.com", "password123").await;
118 h.grant_creator(seller_b).await;
119 h.client.post_form("/logout", "").await;
120 h.login("dcseller_b", "password123").await;
121
122 // Seller B tries to delete Seller A's code
123 let resp = h.client.delete(&format!("/api/promo-codes/{}", code_id)).await;
124 assert_eq!(resp.status, 403, "Deleting another user's code should return 403");
125 }
126
127 // =============================================================================
128 // Free trial promo code tests
129 // =============================================================================
130
131 #[tokio::test]
132 async fn free_trial_code_create() {
133 let mut h = TestHarness::new().await;
134 let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
135
136 // Create a free trial code with 14-day trial
137 let resp = h.client.post_form(
138 "/api/promo-codes",
139 "code=TRIAL14&code_purpose=free_trial&trial_days=14",
140 ).await;
141 assert!(resp.status.is_success(), "Create free trial code failed: {} {}", resp.status, resp.text);
142 let code: Value = resp.json();
143 assert_eq!(code["code"].as_str().unwrap(), "TRIAL14");
144 assert_eq!(code["trial_days"].as_i64().unwrap(), 14);
145 }
146
147 #[tokio::test]
148 async fn free_trial_code_reject_zero_days() {
149 let mut h = TestHarness::new().await;
150 let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
151
152 // trial_days = 0 should be rejected
153 let resp = h.client.post_form(
154 "/api/promo-codes",
155 "code=BADTRIAL&code_purpose=free_trial&trial_days=0",
156 ).await;
157 assert_eq!(resp.status, 400, "trial_days=0 should return 400");
158 }
159
160 #[tokio::test]
161 async fn free_trial_code_reject_at_item_checkout() {
162 let mut h = TestHarness::new().await;
163 let (_user_id, _project_id, item_id) = setup_creator_with_item(&mut h).await;
164
165 // Create a free trial code
166 let resp = h.client.post_form(
167 "/api/promo-codes",
168 "code=TRIALBAD&code_purpose=free_trial&trial_days=7",
169 ).await;
170 assert!(resp.status.is_success(), "Create trial code failed: {}", resp.text);
171
172 // Switch to buyer
173 h.client.post_form("/logout", "").await;
174 let _buyer_id = h.signup("trialbuyer", "trialbuyer@test.com", "password456").await;
175
176 // Try to use trial code at item checkout — should fail because trial codes
177 // are only for subscriptions
178 let resp = h.client.post_form(
179 &format!("/stripe/checkout/{}", item_id),
180 "promo_code=TRIALBAD",
181 ).await;
182 // Should get a 400 or redirect with error (trial codes not valid at item checkout)
183 assert!(resp.status == 400 || resp.text.contains("Trial codes can only be used for subscriptions"),
184 "Trial code should be rejected at item checkout: {} {}", resp.status, resp.text);
185 }
186
187 // =============================================================================
188 // Promo code expiry tests
189 // =============================================================================
190
191 #[tokio::test]
192 async fn discount_code_expired_rejected() {
193 let mut h = TestHarness::new().await;
194 let (user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
195
196 // Insert expired code directly (API now rejects past expiry dates)
197 sqlx::query(
198 "INSERT INTO promo_codes (creator_id, code, code_purpose, discount_type, discount_value, expires_at) \
199 VALUES ($1, 'EXPIRED1', 'discount', 'percentage', 50, '2020-01-01'::timestamptz)"
200 )
201 .bind(user_id)
202 .execute(&h.db)
203 .await
204 .unwrap();
205 let key_code = "EXPIRED1";
206
207 // Switch to buyer
208 h.client.post_form("/logout", "").await;
209 let _buyer_id = h.signup("expirybuyer", "expirybuyer@test.com", "password456").await;
210
211 // Try to use expired code at item checkout — should fail
212 let resp = h.client.post_form(
213 &format!("/stripe/checkout/{}", _item_id),
214 &format!("promo_code={}", key_code),
215 ).await;
216 assert!(resp.status == 400 || resp.text.contains("expired"),
217 "Expired code should be rejected: {} {}", resp.status, resp.text);
218 }
219
220 #[tokio::test]
221 async fn discount_code_future_expiry_accepted() {
222 let mut h = TestHarness::new().await;
223 let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await;
224
225 // Create a code with future expiry
226 let resp = h.client.post_form(
227 "/api/promo-codes",
228 "code=FUTURE1&code_purpose=discount&discount_type=percentage&discount_value=100&expires_at=2099-12-31",
229 ).await;
230 assert!(resp.status.is_success(), "Create future-expiry code failed: {} {}", resp.status, resp.text);
231 let code: Value = resp.json();
232 assert_eq!(code["code"].as_str().unwrap(), "FUTURE1");
233
234 // Switch to buyer and use it
235 h.client.post_form("/logout", "").await;
236 let _buyer_id = h.signup("futurebuyer", "futurebuyer@test.com", "password456").await;
237
238 // Use 100% discount code at item checkout — should succeed (free claim path)
239 let resp = h.client.post_form(
240 &format!("/stripe/checkout/{}", _item_id),
241 "promo_code=FUTURE1",
242 ).await;
243 // 100% discount → free claim → redirect to /library?purchase=success
244 assert!(resp.status.is_redirection() || resp.status.is_success(),
245 "Future-expiry code should be accepted: {} {}", resp.status, resp.text);
246 }
247
248 // =============================================================================
249 // Project-scoped promo code test
250 // =============================================================================
251
252 #[tokio::test]
253 async fn discount_code_project_scoped() {
254 let mut h = TestHarness::new().await;
255 let (_user_id, project_id, _item_id) = setup_creator_with_item(&mut h).await;
256
257 // Create code scoped to the project
258 let resp = h.client.post_form(
259 "/api/promo-codes",
260 &format!("code=PROJ10&code_purpose=discount&discount_type=percentage&discount_value=10&project_id={}", project_id),
261 ).await;
262 assert!(resp.status.is_success(), "Create project-scoped code failed: {} {}", resp.status, resp.text);
263 let code: Value = resp.json();
264 assert_eq!(code["code"].as_str().unwrap(), "PROJ10");
265
266 // List codes — project-scoped code should appear
267 let resp = h.client.get("/api/promo-codes").await;
268 assert!(resp.status.is_success());
269 let list: Value = resp.json();
270 let data = list["data"].as_array().expect("data should be array");
271 assert!(data.iter().any(|c| c["code"].as_str() == Some("PROJ10")),
272 "Project-scoped code should appear in listing");
273 }
274