From d8e3d48caa403470969fa219e8069c371f22051e Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 27 May 2025 21:15:08 +0300 Subject: [PATCH 1/8] feat: Improve database handling of model indices - Ensure all model types always have a primary key index entry. - Simplify `get_all()` implementation by relying on primary key index. - Remove redundant code in `get_all()` for finding all object IDs. - Improve error handling and clarity in database operations. --- heromodels/src/db/hero.rs | 64 +++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index dff28b1..47fd7c4 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -323,6 +323,16 @@ where assigned_id }; + // Always create a primary key index entry for this model type + // This ensures get_all() can find all objects of this type, even if they have no explicit indexed fields + let primary_index_key = format!("{}::primary", M::db_prefix()); + let mut primary_ids: HashSet = + Self::get_tst_value(&mut index_db, &primary_index_key)? + .unwrap_or_else(HashSet::new); + primary_ids.insert(assigned_id); + let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?; + index_db.set(&primary_index_key, raw_primary_ids)?; + // Now add the new indices for index_key in indices_to_add { let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value); @@ -420,6 +430,22 @@ where } } + // Also remove from the primary key index + let primary_index_key = format!("{}::primary", M::db_prefix()); + if let Some(mut primary_ids) = + Self::get_tst_value::>(&mut index_db, &primary_index_key)? + { + primary_ids.remove(&id); + if primary_ids.is_empty() { + // This was the last object of this type, remove the primary index entirely + index_db.delete(&primary_index_key)?; + } else { + // There are still other objects of this type, write back updated set + let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?; + index_db.set(&primary_index_key, raw_primary_ids)?; + } + } + // Finally delete the object itself Ok(data_db.delete(id)?) } @@ -428,36 +454,16 @@ where let mut index_db = self.index.lock().expect("can lock index DB"); let mut data_db = self.data.lock().expect("can lock data DB"); - let prefix = M::db_prefix(); - let mut all_object_ids: HashSet = HashSet::new(); - - // Use getall to find all index entries (values are serialized HashSet) for the given model prefix. - match index_db.getall(prefix) { - Ok(list_of_raw_ids_set_bytes) => { - for raw_ids_set_bytes in list_of_raw_ids_set_bytes { - // Each item in the list is a bincode-serialized HashSet of object IDs. - match bincode::serde::decode_from_slice::, _>(&raw_ids_set_bytes, BINCODE_CONFIG) { - Ok((ids_set, _)) => { // Destructure the tuple (HashSet, usize) - all_object_ids.extend(ids_set); - } - Err(e) => { - // If deserialization of an ID set fails, propagate as a decode error. - return Err(super::Error::Decode(e)); - } - } + // Look for the primary key index entry for this model type + let primary_index_key = format!("{}::primary", M::db_prefix()); + let all_object_ids: HashSet = + match Self::get_tst_value(&mut index_db, &primary_index_key)? { + Some(ids) => ids, + None => { + // No primary index found, meaning no objects of this type exist + return Ok(Vec::new()); } - } - Err(tst::Error::PrefixNotFound(_)) => { - // No index entries found for this prefix, meaning no objects of this type exist. - // Note: tst::getall might return Ok(vec![]) in this case instead of PrefixNotFound. - // Depending on tst implementation, this arm might be redundant if getall returns empty vec. - return Ok(Vec::new()); - } - Err(e) => { - // Other TST errors. - return Err(super::Error::DB(e)); - } - } + }; let mut results: Vec = Vec::with_capacity(all_object_ids.len()); for obj_id in all_object_ids { From 331915c6cbb8c0367adac383d711acf456002a7b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 12:52:32 +0300 Subject: [PATCH 2/8] feat: Enhance calendar example with user and attendee management - Add user creation and management to the calendar example. - Integrate user IDs into attendees for improved data integrity. - Improve event manipulation by adding and removing attendees by ID. - Enhance calendar example to demonstrate event and calendar retrieval. - Enhance the calendar example with database storage for events. - Modify the calendar example to manage events by ID instead of title. --- heromodels/examples/calendar_example/main.rs | 442 +++++++++++++++---- heromodels/src/models/calendar/calendar.rs | 97 ++-- heromodels/src/models/calendar/rhai.rs | 145 +++--- 3 files changed, 518 insertions(+), 166 deletions(-) diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index 6afec70..0008d38 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -1,6 +1,7 @@ use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; -use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event}; +use heromodels::models::User; +use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event}; use heromodels_core::Model; fn main() { @@ -11,122 +12,401 @@ fn main() { println!("Hero Models - Calendar Usage Example"); println!("===================================="); + // --- Create Users First --- + println!("\n--- Creating Users ---"); + let user1 = User::new() + .username("alice_johnson") + .email("alice.johnson@company.com") + .full_name("Alice Johnson") + .is_active(true) + .build(); + + let user2 = User::new() + .username("bob_smith") + .email("bob.smith@company.com") + .full_name("Bob Smith") + .is_active(true) + .build(); + + let user3 = User::new() + .username("carol_davis") + .email("carol.davis@company.com") + .full_name("Carol Davis") + .is_active(true) + .build(); + + // Store users in database and get their IDs + let user_collection = db.collection::().expect("can open user collection"); + + let (user1_id, stored_user1) = user_collection.set(&user1).expect("can set user1"); + let (user2_id, stored_user2) = user_collection.set(&user2).expect("can set user2"); + let (user3_id, stored_user3) = user_collection.set(&user3).expect("can set user3"); + + println!("Created users:"); + println!("- User 1 (ID: {}): {}", user1_id, stored_user1.full_name); + println!("- User 2 (ID: {}): {}", user2_id, stored_user2.full_name); + println!("- User 3 (ID: {}): {}", user3_id, stored_user3.full_name); + // --- Create Attendees --- - let attendee1 = Attendee::new("user_123".to_string()) - .status(AttendanceStatus::Accepted); - let attendee2 = Attendee::new("user_456".to_string()) - .status(AttendanceStatus::Tentative); - let attendee3 = Attendee::new("user_789".to_string()); // Default NoResponse + println!("\n--- Creating Attendees ---"); + let attendee1 = Attendee::new(user1_id).status(AttendanceStatus::Accepted); + let attendee2 = Attendee::new(user2_id).status(AttendanceStatus::Tentative); + let attendee3 = Attendee::new(user3_id); // Default NoResponse - // --- Create Events --- - let now = Utc::now(); - let event1 = Event::new( - "event_alpha".to_string(), - "Team Meeting", - now + Duration::seconds(3600), // Using Duration::seconds for more explicit chrono 0.4 compatibility - now + Duration::seconds(7200), - ) - .description("Weekly sync-up meeting.") - .location("Conference Room A") - .add_attendee(attendee1.clone()) - .add_attendee(attendee2.clone()); + // Store attendees in database and get their IDs + let attendee_collection = db + .collection::() + .expect("can open attendee collection"); - let event2 = Event::new( - "event_beta".to_string(), - "Project Brainstorm", - now + Duration::days(1), - now + Duration::days(1) + Duration::seconds(5400), // 90 minutes - ) - .description("Brainstorming session for new project features.") - .add_attendee(attendee1.clone()) - .add_attendee(attendee3.clone()); + let (attendee1_id, stored_attendee1) = attendee_collection + .set(&attendee1) + .expect("can set attendee1"); + let (attendee2_id, stored_attendee2) = attendee_collection + .set(&attendee2) + .expect("can set attendee2"); + let (attendee3_id, stored_attendee3) = attendee_collection + .set(&attendee3) + .expect("can set attendee3"); - let event3_for_calendar2 = Event::new( - "event_gamma".to_string(), - "Client Call", - now + Duration::days(2), - now + Duration::days(2) + Duration::seconds(3600) + println!("Created attendees:"); + println!( + "- Attendee 1 (ID: {}): Contact ID {}, Status: {:?}", + attendee1_id, stored_attendee1.contact_id, stored_attendee1.status + ); + println!( + "- Attendee 2 (ID: {}): Contact ID {}, Status: {:?}", + attendee2_id, stored_attendee2.contact_id, stored_attendee2.status + ); + println!( + "- Attendee 3 (ID: {}): Contact ID {}, Status: {:?}", + attendee3_id, stored_attendee3.contact_id, stored_attendee3.status ); - // --- Create Calendars --- - // Note: Calendar::new directly returns Calendar, no separate .build() step like the user example. + // --- Create Events with Attendees --- + println!("\n--- Creating Events with Attendees ---"); + let now = Utc::now(); - // Create a calendar with auto-generated ID + let event1 = Event::new( + "Team Meeting", + now + Duration::hours(1), + now + Duration::hours(2), + ) + .description("Weekly sync-up meeting to discuss project progress.") + .location("Conference Room A") + .add_attendee(attendee1_id) + .add_attendee(attendee2_id); + + let event2 = Event::new( + "Project Brainstorm", + now + Duration::days(1), + now + Duration::days(1) + Duration::minutes(90), + ) + .description("Brainstorming session for new project features.") + .location("Innovation Lab") + .add_attendee(attendee1_id) + .add_attendee(attendee3_id); + + let event3 = Event::new( + "Client Call", + now + Duration::days(2), + now + Duration::days(2) + Duration::hours(1), + ) + .description("Quarterly review with key client.") + .add_attendee(attendee2_id); + + println!("Created events:"); + println!( + "- Event 1: '{}' at {} with {} attendees", + event1.title, + event1.start_time.format("%Y-%m-%d %H:%M"), + event1.attendees.len() + ); + println!( + " Location: {}", + event1 + .location + .as_ref() + .unwrap_or(&"Not specified".to_string()) + ); + println!(" Attendee IDs: {:?}", event1.attendees); + + println!( + "- Event 2: '{}' at {} with {} attendees", + event2.title, + event2.start_time.format("%Y-%m-%d %H:%M"), + event2.attendees.len() + ); + println!( + " Location: {}", + event2 + .location + .as_ref() + .unwrap_or(&"Not specified".to_string()) + ); + println!(" Attendee IDs: {:?}", event2.attendees); + + println!( + "- Event 3: '{}' at {} with {} attendees", + event3.title, + event3.start_time.format("%Y-%m-%d %H:%M"), + event3.attendees.len() + ); + println!(" Attendee IDs: {:?}", event3.attendees); + + // --- Demonstrate Event Manipulation --- + println!("\n--- Demonstrating Event Manipulation ---"); + + // Reschedule an event + let new_start = now + Duration::hours(2); + let new_end = now + Duration::hours(3); + let mut updated_event1 = event1.clone(); + updated_event1 = updated_event1.reschedule(new_start, new_end); + println!( + "Rescheduled '{}' to {}", + updated_event1.title, + new_start.format("%Y-%m-%d %H:%M") + ); + + // Remove an attendee + updated_event1 = updated_event1.remove_attendee(attendee1_id); + println!( + "Removed attendee {} from '{}'. Remaining attendee IDs: {:?}", + attendee1_id, updated_event1.title, updated_event1.attendees + ); + + // Add a new attendee + updated_event1 = updated_event1.add_attendee(attendee3_id); + println!( + "Added attendee {} to '{}'. Current attendee IDs: {:?}", + attendee3_id, updated_event1.title, updated_event1.attendees + ); + + // --- Store Events in Database --- + // Now that Event is a proper database model, we need to store events first + println!("\n--- Storing Events in Database ---"); + + let event_collection = db.collection::().expect("can open event collection"); + + // Store events and get their auto-generated IDs + let (event1_id, stored_event1) = event_collection.set(&event1).expect("can set event1"); + let (event2_id, stored_event2) = event_collection.set(&event2).expect("can set event2"); + let (event3_id, stored_event3) = event_collection.set(&event3).expect("can set event3"); + + println!("Stored events in database:"); + println!("- Event ID {}: '{}'", event1_id, stored_event1.title); + println!("- Event ID {}: '{}'", event2_id, stored_event2.title); + println!("- Event ID {}: '{}'", event3_id, stored_event3.title); + + // --- Create Calendars --- + // Now calendars store the actual database IDs of the events + println!("\n--- Creating Calendars ---"); + + // Create a calendar with auto-generated ID and the stored event IDs let calendar1 = Calendar::new(None, "Work Calendar") .description("Calendar for all work-related events.") - .add_event(event1.clone()) - .add_event(event2.clone()); + .add_event(event1_id as i64) + .add_event(event2_id as i64); - // Create a calendar with auto-generated ID (explicit IDs are no longer supported) - let calendar2 = Calendar::new(None, "Personal Calendar") - .add_event(event3_for_calendar2.clone()); + // Create another calendar with auto-generated ID + let calendar2 = Calendar::new(None, "Personal Calendar").add_event(event3_id as i64); + println!("Created calendars with event IDs:"); + println!( + "- Calendar 1: '{}' with events: {:?}", + calendar1.name, calendar1.events + ); + println!( + "- Calendar 2: '{}' with events: {:?}", + calendar2.name, calendar2.events + ); // --- Store Calendars in DB --- - let cal_collection = db.collection::().expect("can open calendar collection"); + let cal_collection = db + .collection::() + .expect("can open calendar collection"); let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1"); let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2"); - println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name); - println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name); + println!( + "Created calendar1 (ID: {}): Name - '{}'", + calendar1.get_id(), + calendar1.name + ); + println!( + "Created calendar2 (ID: {}): Name - '{}'", + calendar2.get_id(), + calendar2.name + ); // --- Retrieve a Calendar by ID --- - let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1"); - assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB"); + let stored_calendar1_opt = cal_collection + .get_by_id(calendar1.get_id()) + .expect("can try to load calendar1"); + assert!( + stored_calendar1_opt.is_some(), + "Calendar1 should be found in DB" + ); let mut stored_calendar1 = stored_calendar1_opt.unwrap(); - println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len()); + println!( + "\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", + stored_calendar1.name, + stored_calendar1.events.len() + ); assert_eq!(stored_calendar1.name, "Work Calendar"); assert_eq!(stored_calendar1.events.len(), 2); - assert_eq!(stored_calendar1.events[0].title, "Team Meeting"); + assert_eq!(stored_calendar1.events[0], event1_id as i64); // Check event ID - // --- Modify a Calendar (Reschedule an Event) --- - let event_id_to_reschedule = event1.id.as_str(); - let new_start_time = now + Duration::seconds(10800); // 3 hours from now - let new_end_time = now + Duration::seconds(14400); // 4 hours from now + // --- Modify a Calendar (Add/Remove Events) --- + // Since events are just IDs, we can add and remove them easily + println!("\n--- Modifying Calendar ---"); - stored_calendar1 = stored_calendar1.update_event(event_id_to_reschedule, |event_to_update| { - println!("Rescheduling event '{}'...", event_to_update.title); - event_to_update.reschedule(new_start_time, new_end_time) - }); + // Create and store a new event + let new_event = Event::new( + "1-on-1 Meeting", + now + Duration::days(3), + now + Duration::days(3) + Duration::minutes(30), + ) + .description("One-on-one meeting with team member.") + .location("Office"); - let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) - .expect("Rescheduled event should exist"); - assert_eq!(rescheduled_event.start_time, new_start_time); - assert_eq!(rescheduled_event.end_time, new_end_time); - println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title); + let (new_event_id, _stored_new_event) = + event_collection.set(&new_event).expect("can set new event"); + println!("Created new event with ID: {}", new_event_id); + + // Add the new event ID to the calendar + stored_calendar1 = stored_calendar1.add_event(new_event_id as i64); + assert_eq!(stored_calendar1.events.len(), 3); + println!( + "Added event ID {} to calendar1. Total events: {}", + new_event_id, + stored_calendar1.events.len() + ); + + // Remove an event ID from the calendar + stored_calendar1 = stored_calendar1.remove_event(event2_id as i64); // Remove "Project Brainstorm" + assert_eq!(stored_calendar1.events.len(), 2); + println!( + "Removed event ID {} from calendar1. Total events: {}", + event2_id, + stored_calendar1.events.len() + ); // --- Store the modified calendar --- - let (_, mut stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set modified calendar1"); - let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1"); + let (_, _stored_calendar1) = cal_collection + .set(&stored_calendar1) + .expect("can set modified calendar1"); + + // Verify the changes were persisted + let re_retrieved_calendar1_opt = cal_collection + .get_by_id(calendar1.get_id()) + .expect("can try to load modified calendar1"); let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap(); - let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule) - .expect("Rescheduled event should exist in re-retrieved calendar"); - assert_eq!(re_retrieved_event.start_time, new_start_time, "Reschedule not persisted correctly"); + assert_eq!(re_retrieved_calendar1.events.len(), 2); + assert!(re_retrieved_calendar1.events.contains(&(event1_id as i64))); // Team Meeting still there + assert!( + re_retrieved_calendar1 + .events + .contains(&(new_event_id as i64)) + ); // New event added + assert!(!re_retrieved_calendar1.events.contains(&(event2_id as i64))); // Project Brainstorm removed - println!("\nModified and re-saved calendar1. Rescheduled event start time: {}", re_retrieved_event.start_time); - - // --- Add a new event to an existing calendar --- - let event4_new = Event::new( - "event_delta".to_string(), - "1-on-1", - now + Duration::days(3), - now + Duration::days(3) + Duration::seconds(1800) // 30 minutes + println!( + "\nModified and re-saved calendar1. Final events: {:?}", + re_retrieved_calendar1.events ); - stored_calendar1 = stored_calendar1.add_event(event4_new); - assert_eq!(stored_calendar1.events.len(), 3); - let (_, stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event"); - println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len()); // --- Delete a Calendar --- - cal_collection.delete_by_id(calendar2.get_id()).expect("can delete calendar2"); - let deleted_calendar2_opt = cal_collection.get_by_id(calendar2.get_id()).expect("can try to load deleted calendar2"); - assert!(deleted_calendar2_opt.is_none(), "Calendar2 should be deleted from DB"); + cal_collection + .delete_by_id(calendar2.get_id()) + .expect("can delete calendar2"); + let deleted_calendar2_opt = cal_collection + .get_by_id(calendar2.get_id()) + .expect("can try to load deleted calendar2"); + assert!( + deleted_calendar2_opt.is_none(), + "Calendar2 should be deleted from DB" + ); println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id()); println!("Calendar model DB Prefix: {}", Calendar::db_prefix()); + // --- Demonstrate Event Retrieval --- + println!("\n--- Retrieving Events from Database ---"); + + // Get all events + let all_events = event_collection.get_all().expect("can list all events"); + println!("All events in database:"); + for event in &all_events { + println!( + "- Event ID: {}, Title: '{}', Start: {}, Attendees: {}", + event.get_id(), + event.title, + event.start_time.format("%Y-%m-%d %H:%M"), + event.attendees.len() + ); + } + println!("Total events in DB: {}", all_events.len()); + + // Retrieve specific events by ID and show attendee details + println!("\nRetrieving specific events:"); + if let Some(retrieved_event1) = event_collection + .get_by_id(event1_id) + .expect("can try to get event1") + { + println!( + "Retrieved Event 1: '{}' with {} attendees", + retrieved_event1.title, + retrieved_event1.attendees.len() + ); + + // Look up attendee details for each attendee ID + for &attendee_id in &retrieved_event1.attendees { + if let Some(attendee) = attendee_collection + .get_by_id(attendee_id) + .expect("can try to get attendee") + { + // Look up user details for the attendee's contact_id + if let Some(user) = user_collection + .get_by_id(attendee.contact_id) + .expect("can try to get user") + { + println!( + " - Attendee ID {}: {} (User: {}, Status: {:?})", + attendee_id, user.full_name, attendee.contact_id, attendee.status + ); + } + } + } + } + + // --- List All Calendars --- + println!("\n--- Listing All Calendars ---"); + let all_calendars = cal_collection.get_all().expect("can list all calendars"); + for calendar in &all_calendars { + println!( + "- Calendar ID: {}, Name: '{}', Events: {:?}", + calendar.get_id(), + calendar.name, + calendar.events + ); + + // Show which events are in this calendar + for &event_id in &calendar.events { + if let Some(event) = event_collection + .get_by_id(event_id as u32) + .expect("can try to get event") + { + println!(" * Event: '{}'", event.title); + } + } + } + println!("Total calendars in DB: {}", all_calendars.len()); + println!("\nExample finished. DB stored at {}", db_path); - println!("To clean up, you can manually delete the directory: {}", db_path); + println!( + "To clean up, you can manually delete the directory: {}", + db_path + ); } diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index 2ca849c..d883060 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -1,32 +1,50 @@ use chrono::{DateTime, Utc}; use heromodels_core::BaseModelData; use heromodels_derive::model; -use rhai_autobind_macros::rhai_model_export; use rhai::{CustomType, TypeBuilder}; +use rhai_autobind_macros::rhai_model_export; use serde::{Deserialize, Serialize}; /// Represents the status of an attendee for an event #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum AttendanceStatus { - Accepted, - Declined, - Tentative, - NoResponse, + Accepted = 0, + Declined = 1, + Tentative = 2, + NoResponse = 3, } /// Represents an attendee of an event -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub struct Attendee { + /// Base model data + pub base_data: BaseModelData, /// ID of the user attending - // Assuming user_id might be queryable pub contact_id: u32, /// Attendance status of the user for the event pub status: AttendanceStatus, } impl Attendee { + /// Creates a new attendee with auto-generated ID pub fn new(contact_id: u32) -> Self { Self { + base_data: BaseModelData::new(), // ID will be auto-generated by OurDB + contact_id, + status: AttendanceStatus::NoResponse, + } + } + + /// Creates a new attendee with optional ID (use None for auto-generated ID) + pub fn new_with_id(id: Option, contact_id: u32) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + + Self { + base_data, contact_id, status: AttendanceStatus::NoResponse, } @@ -39,10 +57,10 @@ impl Attendee { } /// Represents an event in a calendar +#[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Event { /// Base model data - #[serde(flatten)] pub base_data: BaseModelData, /// Title of the event pub title: String, @@ -52,17 +70,40 @@ pub struct Event { pub start_time: DateTime, /// End time of the event pub end_time: DateTime, - /// List of attendees for the event - pub attendees: Vec, + /// List of attendee IDs for the event + pub attendees: Vec, /// Optional location of the event pub location: Option, } impl Event { - /// Creates a new event + /// Creates a new event with auto-generated ID pub fn new(title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { Self { - base_data: BaseModelData::new(), + base_data: BaseModelData::new(), // ID will be auto-generated by OurDB + title: title.to_string(), + description: None, + start_time, + end_time, + attendees: Vec::new(), + location: None, + } + } + + /// Creates a new event with optional ID (use None for auto-generated ID) + pub fn new_with_id( + id: Option, + title: impl ToString, + start_time: DateTime, + end_time: DateTime, + ) -> Self { + let mut base_data = BaseModelData::new(); + if let Some(id) = id { + base_data.update_id(id); + } + + Self { + base_data, title: title.to_string(), description: None, start_time, @@ -90,26 +131,18 @@ impl Event { self } - /// Adds an attendee to the event - pub fn add_attendee(mut self, attendee: Attendee) -> Self { - // Prevent duplicate attendees by contact_id - if !self.attendees.iter().any(|a| a.contact_id == attendee.contact_id) { - self.attendees.push(attendee); + /// Adds an attendee ID to the event + pub fn add_attendee(mut self, attendee_id: u32) -> Self { + // Prevent duplicate attendees by ID + if !self.attendees.iter().any(|&a_id| a_id == attendee_id) { + self.attendees.push(attendee_id); } self } - /// Removes an attendee from the event by user_id - pub fn remove_attendee(mut self, contact_id: u32) -> Self { - self.attendees.retain(|a| a.contact_id != contact_id); - self - } - - /// Updates the status of an existing attendee - pub fn update_attendee_status(mut self, contact_id: u32, status: AttendanceStatus) -> Self { - if let Some(attendee) = self.attendees.iter_mut().find(|a| a.contact_id == contact_id) { - attendee.status = status; - } + /// Removes an attendee from the event by attendee ID + pub fn remove_attendee(mut self, attendee_id: u32) -> Self { + self.attendees.retain(|&a_id| a_id != attendee_id); self } @@ -130,14 +163,11 @@ impl Event { } /// Represents a calendar with events -#[rhai_model_export( - db_type = "std::sync::Arc", -)] +#[rhai_model_export(db_type = "std::sync::Arc")] #[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Calendar { /// Base model data - #[serde(flatten)] pub base_data: BaseModelData, /// Name of the calendar @@ -194,7 +224,8 @@ impl Calendar { /// Removes an event from the calendar by its ID pub fn remove_event(mut self, event_id_to_remove: i64) -> Self { - self.events.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove); + self.events + .retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove); self } } diff --git a/heromodels/src/models/calendar/rhai.rs b/heromodels/src/models/calendar/rhai.rs index d4e1f12..b3415da 100644 --- a/heromodels/src/models/calendar/rhai.rs +++ b/heromodels/src/models/calendar/rhai.rs @@ -1,17 +1,16 @@ -use rhai::{Engine, EvalAltResult, NativeCallContext, ImmutableString}; +use rhai::{Engine, EvalAltResult, ImmutableString, NativeCallContext}; use std::sync::Arc; -use heromodels_core::BaseModelData; +use super::calendar::{AttendanceStatus, Attendee, Calendar, Event}; use crate::db::hero::OurDB; -use super::calendar::{Calendar, Event, Attendee, AttendanceStatus}; -use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method}; use adapter_macros::rhai_timestamp_helpers; +use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method}; +use heromodels_core::BaseModelData; // Helper function for get_all_calendars registration - fn new_calendar_rhai(name: String) -> Result> { - Ok(Calendar::new(None, name)) + Ok(Calendar::new(None, name)) } fn new_event_rhai( @@ -20,71 +19,113 @@ fn new_event_rhai( start_time_ts: i64, end_time_ts: i64, ) -> Result> { - let start_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts) - .map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime( - format!("Failed to convert start_time for Event: {}", e_str).into(), - context.position(), - )))?; - - let end_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts) - .map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime( - format!("Failed to convert end_time for Event: {}", e_str).into(), - context.position(), - )))?; - - Ok(Event::new(title_rhai.to_string(), start_time, end_time)) -} - -pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc) { - engine.register_fn("name", move |calendar: Calendar, name: String| Calendar::name(calendar, name)); - engine.register_fn("description", move |calendar: Calendar, description: String| Calendar::description(calendar, description)); - engine.register_fn("add_event", Calendar::add_event); - // Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32. - // This might require adjustment if events are fetched by ID from the DB via Calendar.events. - - engine.register_fn("new_event", |context: NativeCallContext, title_rhai: ImmutableString, start_time_ts: i64, end_time_ts: i64| -> Result> { - new_event_rhai(context, title_rhai, start_time_ts, end_time_ts) - }); - engine.register_fn("title", move |event: Event, title: String| Event::title(event, title)); - engine.register_fn("description", move |event: Event, description: String| Event::description(event, description)); - engine.register_fn("add_attendee", move |event: Event, attendee: Attendee| Event::add_attendee(event, attendee)); - engine.register_fn("remove_attendee", adapt_rhai_i64_input_method!(Event, remove_attendee, u32)); - engine.register_fn("update_attendee_status", move |context: NativeCallContext, event: Event, contact_id_i64: i64, status: AttendanceStatus| -> Result> { - let contact_id_u32: u32 = contact_id_i64.try_into().map_err(|_e| { - Box::new(EvalAltResult::ErrorArithmetic( - format!("Conversion error for contact_id in Event::update_attendee_status from i64 to u32"), + let start_time = + rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts).map_err(|e_str| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to convert start_time for Event: {}", e_str).into(), context.position(), )) })?; - Ok(event.update_attendee_status(contact_id_u32, status)) + + let end_time = + rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts).map_err(|e_str| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to convert end_time for Event: {}", e_str).into(), + context.position(), + )) + })?; + + Ok(Event::new(title_rhai.to_string(), start_time, end_time)) +} + +pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc) { + engine.register_fn("name", move |calendar: Calendar, name: String| { + Calendar::name(calendar, name) }); + engine.register_fn( + "description", + move |calendar: Calendar, description: String| Calendar::description(calendar, description), + ); + engine.register_fn("add_event", Calendar::add_event); + // Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32. + // This might require adjustment if events are fetched by ID from the DB via Calendar.events. + + engine.register_fn( + "new_event", + |context: NativeCallContext, + title_rhai: ImmutableString, + start_time_ts: i64, + end_time_ts: i64| + -> Result> { + new_event_rhai(context, title_rhai, start_time_ts, end_time_ts) + }, + ); + engine.register_fn("title", move |event: Event, title: String| { + Event::title(event, title) + }); + engine.register_fn("description", move |event: Event, description: String| { + Event::description(event, description) + }); + engine.register_fn( + "add_attendee", + adapt_rhai_i64_input_method!(Event, add_attendee, u32), + ); + engine.register_fn( + "remove_attendee", + adapt_rhai_i64_input_method!(Event, remove_attendee, u32), + ); engine.register_fn("new_attendee", adapt_rhai_i64_input_fn!(Attendee::new, u32)); - engine.register_fn("new_calendar", |name: String| -> Result> { new_calendar_rhai(name) }); + engine.register_fn( + "new_calendar", + |name: String| -> Result> { new_calendar_rhai(name) }, + ); // Register a function to get the database instance engine.register_fn("get_db", move || db.clone()); - + // Register getters for Calendar - engine.register_get("id", |c: &mut Calendar| -> Result> { Ok(c.base_data.id as i64) }); - engine.register_get("name", |c: &mut Calendar| -> Result> { - // println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print - Ok(c.name.clone()) - }); - engine.register_get("description", |c: &mut Calendar| -> Result, Box> { Ok(c.description.clone()) }); + engine.register_get( + "id", + |c: &mut Calendar| -> Result> { Ok(c.base_data.id as i64) }, + ); + engine.register_get( + "name", + |c: &mut Calendar| -> Result> { + // println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print + Ok(c.name.clone()) + }, + ); + engine.register_get( + "description", + |c: &mut Calendar| -> Result, Box> { + Ok(c.description.clone()) + }, + ); // Register getter for Calendar.base_data - engine.register_get("base_data", |c: &mut Calendar| -> Result> { Ok(c.base_data.clone()) }); + engine.register_get( + "base_data", + |c: &mut Calendar| -> Result> { Ok(c.base_data.clone()) }, + ); // Register getters for BaseModelData - engine.register_get("id", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id.into()) }); + engine.register_get( + "id", + |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id.into()) }, + ); // Database interaction functions for Calendar are expected to be provided by #[rhai_model_export(..)] on the Calendar struct. // Ensure that `db.rs` or similar correctly wires up the `OurDB` methods for these. // Getters for Event - engine.register_get("id", |e: &mut Event| -> Result> { Ok(e.base_data.id as i64) }); - engine.register_get("title", |e: &mut Event| -> Result> { Ok(e.title.clone()) }); + engine.register_get("id", |e: &mut Event| -> Result> { + Ok(e.base_data.id as i64) + }); + engine.register_get( + "title", + |e: &mut Event| -> Result> { Ok(e.title.clone()) }, + ); // Add more getters for Event fields as needed } From 8e2354df433dfeb7c986c5af6ff94c846d353f4b Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 28 May 2025 14:01:25 +0300 Subject: [PATCH 3/8] feat: Enhance calendar example with Event and Calendar features - Add EventStatus enum to represent event states (Draft, Published, Cancelled). - Extend Event model with color, category, reminder, timezone, created_by, all_day and is_recurring fields. - Add color and description fields to Calendar model. - Enhance calendar example to demonstrate new features, including event status changes, and detailed calendar information. - Improve example output for clarity and comprehensiveness. - Add owner_id and is_public fields to Calendar model for enhanced management. 2025-05-28 13:09:05,630 - INFO - Commit message copied to clipboard. --- heromodels/examples/calendar_example/main.rs | 283 +++++++++++++++++-- heromodels/src/models/calendar/calendar.rs | 121 +++++++- heromodels/src/models/calendar/mod.rs | 4 +- 3 files changed, 377 insertions(+), 31 deletions(-) diff --git a/heromodels/examples/calendar_example/main.rs b/heromodels/examples/calendar_example/main.rs index 0008d38..49f1e77 100644 --- a/heromodels/examples/calendar_example/main.rs +++ b/heromodels/examples/calendar_example/main.rs @@ -1,7 +1,7 @@ use chrono::{Duration, Utc}; use heromodels::db::{Collection, Db}; use heromodels::models::User; -use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event}; +use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus}; use heromodels_core::Model; fn main() { @@ -83,7 +83,7 @@ fn main() { ); // --- Create Events with Attendees --- - println!("\n--- Creating Events with Attendees ---"); + println!("\n--- Creating Events with Enhanced Features ---"); let now = Utc::now(); let event1 = Event::new( @@ -93,6 +93,12 @@ fn main() { ) .description("Weekly sync-up meeting to discuss project progress.") .location("Conference Room A") + .color("#FF5722") // Red-orange color + .created_by(user1_id) + .status(EventStatus::Published) + .category("Work") + .reminder_minutes(15) + .timezone("UTC") .add_attendee(attendee1_id) .add_attendee(attendee2_id); @@ -103,6 +109,12 @@ fn main() { ) .description("Brainstorming session for new project features.") .location("Innovation Lab") + .color("#4CAF50") // Green color + .created_by(user2_id) + .status(EventStatus::Draft) + .category("Planning") + .reminder_minutes(30) + .is_recurring(true) .add_attendee(attendee1_id) .add_attendee(attendee3_id); @@ -112,9 +124,27 @@ fn main() { now + Duration::days(2) + Duration::hours(1), ) .description("Quarterly review with key client.") + .color("#9C27B0") // Purple color + .created_by(user3_id) + .status(EventStatus::Published) + .category("Client") + .reminder_minutes(60) .add_attendee(attendee2_id); - println!("Created events:"); + // Create an all-day event + let event4 = Event::new( + "Company Holiday", + now + Duration::days(7), + now + Duration::days(7) + Duration::hours(24), + ) + .description("National holiday - office closed.") + .color("#FFC107") // Amber color + .all_day(true) + .created_by(user1_id) + .status(EventStatus::Published) + .category("Holiday"); + + println!("Created events with enhanced features:"); println!( "- Event 1: '{}' at {} with {} attendees", event1.title, @@ -128,6 +158,22 @@ fn main() { .as_ref() .unwrap_or(&"Not specified".to_string()) ); + println!( + " Color: {}", + event1.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!( + " Category: {}", + event1.category.as_ref().unwrap_or(&"None".to_string()) + ); + println!(" Status: {:?}", event1.status); + println!(" Created by: User ID {}", event1.created_by.unwrap_or(0)); + println!( + " Reminder: {} minutes before", + event1.reminder_minutes.unwrap_or(0) + ); + println!(" All-day: {}", event1.all_day); + println!(" Recurring: {}", event1.is_recurring); println!(" Attendee IDs: {:?}", event1.attendees); println!( @@ -143,6 +189,22 @@ fn main() { .as_ref() .unwrap_or(&"Not specified".to_string()) ); + println!( + " Color: {}", + event2.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!( + " Category: {}", + event2.category.as_ref().unwrap_or(&"None".to_string()) + ); + println!(" Status: {:?}", event2.status); + println!(" Created by: User ID {}", event2.created_by.unwrap_or(0)); + println!( + " Reminder: {} minutes before", + event2.reminder_minutes.unwrap_or(0) + ); + println!(" All-day: {}", event2.all_day); + println!(" Recurring: {}", event2.is_recurring); println!(" Attendee IDs: {:?}", event2.attendees); println!( @@ -151,8 +213,48 @@ fn main() { event3.start_time.format("%Y-%m-%d %H:%M"), event3.attendees.len() ); + println!( + " Location: {}", + event3 + .location + .as_ref() + .unwrap_or(&"Not specified".to_string()) + ); + println!( + " Color: {}", + event3.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!( + " Category: {}", + event3.category.as_ref().unwrap_or(&"None".to_string()) + ); + println!(" Status: {:?}", event3.status); + println!(" Created by: User ID {}", event3.created_by.unwrap_or(0)); + println!( + " Reminder: {} minutes before", + event3.reminder_minutes.unwrap_or(0) + ); + println!(" All-day: {}", event3.all_day); + println!(" Recurring: {}", event3.is_recurring); println!(" Attendee IDs: {:?}", event3.attendees); + println!( + "- Event 4: '{}' at {} (All-day: {})", + event4.title, + event4.start_time.format("%Y-%m-%d"), + event4.all_day + ); + println!( + " Color: {}", + event4.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!( + " Category: {}", + event4.category.as_ref().unwrap_or(&"None".to_string()) + ); + println!(" Status: {:?}", event4.status); + println!(" Created by: User ID {}", event4.created_by.unwrap_or(0)); + // --- Demonstrate Event Manipulation --- println!("\n--- Demonstrating Event Manipulation ---"); @@ -181,6 +283,54 @@ fn main() { attendee3_id, updated_event1.title, updated_event1.attendees ); + // --- Demonstrate Event Status Changes --- + println!("\n--- Demonstrating Event Status Changes ---"); + + // Change event status from Draft to Published + let mut updated_event2 = event2.clone(); + updated_event2 = updated_event2.status(EventStatus::Published); + println!( + "Changed '{}' status from Draft to Published", + updated_event2.title + ); + + // Cancel an event + let mut cancelled_event = event3.clone(); + cancelled_event = cancelled_event.status(EventStatus::Cancelled); + println!("Cancelled event: '{}'", cancelled_event.title); + + // Update event with new features + let enhanced_event = Event::new( + "Enhanced Meeting", + now + Duration::days(5), + now + Duration::days(5) + Duration::hours(2), + ) + .description("Meeting with all new features demonstrated.") + .location("Virtual - Zoom") + .color("#673AB7") // Deep purple + .created_by(user1_id) + .status(EventStatus::Published) + .category("Demo") + .reminder_minutes(45) + .timezone("America/New_York") + .is_recurring(true) + .add_attendee(attendee1_id) + .add_attendee(attendee2_id) + .add_attendee(attendee3_id); + + println!("Created enhanced event with all features:"); + println!(" Title: {}", enhanced_event.title); + println!(" Status: {:?}", enhanced_event.status); + println!(" Category: {}", enhanced_event.category.as_ref().unwrap()); + println!(" Color: {}", enhanced_event.color.as_ref().unwrap()); + println!(" Timezone: {}", enhanced_event.timezone.as_ref().unwrap()); + println!(" Recurring: {}", enhanced_event.is_recurring); + println!( + " Reminder: {} minutes", + enhanced_event.reminder_minutes.unwrap() + ); + println!(" Attendees: {} people", enhanced_event.attendees.len()); + // --- Store Events in Database --- // Now that Event is a proper database model, we need to store events first println!("\n--- Storing Events in Database ---"); @@ -191,34 +341,94 @@ fn main() { let (event1_id, stored_event1) = event_collection.set(&event1).expect("can set event1"); let (event2_id, stored_event2) = event_collection.set(&event2).expect("can set event2"); let (event3_id, stored_event3) = event_collection.set(&event3).expect("can set event3"); + let (event4_id, stored_event4) = event_collection.set(&event4).expect("can set event4"); println!("Stored events in database:"); - println!("- Event ID {}: '{}'", event1_id, stored_event1.title); - println!("- Event ID {}: '{}'", event2_id, stored_event2.title); - println!("- Event ID {}: '{}'", event3_id, stored_event3.title); + println!( + "- Event ID {}: '{}' (Status: {:?})", + event1_id, stored_event1.title, stored_event1.status + ); + println!( + "- Event ID {}: '{}' (Status: {:?})", + event2_id, stored_event2.title, stored_event2.status + ); + println!( + "- Event ID {}: '{}' (Status: {:?})", + event3_id, stored_event3.title, stored_event3.status + ); + println!( + "- Event ID {}: '{}' (All-day: {})", + event4_id, stored_event4.title, stored_event4.all_day + ); // --- Create Calendars --- // Now calendars store the actual database IDs of the events - println!("\n--- Creating Calendars ---"); + println!("\n--- Creating Enhanced Calendars ---"); - // Create a calendar with auto-generated ID and the stored event IDs + // Create a work calendar with enhanced features let calendar1 = Calendar::new(None, "Work Calendar") .description("Calendar for all work-related events.") + .owner_id(user1_id) + .is_public(false) + .color("#2196F3") // Blue color .add_event(event1_id as i64) .add_event(event2_id as i64); - // Create another calendar with auto-generated ID - let calendar2 = Calendar::new(None, "Personal Calendar").add_event(event3_id as i64); + // Create a personal calendar + let calendar2 = Calendar::new(None, "Personal Calendar") + .description("Personal events and appointments.") + .owner_id(user2_id) + .is_public(false) + .color("#E91E63") // Pink color + .add_event(event3_id as i64); - println!("Created calendars with event IDs:"); + // Create a company-wide calendar + let calendar3 = Calendar::new(None, "Company Events") + .description("Company-wide events and holidays.") + .owner_id(user1_id) + .is_public(true) + .color("#FF9800") // Orange color + .add_event(event4_id as i64); + + println!("Created calendars with enhanced features:"); println!( - "- Calendar 1: '{}' with events: {:?}", - calendar1.name, calendar1.events + "- Calendar 1: '{}' with {} events", + calendar1.name, + calendar1.events.len() ); + println!(" Owner: User ID {}", calendar1.owner_id.unwrap_or(0)); + println!(" Public: {}", calendar1.is_public); println!( - "- Calendar 2: '{}' with events: {:?}", - calendar2.name, calendar2.events + " Color: {}", + calendar1.color.as_ref().unwrap_or(&"Default".to_string()) ); + println!(" Events: {:?}", calendar1.events); + + println!( + "- Calendar 2: '{}' with {} events", + calendar2.name, + calendar2.events.len() + ); + println!(" Owner: User ID {}", calendar2.owner_id.unwrap_or(0)); + println!(" Public: {}", calendar2.is_public); + println!( + " Color: {}", + calendar2.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!(" Events: {:?}", calendar2.events); + + println!( + "- Calendar 3: '{}' with {} events", + calendar3.name, + calendar3.events.len() + ); + println!(" Owner: User ID {}", calendar3.owner_id.unwrap_or(0)); + println!(" Public: {}", calendar3.is_public); + println!( + " Color: {}", + calendar3.color.as_ref().unwrap_or(&"Default".to_string()) + ); + println!(" Events: {:?}", calendar3.events); // --- Store Calendars in DB --- let cal_collection = db @@ -227,16 +437,28 @@ fn main() { let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1"); let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2"); + let (_, calendar3) = cal_collection.set(&calendar3).expect("can set calendar3"); println!( - "Created calendar1 (ID: {}): Name - '{}'", + "Created calendar1 (ID: {}): Name - '{}' (Owner: {}, Public: {})", calendar1.get_id(), - calendar1.name + calendar1.name, + calendar1.owner_id.unwrap_or(0), + calendar1.is_public ); println!( - "Created calendar2 (ID: {}): Name - '{}'", + "Created calendar2 (ID: {}): Name - '{}' (Owner: {}, Public: {})", calendar2.get_id(), - calendar2.name + calendar2.name, + calendar2.owner_id.unwrap_or(0), + calendar2.is_public + ); + println!( + "Created calendar3 (ID: {}): Name - '{}' (Owner: {}, Public: {})", + calendar3.get_id(), + calendar3.name, + calendar3.owner_id.unwrap_or(0), + calendar3.is_public ); // --- Retrieve a Calendar by ID --- @@ -382,23 +604,36 @@ fn main() { } // --- List All Calendars --- - println!("\n--- Listing All Calendars ---"); + println!("\n--- Listing All Enhanced Calendars ---"); let all_calendars = cal_collection.get_all().expect("can list all calendars"); for calendar in &all_calendars { println!( - "- Calendar ID: {}, Name: '{}', Events: {:?}", + "- Calendar ID: {}, Name: '{}', Owner: {}, Public: {}, Color: {}", calendar.get_id(), calendar.name, - calendar.events + calendar.owner_id.unwrap_or(0), + calendar.is_public, + calendar.color.as_ref().unwrap_or(&"Default".to_string()) ); + println!( + " Description: {}", + calendar.description.as_ref().unwrap_or(&"None".to_string()) + ); + println!(" Events: {:?}", calendar.events); - // Show which events are in this calendar + // Show which events are in this calendar with their details for &event_id in &calendar.events { if let Some(event) = event_collection .get_by_id(event_id as u32) .expect("can try to get event") { - println!(" * Event: '{}'", event.title); + println!( + " * Event: '{}' (Status: {:?}, Category: {}, All-day: {})", + event.title, + event.status, + event.category.as_ref().unwrap_or(&"None".to_string()), + event.all_day + ); } } } diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index d883060..b93b2b3 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -14,6 +14,14 @@ pub enum AttendanceStatus { NoResponse = 3, } +/// Represents the status of an event +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum EventStatus { + Draft = 0, + Published = 1, + Cancelled = 2, +} + /// Represents an attendee of an event #[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] @@ -74,6 +82,22 @@ pub struct Event { pub attendees: Vec, /// Optional location of the event pub location: Option, + /// Color for the event (hex color code) + pub color: Option, + /// Whether this is an all-day event + pub all_day: bool, + /// ID of the user who created the event + pub created_by: Option, + /// Status of the event + pub status: EventStatus, + /// Whether this is a recurring event + pub is_recurring: bool, + /// Optional timezone for display purposes + pub timezone: Option, + /// Optional category/tag for the event + pub category: Option, + /// Optional reminder settings (minutes before event) + pub reminder_minutes: Option, } impl Event { @@ -87,6 +111,14 @@ impl Event { end_time, attendees: Vec::new(), location: None, + color: Some("#4285F4".to_string()), // Default blue color + all_day: false, + created_by: None, + status: EventStatus::Published, + is_recurring: false, + timezone: None, + category: None, + reminder_minutes: None, } } @@ -110,6 +142,14 @@ impl Event { end_time, attendees: Vec::new(), location: None, + color: Some("#4285F4".to_string()), // Default blue color + all_day: false, + created_by: None, + status: EventStatus::Published, + is_recurring: false, + timezone: None, + category: None, + reminder_minutes: None, } } @@ -131,6 +171,54 @@ impl Event { self } + /// Sets the color for the event + pub fn color(mut self, color: impl ToString) -> Self { + self.color = Some(color.to_string()); + self + } + + /// Sets whether this is an all-day event + pub fn all_day(mut self, all_day: bool) -> Self { + self.all_day = all_day; + self + } + + /// Sets the creator of the event + pub fn created_by(mut self, user_id: u32) -> Self { + self.created_by = Some(user_id); + self + } + + /// Sets the status of the event + pub fn status(mut self, status: EventStatus) -> Self { + self.status = status; + self + } + + /// Sets whether this is a recurring event + pub fn is_recurring(mut self, is_recurring: bool) -> Self { + self.is_recurring = is_recurring; + self + } + + /// Sets the timezone for the event + pub fn timezone(mut self, timezone: impl ToString) -> Self { + self.timezone = Some(timezone.to_string()); + self + } + + /// Sets the category for the event + pub fn category(mut self, category: impl ToString) -> Self { + self.category = Some(category.to_string()); + self + } + + /// Sets reminder minutes before the event + pub fn reminder_minutes(mut self, minutes: i32) -> Self { + self.reminder_minutes = Some(minutes); + self + } + /// Adds an attendee ID to the event pub fn add_attendee(mut self, attendee_id: u32) -> Self { // Prevent duplicate attendees by ID @@ -169,16 +257,18 @@ impl Event { pub struct Calendar { /// Base model data pub base_data: BaseModelData, - /// Name of the calendar pub name: String, - /// Optional description of the calendar pub description: Option, - - /// List of events in the calendar - // For now, events are embedded. If they become separate models, this would be Vec<[IDType]>. + /// List of event IDs in the calendar pub events: Vec, + /// ID of the user who owns this calendar + pub owner_id: Option, + /// Whether this calendar is public + pub is_public: bool, + /// Color theme for the calendar (hex color code) + pub color: Option, } impl Calendar { @@ -198,6 +288,9 @@ impl Calendar { name: name.to_string(), description: None, events: Vec::new(), + owner_id: None, + is_public: false, + color: Some("#4285F4".to_string()), // Default blue color } } @@ -213,6 +306,24 @@ impl Calendar { self } + /// Sets the owner of the calendar + pub fn owner_id(mut self, user_id: u32) -> Self { + self.owner_id = Some(user_id); + self + } + + /// Sets whether the calendar is public + pub fn is_public(mut self, is_public: bool) -> Self { + self.is_public = is_public; + self + } + + /// Sets the color for the calendar + pub fn color(mut self, color: impl ToString) -> Self { + self.color = Some(color.to_string()); + self + } + /// Adds an event to the calendar pub fn add_event(mut self, event_id: i64) -> Self { // Prevent duplicate events by id diff --git a/heromodels/src/models/calendar/mod.rs b/heromodels/src/models/calendar/mod.rs index 35fae98..ec9f694 100644 --- a/heromodels/src/models/calendar/mod.rs +++ b/heromodels/src/models/calendar/mod.rs @@ -2,6 +2,6 @@ pub mod calendar; pub mod rhai; -// Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs -pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus}; +// Re-export Calendar, Event, Attendee, AttendanceStatus, and EventStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs +pub use self::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus}; pub use rhai::register_rhai_engine_functions as register_calendar_rhai_module; From 20c075ec51e6cf3366ea3038528f42cd1c92fd55 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 3 Jun 2025 15:32:28 +0300 Subject: [PATCH 4/8] feat: Add JSON serialization/deserialization to Event struct - Added `to_json` method to serialize Event to JSON string. - Added `from_json` method to deserialize Event from JSON string. - This allows for easier data exchange and persistence. --- heromodels/src/models/calendar/calendar.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index b93b2b3..13732f5 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -101,6 +101,16 @@ pub struct Event { } impl Event { + /// Converts the event to a JSON string + pub fn to_json(&self) -> Result { + serde_json::to_string(self) + } + + /// Creates an event from a JSON string + pub fn from_json(json: &str) -> Result { + serde_json::from_str(json) + } + /// Creates a new event with auto-generated ID pub fn new(title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { Self { From 970299b1a4d0c8440038ca587a3fe46c91ffba5a Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 10 Jun 2025 15:41:49 +0300 Subject: [PATCH 5/8] feat: Add reminder functionality to ContractSigner model - Added `last_reminder_mail_sent_at` field to track reminder timestamps. - Implemented `can_send_reminder` to check if a reminder can be sent based on a 30-minute cooldown. - Added `reminder_cooldown_remaining` to get remaining cooldown time. - Added `mark_reminder_sent` to update reminder timestamp. - Added example demonstrating reminder functionality in `legal_contract_example.rs`. - Added tests for reminder functionality in `test_reminder_functionality.rs`. - Updated Rhai scripts to include reminder-related functions and tests. - Improved formatting and clarity in example code. --- heromodels/examples/legal_contract_example.rs | 56 +- heromodels/examples/legal_rhai/legal.rhai | 32 +- .../examples/test_reminder_functionality.rs | 108 +++ heromodels/src/models/legal/contract.rs | 66 +- heromodels/src/models/legal/rhai.rs | 738 ++++++++++++++---- specs/models/legal/contract.v | 1 + 6 files changed, 828 insertions(+), 173 deletions(-) create mode 100644 heromodels/examples/test_reminder_functionality.rs diff --git a/heromodels/examples/legal_contract_example.rs b/heromodels/examples/legal_contract_example.rs index cc0fe44..7dcd822 100644 --- a/heromodels/examples/legal_contract_example.rs +++ b/heromodels/examples/legal_contract_example.rs @@ -70,7 +70,7 @@ fn main() { .add_signer(signer2.clone()) .add_revision(revision1.clone()) .add_revision(revision2.clone()); - + // The `#[model]` derive handles `created_at` and `updated_at` in `base_data`. // `base_data.touch()` might be called internally by setters or needs explicit call if fields are set directly. // For builder pattern, the final state of `base_data.updated_at` reflects the time of the last builder call if `touch()` is implicit. @@ -87,7 +87,7 @@ fn main() { println!("\n--- Contract Details After Signing ---"); println!("{:#?}", contract); - + println!("\n--- Accessing Specific Fields ---"); println!("Contract Title: {}", contract.title); println!("Contract Status: {:?}", contract.status); @@ -97,7 +97,10 @@ fn main() { println!("Updated At (timestamp): {}", contract.base_data.modified_at); // From BaseModelData if let Some(first_signer_details) = contract.signers.first() { - println!("\nFirst Signer: {} ({})", first_signer_details.name, first_signer_details.email); + println!( + "\nFirst Signer: {} ({})", + first_signer_details.name, first_signer_details.email + ); println!(" Status: {:?}", first_signer_details.status); if let Some(signed_time) = first_signer_details.signed_at { println!(" Signed At: {}", signed_time); @@ -110,6 +113,51 @@ fn main() { println!(" Created By: {}", latest_rev.created_by); println!(" Revision Created At: {}", latest_rev.created_at); } - + + // Demonstrate reminder functionality + println!("\n--- Reminder Functionality Demo ---"); + let current_time = current_timestamp_secs(); + + // Check if we can send reminders to signers + for (i, signer) in contract.signers.iter().enumerate() { + println!("\nSigner {}: {} ({})", i + 1, signer.name, signer.email); + println!(" Status: {:?}", signer.status); + + if signer.last_reminder_mail_sent_at.is_none() { + println!(" Last reminder: Never sent"); + } else { + println!( + " Last reminder: {}", + signer.last_reminder_mail_sent_at.unwrap() + ); + } + + let can_send = signer.can_send_reminder(current_time); + println!(" Can send reminder now: {}", can_send); + + if let Some(remaining) = signer.reminder_cooldown_remaining(current_time) { + println!(" Cooldown remaining: {} seconds", remaining); + } else { + println!(" No cooldown active"); + } + } + + // Simulate sending a reminder to the first signer + if let Some(first_signer) = contract.signers.get_mut(0) { + if first_signer.can_send_reminder(current_time) { + println!("\nSimulating reminder sent to: {}", first_signer.name); + first_signer.mark_reminder_sent(current_time); + println!( + " Reminder timestamp updated to: {}", + first_signer.last_reminder_mail_sent_at.unwrap() + ); + + // Check cooldown after sending + if let Some(remaining) = first_signer.reminder_cooldown_remaining(current_time) { + println!(" New cooldown: {} seconds (30 minutes)", remaining); + } + } + } + println!("\nLegal Contract Model demonstration complete."); } diff --git a/heromodels/examples/legal_rhai/legal.rhai b/heromodels/examples/legal_rhai/legal.rhai index 11e494a..e553e74 100644 --- a/heromodels/examples/legal_rhai/legal.rhai +++ b/heromodels/examples/legal_rhai/legal.rhai @@ -39,14 +39,17 @@ let signer1 = new_contract_signer(signer1_id, "Alice Wonderland", "alice@example print(`Signer 1 ID: ${signer1.id}, Name: ${signer1.name}, Email: ${signer1.email}`); print(`Signer 1 Status: ${signer1.status}, Comments: ${format_optional_string(signer1.comments, "N/A")}`); print(`Signer 1 Signed At: ${format_optional_int(signer1.signed_at, "Not signed")}`); +print(`Signer 1 Last Reminder: ${format_optional_int(signer1.last_reminder_mail_sent_at, "Never sent")}`); let signer2_id = "signer-uuid-002"; let signer2 = new_contract_signer(signer2_id, "Bob The Builder", "bob@example.com") .status(SignerStatusConstants::Signed) .signed_at(1678886400) // Example timestamp - .comments("Bob has already signed."); + .comments("Bob has already signed.") + .last_reminder_mail_sent_at(1678880000); // Example reminder timestamp print(`Signer 2 ID: ${signer2.id}, Name: ${signer2.name}, Status: ${signer2.status}, Signed At: ${format_optional_int(signer2.signed_at, "N/A")}`); +print(`Signer 2 Last Reminder: ${format_optional_int(signer2.last_reminder_mail_sent_at, "Never sent")}`); // --- Test ContractRevision Model --- print("\n--- Testing ContractRevision Model ---"); @@ -116,4 +119,31 @@ print("Updated Contract saved."); let final_retrieved_contract = get_contract_by_id(contract1_base_id); print(`Final Retrieved Contract - Status: ${final_retrieved_contract.status}, Description: '${final_retrieved_contract.description}'`); +// --- Test Reminder Functionality --- +print("\n--- Testing Reminder Functionality ---"); +let current_time = 1678900000; // Example current timestamp + +// Test reminder functionality on signers +if final_retrieved_contract.signers.len() > 0 { + let test_signer = final_retrieved_contract.signers[0]; + print(`Testing reminder for signer: ${test_signer.name}`); + + let can_send = can_send_reminder(test_signer, current_time); + print(`Can send reminder: ${can_send}`); + + let cooldown_remaining = reminder_cooldown_remaining(test_signer, current_time); + print(`Cooldown remaining: ${format_optional_int(cooldown_remaining, "No cooldown")}`); + + // Simulate sending a reminder + if can_send { + print("Simulating reminder sent..."); + mark_reminder_sent(test_signer, current_time); + print("Reminder timestamp updated"); + + // Check cooldown after sending + let new_cooldown = reminder_cooldown_remaining(test_signer, current_time); + print(`New cooldown: ${format_optional_int(new_cooldown, "No cooldown")} seconds`); + } +} + print("\nLegal Rhai example script finished."); diff --git a/heromodels/examples/test_reminder_functionality.rs b/heromodels/examples/test_reminder_functionality.rs new file mode 100644 index 0000000..b4954fd --- /dev/null +++ b/heromodels/examples/test_reminder_functionality.rs @@ -0,0 +1,108 @@ +use heromodels::models::legal::{ContractSigner, SignerStatus}; + +// Helper function to get current timestamp +fn current_timestamp_secs() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn main() { + println!("Testing ContractSigner Reminder Functionality"); + println!("==============================================\n"); + + // Test 1: Create a new signer (should have no reminder timestamp) + println!("Test 1: New signer creation"); + let mut signer = ContractSigner::new( + "test-signer-001".to_string(), + "Test User".to_string(), + "test@example.com".to_string(), + ); + + println!(" Signer created: {}", signer.name); + println!(" Last reminder: {:?}", signer.last_reminder_mail_sent_at); + assert_eq!(signer.last_reminder_mail_sent_at, None); + println!(" ✓ New signer has no reminder timestamp\n"); + + // Test 2: Check if reminder can be sent (should be true for new signer) + println!("Test 2: Can send reminder check"); + let current_time = current_timestamp_secs(); + let can_send = signer.can_send_reminder(current_time); + println!(" Can send reminder: {}", can_send); + assert!(can_send); + println!(" ✓ New signer can receive reminders\n"); + + // Test 3: Check cooldown remaining (should be None for new signer) + println!("Test 3: Cooldown remaining check"); + let cooldown = signer.reminder_cooldown_remaining(current_time); + println!(" Cooldown remaining: {:?}", cooldown); + assert_eq!(cooldown, None); + println!(" ✓ New signer has no cooldown\n"); + + // Test 4: Mark reminder as sent + println!("Test 4: Mark reminder as sent"); + signer.mark_reminder_sent(current_time); + println!(" Reminder marked as sent at: {}", current_time); + println!(" Last reminder timestamp: {:?}", signer.last_reminder_mail_sent_at); + assert_eq!(signer.last_reminder_mail_sent_at, Some(current_time)); + println!(" ✓ Reminder timestamp updated correctly\n"); + + // Test 5: Check if reminder can be sent immediately after (should be false) + println!("Test 5: Immediate retry check"); + let can_send_again = signer.can_send_reminder(current_time); + println!(" Can send reminder immediately: {}", can_send_again); + assert!(!can_send_again); + println!(" ✓ Cannot send reminder immediately after sending\n"); + + // Test 6: Check cooldown remaining (should be 30 minutes) + println!("Test 6: Cooldown after sending"); + let cooldown_after = signer.reminder_cooldown_remaining(current_time); + println!(" Cooldown remaining: {:?} seconds", cooldown_after); + assert_eq!(cooldown_after, Some(30 * 60)); // 30 minutes = 1800 seconds + println!(" ✓ Cooldown is exactly 30 minutes\n"); + + // Test 7: Test after cooldown period + println!("Test 7: After cooldown period"); + let future_time = current_time + (31 * 60); // 31 minutes later + let can_send_later = signer.can_send_reminder(future_time); + let cooldown_later = signer.reminder_cooldown_remaining(future_time); + println!(" Time: {} (31 minutes later)", future_time); + println!(" Can send reminder: {}", can_send_later); + println!(" Cooldown remaining: {:?}", cooldown_later); + assert!(can_send_later); + assert_eq!(cooldown_later, None); + println!(" ✓ Can send reminder after cooldown period\n"); + + // Test 8: Test builder pattern with reminder timestamp + println!("Test 8: Builder pattern with reminder timestamp"); + let signer_with_reminder = ContractSigner::new( + "test-signer-002".to_string(), + "Another User".to_string(), + "another@example.com".to_string(), + ) + .status(SignerStatus::Pending) + .last_reminder_mail_sent_at(current_time - (20 * 60)) // 20 minutes ago + .comments("Test signer with reminder"); + + println!(" Signer: {}", signer_with_reminder.name); + println!(" Last reminder: {:?}", signer_with_reminder.last_reminder_mail_sent_at); + println!(" Can send reminder: {}", signer_with_reminder.can_send_reminder(current_time)); + + let remaining = signer_with_reminder.reminder_cooldown_remaining(current_time); + println!(" Cooldown remaining: {:?} seconds", remaining); + assert_eq!(remaining, Some(10 * 60)); // 10 minutes remaining + println!(" ✓ Builder pattern works correctly\n"); + + // Test 9: Test clear reminder timestamp + println!("Test 9: Clear reminder timestamp"); + let cleared_signer = signer_with_reminder.clear_last_reminder_mail_sent_at(); + println!(" Last reminder after clear: {:?}", cleared_signer.last_reminder_mail_sent_at); + println!(" Can send reminder: {}", cleared_signer.can_send_reminder(current_time)); + assert_eq!(cleared_signer.last_reminder_mail_sent_at, None); + assert!(cleared_signer.can_send_reminder(current_time)); + println!(" ✓ Clear reminder timestamp works correctly\n"); + + println!("All tests passed! ✅"); + println!("ContractSigner reminder functionality is working correctly."); +} diff --git a/heromodels/src/models/legal/contract.rs b/heromodels/src/models/legal/contract.rs index 4e871fc..c92acd0 100644 --- a/heromodels/src/models/legal/contract.rs +++ b/heromodels/src/models/legal/contract.rs @@ -1,7 +1,7 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; -use std::fmt; use serde::{Deserialize, Serialize}; +use std::fmt; // --- Enums --- @@ -86,6 +86,7 @@ pub struct ContractSigner { pub status: SignerStatus, pub signed_at: Option, // Timestamp pub comments: Option, + pub last_reminder_mail_sent_at: Option, // Unix timestamp of last reminder sent } impl ContractSigner { @@ -97,6 +98,7 @@ impl ContractSigner { status: SignerStatus::default(), signed_at: None, comments: None, + last_reminder_mail_sent_at: None, } } @@ -109,7 +111,7 @@ impl ContractSigner { self.signed_at = Some(signed_at); self } - + pub fn clear_signed_at(mut self) -> Self { self.signed_at = None; self @@ -124,6 +126,48 @@ impl ContractSigner { self.comments = None; self } + + pub fn last_reminder_mail_sent_at(mut self, timestamp: u64) -> Self { + self.last_reminder_mail_sent_at = Some(timestamp); + self + } + + pub fn clear_last_reminder_mail_sent_at(mut self) -> Self { + self.last_reminder_mail_sent_at = None; + self + } + + /// Helper method to check if a reminder can be sent (30-minute rate limiting) + pub fn can_send_reminder(&self, current_timestamp: u64) -> bool { + match self.last_reminder_mail_sent_at { + None => true, // No reminder sent yet + Some(last_sent) => { + let thirty_minutes_in_seconds = 30 * 60; // 30 minutes = 1800 seconds + current_timestamp >= last_sent + thirty_minutes_in_seconds + } + } + } + + /// Helper method to get remaining cooldown time in seconds + pub fn reminder_cooldown_remaining(&self, current_timestamp: u64) -> Option { + match self.last_reminder_mail_sent_at { + None => None, // No cooldown if no reminder sent yet + Some(last_sent) => { + let thirty_minutes_in_seconds = 30 * 60; // 30 minutes = 1800 seconds + let cooldown_end = last_sent + thirty_minutes_in_seconds; + if current_timestamp < cooldown_end { + Some(cooldown_end - current_timestamp) + } else { + None // Cooldown has expired + } + } + } + } + + /// Helper method to update the reminder timestamp to current time + pub fn mark_reminder_sent(&mut self, current_timestamp: u64) { + self.last_reminder_mail_sent_at = Some(current_timestamp); + } } // --- Main Contract Model --- @@ -139,21 +183,21 @@ pub struct Contract { pub title: String, pub description: String, - + #[index] pub contract_type: String, - + #[index] pub status: crate::models::ContractStatus, // Use re-exported path for #[model] macro - + pub created_by: String, pub terms_and_conditions: String, - + pub start_date: Option, pub end_date: Option, pub renewal_period_days: Option, pub next_renewal_date: Option, - + pub signers: Vec, pub revisions: Vec, pub current_version: u32, @@ -217,7 +261,7 @@ impl Contract { self.start_date = Some(start_date); self } - + pub fn clear_start_date(mut self) -> Self { self.start_date = None; self @@ -257,7 +301,7 @@ impl Contract { self.signers.push(signer); self } - + pub fn signers(mut self, signers: Vec) -> Self { self.signers = signers; self @@ -272,7 +316,7 @@ impl Contract { self.revisions = revisions; self } - + pub fn current_version(mut self, version: u32) -> Self { self.current_version = version; self @@ -287,7 +331,7 @@ impl Contract { self.last_signed_date = None; self } - + // Example methods for state changes pub fn set_status(&mut self, status: crate::models::ContractStatus) { self.status = status; diff --git a/heromodels/src/models/legal/rhai.rs b/heromodels/src/models/legal/rhai.rs index d374d54..0e89789 100644 --- a/heromodels/src/models/legal/rhai.rs +++ b/heromodels/src/models/legal/rhai.rs @@ -1,51 +1,60 @@ -use rhai::{ - Dynamic, Engine, EvalAltResult, NativeCallContext, Position, Module, Array, -}; +use rhai::{Array, Dynamic, Engine, EvalAltResult, Module, NativeCallContext, Position}; use std::sync::Arc; use crate::db::hero::OurDB; // Updated path based on compiler suggestion // use heromodels_core::BaseModelData; // Removed as fields are accessed via contract.base_data directly -use crate::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; +use crate::models::legal::{ + Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus, +}; use crate::db::Collection; // Import the Collection trait // --- Helper Functions for ID and Timestamp Conversion --- -fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { +fn i64_to_u32( + val: i64, + context_pos: Position, + field_name: &str, + object_name: &str, +) -> Result> { val.try_into().map_err(|_e| { Box::new(EvalAltResult::ErrorArithmetic( format!( "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u32", - field_name, - object_name, - val + field_name, object_name, val ), context_pos, )) }) } -fn i64_to_u64(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { +fn i64_to_u64( + val: i64, + context_pos: Position, + field_name: &str, + object_name: &str, +) -> Result> { val.try_into().map_err(|_e| { Box::new(EvalAltResult::ErrorArithmetic( format!( "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u64", - field_name, - object_name, - val + field_name, object_name, val ), context_pos, )) }) } -fn i64_to_i32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { +fn i64_to_i32( + val: i64, + context_pos: Position, + field_name: &str, + object_name: &str, +) -> Result> { val.try_into().map_err(|_e| { Box::new(EvalAltResult::ErrorArithmetic( format!( "Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to i32", - field_name, - object_name, - val + field_name, object_name, val ), context_pos, )) @@ -73,193 +82,608 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc) { engine.register_static_module("SignerStatusConstants", signer_status_module.into()); engine.register_type_with_name::("SignerStatus"); // Expose the type itself - // --- ContractRevision --- + // --- ContractRevision --- engine.register_type_with_name::("ContractRevision"); engine.register_fn( "new_contract_revision", - move |context: NativeCallContext, version_i64: i64, content: String, created_at_i64: i64, created_by: String| -> Result> { - let version = i64_to_u32(version_i64, context.position(), "version", "new_contract_revision")?; - let created_at = i64_to_u64(created_at_i64, context.position(), "created_at", "new_contract_revision")?; - Ok(ContractRevision::new(version, content, created_at, created_by)) - } + move |context: NativeCallContext, + version_i64: i64, + content: String, + created_at_i64: i64, + created_by: String| + -> Result> { + let version = i64_to_u32( + version_i64, + context.position(), + "version", + "new_contract_revision", + )?; + let created_at = i64_to_u64( + created_at_i64, + context.position(), + "created_at", + "new_contract_revision", + )?; + Ok(ContractRevision::new( + version, content, created_at, created_by, + )) + }, + ); + engine.register_fn( + "comments", + |mut revision: ContractRevision, comments: String| -> ContractRevision { + revision.comments = Some(comments); + revision + }, + ); + engine.register_get( + "version", + |revision: &mut ContractRevision| -> Result> { + Ok(revision.version as i64) + }, + ); + engine.register_get( + "content", + |revision: &mut ContractRevision| -> Result> { + Ok(revision.content.clone()) + }, + ); + engine.register_get( + "created_at", + |revision: &mut ContractRevision| -> Result> { + Ok(revision.created_at as i64) + }, + ); + engine.register_get( + "created_by", + |revision: &mut ContractRevision| -> Result> { + Ok(revision.created_by.clone()) + }, + ); + engine.register_get( + "comments", + |revision: &mut ContractRevision| -> Result> { + Ok(revision + .comments + .clone() + .map_or(Dynamic::UNIT, Dynamic::from)) + }, ); - engine.register_fn("comments", |mut revision: ContractRevision, comments: String| -> ContractRevision { - revision.comments = Some(comments); - revision - }); - engine.register_get("version", |revision: &mut ContractRevision| -> Result> { Ok(revision.version as i64) }); - engine.register_get("content", |revision: &mut ContractRevision| -> Result> { Ok(revision.content.clone()) }); - engine.register_get("created_at", |revision: &mut ContractRevision| -> Result> { Ok(revision.created_at as i64) }); - engine.register_get("created_by", |revision: &mut ContractRevision| -> Result> { Ok(revision.created_by.clone()) }); - engine.register_get("comments", |revision: &mut ContractRevision| -> Result> { - Ok(revision.comments.clone().map_or(Dynamic::UNIT, Dynamic::from)) - }); - // --- ContractSigner --- + // --- ContractSigner --- engine.register_type_with_name::("ContractSigner"); engine.register_fn( "new_contract_signer", |id: String, name: String, email: String| -> ContractSigner { ContractSigner::new(id, name, email) - } + }, + ); + engine.register_fn( + "status", + |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) }, + ); + engine.register_fn( + "signed_at", + |context: NativeCallContext, + signer: ContractSigner, + signed_at_i64: i64| + -> Result> { + let signed_at_u64 = i64_to_u64( + signed_at_i64, + context.position(), + "signed_at", + "ContractSigner.signed_at", + )?; + Ok(signer.signed_at(signed_at_u64)) + }, + ); + engine.register_fn( + "clear_signed_at", + |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() }, + ); + engine.register_fn( + "comments", + |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) }, + ); + engine.register_fn( + "clear_comments", + |signer: ContractSigner| -> ContractSigner { signer.clear_comments() }, ); - engine.register_fn("status", |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) }); - engine.register_fn("signed_at", |context: NativeCallContext, signer: ContractSigner, signed_at_i64: i64| -> Result> { - let signed_at_u64 = i64_to_u64(signed_at_i64, context.position(), "signed_at", "ContractSigner.signed_at")?; - Ok(signer.signed_at(signed_at_u64)) - }); - engine.register_fn("clear_signed_at", |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() }); - engine.register_fn("comments", |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) }); - engine.register_fn("clear_comments", |signer: ContractSigner| -> ContractSigner { signer.clear_comments() }); - engine.register_get("id", |signer: &mut ContractSigner| -> Result> { Ok(signer.id.clone()) }); - engine.register_get("name", |signer: &mut ContractSigner| -> Result> { Ok(signer.name.clone()) }); - engine.register_get("email", |signer: &mut ContractSigner| -> Result> { Ok(signer.email.clone()) }); - engine.register_get("status", |signer: &mut ContractSigner| -> Result> { Ok(signer.status.clone()) }); - engine.register_get("signed_at_ts", |signer: &mut ContractSigner| -> Result> { - Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) - }); - engine.register_get("comments", |signer: &mut ContractSigner| -> Result> { - Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from)) - }); - engine.register_get("signed_at", |signer: &mut ContractSigner| -> Result> { - Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts))) - }); + // Reminder functionality + engine.register_fn( + "last_reminder_mail_sent_at", + |context: NativeCallContext, + signer: ContractSigner, + timestamp_i64: i64| + -> Result> { + let timestamp_u64 = i64_to_u64( + timestamp_i64, + context.position(), + "timestamp", + "ContractSigner.last_reminder_mail_sent_at", + )?; + Ok(signer.last_reminder_mail_sent_at(timestamp_u64)) + }, + ); + engine.register_fn( + "clear_last_reminder_mail_sent_at", + |signer: ContractSigner| -> ContractSigner { signer.clear_last_reminder_mail_sent_at() }, + ); - // --- Contract --- + // Helper methods for reminder logic + engine.register_fn( + "can_send_reminder", + |context: NativeCallContext, + signer: &mut ContractSigner, + current_timestamp_i64: i64| + -> Result> { + let current_timestamp = i64_to_u64( + current_timestamp_i64, + context.position(), + "current_timestamp", + "ContractSigner.can_send_reminder", + )?; + Ok(signer.can_send_reminder(current_timestamp)) + }, + ); + + engine.register_fn( + "reminder_cooldown_remaining", + |context: NativeCallContext, + signer: &mut ContractSigner, + current_timestamp_i64: i64| + -> Result> { + let current_timestamp = i64_to_u64( + current_timestamp_i64, + context.position(), + "current_timestamp", + "ContractSigner.reminder_cooldown_remaining", + )?; + Ok(signer + .reminder_cooldown_remaining(current_timestamp) + .map_or(Dynamic::UNIT, |remaining| Dynamic::from(remaining as i64))) + }, + ); + + engine.register_fn( + "mark_reminder_sent", + |context: NativeCallContext, + signer: &mut ContractSigner, + current_timestamp_i64: i64| + -> Result<(), Box> { + let current_timestamp = i64_to_u64( + current_timestamp_i64, + context.position(), + "current_timestamp", + "ContractSigner.mark_reminder_sent", + )?; + signer.mark_reminder_sent(current_timestamp); + Ok(()) + }, + ); + + engine.register_get( + "id", + |signer: &mut ContractSigner| -> Result> { + Ok(signer.id.clone()) + }, + ); + engine.register_get( + "name", + |signer: &mut ContractSigner| -> Result> { + Ok(signer.name.clone()) + }, + ); + engine.register_get( + "email", + |signer: &mut ContractSigner| -> Result> { + Ok(signer.email.clone()) + }, + ); + engine.register_get( + "status", + |signer: &mut ContractSigner| -> Result> { + Ok(signer.status.clone()) + }, + ); + engine.register_get( + "signed_at_ts", + |signer: &mut ContractSigner| -> Result> { + Ok(signer + .signed_at + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); + engine.register_get( + "comments", + |signer: &mut ContractSigner| -> Result> { + Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from)) + }, + ); + engine.register_get( + "signed_at", + |signer: &mut ContractSigner| -> Result> { + Ok(signer + .signed_at + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts))) + }, + ); + engine.register_get( + "last_reminder_mail_sent_at", + |signer: &mut ContractSigner| -> Result> { + Ok(signer + .last_reminder_mail_sent_at + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); + + // --- Contract --- engine.register_type_with_name::("Contract"); engine.register_fn( "new_contract", - move |context: NativeCallContext, base_id_i64: i64, contract_id: String| -> Result> { + move |context: NativeCallContext, + base_id_i64: i64, + contract_id: String| + -> Result> { let base_id = i64_to_u32(base_id_i64, context.position(), "base_id", "new_contract")?; Ok(Contract::new(base_id, contract_id)) - } + }, ); // Builder methods - engine.register_fn("title", |contract: Contract, title: String| -> Contract { contract.title(title) }); - engine.register_fn("description", |contract: Contract, description: String| -> Contract { contract.description(description) }); - engine.register_fn("contract_type", |contract: Contract, contract_type: String| -> Contract { contract.contract_type(contract_type) }); - engine.register_fn("status", |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) }); - engine.register_fn("created_by", |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) }); - engine.register_fn("terms_and_conditions", |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) }); - - engine.register_fn("start_date", |context: NativeCallContext, contract: Contract, start_date_i64: i64| -> Result> { - let start_date_u64 = i64_to_u64(start_date_i64, context.position(), "start_date", "Contract.start_date")?; - Ok(contract.start_date(start_date_u64)) + engine.register_fn("title", |contract: Contract, title: String| -> Contract { + contract.title(title) }); - engine.register_fn("clear_start_date", |contract: Contract| -> Contract { contract.clear_start_date() }); + engine.register_fn( + "description", + |contract: Contract, description: String| -> Contract { contract.description(description) }, + ); + engine.register_fn( + "contract_type", + |contract: Contract, contract_type: String| -> Contract { + contract.contract_type(contract_type) + }, + ); + engine.register_fn( + "status", + |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) }, + ); + engine.register_fn( + "created_by", + |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) }, + ); + engine.register_fn( + "terms_and_conditions", + |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) }, + ); - engine.register_fn("end_date", |context: NativeCallContext, contract: Contract, end_date_i64: i64| -> Result> { - let end_date_u64 = i64_to_u64(end_date_i64, context.position(), "end_date", "Contract.end_date")?; - Ok(contract.end_date(end_date_u64)) - }); - engine.register_fn("clear_end_date", |contract: Contract| -> Contract { contract.clear_end_date() }); - - engine.register_fn("renewal_period_days", |context: NativeCallContext, contract: Contract, days_i64: i64| -> Result> { - let days_i32 = i64_to_i32(days_i64, context.position(), "renewal_period_days", "Contract.renewal_period_days")?; - Ok(contract.renewal_period_days(days_i32)) - }); - engine.register_fn("clear_renewal_period_days", |contract: Contract| -> Contract { contract.clear_renewal_period_days() }); - - engine.register_fn("next_renewal_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result> { - let date_u64 = i64_to_u64(date_i64, context.position(), "next_renewal_date", "Contract.next_renewal_date")?; - Ok(contract.next_renewal_date(date_u64)) - }); - engine.register_fn("clear_next_renewal_date", |contract: Contract| -> Contract { contract.clear_next_renewal_date() }); - - engine.register_fn("add_signer", |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) }); - engine.register_fn("signers", |contract: Contract, signers_array: Array| -> Contract { - let signers_vec = signers_array.into_iter().filter_map(|s| s.try_cast::()).collect(); - contract.signers(signers_vec) + engine.register_fn( + "start_date", + |context: NativeCallContext, + contract: Contract, + start_date_i64: i64| + -> Result> { + let start_date_u64 = i64_to_u64( + start_date_i64, + context.position(), + "start_date", + "Contract.start_date", + )?; + Ok(contract.start_date(start_date_u64)) + }, + ); + engine.register_fn("clear_start_date", |contract: Contract| -> Contract { + contract.clear_start_date() }); - engine.register_fn("add_revision", |contract: Contract, revision: ContractRevision| -> Contract { contract.add_revision(revision) }); - engine.register_fn("revisions", |contract: Contract, revisions_array: Array| -> Contract { - let revisions_vec = revisions_array.into_iter().filter_map(|r| r.try_cast::()).collect(); - contract.revisions(revisions_vec) + engine.register_fn( + "end_date", + |context: NativeCallContext, + contract: Contract, + end_date_i64: i64| + -> Result> { + let end_date_u64 = i64_to_u64( + end_date_i64, + context.position(), + "end_date", + "Contract.end_date", + )?; + Ok(contract.end_date(end_date_u64)) + }, + ); + engine.register_fn("clear_end_date", |contract: Contract| -> Contract { + contract.clear_end_date() }); - engine.register_fn("current_version", |context: NativeCallContext, contract: Contract, version_i64: i64| -> Result> { - let version_u32 = i64_to_u32(version_i64, context.position(), "current_version", "Contract.current_version")?; - Ok(contract.current_version(version_u32)) - }); + engine.register_fn( + "renewal_period_days", + |context: NativeCallContext, + contract: Contract, + days_i64: i64| + -> Result> { + let days_i32 = i64_to_i32( + days_i64, + context.position(), + "renewal_period_days", + "Contract.renewal_period_days", + )?; + Ok(contract.renewal_period_days(days_i32)) + }, + ); + engine.register_fn( + "clear_renewal_period_days", + |contract: Contract| -> Contract { contract.clear_renewal_period_days() }, + ); - engine.register_fn("last_signed_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result> { - let date_u64 = i64_to_u64(date_i64, context.position(), "last_signed_date", "Contract.last_signed_date")?; - Ok(contract.last_signed_date(date_u64)) + engine.register_fn( + "next_renewal_date", + |context: NativeCallContext, + contract: Contract, + date_i64: i64| + -> Result> { + let date_u64 = i64_to_u64( + date_i64, + context.position(), + "next_renewal_date", + "Contract.next_renewal_date", + )?; + Ok(contract.next_renewal_date(date_u64)) + }, + ); + engine.register_fn( + "clear_next_renewal_date", + |contract: Contract| -> Contract { contract.clear_next_renewal_date() }, + ); + + engine.register_fn( + "add_signer", + |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) }, + ); + engine.register_fn( + "signers", + |contract: Contract, signers_array: Array| -> Contract { + let signers_vec = signers_array + .into_iter() + .filter_map(|s| s.try_cast::()) + .collect(); + contract.signers(signers_vec) + }, + ); + + engine.register_fn( + "add_revision", + |contract: Contract, revision: ContractRevision| -> Contract { + contract.add_revision(revision) + }, + ); + engine.register_fn( + "revisions", + |contract: Contract, revisions_array: Array| -> Contract { + let revisions_vec = revisions_array + .into_iter() + .filter_map(|r| r.try_cast::()) + .collect(); + contract.revisions(revisions_vec) + }, + ); + + engine.register_fn( + "current_version", + |context: NativeCallContext, + contract: Contract, + version_i64: i64| + -> Result> { + let version_u32 = i64_to_u32( + version_i64, + context.position(), + "current_version", + "Contract.current_version", + )?; + Ok(contract.current_version(version_u32)) + }, + ); + + engine.register_fn( + "last_signed_date", + |context: NativeCallContext, + contract: Contract, + date_i64: i64| + -> Result> { + let date_u64 = i64_to_u64( + date_i64, + context.position(), + "last_signed_date", + "Contract.last_signed_date", + )?; + Ok(contract.last_signed_date(date_u64)) + }, + ); + engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { + contract.clear_last_signed_date() }); - engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { contract.clear_last_signed_date() }); // Getters for Contract - engine.register_get("id", |contract: &mut Contract| -> Result> { Ok(contract.base_data.id as i64) }); - engine.register_get("created_at_ts", |contract: &mut Contract| -> Result> { Ok(contract.base_data.created_at as i64) }); - engine.register_get("updated_at_ts", |contract: &mut Contract| -> Result> { Ok(contract.base_data.modified_at as i64) }); - engine.register_get("contract_id", |contract: &mut Contract| -> Result> { Ok(contract.contract_id.clone()) }); - engine.register_get("title", |contract: &mut Contract| -> Result> { Ok(contract.title.clone()) }); - engine.register_get("description", |contract: &mut Contract| -> Result> { Ok(contract.description.clone()) }); - engine.register_get("contract_type", |contract: &mut Contract| -> Result> { Ok(contract.contract_type.clone()) }); - engine.register_get("status", |contract: &mut Contract| -> Result> { Ok(contract.status.clone()) }); - engine.register_get("created_by", |contract: &mut Contract| -> Result> { Ok(contract.created_by.clone()) }); - engine.register_get("terms_and_conditions", |contract: &mut Contract| -> Result> { Ok(contract.terms_and_conditions.clone()) }); - - engine.register_get("start_date", |contract: &mut Contract| -> Result> { - Ok(contract.start_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) - }); - engine.register_get("end_date", |contract: &mut Contract| -> Result> { - Ok(contract.end_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) - }); - engine.register_get("renewal_period_days", |contract: &mut Contract| -> Result> { - Ok(contract.renewal_period_days.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64))) - }); - engine.register_get("next_renewal_date", |contract: &mut Contract| -> Result> { - Ok(contract.next_renewal_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) - }); - engine.register_get("last_signed_date", |contract: &mut Contract| -> Result> { - Ok(contract.last_signed_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) - }); + engine.register_get( + "id", + |contract: &mut Contract| -> Result> { + Ok(contract.base_data.id as i64) + }, + ); + engine.register_get( + "created_at_ts", + |contract: &mut Contract| -> Result> { + Ok(contract.base_data.created_at as i64) + }, + ); + engine.register_get( + "updated_at_ts", + |contract: &mut Contract| -> Result> { + Ok(contract.base_data.modified_at as i64) + }, + ); + engine.register_get( + "contract_id", + |contract: &mut Contract| -> Result> { + Ok(contract.contract_id.clone()) + }, + ); + engine.register_get( + "title", + |contract: &mut Contract| -> Result> { + Ok(contract.title.clone()) + }, + ); + engine.register_get( + "description", + |contract: &mut Contract| -> Result> { + Ok(contract.description.clone()) + }, + ); + engine.register_get( + "contract_type", + |contract: &mut Contract| -> Result> { + Ok(contract.contract_type.clone()) + }, + ); + engine.register_get( + "status", + |contract: &mut Contract| -> Result> { + Ok(contract.status.clone()) + }, + ); + engine.register_get( + "created_by", + |contract: &mut Contract| -> Result> { + Ok(contract.created_by.clone()) + }, + ); + engine.register_get( + "terms_and_conditions", + |contract: &mut Contract| -> Result> { + Ok(contract.terms_and_conditions.clone()) + }, + ); - engine.register_get("current_version", |contract: &mut Contract| -> Result> { Ok(contract.current_version as i64) }); + engine.register_get( + "start_date", + |contract: &mut Contract| -> Result> { + Ok(contract + .start_date + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); + engine.register_get( + "end_date", + |contract: &mut Contract| -> Result> { + Ok(contract + .end_date + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); + engine.register_get( + "renewal_period_days", + |contract: &mut Contract| -> Result> { + Ok(contract + .renewal_period_days + .map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64))) + }, + ); + engine.register_get( + "next_renewal_date", + |contract: &mut Contract| -> Result> { + Ok(contract + .next_renewal_date + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); + engine.register_get( + "last_signed_date", + |contract: &mut Contract| -> Result> { + Ok(contract + .last_signed_date + .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) + }, + ); - engine.register_get("signers", |contract: &mut Contract| -> Result> { - let rhai_array = contract.signers.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) - }); - engine.register_get("revisions", |contract: &mut Contract| -> Result> { - let rhai_array = contract.revisions.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) - }); + engine.register_get( + "current_version", + |contract: &mut Contract| -> Result> { + Ok(contract.current_version as i64) + }, + ); + + engine.register_get( + "signers", + |contract: &mut Contract| -> Result> { + let rhai_array = contract + .signers + .iter() + .cloned() + .map(Dynamic::from) + .collect::(); + Ok(rhai_array) + }, + ); + engine.register_get( + "revisions", + |contract: &mut Contract| -> Result> { + let rhai_array = contract + .revisions + .iter() + .cloned() + .map(Dynamic::from) + .collect::(); + Ok(rhai_array) + }, + ); // Method set_status - engine.register_fn("set_contract_status", |contract: &mut Contract, status: ContractStatus| { - contract.set_status(status); - }); + engine.register_fn( + "set_contract_status", + |contract: &mut Contract, status: ContractStatus| { + contract.set_status(status); + }, + ); // --- Database Interaction --- let captured_db_for_set = Arc::clone(&db); - engine.register_fn("set_contract", + engine.register_fn( + "set_contract", move |contract: Contract| -> Result<(), Box> { captured_db_for_set.set(&contract).map(|_| ()).map_err(|e| { Box::new(EvalAltResult::ErrorRuntime( - format!("Failed to set Contract (ID: {}): {}", contract.base_data.id, e).into(), + format!( + "Failed to set Contract (ID: {}): {}", + contract.base_data.id, e + ) + .into(), Position::NONE, )) }) - }); + }, + ); let captured_db_for_get = Arc::clone(&db); - engine.register_fn("get_contract_by_id", + engine.register_fn( + "get_contract_by_id", move |context: NativeCallContext, id_i64: i64| -> Result> { let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_contract_by_id")?; - - captured_db_for_get.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( - format!("Error getting Contract (ID: {}): {}", id_u32, e).into(), - Position::NONE, - )))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( - format!("Contract with ID {} not found", id_u32).into(), - Position::NONE, - ))) - }); + + captured_db_for_get + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Contract (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Contract with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); } diff --git a/specs/models/legal/contract.v b/specs/models/legal/contract.v index c4c4b71..469abe8 100644 --- a/specs/models/legal/contract.v +++ b/specs/models/legal/contract.v @@ -56,6 +56,7 @@ pub mut: status SignerStatus signed_at ourtime.OurTime // Optional in Rust, OurTime can be zero comments string // Optional in Rust, string can be empty + last_reminder_mail_sent_at ourtime.OurTime // Unix timestamp of last reminder sent } // SignerStatus defines the status of a contract signer From b9abfa50a9bdb82d80cc8918481fe2c732024c89 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Tue, 10 Jun 2025 16:01:43 +0300 Subject: [PATCH 6/8] feat: Remove unused `AttendanceStatus` enum - Remove the `AttendanceStatus` enum from the `calendar` module. --- heromodels/src/models/calendar/rhai.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/heromodels/src/models/calendar/rhai.rs b/heromodels/src/models/calendar/rhai.rs index b3415da..9dc1b77 100644 --- a/heromodels/src/models/calendar/rhai.rs +++ b/heromodels/src/models/calendar/rhai.rs @@ -1,7 +1,7 @@ use rhai::{Engine, EvalAltResult, ImmutableString, NativeCallContext}; use std::sync::Arc; -use super::calendar::{AttendanceStatus, Attendee, Calendar, Event}; +use super::calendar::{Attendee, Calendar, Event}; use crate::db::hero::OurDB; use adapter_macros::rhai_timestamp_helpers; use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method}; From f0a0dd6d73067dd0fd76f609daa80ff1e71bdb15 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Thu, 12 Jun 2025 14:30:58 +0300 Subject: [PATCH 7/8] feat: Add signature functionality to ContractSigner - Add `signature_data` field to `ContractSigner` to store base64 encoded signature image data. Allows for storing visual signatures alongside electronic ones. - Implement `sign` method for `ContractSigner` to handle signing with optional signature data and comments. Improves the flexibility and expressiveness of the signing process. - Add Rhai functions for signature management, including signing with/without data and clearing signature data. Extends the Rhai scripting capabilities for contract management. - Add comprehensive unit tests to cover the new signature functionality. Ensures correctness and robustness of the implementation. - Update examples to demonstrate the new signature functionality. Provides clear usage examples for developers. --- heromodels/examples/legal_contract_example.rs | 61 +++++++ heromodels/examples/legal_rhai/legal.rhai | 27 ++- .../examples/test_signature_functionality.rs | 163 ++++++++++++++++++ heromodels/src/models/legal/contract.rs | 33 ++++ heromodels/src/models/legal/rhai.rs | 38 ++++ specs/models/legal/contract.v | 1 + 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 heromodels/examples/test_signature_functionality.rs diff --git a/heromodels/examples/legal_contract_example.rs b/heromodels/examples/legal_contract_example.rs index 7dcd822..7ae6ce7 100644 --- a/heromodels/examples/legal_contract_example.rs +++ b/heromodels/examples/legal_contract_example.rs @@ -159,5 +159,66 @@ fn main() { } } + // Demonstrate signature functionality + println!("\n--- Signature Functionality Demo ---"); + + // Simulate signing with signature data + if let Some(signer_to_sign) = contract.signers.get_mut(1) { + println!("\nBefore signing:"); + println!( + " Signer: {} ({})", + signer_to_sign.name, signer_to_sign.email + ); + println!(" Status: {:?}", signer_to_sign.status); + println!(" Signed at: {:?}", signer_to_sign.signed_at); + println!(" Signature data: {:?}", signer_to_sign.signature_data); + + // Example base64 signature data (1x1 transparent PNG) + let signature_data = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string(); + + // Sign the contract with signature data + signer_to_sign.sign( + Some(signature_data.clone()), + Some("I agree to all terms and conditions.".to_string()), + ); + + println!("\nAfter signing:"); + println!(" Status: {:?}", signer_to_sign.status); + println!(" Signed at: {:?}", signer_to_sign.signed_at); + println!(" Comments: {:?}", signer_to_sign.comments); + println!( + " Signature data length: {} characters", + signer_to_sign + .signature_data + .as_ref() + .map_or(0, |s| s.len()) + ); + println!( + " Signature data preview: {}...", + signer_to_sign + .signature_data + .as_ref() + .map_or("None".to_string(), |s| s + .chars() + .take(50) + .collect::()) + ); + } + + // Demonstrate signing without signature data + if let Some(first_signer) = contract.signers.get_mut(0) { + println!("\nSigning without signature data:"); + println!(" Signer: {}", first_signer.name); + + first_signer.sign( + None, + Some("Signed electronically without visual signature.".to_string()), + ); + + println!(" Status after signing: {:?}", first_signer.status); + println!(" Signature data: {:?}", first_signer.signature_data); + println!(" Comments: {:?}", first_signer.comments); + } + println!("\nLegal Contract Model demonstration complete."); } diff --git a/heromodels/examples/legal_rhai/legal.rhai b/heromodels/examples/legal_rhai/legal.rhai index e553e74..800e397 100644 --- a/heromodels/examples/legal_rhai/legal.rhai +++ b/heromodels/examples/legal_rhai/legal.rhai @@ -40,16 +40,19 @@ print(`Signer 1 ID: ${signer1.id}, Name: ${signer1.name}, Email: ${signer1.email print(`Signer 1 Status: ${signer1.status}, Comments: ${format_optional_string(signer1.comments, "N/A")}`); print(`Signer 1 Signed At: ${format_optional_int(signer1.signed_at, "Not signed")}`); print(`Signer 1 Last Reminder: ${format_optional_int(signer1.last_reminder_mail_sent_at, "Never sent")}`); +print(`Signer 1 Signature Data: ${format_optional_string(signer1.signature_data, "No signature")}`); let signer2_id = "signer-uuid-002"; let signer2 = new_contract_signer(signer2_id, "Bob The Builder", "bob@example.com") .status(SignerStatusConstants::Signed) .signed_at(1678886400) // Example timestamp .comments("Bob has already signed.") - .last_reminder_mail_sent_at(1678880000); // Example reminder timestamp + .last_reminder_mail_sent_at(1678880000) // Example reminder timestamp + .signature_data("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="); // Example signature print(`Signer 2 ID: ${signer2.id}, Name: ${signer2.name}, Status: ${signer2.status}, Signed At: ${format_optional_int(signer2.signed_at, "N/A")}`); print(`Signer 2 Last Reminder: ${format_optional_int(signer2.last_reminder_mail_sent_at, "Never sent")}`); +print(`Signer 2 Signature Data Length: ${signer2.signature_data.len()} characters`); // --- Test ContractRevision Model --- print("\n--- Testing ContractRevision Model ---"); @@ -146,4 +149,26 @@ if final_retrieved_contract.signers.len() > 0 { } } +// --- Test Signature Functionality --- +print("\n--- Testing Signature Functionality ---"); + +// Test signing with signature data +let test_signer = new_contract_signer("test-signer-001", "Test Signer", "test@example.com"); +print(`Before signing: Status = ${test_signer.status}, Signature Data = ${format_optional_string(test_signer.signature_data, "None")}`); + +// Sign with signature data +sign(test_signer, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "I agree to the terms"); +print(`After signing: Status = ${test_signer.status}, Signature Data Length = ${test_signer.signature_data.len()}`); +print(`Comments: ${format_optional_string(test_signer.comments, "No comments")}`); + +// Test signing without signature data +let test_signer2 = new_contract_signer("test-signer-002", "Test Signer 2", "test2@example.com"); +sign_without_signature(test_signer2, "Electronic signature without visual data"); +print(`Signer 2 after signing: Status = ${test_signer2.status}, Signature Data = ${format_optional_string(test_signer2.signature_data, "None")}`); + +// Test simple signing +let test_signer3 = new_contract_signer("test-signer-003", "Test Signer 3", "test3@example.com"); +sign_simple(test_signer3); +print(`Signer 3 after simple signing: Status = ${test_signer3.status}`); + print("\nLegal Rhai example script finished."); diff --git a/heromodels/examples/test_signature_functionality.rs b/heromodels/examples/test_signature_functionality.rs new file mode 100644 index 0000000..49ef5aa --- /dev/null +++ b/heromodels/examples/test_signature_functionality.rs @@ -0,0 +1,163 @@ +use heromodels::models::legal::{ContractSigner, SignerStatus}; + +fn main() { + println!("Testing ContractSigner Signature Functionality"); + println!("==============================================\n"); + + // Test 1: Create a new signer (should have no signature data) + println!("Test 1: New signer creation"); + let mut signer = ContractSigner::new( + "test-signer-001".to_string(), + "Test User".to_string(), + "test@example.com".to_string(), + ); + + println!(" Signer created: {}", signer.name); + println!(" Status: {:?}", signer.status); + println!(" Signature data: {:?}", signer.signature_data); + assert_eq!(signer.signature_data, None); + assert_eq!(signer.status, SignerStatus::Pending); + println!(" ✓ New signer has no signature data and is pending\n"); + + // Test 2: Sign with signature data + println!("Test 2: Sign with signature data"); + let signature_data = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string(); + let comments = "I agree to all terms and conditions.".to_string(); + + signer.sign(Some(signature_data.clone()), Some(comments.clone())); + + println!(" Status after signing: {:?}", signer.status); + println!(" Signed at: {:?}", signer.signed_at); + println!(" Comments: {:?}", signer.comments); + println!(" Signature data length: {}", signer.signature_data.as_ref().unwrap().len()); + + assert_eq!(signer.status, SignerStatus::Signed); + assert!(signer.signed_at.is_some()); + assert_eq!(signer.signature_data, Some(signature_data)); + assert_eq!(signer.comments, Some(comments)); + println!(" ✓ Signing with signature data works correctly\n"); + + // Test 3: Sign without signature data + println!("Test 3: Sign without signature data"); + let mut signer2 = ContractSigner::new( + "test-signer-002".to_string(), + "Test User 2".to_string(), + "test2@example.com".to_string(), + ); + + signer2.sign(None, Some("Electronic signature without visual data".to_string())); + + println!(" Status: {:?}", signer2.status); + println!(" Signature data: {:?}", signer2.signature_data); + println!(" Comments: {:?}", signer2.comments); + + assert_eq!(signer2.status, SignerStatus::Signed); + assert_eq!(signer2.signature_data, None); + assert!(signer2.comments.is_some()); + println!(" ✓ Signing without signature data works correctly\n"); + + // Test 4: Sign with no comments or signature + println!("Test 4: Simple signing (no signature, no comments)"); + let mut signer3 = ContractSigner::new( + "test-signer-003".to_string(), + "Test User 3".to_string(), + "test3@example.com".to_string(), + ); + + signer3.sign(None, None); + + println!(" Status: {:?}", signer3.status); + println!(" Signature data: {:?}", signer3.signature_data); + println!(" Comments: {:?}", signer3.comments); + + assert_eq!(signer3.status, SignerStatus::Signed); + assert_eq!(signer3.signature_data, None); + assert_eq!(signer3.comments, None); + println!(" ✓ Simple signing works correctly\n"); + + // Test 5: Builder pattern with signature data + println!("Test 5: Builder pattern with signature data"); + let signer_with_signature = ContractSigner::new( + "test-signer-004".to_string(), + "Builder User".to_string(), + "builder@example.com".to_string(), + ) + .status(SignerStatus::Pending) + .signature_data("data:image/png;base64,example") + .comments("Pre-signed with builder pattern"); + + println!(" Signer: {}", signer_with_signature.name); + println!(" Status: {:?}", signer_with_signature.status); + println!(" Signature data: {:?}", signer_with_signature.signature_data); + println!(" Comments: {:?}", signer_with_signature.comments); + + assert_eq!(signer_with_signature.signature_data, Some("data:image/png;base64,example".to_string())); + println!(" ✓ Builder pattern with signature data works correctly\n"); + + // Test 6: Clear signature data + println!("Test 6: Clear signature data"); + let cleared_signer = signer_with_signature.clear_signature_data(); + println!(" Signature data after clear: {:?}", cleared_signer.signature_data); + assert_eq!(cleared_signer.signature_data, None); + println!(" ✓ Clear signature data works correctly\n"); + + // Test 7: Serialization/Deserialization test + println!("Test 7: Serialization/Deserialization"); + let original_signer = ContractSigner::new( + "serialize-test".to_string(), + "Serialize User".to_string(), + "serialize@example.com".to_string(), + ) + .signature_data("test-signature-data") + .comments("Test serialization"); + + // Serialize to JSON + let json = serde_json::to_string(&original_signer).expect("Failed to serialize"); + println!(" Serialized JSON length: {} characters", json.len()); + + // Deserialize from JSON + let deserialized_signer: ContractSigner = serde_json::from_str(&json).expect("Failed to deserialize"); + + println!(" Original signature data: {:?}", original_signer.signature_data); + println!(" Deserialized signature data: {:?}", deserialized_signer.signature_data); + + assert_eq!(original_signer.signature_data, deserialized_signer.signature_data); + assert_eq!(original_signer.name, deserialized_signer.name); + assert_eq!(original_signer.email, deserialized_signer.email); + println!(" ✓ Serialization/Deserialization works correctly\n"); + + // Test 8: Backward compatibility test + println!("Test 8: Backward compatibility"); + // Simulate old JSON without signature_data field + let old_json = r#"{ + "id": "old-signer", + "name": "Old User", + "email": "old@example.com", + "status": "Pending", + "signed_at": null, + "comments": null, + "last_reminder_mail_sent_at": null + }"#; + + let old_signer: ContractSigner = serde_json::from_str(old_json).expect("Failed to deserialize old format"); + println!(" Old signer name: {}", old_signer.name); + println!(" Old signer signature data: {:?}", old_signer.signature_data); + + assert_eq!(old_signer.signature_data, None); + println!(" ✓ Backward compatibility works correctly\n"); + + println!("All tests passed! ✅"); + println!("ContractSigner signature functionality is working correctly."); + + // Summary + println!("\n📋 Summary of Features Tested:"); + println!(" ✅ New signer creation (signature_data: None)"); + println!(" ✅ Signing with signature data"); + println!(" ✅ Signing without signature data"); + println!(" ✅ Simple signing (no data, no comments)"); + println!(" ✅ Builder pattern with signature data"); + println!(" ✅ Clear signature data functionality"); + println!(" ✅ JSON serialization/deserialization"); + println!(" ✅ Backward compatibility with old data"); + println!("\n🎯 Ready for production use!"); +} diff --git a/heromodels/src/models/legal/contract.rs b/heromodels/src/models/legal/contract.rs index c92acd0..a9bbe41 100644 --- a/heromodels/src/models/legal/contract.rs +++ b/heromodels/src/models/legal/contract.rs @@ -2,6 +2,17 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; use serde::{Deserialize, Serialize}; use std::fmt; +use std::time::{SystemTime, UNIX_EPOCH}; + +// --- Helper Functions --- + +/// Helper function to get current timestamp in seconds +fn current_timestamp_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} // --- Enums --- @@ -87,6 +98,7 @@ pub struct ContractSigner { pub signed_at: Option, // Timestamp pub comments: Option, pub last_reminder_mail_sent_at: Option, // Unix timestamp of last reminder sent + pub signature_data: Option, // Base64 encoded signature image data } impl ContractSigner { @@ -99,6 +111,7 @@ impl ContractSigner { signed_at: None, comments: None, last_reminder_mail_sent_at: None, + signature_data: None, } } @@ -137,6 +150,16 @@ impl ContractSigner { self } + pub fn signature_data(mut self, signature_data: impl ToString) -> Self { + self.signature_data = Some(signature_data.to_string()); + self + } + + pub fn clear_signature_data(mut self) -> Self { + self.signature_data = None; + self + } + /// Helper method to check if a reminder can be sent (30-minute rate limiting) pub fn can_send_reminder(&self, current_timestamp: u64) -> bool { match self.last_reminder_mail_sent_at { @@ -168,6 +191,16 @@ impl ContractSigner { pub fn mark_reminder_sent(&mut self, current_timestamp: u64) { self.last_reminder_mail_sent_at = Some(current_timestamp); } + + /// Signs the contract with optional signature data and comments + pub fn sign(&mut self, signature_data: Option, comments: Option) { + self.status = SignerStatus::Signed; + self.signed_at = Some(current_timestamp_secs()); + self.signature_data = signature_data; + if let Some(comment) = comments { + self.comments = Some(comment); + } + } } // --- Main Contract Model --- diff --git a/heromodels/src/models/legal/rhai.rs b/heromodels/src/models/legal/rhai.rs index 0e89789..6b760f4 100644 --- a/heromodels/src/models/legal/rhai.rs +++ b/heromodels/src/models/legal/rhai.rs @@ -211,6 +211,18 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc) { |signer: ContractSigner| -> ContractSigner { signer.clear_last_reminder_mail_sent_at() }, ); + // Signature data functionality + engine.register_fn( + "signature_data", + |signer: ContractSigner, signature_data: String| -> ContractSigner { + signer.signature_data(signature_data) + }, + ); + engine.register_fn( + "clear_signature_data", + |signer: ContractSigner| -> ContractSigner { signer.clear_signature_data() }, + ); + // Helper methods for reminder logic engine.register_fn( "can_send_reminder", @@ -263,6 +275,23 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc) { }, ); + // Sign methods + engine.register_fn( + "sign", + |signer: &mut ContractSigner, signature_data: String, comments: String| { + signer.sign(Some(signature_data), Some(comments)); + }, + ); + engine.register_fn( + "sign_without_signature", + |signer: &mut ContractSigner, comments: String| { + signer.sign(None, Some(comments)); + }, + ); + engine.register_fn("sign_simple", |signer: &mut ContractSigner| { + signer.sign(None, None); + }); + engine.register_get( "id", |signer: &mut ContractSigner| -> Result> { @@ -317,6 +346,15 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc) { .map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64))) }, ); + engine.register_get( + "signature_data", + |signer: &mut ContractSigner| -> Result> { + Ok(signer + .signature_data + .as_ref() + .map_or(Dynamic::UNIT, |data| Dynamic::from(data.clone()))) + }, + ); // --- Contract --- engine.register_type_with_name::("Contract"); diff --git a/specs/models/legal/contract.v b/specs/models/legal/contract.v index 469abe8..227bca1 100644 --- a/specs/models/legal/contract.v +++ b/specs/models/legal/contract.v @@ -57,6 +57,7 @@ pub mut: signed_at ourtime.OurTime // Optional in Rust, OurTime can be zero comments string // Optional in Rust, string can be empty last_reminder_mail_sent_at ourtime.OurTime // Unix timestamp of last reminder sent + signature_data string // Base64 encoded signature image data (Optional in Rust) } // SignerStatus defines the status of a contract signer From af83035d892c24a4e8441064e4a19e14aba108a7 Mon Sep 17 00:00:00 2001 From: Mahmoud-Emad Date: Wed, 25 Jun 2025 18:39:47 +0300 Subject: [PATCH 8/8] feat: Add new Rhai example demonstrating payment flow - Add a new Rhai example showcasing complete payment flow for company registration. This includes company creation, payment record creation, successful payment processing, status updates, and handling failed and refunded payments. - Add new example demonstrating payment integration in Rhai scripting. This example showcases the usage of various payment status methods and verifies data from the database after processing payments. - Add new examples to Cargo.toml to facilitate building and running the examples. This makes it easier to integrate and test payment functionality using Rhai scripting. --- heromodels/Cargo.toml | 5 + heromodels/examples/biz_rhai/biz.rs | 37 +- .../examples/biz_rhai/payment_flow.rhai | 229 +++ .../examples/biz_rhai/payment_flow_example.rs | 220 +++ heromodels/examples/payment_flow_example.rs | 214 +++ heromodels/payment_usage.md | 318 ++++ heromodels/src/models/biz/company.rs | 65 +- heromodels/src/models/biz/mod.rs | 7 +- heromodels/src/models/biz/payment.rs | 216 +++ heromodels/src/models/biz/rhai.rs | 1364 ++++++++++++++--- heromodels/src/models/mod.rs | 2 +- heromodels/tests/payment.rs | 313 ++++ 12 files changed, 2717 insertions(+), 273 deletions(-) create mode 100644 heromodels/examples/biz_rhai/payment_flow.rhai create mode 100644 heromodels/examples/biz_rhai/payment_flow_example.rs create mode 100644 heromodels/examples/payment_flow_example.rs create mode 100644 heromodels/payment_usage.md create mode 100644 heromodels/src/models/biz/payment.rs create mode 100644 heromodels/tests/payment.rs diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index 67c5be5..4b317e8 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -83,3 +83,8 @@ required-features = ["rhai"] name = "biz_rhai" path = "examples/biz_rhai/example.rs" required-features = ["rhai"] + +[[example]] +name = "payment_flow_rhai" +path = "examples/biz_rhai/payment_flow_example.rs" +required-features = ["rhai"] diff --git a/heromodels/examples/biz_rhai/biz.rs b/heromodels/examples/biz_rhai/biz.rs index a044707..ad86a7b 100644 --- a/heromodels/examples/biz_rhai/biz.rs +++ b/heromodels/examples/biz_rhai/biz.rs @@ -6,7 +6,9 @@ print("DB instance will be implicitly passed to DB functions."); // --- Enum Constants --- print("\n--- Enum Constants ---"); +print(`CompanyStatus PendingPayment: ${CompanyStatusConstants::PendingPayment}`); print(`CompanyStatus Active: ${CompanyStatusConstants::Active}`); +print(`CompanyStatus Suspended: ${CompanyStatusConstants::Suspended}`); print(`CompanyStatus Inactive: ${CompanyStatusConstants::Inactive}`); print(`BusinessType Coop: ${BusinessTypeConstants::Coop}`); print(`BusinessType Global: ${BusinessTypeConstants::Global}`); @@ -28,18 +30,46 @@ let company1 = new_company(company1_name, company1_reg, company1_inc_date) .business_type(BusinessTypeConstants::Global) .industry("Technology") .description("Leading provider of innovative tech solutions.") - .status(CompanyStatusConstants::Active) + // Note: status defaults to PendingPayment for new companies .fiscal_year_end("12-31") .set_base_created_at(1672531200) .set_base_modified_at(1672531205); -print(`Company 1 Name: ${company1.name}, Status: ${company1.status}`); +print(`Company 1 Name: ${company1.name}, Status: ${company1.status} (default for new companies)`); print(`Company 1 Email: ${company1.email}, Industry: ${company1.industry}`); // Save the company to the database print("\nSaving company1 to database..."); company1 = set_company(company1); // Capture the company with the DB-assigned ID print("Company1 saved."); +// Demonstrate payment flow for the company +print("\n--- Payment Processing for Company ---"); +print("Creating payment record for company registration..."); +let payment_intent_id = `pi_demo_${company1.id}`; +let payment = new_payment( + payment_intent_id, + company1.id, + "yearly", + 500.0, // Setup fee + 99.0, // Monthly fee + 1688.0 // Total amount (setup + 12 months) +); + +payment = set_payment(payment); +print(`Payment created: ${payment.payment_intent_id}, Status: ${payment.status}`); + +// Simulate successful payment processing +print("Processing payment..."); +payment = payment.complete_payment(`cus_demo_${company1.id}`); +payment = set_payment(payment); +print(`Payment completed: ${payment.status}`); + +// Update company status to Active after successful payment +print("Updating company status to Active after payment..."); +company1 = company1.status(CompanyStatusConstants::Active); +company1 = set_company(company1); +print(`Company status updated: ${company1.status}`); + // Retrieve the company print(`\nRetrieving company by ID (${company1.id})...`); let retrieved_company = get_company_by_id(company1.id); @@ -97,10 +127,11 @@ print(`Retrieved Shareholder 2: ${retrieved_sh2.name}, Type: ${retrieved_sh2.typ print("\n--- Testing Update for Company ---"); let updated_company = retrieved_company .description("Leading global provider of cutting-edge technology solutions and services.") - .status(CompanyStatusConstants::Active) + // Note: Company is already Active from payment processing above .phone("+1-555-0199"); // Assume modified_at would be updated by set_company print(`Updated Company - Name: ${updated_company.name}, New Phone: ${updated_company.phone}`); +print(`Company Status: ${updated_company.status} (already Active from payment)`); set_company(updated_company); print("Updated Company saved."); diff --git a/heromodels/examples/biz_rhai/payment_flow.rhai b/heromodels/examples/biz_rhai/payment_flow.rhai new file mode 100644 index 0000000..ce777b3 --- /dev/null +++ b/heromodels/examples/biz_rhai/payment_flow.rhai @@ -0,0 +1,229 @@ +// Payment Flow Rhai Example +// This script demonstrates the complete payment flow for company registration +// using the Rhai scripting interface. + +print("=== Payment Flow Rhai Example ==="); +print("Demonstrating company registration with payment integration"); +print(""); + +// --- Step 1: Create Company with Pending Payment Status --- +print("Step 1: Creating company with pending payment status"); + +let company_name = "InnovateTech Solutions"; +let company_reg = "REG-ITS-2024-001"; +let company_inc_date = 1704067200; // Jan 1, 2024 + +// Create company (status defaults to PendingPayment) +let company = new_company(company_name, company_reg, company_inc_date) + .email("contact@innovatetech.com") + .phone("+1-555-0199") + .website("https://innovatetech.com") + .address("456 Innovation Blvd, Tech Valley, TV 67890") + .business_type(BusinessTypeConstants::Starter) + .industry("Software Development") + .description("Cutting-edge software solutions for modern businesses") + .fiscal_year_end("12-31"); + +print(` Company: ${company.name}`); +print(` Status: ${company.status} (default for new companies)`); +print(` Registration: ${company.registration_number}`); +print(` Email: ${company.email}`); + +// Save company to database +company = set_company(company); +print(` Saved with ID: ${company.id}`); +print(""); + +// --- Step 2: Create Payment Record --- +print("Step 2: Creating payment record"); + +let payment_intent_id = `pi_rhai_${timestamp()}`; +let payment_plan = "yearly"; +let setup_fee = 750.0; +let monthly_fee = 149.0; +let total_amount = setup_fee + (monthly_fee * 12.0); // Setup + 12 months + +let payment = new_payment( + payment_intent_id, + company.id, + payment_plan, + setup_fee, + monthly_fee, + total_amount +); + +print(` Payment Intent ID: ${payment.payment_intent_id}`); +print(` Company ID: ${payment.company_id}`); +print(` Payment Plan: ${payment.payment_plan}`); +print(` Setup Fee: $${payment.setup_fee}`); +print(` Monthly Fee: $${payment.monthly_fee}`); +print(` Total Amount: $${payment.total_amount}`); +print(` Status: ${payment.status} (default for new payments)`); + +// Save payment to database +payment = set_payment(payment); +print(` Saved with ID: ${payment.id}`); +print(""); + +// --- Step 3: Process Payment Successfully --- +print("Step 3: Processing payment..."); + +// Simulate payment processing +print(" Contacting payment processor..."); +print(" Validating payment details..."); +print(" Processing transaction..."); + +// Complete the payment with Stripe customer ID +let stripe_customer_id = `cus_rhai_${timestamp()}`; +payment = payment.complete_payment(stripe_customer_id); + +print(" Payment completed successfully!"); +print(` New Status: ${payment.status}`); +print(` Stripe Customer ID: ${payment.stripe_customer_id}`); + +// Save updated payment +payment = set_payment(payment); +print(""); + +// --- Step 4: Update Company Status to Active --- +print("Step 4: Updating company status to Active"); + +company = company.status(CompanyStatusConstants::Active); +print(` Company: ${company.name}`); +print(` New Status: ${company.status}`); + +// Save updated company +company = set_company(company); +print(" Company status updated successfully!"); +print(""); + +// --- Step 5: Verify Payment Status --- +print("Step 5: Payment status verification"); +print(` Is payment completed? ${payment.is_completed()}`); +print(` Is payment pending? ${payment.is_pending()}`); +print(` Has payment failed? ${payment.has_failed()}`); +print(` Is payment refunded? ${payment.is_refunded()}`); +print(""); + +// --- Step 6: Retrieve Data from Database --- +print("Step 6: Verifying data from database"); + +let retrieved_company = get_company_by_id(company.id); +print(` Retrieved Company: ${retrieved_company.name}`); +print(` Status: ${retrieved_company.status}`); + +let retrieved_payment = get_payment_by_id(payment.id); +print(` Retrieved Payment: ${retrieved_payment.payment_intent_id}`); +print(` Status: ${retrieved_payment.status}`); +print(` Total: $${retrieved_payment.total_amount}`); +print(""); + +// --- Step 7: Demonstrate Failed Payment Scenario --- +print("Step 7: Demonstrating failed payment scenario"); + +// Create another company for failed payment demo +let failed_company = new_company( + "FailureDemo Corp", + "REG-FDC-2024-002", + 1704067200 +) +.email("demo@failurecorp.com") +.business_type(BusinessTypeConstants::Single) +.industry("Consulting"); + +failed_company = set_company(failed_company); +print(` Created company: ${failed_company.name}`); +print(` Status: ${failed_company.status} (pending payment)`); + +// Create payment that will fail +let failed_payment_intent = `pi_fail_${timestamp()}`; +let failed_payment = new_payment( + failed_payment_intent, + failed_company.id, + "monthly", + 300.0, + 59.0, + 359.0 +); + +failed_payment = set_payment(failed_payment); + +// Simulate payment failure +print(" Simulating payment failure..."); +failed_payment = failed_payment.fail_payment(); +failed_payment = set_payment(failed_payment); + +print(` Failed Company: ${failed_company.name}`); +print(` Company Status: ${failed_company.status} (remains pending)`); +print(` Payment Status: ${failed_payment.status}`); +print(` Payment failed: ${failed_payment.has_failed()}`); +print(""); + +// --- Step 8: Demonstrate Payment Refund --- +print("Step 8: Demonstrating payment refund scenario"); + +// Create a payment to refund +let refund_company = new_company( + "RefundDemo Inc", + "REG-RDI-2024-003", + 1704067200 +) +.email("refund@demo.com") +.business_type(BusinessTypeConstants::Twin); + +refund_company = set_company(refund_company); + +let refund_payment = new_payment( + `pi_refund_${timestamp()}`, + refund_company.id, + "monthly", + 200.0, + 39.0, + 239.0 +); + +refund_payment = set_payment(refund_payment); + +// First complete the payment +refund_payment = refund_payment.complete_payment(`cus_refund_${timestamp()}`); +refund_payment = set_payment(refund_payment); +print(` Payment completed: ${refund_payment.is_completed()}`); + +// Then refund it +refund_payment = refund_payment.refund_payment(); +refund_payment = set_payment(refund_payment); + +print(` Payment refunded: ${refund_payment.is_refunded()}`); +print(` Refund Status: ${refund_payment.status}`); +print(""); + +// --- Summary --- +print("=== Payment Flow Example Complete ==="); +print("Summary of demonstrated features:"); +print("✓ Company creation with PendingPayment status (default)"); +print("✓ Payment record creation and database persistence"); +print("✓ Successful payment processing and status updates"); +print("✓ Company status transition from PendingPayment to Active"); +print("✓ Payment status verification methods"); +print("✓ Database retrieval and verification"); +print("✓ Failed payment scenario handling"); +print("✓ Payment refund processing"); +print(""); +print("Key Payment Statuses:"); +print("- Pending: Initial state for new payments"); +print("- Completed: Payment successfully processed"); +print("- Failed: Payment processing failed"); +print("- Refunded: Previously completed payment was refunded"); +print(""); +print("Key Company Statuses:"); +print("- PendingPayment: Default for new companies (awaiting payment)"); +print("- Active: Payment completed, company is operational"); +print("- Suspended: Company temporarily suspended"); +print("- Inactive: Company deactivated"); + +// Helper function to get current timestamp +fn timestamp() { + // This would normally return current timestamp + // For demo purposes, we'll use a static value + 1704067200 +} diff --git a/heromodels/examples/biz_rhai/payment_flow_example.rs b/heromodels/examples/biz_rhai/payment_flow_example.rs new file mode 100644 index 0000000..61b9986 --- /dev/null +++ b/heromodels/examples/biz_rhai/payment_flow_example.rs @@ -0,0 +1,220 @@ +// Payment Flow Rhai Example Runner +// This example runs the payment_flow.rhai script to demonstrate +// the payment integration using Rhai scripting. + +use heromodels::db::hero::OurDB; +use heromodels::models::biz::register_biz_rhai_module; +use rhai::Engine; +use std::sync::Arc; + +fn main() -> Result<(), Box> { + println!("=== Payment Flow Rhai Example Runner ==="); + println!("Running payment flow demonstration using Rhai scripting\n"); + + // Initialize database + let db = Arc::new( + OurDB::new("/tmp/payment_flow_rhai_example", true) + .map_err(|e| format!("DB Error: {}", e))?, + ); + + // Create and configure Rhai engine + let mut engine = Engine::new(); + + // Register the business models module with the engine + register_biz_rhai_module(&mut engine, Arc::clone(&db)); + + // Add a timestamp function for the Rhai script + engine.register_fn("timestamp", || -> i64 { chrono::Utc::now().timestamp() }); + + // Read and execute the Rhai script + let script_path = "examples/biz_rhai/payment_flow.rhai"; + + match std::fs::read_to_string(script_path) { + Ok(script_content) => { + println!("Executing Rhai script: {}\n", script_path); + + match engine.eval::<()>(&script_content) { + Ok(_) => { + println!("\n✅ Rhai script executed successfully!"); + } + Err(e) => { + eprintln!("❌ Error executing Rhai script: {}", e); + return Err(Box::new(e)); + } + } + } + Err(e) => { + eprintln!("❌ Error reading script file {}: {}", script_path, e); + println!("Note: Make sure to run this example from the project root directory."); + return Err(Box::new(e)); + } + } + + println!("\n=== Example Complete ==="); + println!("The payment flow has been successfully demonstrated using Rhai scripting."); + println!("This shows how the payment integration can be used in scripted environments."); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use heromodels::db::Collection; + use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus}; + + #[test] + fn test_rhai_payment_integration() -> Result<(), Box> { + // Test that we can create and manipulate payment objects through Rhai + let db_config = OurDBConfig { + path: "/tmp/test_rhai_payment".to_string(), + incremental_mode: true, + file_size: None, + keysize: None, + reset: Some(true), + }; + + let db = Arc::new(OurDB::new(db_config)?); + let mut engine = Engine::new(); + register_biz_rhai_module(&mut engine, Arc::clone(&db)); + + // Test creating a company through Rhai + let company_script = r#" + let company = new_company("Test Company", "TEST-001", 1704067200) + .email("test@example.com") + .status(CompanyStatusConstants::PendingPayment); + company = set_company(company); + company.id + "#; + + let company_id: i64 = engine.eval(company_script)?; + assert!(company_id > 0); + + // Test creating a payment through Rhai + let payment_script = format!( + r#" + let payment = new_payment("pi_test_123", {}, "monthly", 100.0, 50.0, 150.0); + payment = set_payment(payment); + payment.id + "#, + company_id + ); + + let payment_id: i64 = engine.eval(&payment_script)?; + assert!(payment_id > 0); + + // Test completing payment through Rhai + let complete_script = format!( + r#" + let payment = get_payment_by_id({}); + payment = payment.complete_payment("cus_test_123"); + payment = set_payment(payment); + payment.is_completed() + "#, + payment_id + ); + + let is_completed: bool = engine.eval(&complete_script)?; + assert!(is_completed); + + // Verify in database + let payment: Payment = db.get_by_id(payment_id as u32)?.unwrap(); + assert_eq!(payment.status, PaymentStatus::Completed); + assert_eq!(payment.stripe_customer_id, Some("cus_test_123".to_string())); + + Ok(()) + } + + #[test] + fn test_payment_status_constants() -> Result<(), Box> { + // Test that payment status constants are available in Rhai + let db_config = OurDBConfig { + path: "/tmp/test_payment_constants".to_string(), + incremental_mode: true, + file_size: None, + keysize: None, + reset: Some(true), + }; + + let db = Arc::new(OurDB::new(db_config)?); + let mut engine = Engine::new(); + register_biz_rhai_module(&mut engine, Arc::clone(&db)); + + // Test that we can access payment status constants + let constants_script = r#" + let payment = new_payment("pi_test", 1, "monthly", 100.0, 50.0, 150.0); + + // Test status transitions + payment = payment.status(PaymentStatusConstants::Pending); + let is_pending = payment.is_pending(); + + payment = payment.status(PaymentStatusConstants::Completed); + let is_completed = payment.is_completed(); + + payment = payment.status(PaymentStatusConstants::Failed); + let has_failed = payment.has_failed(); + + payment = payment.status(PaymentStatusConstants::Refunded); + let is_refunded = payment.is_refunded(); + + [is_pending, is_completed, has_failed, is_refunded] + "#; + + let results: Vec = engine.eval(constants_script)?; + assert_eq!(results, vec![true, true, true, true]); + + Ok(()) + } + + #[test] + fn test_company_status_integration() -> Result<(), Box> { + // Test the integration between company and payment status + let db_config = OurDBConfig { + path: "/tmp/test_status_integration".to_string(), + incremental_mode: true, + file_size: None, + keysize: None, + reset: Some(true), + }; + + let db = Arc::new(OurDB::new(db_config)?); + let mut engine = Engine::new(); + register_biz_rhai_module(&mut engine, Arc::clone(&db)); + + let integration_script = r#" + // Create company (defaults to PendingPayment) + let company = new_company("Integration Test", "INT-001", 1704067200); + company = set_company(company); + + // Create payment + let payment = new_payment("pi_int_test", company.id, "yearly", 500.0, 99.0, 1688.0); + payment = set_payment(payment); + + // Complete payment + payment = payment.complete_payment("cus_int_test"); + payment = set_payment(payment); + + // Update company to active + company = company.status(CompanyStatusConstants::Active); + company = set_company(company); + + [payment.is_completed(), company.status] + "#; + + let results: Vec = engine.eval(integration_script)?; + + // Check that payment is completed + assert!(results[0].as_bool().unwrap()); + + // Check that company status is Active (we can't directly compare enum in Rhai result) + // So we'll verify by retrieving from database + let companies: Vec = db.get_all()?; + let company = companies + .into_iter() + .find(|c| c.name == "Integration Test") + .unwrap(); + assert_eq!(company.status, CompanyStatus::Active); + + Ok(()) + } +} diff --git a/heromodels/examples/payment_flow_example.rs b/heromodels/examples/payment_flow_example.rs new file mode 100644 index 0000000..c07eaa1 --- /dev/null +++ b/heromodels/examples/payment_flow_example.rs @@ -0,0 +1,214 @@ +// Payment Flow Example +// This example demonstrates the complete payment flow for company registration, +// including company creation with pending payment status, payment processing, +// and status transitions. + +use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment}; + +fn main() { + println!("=== Payment Flow Example ==="); + println!("Demonstrating company registration with payment integration\n"); + + // Step 1: Create a company with pending payment status + println!("Step 1: Creating company with pending payment status"); + let company = Company::new( + "TechStart Inc.".to_string(), + "REG-TS-2024-001".to_string(), + chrono::Utc::now().timestamp(), + ) + .email("contact@techstart.com".to_string()) + .phone("+1-555-0123".to_string()) + .website("https://techstart.com".to_string()) + .address("123 Startup Ave, Innovation City, IC 12345".to_string()) + .business_type(BusinessType::Starter) + .industry("Technology".to_string()) + .description("A promising tech startup focused on AI solutions".to_string()) + // Note: status defaults to PendingPayment, so we don't need to set it explicitly + .fiscal_year_end("12-31".to_string()); + + println!(" Company: {}", company.name); + println!(" Status: {:?}", company.status); + println!(" Registration: {}", company.registration_number); + println!(" Company created successfully!\n"); + + // Step 2: Create a payment record for the company + println!("Step 2: Creating payment record"); + let payment_intent_id = format!("pi_test_{}", chrono::Utc::now().timestamp()); + let payment = Payment::new( + payment_intent_id.clone(), + 1, // Mock company ID for this example + "yearly".to_string(), + 500.0, // Setup fee + 99.0, // Monthly fee + 1688.0, // Total amount (setup + 12 months) + ); + + println!(" Payment Intent ID: {}", payment.payment_intent_id); + println!(" Company ID: {}", payment.company_id); + println!(" Payment Plan: {}", payment.payment_plan); + println!(" Setup Fee: ${:.2}", payment.setup_fee); + println!(" Monthly Fee: ${:.2}", payment.monthly_fee); + println!(" Total Amount: ${:.2}", payment.total_amount); + println!(" Status: {:?}", payment.status); + println!(" Payment record created successfully!\n"); + + // Step 3: Simulate payment processing + println!("Step 3: Processing payment..."); + + // Simulate some processing time + std::thread::sleep(std::time::Duration::from_millis(100)); + + // Complete the payment with Stripe customer ID + let stripe_customer_id = Some(format!("cus_test_{}", chrono::Utc::now().timestamp())); + let completed_payment = payment.complete_payment(stripe_customer_id.clone()); + + println!(" Payment completed successfully!"); + println!(" New Status: {:?}", completed_payment.status); + println!( + " Stripe Customer ID: {:?}", + completed_payment.stripe_customer_id + ); + + // Step 4: Update company status to Active + println!("\nStep 4: Updating company status to Active"); + let active_company = company.status(CompanyStatus::Active); + + println!(" Company: {}", active_company.name); + println!(" New Status: {:?}", active_company.status); + println!(" Company status updated successfully!\n"); + + // Step 5: Demonstrate payment status checks + println!("Step 5: Payment status verification"); + println!( + " Is payment completed? {}", + completed_payment.is_completed() + ); + println!(" Is payment pending? {}", completed_payment.is_pending()); + println!(" Has payment failed? {}", completed_payment.has_failed()); + println!(" Is payment refunded? {}", completed_payment.is_refunded()); + + // Step 6: Demonstrate failed payment scenario + println!("\nStep 6: Demonstrating failed payment scenario"); + + // Create another company + let failed_company = Company::new( + "FailCorp Ltd.".to_string(), + "REG-FC-2024-002".to_string(), + chrono::Utc::now().timestamp(), + ) + .email("contact@failcorp.com".to_string()) + .business_type(BusinessType::Single) + .industry("Consulting".to_string()); + + // Create payment for failed scenario + let failed_payment_intent = format!("pi_fail_{}", chrono::Utc::now().timestamp()); + let failed_payment = Payment::new( + failed_payment_intent, + 2, // Mock company ID + "monthly".to_string(), + 250.0, + 49.0, + 299.0, + ); + + // Simulate payment failure + let failed_payment = failed_payment.fail_payment(); + + println!(" Failed Company: {}", failed_company.name); + println!( + " Company Status: {:?} (remains pending)", + failed_company.status + ); + println!(" Payment Status: {:?}", failed_payment.status); + println!(" Payment failed: {}", failed_payment.has_failed()); + + println!("\n=== Payment Flow Example Complete ==="); + println!("Summary:"); + println!("- Created companies with PendingPayment status by default"); + println!("- Processed successful payment and updated company to Active"); + println!("- Demonstrated failed payment scenario"); + println!("- All operations completed successfully without database persistence"); + println!("- For database examples, see the Rhai examples or unit tests"); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_payment_flow() { + // Test the basic payment flow without database persistence + let company = Company::new( + "Test Company".to_string(), + "TEST-001".to_string(), + chrono::Utc::now().timestamp(), + ); + + // Verify default status is PendingPayment + assert_eq!(company.status, CompanyStatus::PendingPayment); + + // Create payment + let payment = Payment::new( + "pi_test_123".to_string(), + 1, // Mock company ID + "monthly".to_string(), + 100.0, + 50.0, + 150.0, + ); + + // Verify default payment status is Pending + assert_eq!(payment.status, PaymentStatus::Pending); + assert!(payment.is_pending()); + assert!(!payment.is_completed()); + + // Complete payment + let completed_payment = payment.complete_payment(Some("cus_test_123".to_string())); + assert_eq!(completed_payment.status, PaymentStatus::Completed); + assert!(completed_payment.is_completed()); + assert!(!completed_payment.is_pending()); + + // Update company status + let active_company = company.status(CompanyStatus::Active); + assert_eq!(active_company.status, CompanyStatus::Active); + } + + #[test] + fn test_payment_failure() { + let payment = Payment::new( + "pi_fail_123".to_string(), + 1, + "yearly".to_string(), + 500.0, + 99.0, + 1688.0, + ); + + let failed_payment = payment.fail_payment(); + assert_eq!(failed_payment.status, PaymentStatus::Failed); + assert!(failed_payment.has_failed()); + assert!(!failed_payment.is_completed()); + } + + #[test] + fn test_payment_refund() { + let payment = Payment::new( + "pi_refund_123".to_string(), + 1, + "monthly".to_string(), + 250.0, + 49.0, + 299.0, + ); + + // First complete the payment + let completed_payment = payment.complete_payment(Some("cus_123".to_string())); + assert!(completed_payment.is_completed()); + + // Then refund it + let refunded_payment = completed_payment.refund_payment(); + assert_eq!(refunded_payment.status, PaymentStatus::Refunded); + assert!(refunded_payment.is_refunded()); + assert!(!refunded_payment.is_completed()); + } +} diff --git a/heromodels/payment_usage.md b/heromodels/payment_usage.md new file mode 100644 index 0000000..666b448 --- /dev/null +++ b/heromodels/payment_usage.md @@ -0,0 +1,318 @@ +# Payment Model Usage Guide + +This document provides comprehensive instructions for AI assistants on how to use the Payment model in the heromodels repository. + +## Overview + +The Payment model represents a payment transaction in the system, typically associated with company registration or subscription payments. It integrates with Stripe for payment processing and maintains comprehensive status tracking. + +## Model Structure + +```rust +pub struct Payment { + pub base_data: BaseModelData, // Auto-managed ID, timestamps, comments + pub payment_intent_id: String, // Stripe payment intent ID + pub company_id: u32, // Foreign key to Company + pub payment_plan: String, // "monthly", "yearly", "two_year" + pub setup_fee: f64, // One-time setup fee + pub monthly_fee: f64, // Recurring monthly fee + pub total_amount: f64, // Total amount paid + pub currency: String, // Currency code (defaults to "usd") + pub status: PaymentStatus, // Current payment status + pub stripe_customer_id: Option, // Stripe customer ID (set on completion) + pub created_at: i64, // Payment creation timestamp + pub completed_at: Option, // Payment completion timestamp +} + +pub enum PaymentStatus { + Pending, // Initial state - payment created but not processed + Processing, // Payment is being processed by Stripe + Completed, // Payment successfully completed + Failed, // Payment processing failed + Refunded, // Payment was refunded +} +``` + +## Basic Usage + +### 1. Creating a New Payment + +```rust +use heromodels::models::biz::{Payment, PaymentStatus}; + +// Create a new payment with required fields +let payment = Payment::new( + "pi_1234567890".to_string(), // Stripe payment intent ID + company_id, // Company ID from database + "monthly".to_string(), // Payment plan + 100.0, // Setup fee + 49.99, // Monthly fee + 149.99, // Total amount +); + +// Payment defaults: +// - status: PaymentStatus::Pending +// - currency: "usd" +// - stripe_customer_id: None +// - created_at: current timestamp +// - completed_at: None +``` + +### 2. Using Builder Pattern + +```rust +let payment = Payment::new( + "pi_1234567890".to_string(), + company_id, + "yearly".to_string(), + 500.0, + 99.99, + 1699.88, +) +.currency("eur".to_string()) +.stripe_customer_id(Some("cus_existing_customer".to_string())); +``` + +### 3. Database Operations + +```rust +use heromodels::db::Collection; + +// Save payment to database +let db = get_db()?; +let (payment_id, saved_payment) = db.set(&payment)?; + +// Retrieve payment by ID +let retrieved_payment: Payment = db.get_by_id(payment_id)?.unwrap(); + +// Update payment +let updated_payment = saved_payment.complete_payment(Some("cus_new_customer".to_string())); +let (_, final_payment) = db.set(&updated_payment)?; +``` + +## Payment Status Management + +### Status Transitions + +```rust +// 1. Start with Pending status (default) +let payment = Payment::new(/* ... */); +assert!(payment.is_pending()); + +// 2. Mark as processing when Stripe starts processing +let processing_payment = payment.process_payment(); +assert!(processing_payment.is_processing()); + +// 3. Complete payment when Stripe confirms success +let completed_payment = processing_payment.complete_payment(Some("cus_123".to_string())); +assert!(completed_payment.is_completed()); +assert!(completed_payment.completed_at.is_some()); + +// 4. Handle failure if payment fails +let failed_payment = processing_payment.fail_payment(); +assert!(failed_payment.has_failed()); + +// 5. Refund if needed +let refunded_payment = completed_payment.refund_payment(); +assert!(refunded_payment.is_refunded()); +``` + +### Status Check Methods + +```rust +// Check current status +if payment.is_pending() { + // Show "Payment Pending" UI +} else if payment.is_processing() { + // Show "Processing Payment" UI +} else if payment.is_completed() { + // Show "Payment Successful" UI + // Enable company features +} else if payment.has_failed() { + // Show "Payment Failed" UI + // Offer retry option +} else if payment.is_refunded() { + // Show "Payment Refunded" UI +} +``` + +## Integration with Company Model + +### Complete Payment Flow + +```rust +use heromodels::models::biz::{Company, CompanyStatus, Payment, PaymentStatus}; + +// 1. Create company with pending payment status +let company = Company::new( + "TechStart Inc.".to_string(), + "REG-TS-2024-001".to_string(), + chrono::Utc::now().timestamp(), +) +.email("contact@techstart.com".to_string()) +.status(CompanyStatus::PendingPayment); + +let (company_id, company) = db.set(&company)?; + +// 2. Create payment for the company +let payment = Payment::new( + stripe_payment_intent_id, + company_id, + "yearly".to_string(), + 500.0, // Setup fee + 99.0, // Monthly fee + 1688.0, // Total (setup + 12 months) +); + +let (payment_id, payment) = db.set(&payment)?; + +// 3. Process payment through Stripe +let processing_payment = payment.process_payment(); +let (_, processing_payment) = db.set(&processing_payment)?; + +// 4. On successful Stripe webhook +let completed_payment = processing_payment.complete_payment(Some(stripe_customer_id)); +let (_, completed_payment) = db.set(&completed_payment)?; + +// 5. Activate company +let active_company = company.status(CompanyStatus::Active); +let (_, active_company) = db.set(&active_company)?; +``` + +## Database Indexing + +The Payment model provides custom indexes for efficient querying: + +```rust +// Indexed fields for fast lookups: +// - payment_intent_id: Find payment by Stripe intent ID +// - company_id: Find all payments for a company +// - status: Find payments by status + +// Example queries (conceptual - actual implementation depends on your query layer) +// let pending_payments = db.find_by_index("status", "Pending")?; +// let company_payments = db.find_by_index("company_id", company_id.to_string())?; +// let stripe_payment = db.find_by_index("payment_intent_id", "pi_1234567890")?; +``` + +## Error Handling Best Practices + +```rust +use heromodels::db::DbError; + +fn process_payment_flow(payment_intent_id: String, company_id: u32) -> Result { + let db = get_db()?; + + // Create payment + let payment = Payment::new( + payment_intent_id, + company_id, + "monthly".to_string(), + 100.0, + 49.99, + 149.99, + ); + + // Save to database + let (payment_id, payment) = db.set(&payment)?; + + // Process through Stripe (external API call) + match process_stripe_payment(&payment.payment_intent_id) { + Ok(stripe_customer_id) => { + // Success: complete payment + let completed_payment = payment.complete_payment(Some(stripe_customer_id)); + let (_, final_payment) = db.set(&completed_payment)?; + Ok(final_payment) + } + Err(_) => { + // Failure: mark as failed + let failed_payment = payment.fail_payment(); + let (_, final_payment) = db.set(&failed_payment)?; + Ok(final_payment) + } + } +} +``` + +## Testing + +The Payment model includes comprehensive tests in `tests/payment.rs`. When working with payments: + +1. **Always test status transitions** +2. **Verify timestamp handling** +3. **Test database persistence** +4. **Test integration with Company model** +5. **Test builder pattern methods** + +```bash +# Run payment tests +cargo test payment + +# Run specific test +cargo test test_payment_completion +``` + +## Common Patterns + +### 1. Payment Retry Logic +```rust +fn retry_failed_payment(payment: Payment) -> Payment { + if payment.has_failed() { + // Reset to pending for retry + Payment::new( + payment.payment_intent_id, + payment.company_id, + payment.payment_plan, + payment.setup_fee, + payment.monthly_fee, + payment.total_amount, + ) + .currency(payment.currency) + } else { + payment + } +} +``` + +### 2. Payment Summary +```rust +fn get_payment_summary(payment: &Payment) -> String { + format!( + "Payment {} for company {}: {} {} ({})", + payment.payment_intent_id, + payment.company_id, + payment.total_amount, + payment.currency.to_uppercase(), + payment.status + ) +} +``` + +### 3. Payment Validation +```rust +fn validate_payment(payment: &Payment) -> Result<(), String> { + if payment.total_amount <= 0.0 { + return Err("Total amount must be positive".to_string()); + } + if payment.payment_intent_id.is_empty() { + return Err("Payment intent ID is required".to_string()); + } + if payment.company_id == 0 { + return Err("Valid company ID is required".to_string()); + } + Ok(()) +} +``` + +## Key Points for AI Assistants + +1. **Always use auto-generated IDs** - Don't manually set IDs, let OurDB handle them +2. **Follow status flow** - Pending → Processing → Completed/Failed → (optionally) Refunded +3. **Update timestamps** - `completed_at` is automatically set when calling `complete_payment()` +4. **Use builder pattern** - For optional fields and cleaner code +5. **Test thoroughly** - Payment logic is critical, always verify with tests +6. **Handle errors gracefully** - Payment failures should be tracked, not ignored +7. **Integrate with Company** - Payments typically affect company status +8. **Use proper indexing** - Leverage indexed fields for efficient queries + +This model follows the heromodels patterns and integrates seamlessly with the existing codebase architecture. diff --git a/heromodels/src/models/biz/company.rs b/heromodels/src/models/biz/company.rs index 7fcd02d..ae55759 100644 --- a/heromodels/src/models/biz/company.rs +++ b/heromodels/src/models/biz/company.rs @@ -1,19 +1,21 @@ -use serde::{Deserialize, Serialize}; -use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index}; -use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)] +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; // For #[derive(CustomType)] // --- Enums --- #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum CompanyStatus { - Active, - Inactive, - Suspended, + PendingPayment, // Company created but payment not completed + Active, // Payment completed, company is active + Suspended, // Company suspended (e.g., payment issues) + Inactive, // Company deactivated } impl Default for CompanyStatus { fn default() -> Self { - CompanyStatus::Inactive + CompanyStatus::PendingPayment } } @@ -34,6 +36,7 @@ impl Default for BusinessType { // --- Company Struct --- +#[model] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType pub struct Company { pub base_data: BaseModelData, @@ -51,55 +54,11 @@ pub struct Company { pub status: CompanyStatus, } -// --- Model Trait Implementation --- - -impl Model for Company { - fn db_prefix() -> &'static str { - "company" - } - - fn get_id(&self) -> u32 { - self.base_data.id - } - - fn base_data_mut(&mut self) -> &mut BaseModelData { - &mut self.base_data - } - - // Override db_keys to provide custom indexes if needed - fn db_keys(&self) -> Vec { - vec![ - IndexKeyBuilder::new("name").value(self.name.clone()).build(), - IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(), - // Add other relevant keys, e.g., by status or type if frequently queried - ] - } -} - -// --- Index Implementations (Example) --- - -pub struct CompanyNameIndex; -impl Index for CompanyNameIndex { - type Model = Company; - type Key = str; - fn key() -> &'static str { - "name" - } -} - -pub struct CompanyRegistrationNumberIndex; -impl Index for CompanyRegistrationNumberIndex { - type Model = Company; - type Key = str; - fn key() -> &'static str { - "registration_number" - } -} - // --- Builder Pattern --- impl Company { - pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64 + pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { + // incorporation_date to i64 Self { base_data: BaseModelData::new(), name, diff --git a/heromodels/src/models/biz/mod.rs b/heromodels/src/models/biz/mod.rs index 27a1718..15ace26 100644 --- a/heromodels/src/models/biz/mod.rs +++ b/heromodels/src/models/biz/mod.rs @@ -2,23 +2,24 @@ // Sub-modules will be declared here pub mod company; +pub mod payment; pub mod product; // pub mod sale; // pub mod shareholder; // pub mod user; // Re-export main types from sub-modules -pub use company::{Company, CompanyStatus, BusinessType}; +pub use company::{BusinessType, Company, CompanyStatus}; +pub use payment::{Payment, PaymentStatus}; pub mod shareholder; +pub use product::{Product, ProductComponent, ProductStatus, ProductType}; pub use shareholder::{Shareholder, ShareholderType}; -pub use product::{Product, ProductType, ProductStatus, ProductComponent}; pub mod sale; pub use sale::{Sale, SaleItem, SaleStatus}; // pub use user::{User}; // Assuming a simple User model for now - #[cfg(feature = "rhai")] pub mod rhai; #[cfg(feature = "rhai")] diff --git a/heromodels/src/models/biz/payment.rs b/heromodels/src/models/biz/payment.rs new file mode 100644 index 0000000..d678889 --- /dev/null +++ b/heromodels/src/models/biz/payment.rs @@ -0,0 +1,216 @@ +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use rhai::{CustomType, TypeBuilder}; +use serde::{Deserialize, Serialize}; // For #[derive(CustomType)] + +// --- Enums --- + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum PaymentStatus { + Pending, + Processing, + Completed, + Failed, + Refunded, +} + +impl std::fmt::Display for PaymentStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PaymentStatus::Pending => write!(f, "Pending"), + PaymentStatus::Processing => write!(f, "Processing"), + PaymentStatus::Completed => write!(f, "Completed"), + PaymentStatus::Failed => write!(f, "Failed"), + PaymentStatus::Refunded => write!(f, "Refunded"), + } + } +} + +impl Default for PaymentStatus { + fn default() -> Self { + PaymentStatus::Pending + } +} + +// --- Payment Struct --- +#[model] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +pub struct Payment { + pub base_data: BaseModelData, + + // Stripe payment intent ID for tracking + #[index] + pub payment_intent_id: String, + + // Reference to the company this payment is for + #[index] + pub company_id: u32, + + // Payment plan details + pub payment_plan: String, // "monthly", "yearly", "two_year" + pub setup_fee: f64, + pub monthly_fee: f64, + pub total_amount: f64, + pub currency: String, // "usd" + + pub status: PaymentStatus, + pub stripe_customer_id: Option, + pub created_at: i64, // Timestamp + pub completed_at: Option, // Completion timestamp +} + +// Model trait implementation is automatically generated by #[model] attribute + +// --- Builder Pattern --- + +impl Payment { + pub fn new( + payment_intent_id: String, + company_id: u32, + payment_plan: String, + setup_fee: f64, + monthly_fee: f64, + total_amount: f64, + ) -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + base_data: BaseModelData::new(), + payment_intent_id, + company_id, + payment_plan, + setup_fee, + monthly_fee, + total_amount, + currency: "usd".to_string(), // Default to USD + status: PaymentStatus::default(), + stripe_customer_id: None, + created_at: now, + completed_at: None, + } + } + + pub fn payment_intent_id(mut self, payment_intent_id: String) -> Self { + self.payment_intent_id = payment_intent_id; + self + } + + pub fn company_id(mut self, company_id: u32) -> Self { + self.company_id = company_id; + self + } + + pub fn payment_plan(mut self, payment_plan: String) -> Self { + self.payment_plan = payment_plan; + self + } + + pub fn setup_fee(mut self, setup_fee: f64) -> Self { + self.setup_fee = setup_fee; + self + } + + pub fn monthly_fee(mut self, monthly_fee: f64) -> Self { + self.monthly_fee = monthly_fee; + self + } + + pub fn total_amount(mut self, total_amount: f64) -> Self { + self.total_amount = total_amount; + self + } + + pub fn status(mut self, status: PaymentStatus) -> Self { + self.status = status; + self + } + + pub fn stripe_customer_id(mut self, stripe_customer_id: Option) -> Self { + self.stripe_customer_id = stripe_customer_id; + self + } + + pub fn currency(mut self, currency: String) -> Self { + self.currency = currency; + self + } + + pub fn created_at(mut self, created_at: i64) -> Self { + self.created_at = created_at; + self + } + + pub fn completed_at(mut self, completed_at: Option) -> Self { + self.completed_at = completed_at; + self + } + + // --- Business Logic Methods --- + + /// Complete the payment with optional Stripe customer ID + pub fn complete_payment(mut self, stripe_customer_id: Option) -> Self { + self.status = PaymentStatus::Completed; + self.stripe_customer_id = stripe_customer_id; + self.completed_at = Some(chrono::Utc::now().timestamp()); + self.base_data.update_modified(); + self + } + + /// Mark payment as processing + pub fn process_payment(mut self) -> Self { + self.status = PaymentStatus::Processing; + self.base_data.update_modified(); + self + } + + /// Mark payment as failed + pub fn fail_payment(mut self) -> Self { + self.status = PaymentStatus::Failed; + self.base_data.update_modified(); + self + } + + /// Refund the payment + pub fn refund_payment(mut self) -> Self { + self.status = PaymentStatus::Refunded; + self.base_data.update_modified(); + self + } + + /// Check if payment is completed + pub fn is_completed(&self) -> bool { + self.status == PaymentStatus::Completed + } + + /// Check if payment is pending + pub fn is_pending(&self) -> bool { + self.status == PaymentStatus::Pending + } + + /// Check if payment is processing + pub fn is_processing(&self) -> bool { + self.status == PaymentStatus::Processing + } + + /// Check if payment has failed + pub fn has_failed(&self) -> bool { + self.status == PaymentStatus::Failed + } + + /// Check if payment is refunded + pub fn is_refunded(&self) -> bool { + self.status == PaymentStatus::Refunded + } + + // Setter for base_data fields if needed directly + pub fn set_base_created_at(mut self, created_at: i64) -> Self { + self.base_data.created_at = created_at; + self + } + + pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { + self.base_data.modified_at = modified_at; + self + } +} + +// Tests for Payment model are located in tests/payment.rs diff --git a/heromodels/src/models/biz/rhai.rs b/heromodels/src/models/biz/rhai.rs index 7a8c2ec..327a028 100644 --- a/heromodels/src/models/biz/rhai.rs +++ b/heromodels/src/models/biz/rhai.rs @@ -1,12 +1,13 @@ -use rhai::{Engine, Module, Dynamic, EvalAltResult, Position}; -use std::sync::Arc; +use super::company::{BusinessType, Company, CompanyStatus}; +use super::payment::{Payment, PaymentStatus}; use crate::db::Collection; // For db.set and db.get_by_id use crate::db::hero::OurDB; -use super::company::{Company, CompanyStatus, BusinessType}; -use crate::models::biz::shareholder::{Shareholder, ShareholderType}; -use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent}; +use crate::models::biz::product::{Product, ProductComponent, ProductStatus, ProductType}; use crate::models::biz::sale::{Sale, SaleItem, SaleStatus}; +use crate::models::biz::shareholder::{Shareholder, ShareholderType}; use heromodels_core::Model; +use rhai::{Dynamic, Engine, EvalAltResult, Module, Position}; +use std::sync::Arc; // Helper function to convert i64 to u32, returning a Rhai error if conversion fails fn id_from_i64(id_val: i64) -> Result> { @@ -23,9 +24,13 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { // --- Enum Constants: CompanyStatus --- let mut status_constants_module = Module::new(); + status_constants_module.set_var( + "PendingPayment", + Dynamic::from(CompanyStatus::PendingPayment.clone()), + ); status_constants_module.set_var("Active", Dynamic::from(CompanyStatus::Active.clone())); - status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); status_constants_module.set_var("Suspended", Dynamic::from(CompanyStatus::Suspended.clone())); + status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); engine.register_static_module("CompanyStatusConstants", status_constants_module.into()); engine.register_type_with_name::("CompanyStatus"); @@ -36,80 +41,319 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { business_type_constants_module.set_var("Twin", Dynamic::from(BusinessType::Twin.clone())); business_type_constants_module.set_var("Starter", Dynamic::from(BusinessType::Starter.clone())); business_type_constants_module.set_var("Global", Dynamic::from(BusinessType::Global.clone())); - engine.register_static_module("BusinessTypeConstants", business_type_constants_module.into()); + engine.register_static_module( + "BusinessTypeConstants", + business_type_constants_module.into(), + ); engine.register_type_with_name::("BusinessType"); - // --- Company --- + // --- Company --- engine.register_type_with_name::("Company"); // Constructor - engine.register_fn("new_company", |name: String, registration_number: String, incorporation_date: i64| -> Result> { Ok(Company::new(name, registration_number, incorporation_date)) }); + engine.register_fn( + "new_company", + |name: String, + registration_number: String, + incorporation_date: i64| + -> Result> { + Ok(Company::new(name, registration_number, incorporation_date)) + }, + ); // Getters for Company - engine.register_get("id", |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }); - engine.register_get("created_at", |company: &mut Company| -> Result> { Ok(company.base_data.created_at) }); - engine.register_get("modified_at", |company: &mut Company| -> Result> { Ok(company.base_data.modified_at) }); - engine.register_get("name", |company: &mut Company| -> Result> { Ok(company.name.clone()) }); - engine.register_get("registration_number", |company: &mut Company| -> Result> { Ok(company.registration_number.clone()) }); - engine.register_get("incorporation_date", |company: &mut Company| -> Result> { Ok(company.incorporation_date as i64) }); - engine.register_get("fiscal_year_end", |company: &mut Company| -> Result> { Ok(company.fiscal_year_end.clone()) }); - engine.register_get("email", |company: &mut Company| -> Result> { Ok(company.email.clone()) }); - engine.register_get("phone", |company: &mut Company| -> Result> { Ok(company.phone.clone()) }); - engine.register_get("website", |company: &mut Company| -> Result> { Ok(company.website.clone()) }); - engine.register_get("address", |company: &mut Company| -> Result> { Ok(company.address.clone()) }); - engine.register_get("business_type", |company: &mut Company| -> Result> { Ok(company.business_type.clone()) }); - engine.register_get("industry", |company: &mut Company| -> Result> { Ok(company.industry.clone()) }); - engine.register_get("description", |company: &mut Company| -> Result> { Ok(company.description.clone()) }); - engine.register_get("status", |company: &mut Company| -> Result> { Ok(company.status.clone()) }); + engine.register_get( + "id", + |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }, + ); + engine.register_get( + "created_at", + |company: &mut Company| -> Result> { + Ok(company.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |company: &mut Company| -> Result> { + Ok(company.base_data.modified_at) + }, + ); + engine.register_get( + "name", + |company: &mut Company| -> Result> { Ok(company.name.clone()) }, + ); + engine.register_get( + "registration_number", + |company: &mut Company| -> Result> { + Ok(company.registration_number.clone()) + }, + ); + engine.register_get( + "incorporation_date", + |company: &mut Company| -> Result> { + Ok(company.incorporation_date as i64) + }, + ); + engine.register_get( + "fiscal_year_end", + |company: &mut Company| -> Result> { + Ok(company.fiscal_year_end.clone()) + }, + ); + engine.register_get( + "email", + |company: &mut Company| -> Result> { Ok(company.email.clone()) }, + ); + engine.register_get( + "phone", + |company: &mut Company| -> Result> { Ok(company.phone.clone()) }, + ); + engine.register_get( + "website", + |company: &mut Company| -> Result> { + Ok(company.website.clone()) + }, + ); + engine.register_get( + "address", + |company: &mut Company| -> Result> { + Ok(company.address.clone()) + }, + ); + engine.register_get( + "business_type", + |company: &mut Company| -> Result> { + Ok(company.business_type.clone()) + }, + ); + engine.register_get( + "industry", + |company: &mut Company| -> Result> { + Ok(company.industry.clone()) + }, + ); + engine.register_get( + "description", + |company: &mut Company| -> Result> { + Ok(company.description.clone()) + }, + ); + engine.register_get( + "status", + |company: &mut Company| -> Result> { + Ok(company.status.clone()) + }, + ); // Builder methods for Company - engine.register_fn("fiscal_year_end", |company: Company, fiscal_year_end: String| -> Result> { Ok(company.fiscal_year_end(fiscal_year_end)) }); - engine.register_fn("email", |company: Company, email: String| -> Result> { Ok(company.email(email)) }); - engine.register_fn("phone", |company: Company, phone: String| -> Result> { Ok(company.phone(phone)) }); - engine.register_fn("website", |company: Company, website: String| -> Result> { Ok(company.website(website)) }); - engine.register_fn("address", |company: Company, address: String| -> Result> { Ok(company.address(address)) }); - engine.register_fn("business_type", |company: Company, business_type: BusinessType| -> Result> { Ok(company.business_type(business_type)) }); - engine.register_fn("industry", |company: Company, industry: String| -> Result> { Ok(company.industry(industry)) }); - engine.register_fn("description", |company: Company, description: String| -> Result> { Ok(company.description(description)) }); - engine.register_fn("status", |company: Company, status: CompanyStatus| -> Result> { Ok(company.status(status)) }); - engine.register_fn("set_base_created_at", |company: Company, created_at: i64| -> Result> { Ok(company.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |company: Company, modified_at: i64| -> Result> { Ok(company.set_base_modified_at(modified_at)) }); + engine.register_fn( + "fiscal_year_end", + |company: Company, fiscal_year_end: String| -> Result> { + Ok(company.fiscal_year_end(fiscal_year_end)) + }, + ); + engine.register_fn( + "email", + |company: Company, email: String| -> Result> { + Ok(company.email(email)) + }, + ); + engine.register_fn( + "phone", + |company: Company, phone: String| -> Result> { + Ok(company.phone(phone)) + }, + ); + engine.register_fn( + "website", + |company: Company, website: String| -> Result> { + Ok(company.website(website)) + }, + ); + engine.register_fn( + "address", + |company: Company, address: String| -> Result> { + Ok(company.address(address)) + }, + ); + engine.register_fn( + "business_type", + |company: Company, business_type: BusinessType| -> Result> { + Ok(company.business_type(business_type)) + }, + ); + engine.register_fn( + "industry", + |company: Company, industry: String| -> Result> { + Ok(company.industry(industry)) + }, + ); + engine.register_fn( + "description", + |company: Company, description: String| -> Result> { + Ok(company.description(description)) + }, + ); + engine.register_fn( + "status", + |company: Company, status: CompanyStatus| -> Result> { + Ok(company.status(status)) + }, + ); + engine.register_fn( + "set_base_created_at", + |company: Company, created_at: i64| -> Result> { + Ok(company.set_base_created_at(created_at)) + }, + ); + engine.register_fn( + "set_base_modified_at", + |company: Company, modified_at: i64| -> Result> { + Ok(company.set_base_modified_at(modified_at)) + }, + ); // --- Enum Constants: ShareholderType --- let mut shareholder_type_constants_module = Module::new(); - shareholder_type_constants_module.set_var("Individual", Dynamic::from(ShareholderType::Individual.clone())); - shareholder_type_constants_module.set_var("Corporate", Dynamic::from(ShareholderType::Corporate.clone())); - engine.register_static_module("ShareholderTypeConstants", shareholder_type_constants_module.into()); + shareholder_type_constants_module.set_var( + "Individual", + Dynamic::from(ShareholderType::Individual.clone()), + ); + shareholder_type_constants_module.set_var( + "Corporate", + Dynamic::from(ShareholderType::Corporate.clone()), + ); + engine.register_static_module( + "ShareholderTypeConstants", + shareholder_type_constants_module.into(), + ); engine.register_type_with_name::("ShareholderType"); // --- Shareholder --- engine.register_type_with_name::("Shareholder"); // Constructor for Shareholder (minimal, takes only ID) - engine.register_fn("new_shareholder", || -> Result> { Ok(Shareholder::new()) }); + engine.register_fn( + "new_shareholder", + || -> Result> { Ok(Shareholder::new()) }, + ); // Getters for Shareholder - engine.register_get("id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.get_id() as i64) }); - engine.register_get("created_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.created_at) }); - engine.register_get("modified_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.modified_at) }); - engine.register_get("company_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.company_id as i64) }); - engine.register_get("user_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.user_id as i64) }); - engine.register_get("name", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.name.clone()) }); - engine.register_get("shares", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.shares) }); - engine.register_get("percentage", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.percentage) }); - engine.register_get("type_", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.type_.clone()) }); - engine.register_get("since", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.since) }); + engine.register_get( + "id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.get_id() as i64) + }, + ); + engine.register_get( + "created_at", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.base_data.modified_at) + }, + ); + engine.register_get( + "company_id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.company_id as i64) + }, + ); + engine.register_get( + "user_id", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.user_id as i64) + }, + ); + engine.register_get( + "name", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.name.clone()) + }, + ); + engine.register_get( + "shares", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.shares) + }, + ); + engine.register_get( + "percentage", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.percentage) + }, + ); + engine.register_get( + "type_", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.type_.clone()) + }, + ); + engine.register_get( + "since", + |shareholder: &mut Shareholder| -> Result> { + Ok(shareholder.since) + }, + ); // Builder methods for Shareholder - engine.register_fn("company_id", |shareholder: Shareholder, company_id: i64| -> Result> { Ok(shareholder.company_id(company_id as u32)) }); - engine.register_fn("user_id", |shareholder: Shareholder, user_id: i64| -> Result> { Ok(shareholder.user_id(user_id as u32)) }); - engine.register_fn("name", |shareholder: Shareholder, name: String| -> Result> { Ok(shareholder.name(name)) }); - engine.register_fn("shares", |shareholder: Shareholder, shares: f64| -> Result> { Ok(shareholder.shares(shares)) }); - engine.register_fn("percentage", |shareholder: Shareholder, percentage: f64| -> Result> { Ok(shareholder.percentage(percentage)) }); - engine.register_fn("type_", |shareholder: Shareholder, type_: ShareholderType| -> Result> { Ok(shareholder.type_(type_)) }); - engine.register_fn("since", |shareholder: Shareholder, since: i64| -> Result> { Ok(shareholder.since(since)) }); - engine.register_fn("set_base_created_at", |shareholder: Shareholder, created_at: i64| -> Result> { Ok(shareholder.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |shareholder: Shareholder, modified_at: i64| -> Result> { Ok(shareholder.set_base_modified_at(modified_at)) }); - + engine.register_fn( + "company_id", + |shareholder: Shareholder, company_id: i64| -> Result> { + Ok(shareholder.company_id(company_id as u32)) + }, + ); + engine.register_fn( + "user_id", + |shareholder: Shareholder, user_id: i64| -> Result> { + Ok(shareholder.user_id(user_id as u32)) + }, + ); + engine.register_fn( + "name", + |shareholder: Shareholder, name: String| -> Result> { + Ok(shareholder.name(name)) + }, + ); + engine.register_fn( + "shares", + |shareholder: Shareholder, shares: f64| -> Result> { + Ok(shareholder.shares(shares)) + }, + ); + engine.register_fn( + "percentage", + |shareholder: Shareholder, percentage: f64| -> Result> { + Ok(shareholder.percentage(percentage)) + }, + ); + engine.register_fn( + "type_", + |shareholder: Shareholder, + type_: ShareholderType| + -> Result> { Ok(shareholder.type_(type_)) }, + ); + engine.register_fn( + "since", + |shareholder: Shareholder, since: i64| -> Result> { + Ok(shareholder.since(since)) + }, + ); + engine.register_fn( + "set_base_created_at", + |shareholder: Shareholder, created_at: i64| -> Result> { + Ok(shareholder.set_base_created_at(created_at)) + }, + ); + engine.register_fn( + "set_base_modified_at", + |shareholder: Shareholder, modified_at: i64| -> Result> { + Ok(shareholder.set_base_modified_at(modified_at)) + }, + ); // --- Enum Constants: ProductType --- let mut product_type_constants_module = Module::new(); @@ -120,9 +364,16 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { // --- Enum Constants: ProductStatus --- let mut product_status_constants_module = Module::new(); - product_status_constants_module.set_var("Available", Dynamic::from(ProductStatus::Available.clone())); - product_status_constants_module.set_var("Unavailable", Dynamic::from(ProductStatus::Unavailable.clone())); - engine.register_static_module("ProductStatusConstants", product_status_constants_module.into()); + product_status_constants_module + .set_var("Available", Dynamic::from(ProductStatus::Available.clone())); + product_status_constants_module.set_var( + "Unavailable", + Dynamic::from(ProductStatus::Unavailable.clone()), + ); + engine.register_static_module( + "ProductStatusConstants", + product_status_constants_module.into(), + ); engine.register_type_with_name::("ProductStatus"); // --- Enum Constants: SaleStatus --- @@ -133,200 +384,887 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { engine.register_static_module("SaleStatusConstants", sale_status_module.into()); engine.register_type_with_name::("SaleStatus"); + // --- Enum Constants: PaymentStatus --- + let mut payment_status_module = Module::new(); + payment_status_module.set_var("Pending", Dynamic::from(PaymentStatus::Pending.clone())); + payment_status_module.set_var("Completed", Dynamic::from(PaymentStatus::Completed.clone())); + payment_status_module.set_var("Failed", Dynamic::from(PaymentStatus::Failed.clone())); + payment_status_module.set_var("Refunded", Dynamic::from(PaymentStatus::Refunded.clone())); + engine.register_static_module("PaymentStatusConstants", payment_status_module.into()); + engine.register_type_with_name::("PaymentStatus"); + // --- ProductComponent --- - engine.register_type_with_name::("ProductComponent") - .register_fn("new_product_component", |name: String| -> Result> { Ok(ProductComponent::new(name)) }) - .register_get("name", |pc: &mut ProductComponent| -> Result> { Ok(pc.name.clone()) }) - .register_fn("name", |pc: ProductComponent, name: String| -> Result> { Ok(pc.name(name)) }) - .register_get("description", |pc: &mut ProductComponent| -> Result> { Ok(pc.description.clone()) }) - .register_fn("description", |pc: ProductComponent, description: String| -> Result> { Ok(pc.description(description)) }) - .register_get("quantity", |pc: &mut ProductComponent| -> Result> { Ok(pc.quantity as i64) }) - .register_fn("quantity", |pc: ProductComponent, quantity: i64| -> Result> { Ok(pc.quantity(quantity as u32)) }); + engine + .register_type_with_name::("ProductComponent") + .register_fn( + "new_product_component", + |name: String| -> Result> { + Ok(ProductComponent::new(name)) + }, + ) + .register_get( + "name", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.name.clone()) + }, + ) + .register_fn( + "name", + |pc: ProductComponent, name: String| -> Result> { + Ok(pc.name(name)) + }, + ) + .register_get( + "description", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.description.clone()) + }, + ) + .register_fn( + "description", + |pc: ProductComponent, + description: String| + -> Result> { + Ok(pc.description(description)) + }, + ) + .register_get( + "quantity", + |pc: &mut ProductComponent| -> Result> { + Ok(pc.quantity as i64) + }, + ) + .register_fn( + "quantity", + |pc: ProductComponent, quantity: i64| -> Result> { + Ok(pc.quantity(quantity as u32)) + }, + ); // --- Product --- - engine.register_type_with_name::("Product") - .register_fn("new_product", || -> Result> { Ok(Product::new()) }) - // Getters for Product - .register_get("id", |p: &mut Product| -> Result> { Ok(p.base_data.id as i64) }) - .register_get("name", |p: &mut Product| -> Result> { Ok(p.name.clone()) }) - .register_get("description", |p: &mut Product| -> Result> { Ok(p.description.clone()) }) - .register_get("price", |p: &mut Product| -> Result> { Ok(p.price) }) - .register_get("type_", |p: &mut Product| -> Result> { Ok(p.type_.clone()) }) - .register_get("category", |p: &mut Product| -> Result> { Ok(p.category.clone()) }) - .register_get("status", |p: &mut Product| -> Result> { Ok(p.status.clone()) }) - .register_get("max_amount", |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }) - .register_get("purchase_till", |p: &mut Product| -> Result> { Ok(p.purchase_till) }) - .register_get("active_till", |p: &mut Product| -> Result> { Ok(p.active_till) }) - .register_get("components", |p: &mut Product| -> Result> { - let rhai_array = p.components.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) + engine + .register_type_with_name::("Product") + .register_fn("new_product", || -> Result> { + Ok(Product::new()) }) + // Getters for Product + .register_get("id", |p: &mut Product| -> Result> { + Ok(p.base_data.id as i64) + }) + .register_get( + "name", + |p: &mut Product| -> Result> { Ok(p.name.clone()) }, + ) + .register_get( + "description", + |p: &mut Product| -> Result> { Ok(p.description.clone()) }, + ) + .register_get( + "price", + |p: &mut Product| -> Result> { Ok(p.price) }, + ) + .register_get( + "type_", + |p: &mut Product| -> Result> { Ok(p.type_.clone()) }, + ) + .register_get( + "category", + |p: &mut Product| -> Result> { Ok(p.category.clone()) }, + ) + .register_get( + "status", + |p: &mut Product| -> Result> { Ok(p.status.clone()) }, + ) + .register_get( + "max_amount", + |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }, + ) + .register_get( + "purchase_till", + |p: &mut Product| -> Result> { Ok(p.purchase_till) }, + ) + .register_get( + "active_till", + |p: &mut Product| -> Result> { Ok(p.active_till) }, + ) + .register_get( + "components", + |p: &mut Product| -> Result> { + let rhai_array = p + .components + .iter() + .cloned() + .map(Dynamic::from) + .collect::(); + Ok(rhai_array) + }, + ) // Getters for BaseModelData fields - .register_get("created_at", |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }) - .register_get("modified_at", |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }) - .register_get("comments", |p: &mut Product| -> Result, Box> { Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) }) + .register_get( + "created_at", + |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }, + ) + .register_get( + "modified_at", + |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }, + ) + .register_get( + "comments", + |p: &mut Product| -> Result, Box> { + Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) + }, + ) // Builder methods for Product - .register_fn("name", |p: Product, name: String| -> Result> { Ok(p.name(name)) }) - .register_fn("description", |p: Product, description: String| -> Result> { Ok(p.description(description)) }) - .register_fn("price", |p: Product, price: f64| -> Result> { Ok(p.price(price)) }) - .register_fn("type_", |p: Product, type_: ProductType| -> Result> { Ok(p.type_(type_)) }) - .register_fn("category", |p: Product, category: String| -> Result> { Ok(p.category(category)) }) - .register_fn("status", |p: Product, status: ProductStatus| -> Result> { Ok(p.status(status)) }) - .register_fn("max_amount", |p: Product, max_amount: i64| -> Result> { Ok(p.max_amount(max_amount as u16)) }) - .register_fn("purchase_till", |p: Product, purchase_till: i64| -> Result> { Ok(p.purchase_till(purchase_till)) }) - .register_fn("active_till", |p: Product, active_till: i64| -> Result> { Ok(p.active_till(active_till)) }) - .register_fn("add_component", |p: Product, component: ProductComponent| -> Result> { Ok(p.add_component(component)) }) - .register_fn("components", |p: Product, components: Vec| -> Result> { Ok(p.components(components)) }) - .register_fn("set_base_created_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_modified_at(time)) }) - .register_fn("add_base_comment_id", |p: Product, comment_id: i64| -> Result> { Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) }) - .register_fn("set_base_comment_ids", |p: Product, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(p.set_base_comment_ids(u32_ids)) - }); + .register_fn( + "name", + |p: Product, name: String| -> Result> { Ok(p.name(name)) }, + ) + .register_fn( + "description", + |p: Product, description: String| -> Result> { + Ok(p.description(description)) + }, + ) + .register_fn( + "price", + |p: Product, price: f64| -> Result> { Ok(p.price(price)) }, + ) + .register_fn( + "type_", + |p: Product, type_: ProductType| -> Result> { + Ok(p.type_(type_)) + }, + ) + .register_fn( + "category", + |p: Product, category: String| -> Result> { + Ok(p.category(category)) + }, + ) + .register_fn( + "status", + |p: Product, status: ProductStatus| -> Result> { + Ok(p.status(status)) + }, + ) + .register_fn( + "max_amount", + |p: Product, max_amount: i64| -> Result> { + Ok(p.max_amount(max_amount as u16)) + }, + ) + .register_fn( + "purchase_till", + |p: Product, purchase_till: i64| -> Result> { + Ok(p.purchase_till(purchase_till)) + }, + ) + .register_fn( + "active_till", + |p: Product, active_till: i64| -> Result> { + Ok(p.active_till(active_till)) + }, + ) + .register_fn( + "add_component", + |p: Product, component: ProductComponent| -> Result> { + Ok(p.add_component(component)) + }, + ) + .register_fn( + "components", + |p: Product, + components: Vec| + -> Result> { Ok(p.components(components)) }, + ) + .register_fn( + "set_base_created_at", + |p: Product, time: i64| -> Result> { + Ok(p.set_base_created_at(time)) + }, + ) + .register_fn( + "set_base_modified_at", + |p: Product, time: i64| -> Result> { + Ok(p.set_base_modified_at(time)) + }, + ) + .register_fn( + "add_base_comment_id", + |p: Product, comment_id: i64| -> Result> { + Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) + }, + ) + .register_fn( + "set_base_comment_ids", + |p: Product, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids + .into_iter() + .map(id_from_i64) + .collect::, _>>()?; + Ok(p.set_base_comment_ids(u32_ids)) + }, + ); // --- SaleItem --- engine.register_type_with_name::("SaleItem"); - engine.register_fn("new_sale_item", |product_id_i64: i64, name: String, quantity_i64: i64, unit_price: f64, subtotal: f64| -> Result> { - Ok(SaleItem::new(id_from_i64(product_id_i64)?, name, quantity_i64 as i32, unit_price, subtotal)) - }); + engine.register_fn( + "new_sale_item", + |product_id_i64: i64, + name: String, + quantity_i64: i64, + unit_price: f64, + subtotal: f64| + -> Result> { + Ok(SaleItem::new( + id_from_i64(product_id_i64)?, + name, + quantity_i64 as i32, + unit_price, + subtotal, + )) + }, + ); // Getters for SaleItem - engine.register_get("product_id", |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }); - engine.register_get("name", |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }); - engine.register_get("quantity", |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }); - engine.register_get("unit_price", |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }); - engine.register_get("subtotal", |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }); - engine.register_get("service_active_until", |si: &mut SaleItem| -> Result, Box> { Ok(si.service_active_until) }); + engine.register_get( + "product_id", + |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }, + ); + engine.register_get( + "name", + |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }, + ); + engine.register_get( + "quantity", + |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }, + ); + engine.register_get( + "unit_price", + |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }, + ); + engine.register_get( + "subtotal", + |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }, + ); + engine.register_get( + "service_active_until", + |si: &mut SaleItem| -> Result, Box> { + Ok(si.service_active_until) + }, + ); // Builder-style methods for SaleItem - engine.register_type_with_name::("SaleItem") - .register_fn("product_id", |item: SaleItem, product_id_i64: i64| -> Result> { Ok(item.product_id(id_from_i64(product_id_i64)?)) }) - .register_fn("name", |item: SaleItem, name: String| -> Result> { Ok(item.name(name)) }) - .register_fn("quantity", |item: SaleItem, quantity_i64: i64| -> Result> { Ok(item.quantity(quantity_i64 as i32)) }) - .register_fn("unit_price", |item: SaleItem, unit_price: f64| -> Result> { Ok(item.unit_price(unit_price)) }) - .register_fn("subtotal", |item: SaleItem, subtotal: f64| -> Result> { Ok(item.subtotal(subtotal)) }) - .register_fn("service_active_until", |item: SaleItem, until: Option| -> Result> { Ok(item.service_active_until(until)) }); + engine + .register_type_with_name::("SaleItem") + .register_fn( + "product_id", + |item: SaleItem, product_id_i64: i64| -> Result> { + Ok(item.product_id(id_from_i64(product_id_i64)?)) + }, + ) + .register_fn( + "name", + |item: SaleItem, name: String| -> Result> { + Ok(item.name(name)) + }, + ) + .register_fn( + "quantity", + |item: SaleItem, quantity_i64: i64| -> Result> { + Ok(item.quantity(quantity_i64 as i32)) + }, + ) + .register_fn( + "unit_price", + |item: SaleItem, unit_price: f64| -> Result> { + Ok(item.unit_price(unit_price)) + }, + ) + .register_fn( + "subtotal", + |item: SaleItem, subtotal: f64| -> Result> { + Ok(item.subtotal(subtotal)) + }, + ) + .register_fn( + "service_active_until", + |item: SaleItem, until: Option| -> Result> { + Ok(item.service_active_until(until)) + }, + ); // --- Sale --- engine.register_type_with_name::("Sale"); - engine.register_fn("new_sale", |company_id_i64: i64, buyer_name: String, buyer_email: String, total_amount: f64, status: SaleStatus, sale_date: i64| -> Result> { Ok(Sale::new(id_from_i64(company_id_i64)?, buyer_name, buyer_email, total_amount, status, sale_date)) }); + engine.register_fn( + "new_sale", + |company_id_i64: i64, + buyer_name: String, + buyer_email: String, + total_amount: f64, + status: SaleStatus, + sale_date: i64| + -> Result> { + Ok(Sale::new( + id_from_i64(company_id_i64)?, + buyer_name, + buyer_email, + total_amount, + status, + sale_date, + )) + }, + ); // Getters for Sale - engine.register_get("id", |s: &mut Sale| -> Result> { Ok(s.get_id() as i64) }); - engine.register_get("customer_id", |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }); - engine.register_get("buyer_name", |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }); - engine.register_get("buyer_email", |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }); - engine.register_get("total_amount", |s: &mut Sale| -> Result> { Ok(s.total_amount) }); - engine.register_get("status", |s: &mut Sale| -> Result> { Ok(s.status.clone()) }); - engine.register_get("sale_date", |s: &mut Sale| -> Result> { Ok(s.sale_date) }); - engine.register_get("items", |s: &mut Sale| -> Result> { - Ok(s.items.iter().cloned().map(Dynamic::from).collect::()) + engine.register_get("id", |s: &mut Sale| -> Result> { + Ok(s.get_id() as i64) }); - engine.register_get("notes", |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }); - engine.register_get("created_at", |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }); - engine.register_get("modified_at", |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }); + engine.register_get( + "customer_id", + |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }, + ); + engine.register_get( + "buyer_name", + |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }, + ); + engine.register_get( + "buyer_email", + |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }, + ); + engine.register_get( + "total_amount", + |s: &mut Sale| -> Result> { Ok(s.total_amount) }, + ); + engine.register_get( + "status", + |s: &mut Sale| -> Result> { Ok(s.status.clone()) }, + ); + engine.register_get( + "sale_date", + |s: &mut Sale| -> Result> { Ok(s.sale_date) }, + ); + engine.register_get( + "items", + |s: &mut Sale| -> Result> { + Ok(s.items + .iter() + .cloned() + .map(Dynamic::from) + .collect::()) + }, + ); + engine.register_get( + "notes", + |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }, + ); + engine.register_get( + "created_at", + |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }, + ); + engine.register_get( + "modified_at", + |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }, + ); // engine.register_get("uuid", |s: &mut Sale| -> Result, Box> { Ok(s.base_data().uuid.clone()) }); // UUID not in BaseModelData - engine.register_get("comments", |s: &mut Sale| -> Result> { - Ok(s.base_data.comments.iter().map(|&id| Dynamic::from(id as i64)).collect::()) - }); + engine.register_get( + "comments", + |s: &mut Sale| -> Result> { + Ok(s.base_data + .comments + .iter() + .map(|&id| Dynamic::from(id as i64)) + .collect::()) + }, + ); // Builder-style methods for Sale - engine.register_type_with_name::("Sale") - .register_fn("customer_id", |s: Sale, customer_id_i64: i64| -> Result> { Ok(s.company_id(id_from_i64(customer_id_i64)?)) }) - .register_fn("buyer_name", |s: Sale, buyer_name: String| -> Result> { Ok(s.buyer_name(buyer_name)) }) - .register_fn("buyer_email", |s: Sale, buyer_email: String| -> Result> { Ok(s.buyer_email(buyer_email)) }) - .register_fn("total_amount", |s: Sale, total_amount: f64| -> Result> { Ok(s.total_amount(total_amount)) }) - .register_fn("status", |s: Sale, status: SaleStatus| -> Result> { Ok(s.status(status)) }) - .register_fn("sale_date", |s: Sale, sale_date: i64| -> Result> { Ok(s.sale_date(sale_date)) }) - .register_fn("add_item", |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }) - .register_fn("items", |s: Sale, items: Vec| -> Result> { Ok(s.items(items)) }) - .register_fn("notes", |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }) - .register_fn("set_base_id", |s: Sale, id_i64: i64| -> Result> { Ok(s.set_base_id(id_from_i64(id_i64)?)) }) + engine + .register_type_with_name::("Sale") + .register_fn( + "customer_id", + |s: Sale, customer_id_i64: i64| -> Result> { + Ok(s.company_id(id_from_i64(customer_id_i64)?)) + }, + ) + .register_fn( + "buyer_name", + |s: Sale, buyer_name: String| -> Result> { + Ok(s.buyer_name(buyer_name)) + }, + ) + .register_fn( + "buyer_email", + |s: Sale, buyer_email: String| -> Result> { + Ok(s.buyer_email(buyer_email)) + }, + ) + .register_fn( + "total_amount", + |s: Sale, total_amount: f64| -> Result> { + Ok(s.total_amount(total_amount)) + }, + ) + .register_fn( + "status", + |s: Sale, status: SaleStatus| -> Result> { + Ok(s.status(status)) + }, + ) + .register_fn( + "sale_date", + |s: Sale, sale_date: i64| -> Result> { + Ok(s.sale_date(sale_date)) + }, + ) + .register_fn( + "add_item", + |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }, + ) + .register_fn( + "items", + |s: Sale, items: Vec| -> Result> { + Ok(s.items(items)) + }, + ) + .register_fn( + "notes", + |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }, + ) + .register_fn( + "set_base_id", + |s: Sale, id_i64: i64| -> Result> { + Ok(s.set_base_id(id_from_i64(id_i64)?)) + }, + ) // .register_fn("set_base_uuid", |s: Sale, uuid: Option| -> Result> { Ok(s.set_base_uuid(uuid)) }) // UUID not in BaseModelData - .register_fn("set_base_created_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_modified_at(time)) }) - .register_fn("add_base_comment", |s: Sale, comment_id_i64: i64| -> Result> { Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) }) - .register_fn("set_base_comments", |s: Sale, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(s.set_base_comments(u32_ids)) - }); + .register_fn( + "set_base_created_at", + |s: Sale, time: i64| -> Result> { + Ok(s.set_base_created_at(time)) + }, + ) + .register_fn( + "set_base_modified_at", + |s: Sale, time: i64| -> Result> { + Ok(s.set_base_modified_at(time)) + }, + ) + .register_fn( + "add_base_comment", + |s: Sale, comment_id_i64: i64| -> Result> { + Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) + }, + ) + .register_fn( + "set_base_comments", + |s: Sale, comment_ids: Vec| -> Result> { + let u32_ids = comment_ids + .into_iter() + .map(id_from_i64) + .collect::, _>>()?; + Ok(s.set_base_comments(u32_ids)) + }, + ); // DB functions for Product let captured_db_for_set_product = Arc::clone(&db); - engine.register_fn("set_product", move |product: Product| -> Result> { - let original_id_for_error = product.get_id(); - captured_db_for_set_product.set(&product) - .map(|(_id, updated_product)| updated_product) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Product (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_product", + move |product: Product| -> Result> { + let original_id_for_error = product.get_id(); + captured_db_for_set_product + .set(&product) + .map(|(_id, updated_product)| updated_product) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Product (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_prod = Arc::clone(&db); - engine.register_fn("get_product_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_prod.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Product (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Product with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_product_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_prod + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Product (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Product with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // DB functions for Sale let captured_db_for_set_sale = Arc::clone(&db); - engine.register_fn("set_sale", move |sale: Sale| -> Result> { - let original_id_for_error = sale.get_id(); - captured_db_for_set_sale.set(&sale) - .map(|(_id, updated_sale)| updated_sale) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Sale (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_sale", + move |sale: Sale| -> Result> { + let original_id_for_error = sale.get_id(); + captured_db_for_set_sale + .set(&sale) + .map(|(_id, updated_sale)| updated_sale) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Sale (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_sale = Arc::clone(&db); - engine.register_fn("get_sale_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_from_i64(id_i64)?; - captured_db_for_get_sale.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Sale with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_sale_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_from_i64(id_i64)?; + captured_db_for_get_sale + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Sale with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // Mock DB functions for Shareholder let captured_db_for_set_shareholder = Arc::clone(&db); - engine.register_fn("set_shareholder", move |shareholder: Shareholder| -> Result> { - let original_id_for_error = shareholder.get_id(); - captured_db_for_set_shareholder.set(&shareholder) - .map(|(_id, updated_shareholder)| updated_shareholder) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Shareholder (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_shareholder", + move |shareholder: Shareholder| -> Result> { + let original_id_for_error = shareholder.get_id(); + captured_db_for_set_shareholder + .set(&shareholder) + .map(|(_id, updated_shareholder)| updated_shareholder) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Shareholder (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get_sh = Arc::clone(&db); - engine.register_fn("get_shareholder_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_sh.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Shareholder with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_shareholder_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; + captured_db_for_get_sh + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Shareholder with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); // Mock DB functions for Company let captured_db_for_set_company = Arc::clone(&db); - engine.register_fn("set_company", move |company: Company| -> Result> { - let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB - captured_db_for_set_company.set(&company) - .map(|(_id, updated_company)| updated_company) // Use the model returned by db.set() - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Company (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); + engine.register_fn( + "set_company", + move |company: Company| -> Result> { + let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB + captured_db_for_set_company + .set(&company) + .map(|(_id, updated_company)| updated_company) // Use the model returned by db.set() + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Company (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); let captured_db_for_get = Arc::clone(&db); - engine.register_fn("get_company_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow - captured_db_for_get.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Company (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Company with ID {} not found", id_u32).into(), Position::NONE))) - }); + engine.register_fn( + "get_company_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow + captured_db_for_get + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Company (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Company with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); + + // --- Payment --- + engine.register_type_with_name::("Payment"); + + // Constructor for Payment + engine.register_fn( + "new_payment", + |payment_intent_id: String, + company_id: i64, + payment_plan: String, + setup_fee: f64, + monthly_fee: f64, + total_amount: f64| + -> Result> { + Ok(Payment::new( + payment_intent_id, + id_from_i64(company_id)?, + payment_plan, + setup_fee, + monthly_fee, + total_amount, + )) + }, + ); + + // Getters for Payment + engine.register_get( + "id", + |payment: &mut Payment| -> Result> { Ok(payment.get_id() as i64) }, + ); + engine.register_get( + "created_at", + |payment: &mut Payment| -> Result> { + Ok(payment.base_data.created_at) + }, + ); + engine.register_get( + "modified_at", + |payment: &mut Payment| -> Result> { + Ok(payment.base_data.modified_at) + }, + ); + engine.register_get( + "payment_intent_id", + |payment: &mut Payment| -> Result> { + Ok(payment.payment_intent_id.clone()) + }, + ); + engine.register_get( + "company_id", + |payment: &mut Payment| -> Result> { + Ok(payment.company_id as i64) + }, + ); + engine.register_get( + "payment_plan", + |payment: &mut Payment| -> Result> { + Ok(payment.payment_plan.clone()) + }, + ); + engine.register_get( + "setup_fee", + |payment: &mut Payment| -> Result> { Ok(payment.setup_fee) }, + ); + engine.register_get( + "monthly_fee", + |payment: &mut Payment| -> Result> { Ok(payment.monthly_fee) }, + ); + engine.register_get( + "total_amount", + |payment: &mut Payment| -> Result> { Ok(payment.total_amount) }, + ); + engine.register_get( + "status", + |payment: &mut Payment| -> Result> { + Ok(payment.status.clone()) + }, + ); + engine.register_get( + "stripe_customer_id", + |payment: &mut Payment| -> Result, Box> { + Ok(payment.stripe_customer_id.clone()) + }, + ); + + // Builder methods for Payment + engine.register_fn( + "payment_intent_id", + |payment: Payment, payment_intent_id: String| -> Result> { + Ok(payment.payment_intent_id(payment_intent_id)) + }, + ); + engine.register_fn( + "company_id", + |payment: Payment, company_id: i64| -> Result> { + Ok(payment.company_id(id_from_i64(company_id)?)) + }, + ); + engine.register_fn( + "payment_plan", + |payment: Payment, payment_plan: String| -> Result> { + Ok(payment.payment_plan(payment_plan)) + }, + ); + engine.register_fn( + "setup_fee", + |payment: Payment, setup_fee: f64| -> Result> { + Ok(payment.setup_fee(setup_fee)) + }, + ); + engine.register_fn( + "monthly_fee", + |payment: Payment, monthly_fee: f64| -> Result> { + Ok(payment.monthly_fee(monthly_fee)) + }, + ); + engine.register_fn( + "total_amount", + |payment: Payment, total_amount: f64| -> Result> { + Ok(payment.total_amount(total_amount)) + }, + ); + engine.register_fn( + "status", + |payment: Payment, status: PaymentStatus| -> Result> { + Ok(payment.status(status)) + }, + ); + engine.register_fn( + "stripe_customer_id", + |payment: Payment, + stripe_customer_id: Option| + -> Result> { + Ok(payment.stripe_customer_id(stripe_customer_id)) + }, + ); + + // Business logic methods for Payment + engine.register_fn( + "complete_payment", + |payment: Payment, + stripe_customer_id: Option| + -> Result> { + Ok(payment.complete_payment(stripe_customer_id)) + }, + ); + // Overload for string parameter + engine.register_fn( + "complete_payment", + |payment: Payment, stripe_customer_id: String| -> Result> { + Ok(payment.complete_payment(Some(stripe_customer_id))) + }, + ); + engine.register_fn( + "fail_payment", + |payment: Payment| -> Result> { Ok(payment.fail_payment()) }, + ); + engine.register_fn( + "refund_payment", + |payment: Payment| -> Result> { Ok(payment.refund_payment()) }, + ); + + // Status check methods for Payment + engine.register_fn( + "is_completed", + |payment: &mut Payment| -> Result> { Ok(payment.is_completed()) }, + ); + engine.register_fn( + "is_pending", + |payment: &mut Payment| -> Result> { Ok(payment.is_pending()) }, + ); + engine.register_fn( + "has_failed", + |payment: &mut Payment| -> Result> { Ok(payment.has_failed()) }, + ); + engine.register_fn( + "is_refunded", + |payment: &mut Payment| -> Result> { Ok(payment.is_refunded()) }, + ); + + // DB functions for Payment + let captured_db_for_set_payment = Arc::clone(&db); + engine.register_fn( + "set_payment", + move |payment: Payment| -> Result> { + let original_id_for_error = payment.get_id(); + captured_db_for_set_payment + .set(&payment) + .map(|(_id, updated_payment)| updated_payment) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!( + "Failed to set Payment (Original ID: {}): {}", + original_id_for_error, e + ) + .into(), + Position::NONE, + )) + }) + }, + ); + + let captured_db_for_get_payment = Arc::clone(&db); + engine.register_fn( + "get_payment_by_id", + move |id_i64: i64| -> Result> { + let id_u32 = id_from_i64(id_i64)?; + captured_db_for_get_payment + .get_by_id(id_u32) + .map_err(|e| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Error getting Payment (ID: {}): {}", id_u32, e).into(), + Position::NONE, + )) + })? + .ok_or_else(|| { + Box::new(EvalAltResult::ErrorRuntime( + format!("Payment with ID {} not found", id_u32).into(), + Position::NONE, + )) + }) + }, + ); engine.register_global_module(module.into()); } diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index 5145503..850f48c 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -14,7 +14,7 @@ pub mod projects; pub use core::Comment; pub use userexample::User; // pub use productexample::Product; // Temporarily remove -pub use biz::{Sale, SaleItem, SaleStatus}; +pub use biz::{Payment, PaymentStatus, Sale, SaleItem, SaleStatus}; pub use calendar::{AttendanceStatus, Attendee, Calendar, Event}; pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType}; pub use finance::{Account, Asset, AssetType}; diff --git a/heromodels/tests/payment.rs b/heromodels/tests/payment.rs new file mode 100644 index 0000000..04331ec --- /dev/null +++ b/heromodels/tests/payment.rs @@ -0,0 +1,313 @@ +use heromodels::db::Collection; +use heromodels::db::hero::OurDB; +use heromodels::models::biz::{BusinessType, Company, CompanyStatus, Payment, PaymentStatus}; +use heromodels_core::Model; +use std::sync::Arc; + +fn create_test_db() -> Arc { + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = format!("/tmp/payment_test_{}", timestamp); + + // Clean up any existing database at this path + let _ = std::fs::remove_dir_all(&path); + + Arc::new(OurDB::new(path, true).expect("Failed to create test database")) +} + +#[test] +fn test_payment_creation() { + let payment = Payment::new( + "pi_test_123".to_string(), + 1, + "monthly".to_string(), + 100.0, + 50.0, + 150.0, + ); + + assert_eq!(payment.payment_intent_id, "pi_test_123"); + assert_eq!(payment.company_id, 1); + assert_eq!(payment.payment_plan, "monthly"); + assert_eq!(payment.setup_fee, 100.0); + assert_eq!(payment.monthly_fee, 50.0); + assert_eq!(payment.total_amount, 150.0); + assert_eq!(payment.currency, "usd"); + assert_eq!(payment.status, PaymentStatus::Pending); + assert_eq!(payment.stripe_customer_id, None); + assert!(payment.created_at > 0); + assert_eq!(payment.completed_at, None); +} + +#[test] +fn test_payment_status_default() { + let payment = Payment::new( + "pi_test".to_string(), + 1, + "yearly".to_string(), + 500.0, + 99.0, + 1688.0, + ); + + assert_eq!(payment.status, PaymentStatus::Pending); + assert!(payment.is_pending()); + assert!(!payment.is_processing()); + assert!(!payment.is_completed()); + assert!(!payment.has_failed()); + assert!(!payment.is_refunded()); +} + +#[test] +fn test_payment_processing() { + let payment = Payment::new( + "pi_processing_test".to_string(), + 1, + "monthly".to_string(), + 150.0, + 60.0, + 210.0, + ); + + let processing_payment = payment.process_payment(); + + assert_eq!(processing_payment.status, PaymentStatus::Processing); + assert!(processing_payment.is_processing()); + assert!(!processing_payment.is_pending()); + assert!(!processing_payment.is_completed()); + assert!(!processing_payment.has_failed()); + assert!(!processing_payment.is_refunded()); +} + +#[test] +fn test_payment_completion() { + let payment = Payment::new( + "pi_complete_test".to_string(), + 1, + "monthly".to_string(), + 200.0, + 75.0, + 275.0, + ); + + let stripe_customer_id = Some("cus_test_123".to_string()); + let completed_payment = payment.complete_payment(stripe_customer_id.clone()); + + assert_eq!(completed_payment.status, PaymentStatus::Completed); + assert_eq!(completed_payment.stripe_customer_id, stripe_customer_id); + assert!(completed_payment.is_completed()); + assert!(!completed_payment.is_pending()); + assert!(!completed_payment.has_failed()); + assert!(!completed_payment.is_refunded()); + assert!(completed_payment.completed_at.is_some()); + assert!(completed_payment.completed_at.unwrap() > 0); +} + +#[test] +fn test_payment_failure() { + let payment = Payment::new( + "pi_fail_test".to_string(), + 1, + "yearly".to_string(), + 300.0, + 60.0, + 1020.0, + ); + + let failed_payment = payment.fail_payment(); + + assert_eq!(failed_payment.status, PaymentStatus::Failed); + assert!(failed_payment.has_failed()); + assert!(!failed_payment.is_completed()); + assert!(!failed_payment.is_pending()); + assert!(!failed_payment.is_refunded()); +} + +#[test] +fn test_payment_refund() { + let payment = Payment::new( + "pi_refund_test".to_string(), + 1, + "monthly".to_string(), + 150.0, + 45.0, + 195.0, + ); + + // First complete the payment + let completed_payment = payment.complete_payment(Some("cus_refund_test".to_string())); + assert!(completed_payment.is_completed()); + + // Then refund it + let refunded_payment = completed_payment.refund_payment(); + + assert_eq!(refunded_payment.status, PaymentStatus::Refunded); + assert!(refunded_payment.is_refunded()); + assert!(!refunded_payment.is_completed()); + assert!(!refunded_payment.is_pending()); + assert!(!refunded_payment.has_failed()); +} + +#[test] +fn test_payment_builder_pattern() { + let custom_timestamp = 1640995200; // Jan 1, 2022 + let payment = Payment::new( + "pi_builder_test".to_string(), + 1, + "monthly".to_string(), + 100.0, + 50.0, + 150.0, + ) + .payment_plan("yearly".to_string()) + .setup_fee(500.0) + .monthly_fee(99.0) + .total_amount(1688.0) + .currency("eur".to_string()) + .stripe_customer_id(Some("cus_builder_test".to_string())) + .created_at(custom_timestamp) + .completed_at(Some(custom_timestamp + 3600)); + + assert_eq!(payment.payment_plan, "yearly"); + assert_eq!(payment.setup_fee, 500.0); + assert_eq!(payment.monthly_fee, 99.0); + assert_eq!(payment.total_amount, 1688.0); + assert_eq!(payment.currency, "eur"); + assert_eq!( + payment.stripe_customer_id, + Some("cus_builder_test".to_string()) + ); + assert_eq!(payment.created_at, custom_timestamp); + assert_eq!(payment.completed_at, Some(custom_timestamp + 3600)); +} + +#[test] +fn test_payment_database_persistence() { + let db = create_test_db(); + + let payment = Payment::new( + "pi_db_test".to_string(), + 1, + "monthly".to_string(), + 200.0, + 60.0, + 260.0, + ); + + // Save payment + let (payment_id, saved_payment) = db.set(&payment).expect("Failed to save payment"); + assert!(payment_id > 0); + assert_eq!(saved_payment.payment_intent_id, "pi_db_test"); + + // Retrieve payment + let retrieved_payment: Payment = db + .get_by_id(payment_id) + .expect("Failed to get payment") + .unwrap(); + assert_eq!(retrieved_payment.payment_intent_id, "pi_db_test"); + assert_eq!(retrieved_payment.company_id, 1); + assert_eq!(retrieved_payment.status, PaymentStatus::Pending); +} + +#[test] +fn test_payment_status_transitions() { + let db = create_test_db(); + + let payment = Payment::new( + "pi_transition_test".to_string(), + 1, + "yearly".to_string(), + 400.0, + 80.0, + 1360.0, + ); + + let (payment_id, mut payment) = db.set(&payment).expect("Failed to save payment"); + + // Test pending -> completed + payment = payment.complete_payment(Some("cus_transition_test".to_string())); + let (_, mut payment) = db.set(&payment).expect("Failed to update payment"); + assert!(payment.is_completed()); + + // Test completed -> refunded + payment = payment.refund_payment(); + let (_, payment) = db.set(&payment).expect("Failed to update payment"); + assert!(payment.is_refunded()); + + // Verify final state in database + let final_payment: Payment = db + .get_by_id(payment_id) + .expect("Failed to get payment") + .unwrap(); + assert_eq!(final_payment.status, PaymentStatus::Refunded); +} + +#[test] +fn test_payment_timestamps() { + let payment = Payment::new( + "pi_timestamp_test".to_string(), + 1, + "monthly".to_string(), + 100.0, + 50.0, + 150.0, + ); + + let initial_created_at = payment.base_data.created_at; + let initial_modified_at = payment.base_data.modified_at; + + // Complete payment (should update modified_at) + let completed_payment = payment.complete_payment(Some("cus_timestamp_test".to_string())); + + assert_eq!(completed_payment.base_data.created_at, initial_created_at); + assert!(completed_payment.base_data.modified_at >= initial_modified_at); +} + +#[test] +fn test_company_payment_integration() { + let db = create_test_db(); + + // Create company with default PendingPayment status + let company = Company::new( + "Integration Test Corp".to_string(), + "ITC-001".to_string(), + chrono::Utc::now().timestamp(), + ) + .email("test@integration.com".to_string()) + .business_type(BusinessType::Starter); + + let (company_id, company) = db.set(&company).expect("Failed to save company"); + assert_eq!(company.status, CompanyStatus::PendingPayment); + + // Create payment for the company + let payment = Payment::new( + "pi_integration_test".to_string(), + company_id, + "monthly".to_string(), + 250.0, + 55.0, + 305.0, + ); + + let (_payment_id, payment) = db.set(&payment).expect("Failed to save payment"); + assert_eq!(payment.company_id, company_id); + + // Complete payment + let completed_payment = payment.complete_payment(Some("cus_integration_test".to_string())); + let (_, completed_payment) = db + .set(&completed_payment) + .expect("Failed to update payment"); + + // Update company status to Active + let active_company = company.status(CompanyStatus::Active); + let (_, active_company) = db.set(&active_company).expect("Failed to update company"); + + // Verify final states + assert!(completed_payment.is_completed()); + assert_eq!(active_company.status, CompanyStatus::Active); + + // Verify relationship + assert_eq!(completed_payment.company_id, active_company.get_id()); +}