use chrono::{DateTime, Utc};
use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use backbone_orm::company_scope::fetch_optional_scalar_scoped;
use crate::domain::repositories::journal_workflow_repository::{
JournalAuthors, JournalStatusRow, JournalWorkflowRepository,
};
pub struct SqlxJournalWorkflowRepository {
pool: PgPool,
}
impl SqlxJournalWorkflowRepository {
pub fn new(pool: PgPool) -> Self {
Self { pool }
}
}
#[async_trait::async_trait]
impl JournalWorkflowRepository for SqlxJournalWorkflowRepository {
async fn find_status(
&self,
journal_id: Uuid,
) -> anyhow::Result<Option<JournalStatusRow>> {
let row = org_scope::fetch_optional_row_scoped(
&self.pool,
sqlx::query(
r#"SELECT status::text AS status, currency FROM accounting.journals
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(journal_id),
)
.await?;
Ok(row.map(|r| JournalStatusRow {
status: r.get("status"),
currency: r.get("currency"),
}))
}
async fn current_status(
&self,
journal_id: Uuid,
) -> anyhow::Result<Option<String>> {
let s: Option<String> = fetch_optional_scalar_scoped(
&self.pool,
sqlx::query_scalar(
"SELECT status::text FROM accounting.journals \
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(journal_id),
)
.await?;
Ok(s)
}
async fn submit(&self, journal_id: Uuid,) -> anyhow::Result<bool> {
let res = org_scope::execute_scoped(
&self.pool,
sqlx::query(
"UPDATE accounting.journals SET status='pending_approval'::journal_status \
WHERE id=$1 AND status='draft'::journal_status \
AND (metadata->>'deleted_at') IS NULL",
)
.bind(journal_id),
)
.await?;
Ok(res.rows_affected() > 0)
}
async fn submit_as(&self, journal_id: Uuid, submitted_by: Option<Uuid>) -> anyhow::Result<bool> {
let res = org_scope::execute_scoped(
&self.pool,
sqlx::query(
"UPDATE accounting.journals SET status='pending_approval'::journal_status, submitted_by=$2 \
WHERE id=$1 AND status='draft'::journal_status \
AND (metadata->>'deleted_at') IS NULL",
)
.bind(journal_id)
.bind(submitted_by),
)
.await?;
Ok(res.rows_affected() > 0)
}
async fn authors(&self, journal_id: Uuid) -> anyhow::Result<JournalAuthors> {
let row = org_scope::fetch_optional_row_scoped(
&self.pool,
sqlx::query(
"SELECT submitted_by, metadata->>'created_by' AS created_by FROM accounting.journals WHERE id=$1",
)
.bind(journal_id),
)
.await?;
Ok(row
.map(|r| JournalAuthors {
submitted_by: r.get("submitted_by"),
created_by: r
.get::<Option<String>, _>("created_by")
.and_then(|s| Uuid::parse_str(&s).ok()),
})
.unwrap_or_default())
}
async fn self_approval_allowed(&self) -> anyhow::Result<bool> {
let has_settings: Option<bool> = fetch_optional_scalar_scoped(
&self.pool,
sqlx::query_scalar("SELECT to_regclass('platform.sysparams') IS NOT NULL"),
)
.await?;
if has_settings != Some(true) {
return Ok(false);
}
let value: Option<String> = fetch_optional_scalar_scoped(
&self.pool,
sqlx::query_scalar(
"SELECT value FROM platform.sysparams \
WHERE group_name='accounting' AND key='journal_self_approval' AND status::text='active' LIMIT 1",
),
)
.await?;
Ok(value.as_deref().map(str::trim) == Some("allow"))
}
async fn approve(
&self,
journal_id: Uuid,
approved_by: Option<Uuid>,
at: DateTime<Utc>,
) -> anyhow::Result<bool> {
let res = org_scope::execute_scoped(
&self.pool,
sqlx::query(
"UPDATE accounting.journals SET status='approved'::journal_status, approved_at=$1, approved_by=$2 \
WHERE id=$3 AND status='pending_approval'::journal_status \
AND (metadata->>'deleted_at') IS NULL",
)
.bind(at)
.bind(approved_by)
.bind(journal_id),
)
.await?;
Ok(res.rows_affected() > 0)
}
async fn reject(
&self,
journal_id: Uuid,
reason: &str,
rejected_by: Option<Uuid>,
at: DateTime<Utc>,
) -> anyhow::Result<bool> {
let res = org_scope::execute_scoped(
&self.pool,
sqlx::query(
"UPDATE accounting.journals SET status='rejected'::journal_status, rejected_at=$1, \
rejected_by=$2, rejection_reason=$3 \
WHERE id=$4 AND status IN ('draft','pending_approval') \
AND (metadata->>'deleted_at') IS NULL",
)
.bind(at)
.bind(rejected_by)
.bind(reason)
.bind(journal_id),
)
.await?;
Ok(res.rows_affected() > 0)
}
async fn mark_voided(
&self,
journal_id: Uuid,
voided_by: Option<Uuid>,
reason: &str,
at: DateTime<Utc>,
) -> anyhow::Result<()> {
org_scope::execute_scoped(
&self.pool,
sqlx::query(
"UPDATE accounting.journals SET status='voided'::journal_status, is_voided=TRUE, \
voided_at=$1, voided_by=$2, void_reason=$3 WHERE id=$4",
)
.bind(at)
.bind(voided_by)
.bind(reason)
.bind(journal_id),
)
.await?;
Ok(())
}
async fn original_post(
&self,
journal_id: Uuid,
) -> anyhow::Result<Option<Uuid>> {
let id: Option<Uuid> = fetch_optional_scalar_scoped(
&self.pool,
sqlx::query_scalar(
r#"SELECT id FROM accounting.accounting_posts
WHERE journal_id=$1 AND posting_status='posted'::posting_status
AND posting_type='original' LIMIT 1"#,
)
.bind(journal_id),
)
.await?;
Ok(id)
}
}