hostbasket/actix_mvc_app/src/db/contracts.rs
Mahmoud-Emad 464e253739 feat: Enhance contract management with new features
- 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.
2025-06-12 13:53:33 +03:00

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,
}