use anyhow::Result;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use backbone_orm::company_scope::fetch_one_scalar_scoped;
use crate::domain::entity::PayrollEntry;
pub const TABLE_NAME: &str = "payroll.payroll_entries";
pub struct PayrollEntryRepository(
backbone_orm::GenericCrudRepository<PayrollEntry, backbone_orm::SoftDelete>,
);
impl std::ops::Deref for PayrollEntryRepository {
type Target = backbone_orm::GenericCrudRepository<PayrollEntry, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl PayrollEntryRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct NewPayrollEntryRow {
pub id: Uuid,
pub period_year: i32,
pub period_month: i32,
pub period_start: Option<chrono::NaiveDate>,
pub period_end: Option<chrono::NaiveDate>,
pub salary_expense_account_id: Uuid,
pub salary_payable_account_id: Uuid,
}
pub struct RunStateRow {
pub status: String,
}
pub struct RunPeriodRow {
pub status: String,
pub period_year: i32,
pub period_month: i32,
pub period_start: Option<chrono::NaiveDate>,
pub period_end: Option<chrono::NaiveDate>,
pub salary_expense_account_id: Option<Uuid>,
}
pub struct RunPostingRow {
pub status: String,
pub salary_expense_account_id: Option<Uuid>,
pub salary_payable_account_id: Option<Uuid>,
pub total_gross: Decimal,
pub total_deductions: Decimal,
pub total_net: Decimal,
pub journal_id: Option<Uuid>,
pub accounting_post_id: Option<Uuid>,
}
impl PayrollEntryRepository {
pub async fn insert_entry(
&self,
pool: &PgPool,
e: &NewPayrollEntryRow,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"INSERT INTO payroll.payroll_entries
(id, period_year, period_month, period_start, period_end, status,
salary_expense_account_id, salary_payable_account_id,
total_gross, total_deductions, total_net)
VALUES ($1,$2,$3,$6,$7,'draft'::payroll_status,$4,$5,0,0,0)"#,
)
.bind(e.id).bind(e.period_year).bind(e.period_month)
.bind(e.salary_expense_account_id).bind(e.salary_payable_account_id)
.bind(e.period_start).bind(e.period_end),
)
.await?;
Ok(())
}
pub async fn find_state_by_id(
&self,
pool: &PgPool,
run_id: Uuid,
) -> Result<Option<RunStateRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT status::text AS status FROM payroll.payroll_entries
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(run_id),
)
.await?;
Ok(row.map(|r| RunStateRow { status: r.get("status") }))
}
pub async fn find_period_by_id(
&self,
pool: &PgPool,
run_id: Uuid,
) -> Result<Option<RunPeriodRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT status::text AS status, period_year, period_month,
period_start, period_end, salary_expense_account_id
FROM payroll.payroll_entries
WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(run_id),
)
.await?;
Ok(row.map(|r| RunPeriodRow {
status: r.get("status"),
period_year: r.get("period_year"),
period_month: r.get("period_month"),
period_start: r.get("period_start"),
period_end: r.get("period_end"),
salary_expense_account_id: r.get("salary_expense_account_id"),
}))
}
pub async fn mark_processed(
&self,
pool: &PgPool,
run_id: Uuid,
total_gross: Decimal,
total_deductions: Decimal,
total_net: Decimal,
) -> Result<u64, sqlx::Error> {
let done = org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE payroll.payroll_entries
SET status='processed'::payroll_status, total_gross=$2, total_deductions=$3, total_net=$4
WHERE id=$1 AND status='draft'::payroll_status"#,
)
.bind(run_id).bind(total_gross).bind(total_deductions).bind(total_net),
)
.await?;
Ok(done.rows_affected())
}
pub async fn find_for_posting(
&self,
pool: &PgPool,
run_id: Uuid,
) -> Result<Option<RunPostingRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT status::text AS status, salary_expense_account_id, salary_payable_account_id,
total_gross, total_deductions, total_net, journal_id, accounting_post_id
FROM payroll.payroll_entries WHERE id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(run_id),
)
.await?;
Ok(row.map(|r| RunPostingRow {
status: r.get("status"),
salary_expense_account_id: r.get("salary_expense_account_id"),
salary_payable_account_id: r.get("salary_payable_account_id"),
total_gross: r.get("total_gross"), total_deductions: r.get("total_deductions"),
total_net: r.get("total_net"), journal_id: r.get("journal_id"),
accounting_post_id: r.get("accounting_post_id"),
}))
}
pub async fn mark_posted(
&self,
pool: &PgPool,
run_id: Uuid,
posting_date: DateTime<Utc>,
journal_id: Uuid,
post_id: Uuid,
) -> Result<u64, sqlx::Error> {
let done = org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE payroll.payroll_entries
SET status='posted'::payroll_status, posting_date=$2, journal_id=$3, accounting_post_id=$4
WHERE id=$1 AND status='processed'::payroll_status"#,
)
.bind(run_id).bind(posting_date).bind(journal_id).bind(post_id),
)
.await?;
Ok(done.rows_affected())
}
pub async fn fetch_journal_id(
&self,
pool: &PgPool,
run_id: Uuid,
) -> Result<Uuid, sqlx::Error> {
fetch_one_scalar_scoped(
pool,
sqlx::query_scalar("SELECT journal_id FROM payroll.payroll_entries WHERE id=$1")
.bind(run_id),
)
.await
}
}
backbone_core::impl_crud_repository!(PayrollEntryRepository, PayrollEntry, soft_delete);