lariv-rs 0.1.0

Compile-time plugin web application framework built on Axum, SeaORM, Maud, and HTMX
Documentation
use chrono::Utc;
use sea_orm::{
    ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, EntityTrait, PaginatorTrait,
    QueryFilter,
};

use crate::plugins::crm::entities::{
    converted_lead::Entity as ConvertedLeadEntity,
    failed_lead::Entity as FailedLeadEntity,
    lead::{self, Entity as LeadEntity},
    lead_tag_link,
};
use crate::plugins::crm::lead_source::LeadSource;
use crate::plugins::crm::logic::lead_timeline::append_lead_timeline;
use crate::plugins::crm::scope::sql_lead_active;

pub async fn err_if_lead_sealed<C: ConnectionTrait>(db: &C, lead_id: i64) -> Result<(), String> {
    if lead_id == 0 {
        return Ok(());
    }
    let converted = ConvertedLeadEntity::find()
        .filter(crate::plugins::crm::entities::converted_lead::Column::LeadId.eq(lead_id))
        .count(db)
        .await
        .map_err(|e| e.to_string())?;
    if converted > 0 {
        return Err("lead is converted and cannot be changed".to_string());
    }
    let failed = FailedLeadEntity::find()
        .filter(crate::plugins::crm::entities::failed_lead::Column::LeadId.eq(lead_id))
        .count(db)
        .await
        .map_err(|e| e.to_string())?;
    if failed > 0 {
        return Err("lead is failed and cannot be changed".to_string());
    }
    Ok(())
}

pub struct LeadInput {
    pub contact_id: i64,
    pub source: Option<LeadSource>,
    pub notes: Option<String>,
    pub tag_ids: Vec<i64>,
}

pub async fn create_lead<C: ConnectionTrait>(
    db: &C,
    input: LeadInput,
) -> Result<lead::Model, String> {
    if input.contact_id <= 0 {
        return Err("contact is required".to_string());
    }
    let now = Utc::now();
    let model = lead::ActiveModel {
        id: Default::default(),
        created_at: Set(Some(now)),
        updated_at: Set(Some(now)),
        contact_id: Set(input.contact_id),
        source: Set(input.source),
        notes: Set(input.notes),
    };
    let saved = model.insert(db).await.map_err(|e| e.to_string())?;
    sync_lead_tags(db, saved.id, &input.tag_ids).await?;
    append_lead_timeline(db, saved.id, "Lead created").await?;
    Ok(saved)
}

pub async fn update_lead<C: ConnectionTrait>(
    db: &C,
    lead_id: i64,
    input: LeadInput,
) -> Result<lead::Model, String> {
    if input.contact_id <= 0 {
        return Err("contact is required".to_string());
    }
    let existing = LeadEntity::find_by_id(lead_id)
        .one(db)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| "lead not found".to_string())?;
    let now = Utc::now();
    let mut am: lead::ActiveModel = existing.into();
    am.updated_at = Set(Some(now));
    am.contact_id = Set(input.contact_id);
    am.source = Set(input.source);
    am.notes = Set(input.notes);
    let saved = am.update(db).await.map_err(|e| e.to_string())?;
    sync_lead_tags(db, saved.id, &input.tag_ids).await?;
    Ok(saved)
}

pub async fn delete_lead<C: ConnectionTrait>(db: &C, lead_id: i64) -> Result<(), String> {
    err_if_lead_sealed(db, lead_id).await?;
    let existing = LeadEntity::find_by_id(lead_id)
        .filter(sql_lead_active())
        .one(db)
        .await
        .map_err(|e| e.to_string())?
        .ok_or_else(|| "lead not found or not active".to_string())?;
    LeadEntity::delete_by_id(existing.id)
        .exec(db)
        .await
        .map_err(|e| e.to_string())?;
    Ok(())
}

async fn sync_lead_tags<C: ConnectionTrait>(
    db: &C,
    lead_id: i64,
    tag_ids: &[i64],
) -> Result<(), String> {
    lead_tag_link::Entity::delete_many()
        .filter(lead_tag_link::Column::LeadId.eq(lead_id))
        .exec(db)
        .await
        .map_err(|e| e.to_string())?;
    let mut seen = std::collections::BTreeSet::new();
    for &tag_id in tag_ids {
        if tag_id <= 0 || !seen.insert(tag_id) {
            continue;
        }
        lead_tag_link::ActiveModel {
            lead_id: Set(lead_id),
            lead_tag_id: Set(tag_id),
        }
        .insert(db)
        .await
        .map_err(|e| e.to_string())?;
    }
    Ok(())
}