//! Promo code CRUD, validation, free trial, expiry, and project scope tests. use crate::harness::TestHarness; use serde_json::Value; /// Creator setup: signup, grant, re-login, create project + item, publish both. /// Returns (user_id, project_id, item_id). async fn setup_creator_with_item(h: &mut TestHarness) -> (makenotwork::db::UserId, String, String) { let setup = h.create_creator_with_item("dcseller", "digital", 1000).await; h.publish_project_and_item(&setup.project_id, &setup.item_id).await; (setup.user_id, setup.project_id, setup.item_id) } #[tokio::test] async fn discount_code_crud_lifecycle() { let mut h = TestHarness::new().await; let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // Create a percentage discount promo code let resp = h.client.post_form( "/api/promo-codes", "code=SUMMER50&code_purpose=discount&discount_type=percentage&discount_value=50", ).await; assert!(resp.status.is_success(), "Create promo code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); assert_eq!(code["code"].as_str().unwrap(), "SUMMER50"); let code_id = code["id"].as_str().expect("promo code should have id"); // List codes let resp = h.client.get("/api/promo-codes").await; assert!(resp.status.is_success(), "List promo codes failed: {} {}", resp.status, resp.text); let list: Value = resp.json(); let data = list["data"].as_array().expect("data should be array"); assert_eq!(data.len(), 1); assert_eq!(data[0]["code"].as_str().unwrap(), "SUMMER50"); // Delete code let resp = h.client.delete(&format!("/api/promo-codes/{}", code_id)).await; assert_eq!(resp.status, 204, "Delete should return 204 No Content"); // List again — should be empty let resp = h.client.get("/api/promo-codes").await; let list: Value = resp.json(); let data = list["data"].as_array().unwrap(); assert!(data.is_empty(), "Promo codes list should be empty after delete"); } #[tokio::test] async fn discount_code_item_scoped() { let mut h = TestHarness::new().await; let (_user_id, _project_id, item_id) = setup_creator_with_item(&mut h).await; // Create code scoped to the item let resp = h.client.post_form( "/api/promo-codes", &format!("code=ITEM10&code_purpose=discount&discount_type=fixed&discount_value=500&item_id={}", item_id), ).await; assert!(resp.status.is_success(), "Create item-scoped code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); assert_eq!(code["code"].as_str().unwrap(), "ITEM10"); } #[tokio::test] async fn discount_code_validation_errors() { let mut h = TestHarness::new().await; let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // Empty code let resp = h.client.post_form( "/api/promo-codes", "code=&code_purpose=discount&discount_type=percentage&discount_value=50", ).await; assert_eq!(resp.status, 400, "Empty code should return 400"); // Percentage 0 let resp = h.client.post_form( "/api/promo-codes", "code=BAD1&code_purpose=discount&discount_type=percentage&discount_value=0", ).await; assert_eq!(resp.status, 400, "Percentage 0 should return 400"); // Percentage 101 let resp = h.client.post_form( "/api/promo-codes", "code=BAD2&code_purpose=discount&discount_type=percentage&discount_value=101", ).await; assert_eq!(resp.status, 400, "Percentage 101 should return 400"); // Fixed 0 let resp = h.client.post_form( "/api/promo-codes", "code=BAD3&code_purpose=discount&discount_type=fixed&discount_value=0", ).await; assert_eq!(resp.status, 400, "Fixed 0 should return 400"); } #[tokio::test] async fn discount_code_delete_other_users_code() { let mut h = TestHarness::new().await; // Seller A: create a promo code let seller_a = h.signup("dcseller_a", "dcseller_a@test.com", "password123").await; h.grant_creator(seller_a).await; h.client.post_form("/logout", "").await; h.login("dcseller_a", "password123").await; let resp = h.client.post_form( "/api/promo-codes", "code=PRIVCODE&code_purpose=discount&discount_type=percentage&discount_value=25", ).await; assert!(resp.status.is_success(), "Seller A create failed: {}", resp.text); let code: Value = resp.json(); let code_id = code["id"].as_str().unwrap(); // Switch to Seller B h.client.post_form("/logout", "").await; let seller_b = h.signup("dcseller_b", "dcseller_b@test.com", "password123").await; h.grant_creator(seller_b).await; h.client.post_form("/logout", "").await; h.login("dcseller_b", "password123").await; // Seller B tries to delete Seller A's code let resp = h.client.delete(&format!("/api/promo-codes/{}", code_id)).await; assert_eq!(resp.status, 403, "Deleting another user's code should return 403"); } // ============================================================================= // Free trial promo code tests // ============================================================================= #[tokio::test] async fn free_trial_code_create() { let mut h = TestHarness::new().await; let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // Create a free trial code with 14-day trial let resp = h.client.post_form( "/api/promo-codes", "code=TRIAL14&code_purpose=free_trial&trial_days=14", ).await; assert!(resp.status.is_success(), "Create free trial code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); assert_eq!(code["code"].as_str().unwrap(), "TRIAL14"); assert_eq!(code["trial_days"].as_i64().unwrap(), 14); } #[tokio::test] async fn free_trial_code_reject_zero_days() { let mut h = TestHarness::new().await; let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // trial_days = 0 should be rejected let resp = h.client.post_form( "/api/promo-codes", "code=BADTRIAL&code_purpose=free_trial&trial_days=0", ).await; assert_eq!(resp.status, 400, "trial_days=0 should return 400"); } #[tokio::test] async fn free_trial_code_reject_at_item_checkout() { let mut h = TestHarness::new().await; let (_user_id, _project_id, item_id) = setup_creator_with_item(&mut h).await; // Create a free trial code let resp = h.client.post_form( "/api/promo-codes", "code=TRIALBAD&code_purpose=free_trial&trial_days=7", ).await; assert!(resp.status.is_success(), "Create trial code failed: {}", resp.text); // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("trialbuyer", "trialbuyer@test.com", "password456").await; // Try to use trial code at item checkout — should fail because trial codes // are only for subscriptions let resp = h.client.post_form( &format!("/stripe/checkout/{}", item_id), "promo_code=TRIALBAD", ).await; // Should get a 400 or redirect with error (trial codes not valid at item checkout) assert!(resp.status == 400 || resp.text.contains("Trial codes can only be used for subscriptions"), "Trial code should be rejected at item checkout: {} {}", resp.status, resp.text); } // ============================================================================= // Promo code expiry tests // ============================================================================= #[tokio::test] async fn discount_code_expired_rejected() { let mut h = TestHarness::new().await; let (user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // Insert expired code directly (API now rejects past expiry dates) sqlx::query( "INSERT INTO promo_codes (creator_id, code, code_purpose, discount_type, discount_value, expires_at) \ VALUES ($1, 'EXPIRED1', 'discount', 'percentage', 50, '2020-01-01'::timestamptz)" ) .bind(user_id) .execute(&h.db) .await .unwrap(); let key_code = "EXPIRED1"; // Switch to buyer h.client.post_form("/logout", "").await; let _buyer_id = h.signup("expirybuyer", "expirybuyer@test.com", "password456").await; // Try to use expired code at item checkout — should fail let resp = h.client.post_form( &format!("/stripe/checkout/{}", _item_id), &format!("promo_code={}", key_code), ).await; assert!(resp.status == 400 || resp.text.contains("expired"), "Expired code should be rejected: {} {}", resp.status, resp.text); } #[tokio::test] async fn discount_code_future_expiry_accepted() { let mut h = TestHarness::new().await; let (_user_id, _project_id, _item_id) = setup_creator_with_item(&mut h).await; // Create a code with future expiry let resp = h.client.post_form( "/api/promo-codes", "code=FUTURE1&code_purpose=discount&discount_type=percentage&discount_value=100&expires_at=2099-12-31", ).await; assert!(resp.status.is_success(), "Create future-expiry code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); assert_eq!(code["code"].as_str().unwrap(), "FUTURE1"); // Switch to buyer and use it h.client.post_form("/logout", "").await; let _buyer_id = h.signup("futurebuyer", "futurebuyer@test.com", "password456").await; // Use 100% discount code at item checkout — should succeed (free claim path) let resp = h.client.post_form( &format!("/stripe/checkout/{}", _item_id), "promo_code=FUTURE1", ).await; // 100% discount → free claim → redirect to /library?purchase=success assert!(resp.status.is_redirection() || resp.status.is_success(), "Future-expiry code should be accepted: {} {}", resp.status, resp.text); } // ============================================================================= // Project-scoped promo code test // ============================================================================= #[tokio::test] async fn discount_code_project_scoped() { let mut h = TestHarness::new().await; let (_user_id, project_id, _item_id) = setup_creator_with_item(&mut h).await; // Create code scoped to the project let resp = h.client.post_form( "/api/promo-codes", &format!("code=PROJ10&code_purpose=discount&discount_type=percentage&discount_value=10&project_id={}", project_id), ).await; assert!(resp.status.is_success(), "Create project-scoped code failed: {} {}", resp.status, resp.text); let code: Value = resp.json(); assert_eq!(code["code"].as_str().unwrap(), "PROJ10"); // List codes — project-scoped code should appear let resp = h.client.get("/api/promo-codes").await; assert!(resp.status.is_success()); let list: Value = resp.json(); let data = list["data"].as_array().expect("data should be array"); assert!(data.iter().any(|c| c["code"].as_str() == Some("PROJ10")), "Project-scoped code should appear in listing"); }