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.
This commit is contained in:
parent
b9abfa50a9
commit
f0a0dd6d73
@ -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::<String>())
|
||||
);
|
||||
}
|
||||
|
||||
// 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.");
|
||||
}
|
||||
|
@ -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.");
|
||||
|
163
heromodels/examples/test_signature_functionality.rs
Normal file
163
heromodels/examples/test_signature_functionality.rs
Normal file
@ -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!");
|
||||
}
|
@ -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<u64>, // Timestamp
|
||||
pub comments: Option<String>,
|
||||
pub last_reminder_mail_sent_at: Option<u64>, // Unix timestamp of last reminder sent
|
||||
pub signature_data: Option<String>, // 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<String>, comments: Option<String>) {
|
||||
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 ---
|
||||
|
@ -211,6 +211,18 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
|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<OurDB>) {
|
||||
},
|
||||
);
|
||||
|
||||
// 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<String, Box<EvalAltResult>> {
|
||||
@ -317,6 +346,15 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
},
|
||||
);
|
||||
engine.register_get(
|
||||
"signature_data",
|
||||
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer
|
||||
.signature_data
|
||||
.as_ref()
|
||||
.map_or(Dynamic::UNIT, |data| Dynamic::from(data.clone())))
|
||||
},
|
||||
);
|
||||
|
||||
// --- Contract ---
|
||||
engine.register_type_with_name::<Contract>("Contract");
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user