diff --git a/contract/contracts/event_registry/src/error.rs b/contract/contracts/event_registry/src/error.rs index 2e455d2..c472b5c 100644 --- a/contract/contracts/event_registry/src/error.rs +++ b/contract/contracts/event_registry/src/error.rs @@ -72,6 +72,8 @@ pub enum EventRegistryError { InvalidTargetDeadline = 44, /// Admin has already approved this proposal AlreadyApproved = 45, + /// Two or more ticket tiers share the same ID + DuplicateTierId = 46, } impl core::fmt::Display for EventRegistryError { @@ -216,6 +218,9 @@ impl core::fmt::Display for EventRegistryError { EventRegistryError::AlreadyApproved => { write!(f, "Admin has already approved this proposal") } + EventRegistryError::DuplicateTierId => { + write!(f, "Duplicate tier ID: all tier IDs must be unique") + } } } } diff --git a/contract/contracts/event_registry/src/test.rs b/contract/contracts/event_registry/src/test.rs index 5fd0765..47cf02e 100644 --- a/contract/contracts/event_registry/src/test.rs +++ b/contract/contracts/event_registry/src/test.rs @@ -51,6 +51,7 @@ fn test_register_and_get_series() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -68,6 +69,7 @@ fn test_register_and_get_series() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -120,6 +122,7 @@ fn test_issue_and_use_series_pass() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -292,6 +295,7 @@ fn test_storage_operations() { current_supply: 0, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -380,6 +384,7 @@ fn test_get_total_tickets_sold_uses_event_current_supply() { current_supply: 9, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -483,6 +488,7 @@ fn test_organizer_events_list() { current_supply: 0, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -513,6 +519,7 @@ fn test_organizer_events_list() { current_supply: 0, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -672,6 +679,7 @@ fn test_register_event_success() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -789,6 +797,7 @@ fn test_register_event_invalid_target_deadline() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -809,6 +818,7 @@ fn test_register_event_invalid_target_deadline() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -829,6 +839,7 @@ fn test_register_event_invalid_target_deadline() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -865,6 +876,7 @@ fn test_register_event_rejects_contract_as_organizer() { max_supply: 100, milestone_plan: None, tiers: Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -905,6 +917,7 @@ fn test_register_event_rejects_zero_organizer_address() { max_supply: 100, milestone_plan: None, tiers: Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -947,6 +960,7 @@ fn test_register_event_unlimited_supply() { max_supply: 0, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -991,6 +1005,7 @@ fn test_register_duplicate_event_fails() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1009,6 +1024,7 @@ fn test_register_duplicate_event_fails() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1020,6 +1036,66 @@ fn test_register_duplicate_event_fails() { assert_eq!(result, Err(Ok(EventRegistryError::EventAlreadyExists))); } +#[test] +fn test_register_event_duplicate_tier_id_fails() { + let env = Env::default(); + let contract_id = env.register(EventRegistry, ()); + let client = EventRegistryClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let organizer = Address::generate(&env); + let platform_wallet = Address::generate(&env); + let usdc_token = Address::generate(&env); + env.mock_all_auths(); + client.initialize(&admin, &platform_wallet, &500, &usdc_token); + + let metadata_cid = String::from_str( + &env, + "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + ); + + // Build a map with a duplicate tier ID — Soroban's Map silently keeps the + // last value, so the map ends up with one entry instead of two. + let tier = TicketTier { + name: String::from_str(&env, "General"), + price: 1_000, + tier_limit: 50, + current_sold: 0, + is_refundable: true, + auction_config: soroban_sdk::vec![&env], + }; + let mut tiers = Map::new(&env); + tiers.set(String::from_str(&env, "general"), tier.clone()); + // Setting the same key again — this is the duplicate + tiers.set(String::from_str(&env, "general"), tier); + + let result = client.try_register_event(&EventRegistrationArgs { + event_id: String::from_str(&env, "event_dup_tier"), + name: String::from_str(&env, "Dup Tier Event"), + organizer_address: organizer, + payment_address: Address::generate(&env), + metadata_cid, + max_supply: 0, + milestone_plan: None, + tiers, + // Declare two IDs but the map only has one (duplicate was collapsed) + tier_ids: soroban_sdk::vec![ + &env, + String::from_str(&env, "general"), + String::from_str(&env, "general"), + ], + refund_deadline: 0, + restocking_fee: 0, + resale_cap_bps: None, + min_sales_target: None, + target_deadline: None, + banner_cid: None, + tags: None, + }); + + assert_eq!(result, Err(Ok(EventRegistryError::DuplicateTierId))); +} + #[test] fn test_register_event_invalid_metadata_cid_formats() { let env = Env::default(); @@ -1046,6 +1122,7 @@ fn test_register_event_invalid_metadata_cid_formats() { max_supply: 100, milestone_plan: None, tiers: tiers.clone(), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1072,6 +1149,7 @@ fn test_register_event_invalid_metadata_cid_formats() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1098,6 +1176,7 @@ fn test_register_event_invalid_metadata_cid_formats() { max_supply: 100, milestone_plan: None, tiers: Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1142,6 +1221,7 @@ fn test_get_event_payment_info() { max_supply: 50, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1186,6 +1266,7 @@ fn test_update_event_status() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1229,6 +1310,7 @@ fn test_event_inactive_error() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1273,6 +1355,7 @@ fn test_complete_event_lifecycle() { max_supply: 200, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1329,6 +1412,7 @@ fn test_update_metadata_success() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1378,6 +1462,7 @@ fn test_update_metadata_invalid_cid() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1473,6 +1558,7 @@ fn test_set_custom_event_fee() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1529,6 +1615,7 @@ fn test_set_custom_event_fee_exceeds_max() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1599,6 +1686,7 @@ fn test_increment_inventory_success() { max_supply: 10, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1671,6 +1759,7 @@ fn test_increment_inventory_max_supply_exceeded() { max_supply: 2, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1738,6 +1827,7 @@ fn test_increment_inventory_bulk_exceeds_max_supply() { max_supply: 3, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1805,6 +1895,7 @@ fn test_increment_inventory_unlimited_supply() { max_supply: 0, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1890,6 +1981,7 @@ fn test_increment_inventory_inactive_event() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -1950,6 +2042,7 @@ fn test_increment_inventory_persists_across_reads() { max_supply: 50, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2027,6 +2120,7 @@ fn test_tier_limit_exceeds_max_supply() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2087,6 +2181,7 @@ fn test_tier_not_found() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2148,6 +2243,7 @@ fn test_tier_supply_exceeded() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2225,6 +2321,7 @@ fn test_multiple_tiers_inventory() { max_supply: 70, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2299,6 +2396,7 @@ fn test_increment_inventory_supply_overflow() { current_supply: i128::MAX, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2366,6 +2464,7 @@ fn test_increment_inventory_tier_sold_overflow() { current_supply: 0, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2415,6 +2514,7 @@ fn test_update_event_status_noop_skips_event() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2492,6 +2592,7 @@ fn test_blacklist_prevents_event_registration() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2537,6 +2638,7 @@ fn test_update_metadata_noop_skips_event() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2621,6 +2723,7 @@ fn test_blacklist_suspends_active_events() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2747,6 +2850,7 @@ fn test_register_event_with_resale_cap() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: Some(1000), // 10% above face value @@ -2791,6 +2895,7 @@ fn test_register_event_resale_cap_zero() { max_supply: 50, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: Some(0), // No markup allowed @@ -2835,6 +2940,7 @@ fn test_register_event_resale_cap_none() { max_supply: 50, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, // No cap @@ -2879,6 +2985,7 @@ fn test_postpone_event_sets_grace_period() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -2930,6 +3037,7 @@ fn test_register_event_resale_cap_invalid() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: Some(10001), // Over 100% - invalid @@ -2970,6 +3078,7 @@ fn test_cancel_event_success() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 100, resale_cap_bps: None, @@ -3013,6 +3122,7 @@ fn test_archive_event_rejects_active_event() { max_supply: 100, milestone_plan: None, tiers: Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3054,6 +3164,7 @@ fn test_cancel_already_cancelled_fails() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3096,6 +3207,7 @@ fn test_update_status_on_cancelled_event_fails() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3740,6 +3852,7 @@ fn test_register_event_with_banner_cid() { max_supply: 100, milestone_plan: None, tiers: Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3790,6 +3903,7 @@ fn test_goal_met_event_fires_only_once() { max_supply: 100, milestone_plan: None, tiers: soroban_sdk::Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3847,6 +3961,7 @@ fn test_register_event_without_banner_cid() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3914,6 +4029,7 @@ fn test_series_pass_issued_at_timestamp() { max_supply: 50, milestone_plan: None, tiers: soroban_sdk::Map::new(&env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -3991,6 +4107,7 @@ fn base_args( max_supply: 100, milestone_plan, tiers: Map::new(env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -4328,6 +4445,7 @@ fn test_cancelled_status_guard() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -4461,6 +4579,7 @@ fn test_register_event_restocking_fee_exceeds_tier_price_fails() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 6_000_000, resale_cap_bps: None, @@ -4516,6 +4635,7 @@ fn test_register_event_restocking_fee_equal_to_tier_price_succeeds() { max_supply: 100, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 5_000_000, resale_cap_bps: None, @@ -4567,6 +4687,7 @@ fn test_register_event_restocking_fee_zero_always_valid() { max_supply: 50, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, @@ -4845,6 +4966,7 @@ fn tags_base_args(env: &Env, event_id: &str, organizer: &Address) -> EventRegist max_supply: 0, milestone_plan: None, tiers: Map::new(env), + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, diff --git a/contract/contracts/event_registry/src/test_e2e.rs b/contract/contracts/event_registry/src/test_e2e.rs index bfc7b96..290ce11 100644 --- a/contract/contracts/event_registry/src/test_e2e.rs +++ b/contract/contracts/event_registry/src/test_e2e.rs @@ -42,6 +42,7 @@ fn make_event_args( max_supply, milestone_plan: None, tiers, + tier_ids: soroban_sdk::vec![&env], refund_deadline: 0, restocking_fee: 0, resale_cap_bps: None, diff --git a/contract/contracts/event_registry/src/types.rs b/contract/contracts/event_registry/src/types.rs index c76be83..2c598fd 100644 --- a/contract/contracts/event_registry/src/types.rs +++ b/contract/contracts/event_registry/src/types.rs @@ -167,7 +167,13 @@ pub struct EventRegistrationArgs { pub metadata_cid: String, pub max_supply: i128, pub milestone_plan: Option>, + /// Map of tier_id to TicketTier for multi-tiered pricing. pub tiers: Map, + /// Explicit list of all tier IDs being registered. Must have the same + /// length as `tiers` — if a duplicate ID was set in `tiers`, the map + /// silently collapses it and the lengths will differ, triggering + /// `DuplicateTierId`. + pub tier_ids: Vec, pub refund_deadline: u64, pub restocking_fee: i128, /// Optional resale price cap in basis points above face value.