use anyhow::Result;
use chrono::{DateTime, Utc};
use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use crate::domain::entity::Issue;
pub const TABLE_NAME: &str = "support.issues";
pub struct IssueRepository(backbone_orm::GenericCrudRepository<Issue, backbone_orm::SoftDelete>);
impl std::ops::Deref for IssueRepository {
type Target = backbone_orm::GenericCrudRepository<Issue, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl IssueRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct NewIssueRow<'a> {
pub id: Uuid,
pub customer_id: Option<Uuid>,
pub subject: &'a str,
pub description: Option<&'a str>,
pub priority: &'a str,
pub sla_id: Option<Uuid>,
pub opened_at: DateTime<Utc>,
pub response_by: Option<DateTime<Utc>>,
pub resolution_by: Option<DateTime<Utc>>,
}
pub struct PausedIssueRow {
pub paused_at: DateTime<Utc>,
pub first_responded_at: Option<DateTime<Utc>>,
}
pub struct IssueStateRow {
pub status: String,
}
pub struct EscalationCandidateRow {
pub customer_id: Option<Uuid>,
pub subject: String,
pub status: String,
pub escalated_project_id: Option<Uuid>,
}
impl IssueRepository {
pub async fn insert_issue(
&self,
pool: &PgPool,
i: &NewIssueRow<'_>,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"INSERT INTO support.issues
(id, customer_id, subject, description, priority, sla_id, status,
agreement_status, opened_at, response_by, resolution_by, total_paused_mins)
VALUES ($1,$2,$3,$4,$5::issue_priority,$6,'open'::issue_status,
'first_response_due'::agreement_status,$7,$8,$9,0)"#,
)
.bind(i.id)
.bind(i.customer_id)
.bind(i.subject)
.bind(i.description)
.bind(i.priority)
.bind(i.sla_id)
.bind(i.opened_at)
.bind(i.response_by)
.bind(i.resolution_by),
)
.await?;
Ok(())
}
pub async fn record_first_response(
&self,
pool: &PgPool,
issue_id: Uuid,
now: DateTime<Utc>,
) -> Result<u64, sqlx::Error> {
let moved = org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE support.issues
SET first_responded_at=$2, status='replied'::issue_status,
response_breached = (response_by IS NOT NULL AND $2 > response_by),
agreement_status = CASE WHEN response_by IS NOT NULL AND $2 > response_by
THEN 'failed'::agreement_status
ELSE 'resolution_due'::agreement_status END
WHERE id=$1 AND status='open'::issue_status AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(issue_id).bind(now),
)
.await?;
Ok(moved.rows_affected())
}
pub async fn pause(
&self,
pool: &PgPool,
issue_id: Uuid,
now: DateTime<Utc>,
) -> Result<u64, sqlx::Error> {
let moved = org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE support.issues
SET status='on_hold'::issue_status, paused_at=$2
WHERE id=$1 AND status IN ('open','replied') AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(issue_id).bind(now),
)
.await?;
Ok(moved.rows_affected())
}
pub async fn lock_on_hold(
&self,
conn: &mut sqlx::PgConnection,
issue_id: Uuid,
) -> Result<Option<PausedIssueRow>, sqlx::Error> {
let row = sqlx::query(
r#"SELECT paused_at, first_responded_at FROM support.issues
WHERE id=$1 AND status='on_hold'::issue_status FOR UPDATE"#,
)
.bind(issue_id)
.fetch_optional(conn)
.await?;
Ok(row.map(|r| PausedIssueRow {
paused_at: r.get("paused_at"),
first_responded_at: r.get("first_responded_at"),
}))
}
pub async fn apply_resume(
&self,
conn: &mut sqlx::PgConnection,
issue_id: Uuid,
paused_mins: i32,
running: &str,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE support.issues
SET status=$3::issue_status, paused_at=NULL,
total_paused_mins = total_paused_mins + $2,
resolution_by = resolution_by + ($2 * interval '1 minute'),
response_by = CASE WHEN first_responded_at IS NULL
THEN response_by + ($2 * interval '1 minute') ELSE response_by END
WHERE id=$1"#,
)
.bind(issue_id).bind(paused_mins).bind(running)
.execute(conn)
.await?;
Ok(())
}
pub async fn find_state(
&self,
pool: &PgPool,
issue_id: Uuid,
) -> Result<Option<IssueStateRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT status::text AS status FROM support.issues
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(issue_id),
)
.await?;
Ok(row.map(|r| IssueStateRow {
status: r.get("status"),
}))
}
pub async fn resolve(
&self,
pool: &PgPool,
issue_id: Uuid,
now: DateTime<Utc>,
) -> Result<Option<bool>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"UPDATE support.issues
SET status='resolved'::issue_status, resolved_at=$2,
agreement_status = CASE
WHEN (resolution_by IS NULL OR $2 <= resolution_by)
AND NOT response_breached
AND (first_responded_at IS NOT NULL OR response_by IS NULL OR $2 <= response_by)
THEN 'fulfilled'::agreement_status
ELSE 'failed'::agreement_status END
WHERE id=$1 AND status IN ('open','replied')
RETURNING (agreement_status = 'fulfilled'::agreement_status) AS fulfilled"#,
)
.bind(issue_id).bind(now),
)
.await?;
Ok(row.map(|r| r.get("fulfilled")))
}
pub async fn close(&self, pool: &PgPool, issue_id: Uuid) -> Result<u64, sqlx::Error> {
let moved = org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE support.issues SET status='closed'::issue_status
WHERE id=$1 AND status='resolved'::issue_status AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(issue_id),
)
.await?;
Ok(moved.rows_affected())
}
pub async fn find_escalation_candidate(
&self,
pool: &PgPool,
issue_id: Uuid,
) -> Result<Option<EscalationCandidateRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT customer_id, subject, status::text AS status, escalated_project_id
FROM support.issues WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(issue_id),
)
.await?;
Ok(row.map(|r| EscalationCandidateRow {
customer_id: r.get("customer_id"),
subject: r.get("subject"),
status: r.get("status"),
escalated_project_id: r.get("escalated_project_id"),
}))
}
pub async fn claim_escalation(
&self,
conn: &mut sqlx::PgConnection,
issue_id: Uuid,
project_id: Uuid,
) -> Result<u64, sqlx::Error> {
let moved = sqlx::query(
r#"UPDATE support.issues SET escalated_project_id=$2
WHERE id=$1 AND escalated_project_id IS NULL"#,
)
.bind(issue_id)
.bind(project_id)
.execute(conn)
.await?;
Ok(moved.rows_affected())
}
pub async fn fetch_escalated_project_id(
&self,
pool: &PgPool,
issue_id: Uuid,
) -> Result<Uuid, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query("SELECT escalated_project_id FROM support.issues WHERE id=$1")
.bind(issue_id),
)
.await?;
row.map(|r| r.get::<Uuid, _>("escalated_project_id"))
.ok_or(sqlx::Error::RowNotFound)
}
}
backbone_core::impl_crud_repository!(IssueRepository, Issue, soft_delete);