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