- 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.
225 lines
9.3 KiB
Rust
225 lines
9.3 KiB
Rust
use heromodels::models::legal::{
|
|
Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus,
|
|
};
|
|
// If BaseModelData's touch method or new method isn't directly part of the public API
|
|
// of heromodels crate root, we might need to import heromodels_core.
|
|
// For now, assuming `contract.base_data.touch()` would work if BaseModelData has a public `touch` method.
|
|
// Or, if `heromodels::models::BaseModelData` is the path.
|
|
|
|
// A helper for current timestamp (seconds since epoch)
|
|
fn current_timestamp_secs() -> u64 {
|
|
std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
}
|
|
|
|
fn main() {
|
|
println!("Demonstrating Legal Contract Model Usage");
|
|
|
|
// Create contract signers
|
|
let signer1 = ContractSigner::new(
|
|
"signer-uuid-alice-001".to_string(),
|
|
"Alice Wonderland".to_string(),
|
|
"alice@example.com".to_string(),
|
|
)
|
|
.status(SignerStatus::Pending)
|
|
.comments("Awaiting Alice's review and signature.");
|
|
|
|
let signer2 = ContractSigner::new(
|
|
"signer-uuid-bob-002".to_string(),
|
|
"Bob The Builder".to_string(),
|
|
"bob@example.com".to_string(),
|
|
)
|
|
.status(SignerStatus::Signed)
|
|
.signed_at(current_timestamp_secs() - 86400) // Signed yesterday
|
|
.comments("Bob has signed the agreement.");
|
|
|
|
// Create contract revisions
|
|
let revision1 = ContractRevision::new(
|
|
1,
|
|
"Initial draft: This Service Agreement outlines the terms...".to_string(),
|
|
current_timestamp_secs() - (86400 * 2), // 2 days ago
|
|
"user-uuid-creator-charlie".to_string(),
|
|
)
|
|
.comments("Version 1.0 - Initial draft for review.");
|
|
|
|
let revision2 = ContractRevision::new(
|
|
2,
|
|
"Updated draft: Added clause 5.b regarding data privacy...".to_string(),
|
|
current_timestamp_secs() - 86400, // 1 day ago
|
|
"user-uuid-editor-diana".to_string(),
|
|
)
|
|
.comments("Version 2.0 - Incorporated feedback from legal team.");
|
|
|
|
// Create a new contract
|
|
// base_id (u32 for BaseModelData) and contract_id (String for UUID)
|
|
let mut contract = Contract::new(101, "contract-uuid-main-789".to_string())
|
|
.title("Master Service Agreement (MSA) - Q3 2025")
|
|
.description("Agreement for ongoing IT support services between TechSolutions Inc. and ClientCorp LLC.")
|
|
.contract_type("MSA".to_string())
|
|
.status(ContractStatus::PendingSignatures)
|
|
.created_by("user-uuid-admin-eve".to_string())
|
|
.terms_and_conditions("The full terms are detailed in the attached PDF, referenced as 'MSA_Q3_2025_Full.pdf'. This string can hold markdown or JSON summary.")
|
|
.start_date(current_timestamp_secs() + (86400 * 7)) // Starts in 7 days
|
|
.end_date(current_timestamp_secs() + (86400 * (365 + 7))) // Ends in 1 year + 7 days
|
|
.renewal_period_days(30)
|
|
.next_renewal_date(current_timestamp_secs() + (86400 * (365 + 7 - 30))) // Approx. 30 days before end_date
|
|
.current_version(2)
|
|
.add_signer(signer1.clone())
|
|
.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.
|
|
// If not, one might call `contract.base_data.touch()` after building.
|
|
|
|
println!("\n--- Initial Contract Details ---");
|
|
println!("{:#?}", contract);
|
|
|
|
// Simulate a status change and signing
|
|
contract.set_status(ContractStatus::Signed); // This should call base_data.touch()
|
|
contract = contract.last_signed_date(current_timestamp_secs());
|
|
// If set_status doesn't touch, and last_signed_date is not a builder method that touches:
|
|
// contract.base_data.touch(); // Manually update timestamp if needed after direct field manipulation
|
|
|
|
println!("\n--- Contract Details After Signing ---");
|
|
println!("{:#?}", contract);
|
|
|
|
println!("\n--- Accessing Specific Fields ---");
|
|
println!("Contract Title: {}", contract.title);
|
|
println!("Contract Status: {:?}", contract.status);
|
|
println!("Contract ID (UUID): {}", contract.contract_id);
|
|
println!("Base Model ID (u32): {}", contract.base_data.id); // From BaseModelData
|
|
println!("Created At (timestamp): {}", contract.base_data.created_at); // From BaseModelData
|
|
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!(" Status: {:?}", first_signer_details.status);
|
|
if let Some(signed_time) = first_signer_details.signed_at {
|
|
println!(" Signed At: {}", signed_time);
|
|
}
|
|
}
|
|
|
|
if let Some(latest_rev) = contract.revisions.iter().max_by_key(|r| r.version) {
|
|
println!("\nLatest Revision (v{}):", latest_rev.version);
|
|
println!(" Content Snippet: {:.60}...", latest_rev.content);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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.");
|
|
}
|