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:
Mahmoud-Emad
2025-06-12 14:30:58 +03:00
parent b9abfa50a9
commit f0a0dd6d73
6 changed files with 322 additions and 1 deletions

View File

@@ -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 ---

View File

@@ -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");