use anyhow::Result;
use sqlx::PgPool;
use uuid::Uuid;
use backbone_orm::org_scope;
use backbone_orm::company_scope::fetch_optional_scalar_scoped;
use crate::domain::entity::PosOpeningEntry;
pub const TABLE_NAME: &str = "pos.pos_opening_entries";
pub struct PosOpeningEntryRepository(
backbone_orm::GenericCrudRepository<PosOpeningEntry, backbone_orm::SoftDelete>,
);
impl std::ops::Deref for PosOpeningEntryRepository {
type Target = backbone_orm::GenericCrudRepository<PosOpeningEntry, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl PosOpeningEntryRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct NewOpeningEntryRow {
pub id: Uuid,
pub pos_profile_id: Uuid,
pub branch_id: Option<Uuid>,
pub cashier_party_id: Uuid,
pub opened_at: chrono::NaiveDateTime,
pub opening_balances: serde_json::Value,
}
pub struct StaleSessionRow {
pub opening_entry_id: Uuid,
pub pos_profile_id: Uuid,
pub cashier_party_id: Uuid,
pub opened_at: chrono::NaiveDateTime,
}
impl PosOpeningEntryRepository {
pub async fn insert_opening_entry(
&self,
conn: &mut sqlx::PgConnection,
s: &NewOpeningEntryRow,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"INSERT INTO pos.pos_opening_entries
(id, pos_profile_id, branch_id, cashier_party_id, opened_at, opening_balances, status)
VALUES ($1,$2,$3,$4,$5,$6,'open'::pos_session_status)"#,
)
.bind(s.id).bind(s.pos_profile_id).bind(s.branch_id).bind(s.cashier_party_id)
.bind(s.opened_at).bind(sqlx::types::Json(&s.opening_balances))
.execute(conn)
.await?;
Ok(())
}
pub async fn fetch_status(
&self,
pool: &PgPool,
opening_entry_id: Uuid,
) -> Result<Option<String>, sqlx::Error> {
fetch_optional_scalar_scoped(
pool,
sqlx::query_scalar(
"SELECT status::text FROM pos.pos_opening_entries WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(opening_entry_id),
)
.await
}
pub async fn fetch_status_on(
&self,
conn: &mut sqlx::PgConnection,
opening_entry_id: Uuid,
) -> Result<Option<String>, sqlx::Error> {
sqlx::query_scalar(
"SELECT status::text FROM pos.pos_opening_entries WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(opening_entry_id)
.fetch_optional(conn)
.await
}
#[allow(clippy::type_complexity)]
pub async fn fetch_opening_balances(
&self,
pool: &PgPool,
opening_entry_id: Uuid,
) -> Result<Option<Option<sqlx::types::Json<serde_json::Value>>>, sqlx::Error> {
fetch_optional_scalar_scoped(
pool,
sqlx::query_scalar(
"SELECT opening_balances FROM pos.pos_opening_entries WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(opening_entry_id),
)
.await
}
pub async fn has_open_session(
&self,
conn: &mut sqlx::PgConnection,
pos_profile_id: Uuid,
) -> Result<bool, sqlx::Error> {
let found: Option<i32> = sqlx::query_scalar(
r#"SELECT 1 FROM pos.pos_opening_entries
WHERE pos_profile_id=$1 AND status='open'::pos_session_status
AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(pos_profile_id)
.fetch_optional(conn)
.await?;
Ok(found.is_some())
}
pub async fn fetch_profile_id(
&self,
pool: &PgPool,
opening_entry_id: Uuid,
) -> Result<Option<Uuid>, sqlx::Error> {
fetch_optional_scalar_scoped(
pool,
sqlx::query_scalar(
"SELECT pos_profile_id FROM pos.pos_opening_entries WHERE id=$1 AND (metadata->>'deleted_at') IS NULL",
)
.bind(opening_entry_id),
)
.await
}
pub async fn fetch_profile_id_on(
&self,
conn: &mut sqlx::PgConnection,
opening_entry_id: Uuid,
) -> Result<Option<Uuid>, sqlx::Error> {
sqlx::query_scalar("SELECT pos_profile_id FROM pos.pos_opening_entries WHERE id=$1")
.bind(opening_entry_id)
.fetch_optional(conn)
.await
}
pub async fn fetch_variance_accounts_on(
&self,
conn: &mut sqlx::PgConnection,
opening_entry_id: Uuid,
) -> Result<(Option<Uuid>, Option<Uuid>, String), sqlx::Error> {
sqlx::query_as(
r#"SELECT p.cash_account_id, p.write_off_account_id, p.currency
FROM pos.pos_opening_entries o JOIN pos.pos_profiles p ON p.id = o.pos_profile_id
WHERE o.id=$1"#,
)
.bind(opening_entry_id)
.fetch_one(conn)
.await
}
pub async fn mark_closed(
&self,
conn: &mut sqlx::PgConnection,
opening_entry_id: Uuid,
) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE pos.pos_opening_entries SET status='closed'::pos_session_status WHERE id=$1")
.bind(opening_entry_id)
.execute(conn)
.await?;
Ok(())
}
pub async fn claim_stale_sessions(
&self,
conn: &mut sqlx::PgConnection,
cutoff: chrono::NaiveDateTime,
limit: i64,
) -> Result<Vec<StaleSessionRow>, sqlx::Error> {
let rows = sqlx::query_as::<_, (Uuid, Uuid, Uuid, chrono::NaiveDateTime)>(
r#"SELECT id, pos_profile_id, cashier_party_id, opened_at::timestamp
FROM pos.pos_opening_entries
WHERE status='open'::pos_session_status
AND opened_at < $1
AND (metadata->>'stale_session_alerted_at') IS NULL
AND (metadata->>'deleted_at') IS NULL
ORDER BY opened_at
LIMIT $2
FOR UPDATE SKIP LOCKED"#,
)
.bind(cutoff)
.bind(limit)
.fetch_all(conn)
.await?;
Ok(rows
.into_iter()
.map(|(opening_entry_id, pos_profile_id, cashier_party_id, opened_at)| StaleSessionRow {
opening_entry_id,
pos_profile_id,
cashier_party_id,
opened_at,
})
.collect())
}
pub async fn mark_stale_alerted(
&self,
conn: &mut sqlx::PgConnection,
opening_entry_ids: &[Uuid],
at: chrono::DateTime<chrono::Utc>,
) -> Result<(), sqlx::Error> {
sqlx::query(
r#"UPDATE pos.pos_opening_entries
SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{stale_session_alerted_at}', to_jsonb($2::timestamptz))
WHERE id = ANY($1)"#,
)
.bind(opening_entry_ids)
.bind(at)
.execute(conn)
.await?;
Ok(())
}
}
backbone_core::impl_crud_repository!(PosOpeningEntryRepository, PosOpeningEntry, soft_delete);