diff --git a/heromodels/examples/governance_activity_example.rs b/heromodels/examples/governance_activity_example.rs new file mode 100644 index 0000000..a416c42 --- /dev/null +++ b/heromodels/examples/governance_activity_example.rs @@ -0,0 +1,357 @@ +// heromodels/examples/governance_activity_example.rs + +use chrono::{Duration, Utc}; +use heromodels::db::{Collection, Db}; +use heromodels::models::governance::{ + ActivityType, GovernanceActivity, Proposal, ProposalStatus, VoteEventStatus, +}; + +fn main() { + println!("Governance Activity Tracking Example\n"); + + // Create a new DB instance with a unique path, reset before every run + let db_path = format!( + "/tmp/ourdb_governance_activity_example_{}", + chrono::Utc::now().timestamp() + ); + let db = heromodels::db::hero::OurDB::new(&db_path, true).expect("Can create DB"); + + // Get collections for proposals and activities + let proposal_collection = db + .collection::() + .expect("can open proposal collection"); + let activity_collection = db + .collection::() + .expect("can open activity collection"); + + // === STEP 1: Create a proposal and track the activity === + println!("=== Creating a Proposal ==="); + + let now = Utc::now(); + let mut proposal = Proposal::new( + None, // auto-generated ID + "user_creator_123", + "Alice Johnson", + "Community Fund Allocation for Q4", + "Proposal to allocate funds for community projects in the fourth quarter.", + ProposalStatus::Draft, + now, + now, + now, + now + Duration::days(14), + ); + + // Save the proposal + let (proposal_id, saved_proposal) = proposal_collection + .set(&proposal) + .expect("can save proposal"); + proposal = saved_proposal; + + println!( + "Created proposal: '{}' (ID: {})", + proposal.title, proposal_id + ); + + // Track the proposal creation activity + let creation_activity = GovernanceActivity::proposal_created( + proposal_id, + &proposal.title, + &proposal.creator_id, + &proposal.creator_name, + ); + + let (activity_id, _) = activity_collection + .set(&creation_activity) + .expect("can save activity"); + + println!( + "Tracked activity: Proposal creation (Activity ID: {})", + activity_id + ); + + // === STEP 2: Add vote options and track activities === + println!("\n=== Adding Vote Options ==="); + + proposal = proposal.add_option(1, "Approve Allocation", None::); + proposal = proposal.add_option(2, "Reject Allocation", None::); + proposal = proposal.add_option(3, "Abstain", None::); + + // Track vote option additions + for option in &proposal.options { + let option_activity = GovernanceActivity::vote_option_added( + proposal_id, + &proposal.title, + option.id, + &option.text, + Some(&proposal.creator_id), + Some(&proposal.creator_name), + ); + + let (option_activity_id, _) = activity_collection + .set(&option_activity) + .expect("can save option activity"); + + println!( + "Added option '{}' and tracked activity (ID: {})", + option.text, option_activity_id + ); + } + + // === STEP 3: Start voting and track activity === + println!("\n=== Starting Voting Period ==="); + + let voting_start_activity = GovernanceActivity::voting_started( + proposal_id, + &proposal.title, + proposal.vote_start_date, + proposal.vote_end_date, + ); + + let (voting_start_id, _) = activity_collection + .set(&voting_start_activity) + .expect("can save voting start activity"); + + println!("Voting period started (Activity ID: {})", voting_start_id); + + // === STEP 4: Cast votes and track activities === + println!("\n=== Casting Votes ==="); + + // Simulate vote casting + let votes = vec![ + (101, 1, 1, 100, "Bob Smith"), + (102, 2, 2, 50, "Carol Davis"), + (0, 3, 1, 75, "David Wilson"), // auto-generated ballot ID + (0, 4, 3, 20, "Eve Brown"), // auto-generated ballot ID + ]; + + for (ballot_id, user_id, option_id, shares, voter_name) in votes { + // Cast the vote + proposal = proposal.cast_vote( + if ballot_id == 0 { + None + } else { + Some(ballot_id) + }, + user_id, + option_id, + shares, + ); + + // Find the option text + let option_text = proposal + .options + .iter() + .find(|opt| opt.id == option_id) + .map(|opt| opt.text.clone()) + .unwrap_or_else(|| "Unknown".to_string()); + + // Track the vote casting activity + let vote_activity = GovernanceActivity::vote_cast( + proposal_id, + &proposal.title, + ballot_id, + user_id.to_string(), + voter_name, + &option_text, + shares, + ); + + let (vote_activity_id, _) = activity_collection + .set(&vote_activity) + .expect("can save vote activity"); + + println!( + "{} voted '{}' with {} shares (Activity ID: {})", + voter_name, option_text, shares, vote_activity_id + ); + } + + // === STEP 5: Change proposal status and track activity === + println!("\n=== Changing Proposal Status ==="); + + let old_status = format!("{:?}", proposal.status); + proposal = proposal.change_proposal_status(ProposalStatus::Active); + let new_status = format!("{:?}", proposal.status); + + let status_change_activity = GovernanceActivity::proposal_status_changed( + proposal_id, + &proposal.title, + &old_status, + &new_status, + Some("admin_user"), + Some("Admin User"), + ); + + let (status_activity_id, _) = activity_collection + .set(&status_change_activity) + .expect("can save status change activity"); + + println!( + "Proposal status changed from {} to {} (Activity ID: {})", + old_status, new_status, status_activity_id + ); + + // === STEP 6: End voting and track activity === + println!("\n=== Ending Voting Period ==="); + + proposal = proposal.change_vote_event_status(VoteEventStatus::Closed); + + let total_votes = proposal.ballots.len(); + let total_shares: i64 = proposal.ballots.iter().map(|b| b.shares_count).sum(); + + let voting_end_activity = + GovernanceActivity::voting_ended(proposal_id, &proposal.title, total_votes, total_shares); + + let (voting_end_id, _) = activity_collection + .set(&voting_end_activity) + .expect("can save voting end activity"); + + println!( + "Voting period ended. Total votes: {}, Total shares: {} (Activity ID: {})", + total_votes, total_shares, voting_end_id + ); + + // === STEP 7: Display all activities === + println!("\n=== Recent Governance Activities ==="); + + // Try to retrieve activities one by one to identify the problematic one + println!("Trying to retrieve activities by ID..."); + for id in 2..=12 { + match activity_collection.get_by_id(id) { + Ok(Some(activity)) => { + println!( + "ID {}: {} (Type: {:?})", + id, activity.title, activity.activity_type + ); + } + Ok(None) => { + println!("ID {}: Not found", id); + } + Err(e) => { + println!("ID {}: Error - {:?}", id, e); + } + } + } + + let all_activities = match activity_collection.get_all() { + Ok(activities) => { + println!("Successfully retrieved {} activities", activities.len()); + activities + } + Err(e) => { + println!("Error retrieving all activities: {:?}", e); + println!( + "This might be due to enum serialization changes. Continuing with empty list." + ); + Vec::new() + } + }; + + // Sort activities by occurred_at timestamp (most recent first) + let mut sorted_activities = all_activities; + sorted_activities.sort_by(|a, b| b.occurred_at.cmp(&a.occurred_at)); + + for (i, activity) in sorted_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + + // Show additional details for some activities + if !activity.metadata.is_empty() { + println!(" Details: {}", activity.metadata); + } + + if !activity.tags.is_empty() { + println!(" Tags: {:?}", activity.tags); + } + + println!( + " Severity: {:?}, Status: {:?}", + activity.severity, activity.status + ); + println!(); + } + + // === STEP 8: Filter activities by type === + println!("=== Vote-Related Activities ==="); + + let vote_activities: Vec<_> = sorted_activities + .iter() + .filter(|activity| { + matches!( + activity.activity_type, + ActivityType::VoteCast + | ActivityType::VotingStarted + | ActivityType::VotingEnded + | ActivityType::VoteOptionAdded + ) + }) + .collect(); + + for (i, activity) in vote_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + } + + // // === STEP 9: Filter activities by user - "My Requests" === + // println!("\n=== My Requests (Alice Johnson's Activities) ==="); + + // let my_activities: Vec<_> = sorted_activities + // .iter() + // .filter(|activity| activity.actor_name.as_deref() == Some("Alice Johnson")) + // .collect(); + + // if my_activities.is_empty() { + // println!("No activities found for Alice Johnson"); + // } else { + // for (i, activity) in my_activities.iter().enumerate() { + // println!("{}. {}", i + 1, activity.summary()); + + // // Show additional details for user's own activities + // if !activity.metadata.is_empty() { + // println!(" Details: {}", activity.metadata); + // } + + // if !activity.tags.is_empty() { + // println!(" Tags: {:?}", activity.tags); + // } + // println!(); + // } + // } + + // === STEP 10: Filter activities by another user === + println!("=== Bob Smith's Activities ==="); + + let bob_activities: Vec<_> = sorted_activities + .iter() + .filter(|activity| activity.actor_name.as_deref() == Some("Bob Smith")) + .collect(); + + if bob_activities.is_empty() { + println!("No activities found for Bob Smith"); + } else { + for (i, activity) in bob_activities.iter().enumerate() { + println!("{}. {}", i + 1, activity.summary()); + } + } + + // === STEP 9: Show statistics === + println!("\n=== Activity Statistics ==="); + println!("Total activities recorded: {}", sorted_activities.len()); + + let activity_counts = + sorted_activities + .iter() + .fold(std::collections::HashMap::new(), |mut acc, activity| { + *acc.entry(format!("{:?}", activity.activity_type)) + .or_insert(0) += 1; + acc + }); + + for (activity_type, count) in activity_counts { + println!("- {}: {}", activity_type, count); + } + + println!("\nExample finished. DB stored at {}", db_path); + println!( + "To clean up, you can manually delete the directory: {}", + db_path + ); +} diff --git a/heromodels/examples/marketplace_example.rs b/heromodels/examples/marketplace_example.rs new file mode 100644 index 0000000..f60c087 --- /dev/null +++ b/heromodels/examples/marketplace_example.rs @@ -0,0 +1,328 @@ +use chrono::{Duration, Utc}; +use heromodels::db::{Collection, Db}; +use heromodels::models::finance::marketplace::{Bid, Listing, ListingType}; +use heromodels::models::finance::asset::AssetType; +use heromodels_core::Model; + +// Helper function to print listing details +fn print_listing_details(listing: &Listing) { + println!("\n--- Listing Details ---"); + println!("ID: {}", listing.get_id()); + println!("Title: {}", listing.title); + println!("Description: {}", listing.description); + println!("Asset ID: {}", listing.asset_id); + println!("Asset Type: {:?}", listing.asset_type); + println!("Seller ID: {}", listing.seller_id); + println!("Price: {} {}", listing.price, listing.currency); + println!("Listing Type: {:?}", listing.listing_type); + println!("Status: {:?}", listing.status); + + if let Some(expires_at) = listing.expires_at { + println!("Expires At: {}", expires_at); + } else { + println!("Expires At: Never"); + } + + if let Some(sold_at) = listing.sold_at { + println!("Sold At: {}", sold_at); + } + + if let Some(buyer_id) = &listing.buyer_id { + println!("Buyer ID: {}", buyer_id); + } + + if let Some(sale_price) = listing.sale_price { + println!("Sale Price: {} {}", sale_price, listing.currency); + } + + println!("Bids: {}", listing.bids.len()); + println!("Tags: {:?}", listing.tags); + + if let Some(image_url) = &listing.image_url { + println!("Image URL: {}", image_url); + } + + println!("Created At: {}", listing.base_data.created_at); + println!("Modified At: {}", listing.base_data.modified_at); +} + +// Helper function to print bid details +fn print_bid_details(bid: &Bid) { + println!("\n--- Bid Details ---"); + println!("Listing ID: {}", bid.listing_id); + println!("Bidder ID: {}", bid.bidder_id); + println!("Amount: {} {}", bid.amount, bid.currency); + println!("Status: {:?}", bid.status); + println!("Created At: {}", bid.created_at); +} + +fn main() { + // Create a new DB instance in /tmp/marketplace_db, and reset before every run + let db = heromodels::db::hero::OurDB::new("/tmp/marketplace_db", true).expect("Can create DB"); + + println!("Hero Models - Marketplace Example"); + println!("================================"); + + // Create listings with auto-generated IDs + + // Fixed price listing + let fixed_price_listing = Listing::new( + None, // Auto-generated ID + "Vintage Guitar", + "A beautiful vintage guitar in excellent condition", + "asset123", + AssetType::Erc721, // NFT representing a physical item + "seller456", + 1200.0, + "USD", + ListingType::FixedPrice, + Some(Utc::now() + Duration::days(30)), // Expires in 30 days + vec!["music".to_string(), "instrument".to_string(), "vintage".to_string()], + Some("https://example.com/images/vintage_guitar.jpg"), + ); + + // Auction listing + let auction_listing = Listing::new( + None, // Auto-generated ID + "Rare Painting", + "A rare painting from the 19th century", + "asset789", + AssetType::Erc721, // NFT representing a physical item + "seller456", + 5000.0, // Starting price + "USD", + ListingType::Auction, + Some(Utc::now() + Duration::days(7)), // Auction ends in 7 days + vec!["art".to_string(), "painting".to_string(), "antique".to_string()], + Some("https://example.com/images/rare_painting.jpg"), + ); + + // Exchange listing + let exchange_listing = Listing::new( + None, // Auto-generated ID + "Digital Artwork NFT", + "A unique digital artwork as an NFT", + "asset101", + AssetType::Erc1155, // Multi-token for digital art + "seller789", + 0.5, // Price in ETH + "ETH", + ListingType::Exchange, + Some(Utc::now() + Duration::days(14)), // Expires in 14 days + vec!["digital".to_string(), "nft".to_string(), "art".to_string()], + Some("https://example.com/images/digital_artwork.jpg"), + ); + + // Save all listings to database and get their assigned IDs and updated models + let (fixed_price_id, db_fixed_price) = db.collection().expect("can open listing collection").set(&fixed_price_listing).expect("can set listing"); + let (auction_id, db_auction) = db.collection().expect("can open listing collection").set(&auction_listing).expect("can set listing"); + let (exchange_id, db_exchange) = db.collection().expect("can open listing collection").set(&exchange_listing).expect("can set listing"); + + println!("Fixed Price Listing assigned ID: {}", fixed_price_id); + println!("Auction Listing assigned ID: {}", auction_id); + println!("Exchange Listing assigned ID: {}", exchange_id); + + // Print all listings retrieved from database + println!("\n--- Listings Retrieved from Database ---"); + println!("\n1. Fixed Price Listing:"); + print_listing_details(&db_fixed_price); + + println!("\n2. Auction Listing:"); + print_listing_details(&db_auction); + + println!("\n3. Exchange Listing:"); + print_listing_details(&db_exchange); + + // Demonstrate working with bids on an auction listing + println!("\n--- Working with Bids ---"); + + // Create bids for the auction listing + let bid1 = Bid::new( + auction_id, + 101, // Bidder ID + 5200.0, + "USD", + ); + + let bid2 = Bid::new( + auction_id, + 102, // Bidder ID + 5500.0, + "USD", + ); + + // Print the bids + println!("\n1. First Bid:"); + print_bid_details(&bid1); + + println!("\n2. Second Bid:"); + print_bid_details(&bid2); + + // Add bids to the auction listing + let updated_auction = db_auction + .add_bid(bid1.clone()) + .expect("can add first bid") + .add_bid(bid2.clone()) + .expect("can add second bid"); + + // Save the updated auction listing + let (_, db_updated_auction) = db.collection() + .expect("can open listing collection") + .set(&updated_auction) + .expect("can set updated auction"); + + println!("\n3. Auction Listing After Adding Bids:"); + print_listing_details(&db_updated_auction); + + // Demonstrate retrieving the highest bid + if let Some(highest_bid) = db_updated_auction.highest_bid() { + println!("\n4. Highest Bid:"); + print_bid_details(highest_bid); + } + + // Demonstrate completing a sale for the fixed price listing + println!("\n--- Completing a Sale ---"); + + // Complete the fixed price listing sale + let completed_fixed_price = db_fixed_price + .complete_sale("buyer123", 1200.0) + .expect("can complete sale"); + + // Save the updated listing + let (_, db_completed_fixed_price) = db.collection() + .expect("can open listing collection") + .set(&completed_fixed_price) + .expect("can set completed listing"); + + println!("\n1. Fixed Price Listing After Sale:"); + print_listing_details(&db_completed_fixed_price); + + // Demonstrate completing an auction + println!("\n--- Completing an Auction ---"); + + // Complete the auction with the highest bidder + // Store the bidder_id and amount before moving db_updated_auction + let bidder_id = db_updated_auction.highest_bid().unwrap().bidder_id; + let amount = db_updated_auction.highest_bid().unwrap().amount; + + // Now complete the sale + let completed_auction = db_updated_auction + .complete_sale(bidder_id.to_string(), amount) + .expect("can complete auction"); + + // Save the updated auction listing + let (_, db_completed_auction) = db.collection() + .expect("can open listing collection") + .set(&completed_auction) + .expect("can set completed auction"); + + println!("\n1. Auction Listing After Completion:"); + print_listing_details(&db_completed_auction); + + // Demonstrate cancelling a listing + println!("\n--- Cancelling a Listing ---"); + + // Cancel the exchange listing + let cancelled_exchange = db_exchange + .cancel() + .expect("can cancel listing"); + + // Save the updated listing + let (_, db_cancelled_exchange) = db.collection() + .expect("can open listing collection") + .set(&cancelled_exchange) + .expect("can set cancelled listing"); + + println!("\n1. Exchange Listing After Cancellation:"); + print_listing_details(&db_cancelled_exchange); + + // Demonstrate checking for expired listings + println!("\n--- Checking for Expired Listings ---"); + + // Create a listing that's already expired + let expired_listing = Listing::new( + None, // Auto-generated ID + "Already Expired Item", + "This item's listing has already expired", + "asset202", + AssetType::Erc721, // NFT representing a physical item + "seller456", + 50.0, + "USD", + ListingType::FixedPrice, + Some(Utc::now() - Duration::days(1)), // Expired 1 day ago + vec!["expired".to_string()], + None::, + ); + + // Save the expired listing + let (expired_id, db_expired) = db.collection() + .expect("can open listing collection") + .set(&expired_listing) + .expect("can set expired listing"); + + println!("Expired Listing assigned ID: {}", expired_id); + + // Check expiration + let checked_expired = db_expired.check_expiration(); + + // Save the checked listing + let (_, db_checked_expired) = db.collection() + .expect("can open listing collection") + .set(&checked_expired) + .expect("can set checked listing"); + + println!("\n1. Listing After Expiration Check:"); + print_listing_details(&db_checked_expired); + + // Demonstrate updating listing details + println!("\n--- Updating Listing Details ---"); + + // Create a new listing to update + let listing_to_update = Listing::new( + None, // Auto-generated ID + "Original Title", + "Original description", + "asset303", + AssetType::Erc20, // Token for a digital asset + "seller456", + 75.0, + "USD", + ListingType::FixedPrice, + Some(Utc::now() + Duration::days(30)), + vec!["original".to_string()], + None::, + ); + + // Save the listing + let (update_id, db_to_update) = db.collection() + .expect("can open listing collection") + .set(&listing_to_update) + .expect("can set listing to update"); + + println!("Listing to Update assigned ID: {}", update_id); + println!("\n1. Original Listing:"); + print_listing_details(&db_to_update); + + // Update the listing details + let updated_listing = db_to_update + .update_details( + Some("Updated Title"), + Some("Updated description with more details"), + Some(85.0), + Some("https://example.com/images/updated_image.jpg"), + ) + .add_tags(vec!["updated".to_string(), "premium".to_string()]); + + // Save the updated listing + let (_, db_updated_listing) = db.collection() + .expect("can open listing collection") + .set(&updated_listing) + .expect("can set updated listing"); + + println!("\n2. Listing After Update:"); + print_listing_details(&db_updated_listing); + + println!("\n--- Model Information ---"); + println!("Listing DB Prefix: {}", Listing::db_prefix()); +} diff --git a/heromodels/src/models/governance/activity.rs b/heromodels/src/models/governance/activity.rs new file mode 100644 index 0000000..69d0214 --- /dev/null +++ b/heromodels/src/models/governance/activity.rs @@ -0,0 +1,524 @@ +// heromodels/src/models/governance/activity.rs + +use chrono::{DateTime, Utc}; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use rhai_autobind_macros::rhai_model_export; +use serde::{Deserialize, Serialize}; +// use std::collections::HashMap; + +use heromodels_core::BaseModelData; + +/// ActivityType defines the different types of governance activities that can be tracked +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivityType { + ProposalCreated, + ProposalStatusChanged, + VotingStarted, + VotingEnded, + VoteCast, + VoteOptionAdded, + ProposalDeleted, + Custom, +} + +impl Default for ActivityType { + fn default() -> Self { + ActivityType::Custom + } +} + +/// ActivityStatus defines the status of an activity +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivityStatus { + Pending, // Activity is scheduled but not yet executed + InProgress, // Activity is currently being executed + Completed, // Activity has been successfully completed + Failed, // Activity failed to complete + Cancelled, // Activity was cancelled before completion +} + +impl Default for ActivityStatus { + fn default() -> Self { + ActivityStatus::Completed + } +} + +/// GovernanceActivity represents a single activity or event in the governance system +/// This model tracks all significant actions and changes for audit and transparency purposes +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[rhai_model_export(db_type = "std::sync::Arc")] +#[model] +pub struct GovernanceActivity { + pub base_data: BaseModelData, + + /// Type of activity that occurred + pub activity_type: ActivityType, + + /// Status of the activity + pub status: ActivityStatus, + + /// ID of the user who initiated this activity (if applicable) + pub actor_id: Option, + + /// Name of the user who initiated this activity (for display purposes) + pub actor_name: Option, + + /// ID of the target object (e.g., proposal_id, ballot_id, etc.) + pub target_id: Option, + + /// Type of the target object (e.g., "proposal", "ballot", "vote_option") + pub target_type: Option, + + /// Title or brief description of the activity + pub title: String, + + /// Detailed description of what happened + pub description: String, + + /// Additional metadata as a simple string + pub metadata: String, + + /// When the activity occurred + pub occurred_at: DateTime, + + /// When the activity was recorded in the system + pub recorded_at: DateTime, + + /// Optional reference to related activities + pub related_activity_ids: Vec, + + /// Tags for categorization and filtering + pub tags: Vec, + + /// Severity level of the activity (for filtering and alerting) + pub severity: ActivitySeverity, + + /// Whether this activity should be publicly visible + pub is_public: bool, + + /// Optional expiration date for temporary activities + pub expires_at: Option>, +} + +/// ActivitySeverity defines the importance level of an activity +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum ActivitySeverity { + Low, // Routine activities + Normal, // Standard activities + High, // Important activities + Critical, // Critical activities that require attention +} + +impl Default for ActivitySeverity { + fn default() -> Self { + ActivitySeverity::Normal + } +} + +impl GovernanceActivity { + /// Create a new governance activity with auto-generated ID + /// + /// # Arguments + /// * `id` - Optional ID for the activity (use None for auto-generated ID) + /// * `activity_type` - Type of activity that occurred + /// * `title` - Brief title of the activity + /// * `description` - Detailed description of the activity + /// * `actor_id` - Optional ID of the user who initiated the activity + /// * `actor_name` - Optional name of the user who initiated the activity + pub fn new( + id: Option, + activity_type: ActivityType, + title: impl ToString, + description: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + + let now = Utc::now(); + + Self { + base_data, + activity_type, + status: ActivityStatus::Completed, + actor_id: actor_id.map(|id| id.to_string()), + actor_name: actor_name.map(|name| name.to_string()), + target_id: None, + target_type: None, + title: title.to_string(), + description: description.to_string(), + metadata: String::new(), + occurred_at: now, + recorded_at: now, + related_activity_ids: Vec::new(), + tags: Vec::new(), + severity: ActivitySeverity::Normal, + is_public: true, + expires_at: None, + } + } + + /// Set the target of this activity + pub fn with_target(mut self, target_id: u32, target_type: impl ToString) -> Self { + self.target_id = Some(target_id); + self.target_type = Some(target_type.to_string()); + self + } + + /// Set the status of this activity + pub fn with_status(mut self, status: ActivityStatus) -> Self { + self.status = status; + self + } + + /// Set the severity of this activity + pub fn with_severity(mut self, severity: ActivitySeverity) -> Self { + self.severity = severity; + self + } + + /// Set the occurred_at timestamp + pub fn with_occurred_at(mut self, occurred_at: DateTime) -> Self { + self.occurred_at = occurred_at; + self + } + + /// Add metadata to this activity + pub fn with_metadata(mut self, key: impl ToString, value: impl ToString) -> Self { + if !self.metadata.is_empty() { + self.metadata.push_str(", "); + } + self.metadata + .push_str(&format!("{}={}", key.to_string(), value.to_string())); + self + } + + /// Add a tag to this activity + pub fn with_tag(mut self, tag: impl ToString) -> Self { + let tag_str = tag.to_string(); + if !self.tags.contains(&tag_str) { + self.tags.push(tag_str); + } + self + } + + /// Add multiple tags to this activity + pub fn with_tags(mut self, tags: Vec) -> Self { + for tag in tags { + if !self.tags.contains(&tag) { + self.tags.push(tag); + } + } + self + } + + /// Set whether this activity is public + pub fn with_visibility(mut self, is_public: bool) -> Self { + self.is_public = is_public; + self + } + + /// Set an expiration date for this activity + pub fn with_expiration(mut self, expires_at: DateTime) -> Self { + self.expires_at = Some(expires_at); + self + } + + /// Add a related activity ID + pub fn with_related_activity(mut self, activity_id: u32) -> Self { + if !self.related_activity_ids.contains(&activity_id) { + self.related_activity_ids.push(activity_id); + } + self + } + + /// Check if this activity has expired + pub fn is_expired(&self) -> bool { + if let Some(expires_at) = self.expires_at { + Utc::now() > expires_at + } else { + false + } + } + + /// Get a formatted summary of this activity + pub fn summary(&self) -> String { + format!( + "[{}] {} - {} (by {})", + self.occurred_at.format("%Y-%m-%d %H:%M:%S UTC"), + self.title, + self.description, + self.actor_name.as_deref().unwrap_or("System") + ) + } +} + +/// Factory methods for creating common governance activities +impl GovernanceActivity { + /// Create an activity for proposal creation + pub fn proposal_created( + proposal_id: u32, + proposal_title: impl ToString, + creator_id: impl ToString, + creator_name: impl ToString, + ) -> Self { + let creator_name_str = creator_name.to_string(); + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::ProposalCreated, + format!("{} created a new proposal", creator_name_str), + format!( + "{} created a new proposal titled '{}' for community consideration", + creator_name_str, proposal_title_str + ), + Some(creator_id), + Some(creator_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_tag("proposal") + .with_tag("creation") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for proposal status change + pub fn proposal_status_changed( + proposal_id: u32, + proposal_title: impl ToString, + old_status: impl ToString, + new_status: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let old_status_str = old_status.to_string(); + let new_status_str = new_status.to_string(); + let actor_name_str = actor_name + .map(|n| n.to_string()) + .unwrap_or_else(|| "System".to_string()); + + Self::new( + None, + ActivityType::ProposalStatusChanged, + format!( + "{} changed proposal status to {}", + actor_name_str, new_status_str + ), + format!( + "{} changed the status of proposal '{}' from {} to {}", + actor_name_str, proposal_title_str, old_status_str, new_status_str + ), + actor_id, + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("old_status", old_status_str) + .with_metadata("new_status", new_status_str) + .with_tag("proposal") + .with_tag("status_change") + .with_severity(ActivitySeverity::High) + } + + /// Create an activity for vote casting + pub fn vote_cast( + proposal_id: u32, + proposal_title: impl ToString, + ballot_id: u32, + voter_id: impl ToString, + voter_name: impl ToString, + option_text: impl ToString, + shares: i64, + ) -> Self { + let voter_name_str = voter_name.to_string(); + let option_text_str = option_text.to_string(); + let proposal_title_str = proposal_title.to_string(); + + // Create a more natural vote description + let vote_description = if option_text_str.to_lowercase().contains("approve") + || option_text_str.to_lowercase().contains("yes") + { + format!( + "{} voted YES on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else if option_text_str.to_lowercase().contains("reject") + || option_text_str.to_lowercase().contains("no") + { + format!( + "{} voted NO on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else if option_text_str.to_lowercase().contains("abstain") { + format!( + "{} abstained from voting on proposal '{}'", + voter_name_str, proposal_title_str + ) + } else { + format!( + "{} voted '{}' on proposal '{}'", + voter_name_str, option_text_str, proposal_title_str + ) + }; + + Self::new( + None, + ActivityType::VoteCast, + format!("{} submitted a vote", voter_name_str), + format!("{} with {} voting shares", vote_description, shares), + Some(voter_id), + Some(voter_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("ballot_id", ballot_id.to_string()) + .with_metadata("option_text", option_text_str) + .with_metadata("shares", shares.to_string()) + .with_metadata("proposal_title", proposal_title_str) + .with_tag("vote") + .with_tag("ballot") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for voting period start + pub fn voting_started( + proposal_id: u32, + proposal_title: impl ToString, + start_date: DateTime, + end_date: DateTime, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::VotingStarted, + format!("Voting opened for '{}'", proposal_title_str), + format!( + "Community voting has opened for proposal '{}' and will close on {}", + proposal_title_str, + end_date.format("%B %d, %Y at %H:%M UTC") + ), + None::, + Some("System"), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("start_date", start_date.to_rfc3339()) + .with_metadata("end_date", end_date.to_rfc3339()) + .with_tag("voting") + .with_tag("period_start") + .with_severity(ActivitySeverity::High) + .with_occurred_at(start_date) + } + + /// Create an activity for voting period end + pub fn voting_ended( + proposal_id: u32, + proposal_title: impl ToString, + total_votes: usize, + total_shares: i64, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + + Self::new( + None, + ActivityType::VotingEnded, + format!("Voting closed for '{}'", proposal_title_str), + format!( + "Community voting has ended for proposal '{}'. Final results: {} votes cast representing {} total voting shares", + proposal_title_str, + total_votes, + total_shares + ), + None::, + Some("System"), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("total_votes", total_votes.to_string()) + .with_metadata("total_shares", total_shares.to_string()) + .with_tag("voting") + .with_tag("period_end") + .with_severity(ActivitySeverity::High) + } + + /// Create an activity for vote option addition + pub fn vote_option_added( + proposal_id: u32, + proposal_title: impl ToString, + option_id: u8, + option_text: impl ToString, + actor_id: Option, + actor_name: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let option_text_str = option_text.to_string(); + let actor_name_str = actor_name + .map(|n| n.to_string()) + .unwrap_or_else(|| "System".to_string()); + + Self::new( + None, + ActivityType::VoteOptionAdded, + format!("{} added a voting option", actor_name_str), + format!( + "{} added the voting option '{}' to proposal '{}'", + actor_name_str, option_text_str, proposal_title_str + ), + actor_id, + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_metadata("option_id", option_id.to_string()) + .with_metadata("option_text", option_text_str) + .with_tag("vote_option") + .with_tag("addition") + .with_severity(ActivitySeverity::Normal) + } + + /// Create an activity for proposal deletion + pub fn proposal_deleted( + proposal_id: u32, + proposal_title: impl ToString, + actor_id: impl ToString, + actor_name: impl ToString, + reason: Option, + ) -> Self { + let proposal_title_str = proposal_title.to_string(); + let actor_name_str = actor_name.to_string(); + + let description = if let Some(reason) = reason { + format!( + "{} deleted proposal '{}'. Reason: {}", + actor_name_str, + proposal_title_str, + reason.to_string() + ) + } else { + format!( + "{} deleted proposal '{}'", + actor_name_str, proposal_title_str + ) + }; + + Self::new( + None, + ActivityType::ProposalDeleted, + format!("{} deleted a proposal", actor_name_str), + description, + Some(actor_id), + Some(actor_name_str), + ) + .with_target(proposal_id, "proposal") + .with_metadata("proposal_title", proposal_title_str) + .with_tag("proposal") + .with_tag("deletion") + .with_severity(ActivitySeverity::Critical) + } +} diff --git a/heromodels/src/models/governance/mod.rs b/heromodels/src/models/governance/mod.rs index 792f8b9..4e744bc 100644 --- a/heromodels/src/models/governance/mod.rs +++ b/heromodels/src/models/governance/mod.rs @@ -1,7 +1,7 @@ // heromodels/src/models/governance/mod.rs // This module will contain the Proposal model and related types. +pub mod activity; pub mod proposal; -pub use self::proposal::{ - Activity, ActivityType, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption, -}; +pub use self::activity::{ActivityStatus, ActivityType, GovernanceActivity}; +pub use self::proposal::{Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption}; diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index 850f48c..0caa406 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -31,3 +31,10 @@ pub use flow::register_flow_rhai_module; pub use legal::register_legal_rhai_module; #[cfg(feature = "rhai")] pub use projects::register_projects_rhai_module; +pub use calendar::{AttendanceStatus, Attendee, Calendar, Event}; +pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType}; +pub use finance::{Account, Asset, AssetType}; +pub use governance::{ + ActivityStatus, ActivityType, Ballot, GovernanceActivity, Proposal, ProposalStatus, + VoteEventStatus, VoteOption, +};