- Implement comprehensive contract listing with filtering by status and type, and search functionality. - Add contract cloning, sharing, and cancellation features. - Improve contract details view with enhanced UI and activity timeline. - Implement signer management with add/update/delete and status updates, including signature data handling and rejection. - Introduce contract creation and editing functionalities with markdown support. - Add error handling for contract not found scenarios. - Implement reminder system for pending signatures with rate limiting and status tracking. - Add API endpoint for retrieving contract statistics. - Improve logging with more descriptive messages. - Refactor code for better structure and maintainability.
459 lines
15 KiB
Rust
459 lines
15 KiB
Rust
use heromodels::{
|
|
db::{Collection, Db},
|
|
models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus},
|
|
};
|
|
|
|
use super::db::get_db;
|
|
|
|
/// Creates a new contract and saves it to the database. Returns the saved contract and its ID.
|
|
pub fn create_new_contract(
|
|
base_id: u32,
|
|
contract_id: &str,
|
|
title: &str,
|
|
description: &str,
|
|
contract_type: &str,
|
|
status: ContractStatus,
|
|
created_by: &str,
|
|
terms_and_conditions: Option<&str>,
|
|
start_date: Option<u64>,
|
|
end_date: Option<u64>,
|
|
renewal_period_days: Option<u32>,
|
|
) -> Result<(u32, Contract), String> {
|
|
let db = get_db().expect("Can get DB");
|
|
|
|
// Create a new contract using the heromodels Contract::new constructor
|
|
let mut contract = Contract::new(base_id, contract_id.to_string())
|
|
.title(title)
|
|
.description(description)
|
|
.contract_type(contract_type.to_string())
|
|
.status(status)
|
|
.created_by(created_by.to_string());
|
|
|
|
if let Some(terms) = terms_and_conditions {
|
|
contract = contract.terms_and_conditions(terms);
|
|
}
|
|
if let Some(start) = start_date {
|
|
contract = contract.start_date(start);
|
|
}
|
|
if let Some(end) = end_date {
|
|
contract = contract.end_date(end);
|
|
}
|
|
if let Some(renewal) = renewal_period_days {
|
|
contract = contract.renewal_period_days(renewal as i32);
|
|
}
|
|
|
|
// Save the contract to the database
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.expect("can open contract collection");
|
|
let (contract_id, saved_contract) = collection.set(&contract).expect("can save contract");
|
|
|
|
Ok((contract_id, saved_contract))
|
|
}
|
|
|
|
/// Loads all contracts from the database and returns them as a Vec<Contract>.
|
|
pub fn get_contracts() -> Result<Vec<Contract>, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.expect("can open contract collection");
|
|
|
|
// Try to load all contracts, but handle deserialization errors gracefully
|
|
let contracts = match collection.get_all() {
|
|
Ok(contracts) => contracts,
|
|
Err(e) => {
|
|
log::error!("Failed to load contracts from database: {:?}", e);
|
|
vec![] // Return empty vector if there's an error
|
|
}
|
|
};
|
|
Ok(contracts)
|
|
}
|
|
|
|
/// Fetches a single contract by its ID from the database.
|
|
pub fn get_contract_by_id(contract_id: u32) -> Result<Option<Contract>, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
match collection.get_by_id(contract_id) {
|
|
Ok(contract) => Ok(contract),
|
|
Err(e) => {
|
|
log::error!("Error fetching contract by id {}: {:?}", contract_id, e);
|
|
Err(format!("Failed to fetch contract: {:?}", e))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Updates a contract's basic information in the database and returns the updated contract.
|
|
pub fn update_contract(
|
|
contract_id: u32,
|
|
title: &str,
|
|
description: &str,
|
|
contract_type: &str,
|
|
terms_and_conditions: Option<&str>,
|
|
start_date: Option<u64>,
|
|
end_date: Option<u64>,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
// Update the contract fields
|
|
contract = contract
|
|
.title(title)
|
|
.description(description)
|
|
.contract_type(contract_type.to_string());
|
|
|
|
if let Some(terms) = terms_and_conditions {
|
|
contract = contract.terms_and_conditions(terms);
|
|
}
|
|
if let Some(start) = start_date {
|
|
contract = contract.start_date(start);
|
|
}
|
|
if let Some(end) = end_date {
|
|
contract = contract.end_date(end);
|
|
}
|
|
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Updates a contract's status in the database and returns the updated contract.
|
|
pub fn update_contract_status(
|
|
contract_id: u32,
|
|
status: ContractStatus,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
contract = contract.status(status);
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Adds a signer to a contract and returns the updated contract.
|
|
pub fn add_signer_to_contract(
|
|
contract_id: u32,
|
|
signer_id: &str,
|
|
name: &str,
|
|
email: &str,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
let signer =
|
|
ContractSigner::new(signer_id.to_string(), name.to_string(), email.to_string());
|
|
contract = contract.add_signer(signer);
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Adds a revision to a contract and returns the updated contract.
|
|
pub fn add_revision_to_contract(
|
|
contract_id: u32,
|
|
version: u32,
|
|
content: &str,
|
|
created_by: &str,
|
|
comments: Option<&str>,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
let revision = ContractRevision::new(
|
|
version,
|
|
content.to_string(),
|
|
current_timestamp_secs(),
|
|
created_by.to_string(),
|
|
)
|
|
.comments(comments.unwrap_or("").to_string());
|
|
|
|
contract = contract.add_revision(revision);
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Updates a signer's status for a contract and returns the updated contract.
|
|
pub fn update_signer_status(
|
|
contract_id: u32,
|
|
signer_id: &str,
|
|
status: SignerStatus,
|
|
comments: Option<&str>,
|
|
) -> Result<Contract, String> {
|
|
update_signer_status_with_signature(contract_id, signer_id, status, comments, None)
|
|
}
|
|
|
|
/// Updates a signer's status with signature data for a contract and returns the updated contract.
|
|
pub fn update_signer_status_with_signature(
|
|
contract_id: u32,
|
|
signer_id: &str,
|
|
status: SignerStatus,
|
|
comments: Option<&str>,
|
|
signature_data: Option<&str>,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
// Find and update the signer
|
|
let mut signer_found = false;
|
|
for signer in &mut contract.signers {
|
|
if signer.id == signer_id {
|
|
signer.status = status.clone();
|
|
if status == SignerStatus::Signed {
|
|
signer.signed_at = Some(current_timestamp_secs());
|
|
}
|
|
if let Some(comment) = comments {
|
|
signer.comments = Some(comment.to_string());
|
|
}
|
|
if let Some(sig_data) = signature_data {
|
|
signer.signature_data = Some(sig_data.to_string());
|
|
}
|
|
signer_found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !signer_found {
|
|
return Err(format!("Signer with ID {} not found", signer_id));
|
|
}
|
|
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Deletes a contract from the database.
|
|
pub fn delete_contract(contract_id: u32) -> Result<(), String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.map_err(|e| format!("Collection error: {:?}", e))?;
|
|
|
|
collection
|
|
.delete_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to delete contract: {:?}", e))?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Gets contracts by status
|
|
pub fn get_contracts_by_status(status: ContractStatus) -> Result<Vec<Contract>, String> {
|
|
let contracts = get_contracts()?;
|
|
let filtered_contracts = contracts
|
|
.into_iter()
|
|
.filter(|contract| contract.status == status)
|
|
.collect();
|
|
Ok(filtered_contracts)
|
|
}
|
|
|
|
/// Gets contracts by creator
|
|
pub fn get_contracts_by_creator(created_by: &str) -> Result<Vec<Contract>, String> {
|
|
let contracts = get_contracts()?;
|
|
let filtered_contracts = contracts
|
|
.into_iter()
|
|
.filter(|contract| contract.created_by == created_by)
|
|
.collect();
|
|
Ok(filtered_contracts)
|
|
}
|
|
|
|
/// Gets contracts that need renewal (approaching end date)
|
|
pub fn get_contracts_needing_renewal(days_ahead: u64) -> Result<Vec<Contract>, String> {
|
|
let contracts = get_contracts()?;
|
|
let threshold_timestamp = current_timestamp_secs() + (days_ahead * 24 * 60 * 60);
|
|
|
|
let filtered_contracts = contracts
|
|
.into_iter()
|
|
.filter(|contract| {
|
|
if let Some(end_date) = contract.end_date {
|
|
end_date <= threshold_timestamp && contract.status == ContractStatus::Active
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
.collect();
|
|
Ok(filtered_contracts)
|
|
}
|
|
|
|
/// Gets expired contracts
|
|
pub fn get_expired_contracts() -> Result<Vec<Contract>, String> {
|
|
let contracts = get_contracts()?;
|
|
let current_time = current_timestamp_secs();
|
|
|
|
let filtered_contracts = contracts
|
|
.into_iter()
|
|
.filter(|contract| {
|
|
if let Some(end_date) = contract.end_date {
|
|
end_date < current_time && contract.status != ContractStatus::Expired
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
.collect();
|
|
Ok(filtered_contracts)
|
|
}
|
|
|
|
/// Updates multiple contracts to expired status
|
|
pub fn mark_contracts_as_expired(contract_ids: Vec<u32>) -> Result<Vec<Contract>, String> {
|
|
let mut updated_contracts = Vec::new();
|
|
|
|
for contract_id in contract_ids {
|
|
match update_contract_status(contract_id, ContractStatus::Expired) {
|
|
Ok(contract) => updated_contracts.push(contract),
|
|
Err(e) => log::error!("Failed to update contract {}: {}", contract_id, e),
|
|
}
|
|
}
|
|
|
|
Ok(updated_contracts)
|
|
}
|
|
|
|
/// Updates a signer's reminder timestamp for a contract and returns the updated contract.
|
|
pub fn update_signer_reminder_timestamp(
|
|
contract_id: u32,
|
|
signer_id: &str,
|
|
_timestamp: u64,
|
|
) -> Result<Contract, String> {
|
|
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
|
let collection = db
|
|
.collection::<Contract>()
|
|
.expect("can open contract collection");
|
|
|
|
if let Some(mut contract) = collection
|
|
.get_by_id(contract_id)
|
|
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
|
{
|
|
let mut signer_found = false;
|
|
for signer in &mut contract.signers {
|
|
if signer.id == signer_id {
|
|
// TODO: Update reminder timestamp when field is available in heromodels
|
|
// signer.last_reminder_mail_sent_at = Some(timestamp);
|
|
signer_found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if !signer_found {
|
|
return Err(format!("Signer with ID {} not found", signer_id));
|
|
}
|
|
|
|
let (_, updated_contract) = collection
|
|
.set(&contract)
|
|
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
|
Ok(updated_contract)
|
|
} else {
|
|
Err("Contract not found".to_string())
|
|
}
|
|
}
|
|
|
|
/// Gets contract statistics
|
|
pub fn get_contract_statistics() -> Result<ContractStatistics, String> {
|
|
let contracts = get_contracts()?;
|
|
|
|
let total = contracts.len();
|
|
let draft = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::Draft)
|
|
.count();
|
|
let pending = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::PendingSignatures)
|
|
.count();
|
|
let signed = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::Signed)
|
|
.count();
|
|
let active = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::Active)
|
|
.count();
|
|
let expired = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::Expired)
|
|
.count();
|
|
let cancelled = contracts
|
|
.iter()
|
|
.filter(|c| c.status == ContractStatus::Cancelled)
|
|
.count();
|
|
|
|
Ok(ContractStatistics {
|
|
total_contracts: total,
|
|
draft_contracts: draft,
|
|
pending_signature_contracts: pending,
|
|
signed_contracts: signed,
|
|
active_contracts: active,
|
|
expired_contracts: expired,
|
|
cancelled_contracts: cancelled,
|
|
})
|
|
}
|
|
|
|
/// 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()
|
|
}
|
|
|
|
/// Contract statistics structure
|
|
#[derive(Debug, Clone)]
|
|
pub struct ContractStatistics {
|
|
pub total_contracts: usize,
|
|
pub draft_contracts: usize,
|
|
pub pending_signature_contracts: usize,
|
|
pub signed_contracts: usize,
|
|
pub active_contracts: usize,
|
|
pub expired_contracts: usize,
|
|
pub cancelled_contracts: usize,
|
|
}
|