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