use sqlx::{PgPool, Row};
use uuid::Uuid;
use backbone_orm::org_scope;
use crate::domain::entity::PosManagerPin;
pub const TABLE_NAME: &str = "pos.pos_manager_pins";
pub struct PosManagerPinRepository(
backbone_orm::GenericCrudRepository<PosManagerPin, backbone_orm::SoftDelete>,
);
impl std::ops::Deref for PosManagerPinRepository {
type Target = backbone_orm::GenericCrudRepository<PosManagerPin, backbone_orm::SoftDelete>;
fn deref(&self) -> &Self::Target { &self.0 }
}
impl PosManagerPinRepository {
pub fn new(pool: PgPool) -> Self {
Self(backbone_orm::GenericCrudRepository::new(pool, TABLE_NAME))
}
}
pub struct PinCredentialRow {
pub id: Uuid,
pub pin_hash: String,
pub failed_attempts: i32,
pub locked_until: Option<chrono::DateTime<chrono::Utc>>,
}
impl PosManagerPinRepository {
pub async fn upsert_hash_on(
&self,
conn: &mut sqlx::PgConnection,
employee_party_id: Uuid,
pin_hash: &str,
now: chrono::DateTime<chrono::Utc>,
source_ip: Option<&str>,
) -> Result<(), sqlx::Error> {
let existing = sqlx::query(
r#"SELECT id FROM pos.pos_manager_pins
WHERE employee_party_id=$1 AND (metadata->>'deleted_at') IS NULL
FOR UPDATE"#,
)
.bind(employee_party_id)
.fetch_optional(&mut *conn)
.await?;
if let Some(row) = existing {
sqlx::query(
r#"UPDATE pos.pos_manager_pins SET
pin_hash=$2, failed_attempts=0, locked_until=NULL,
last_attempt_at=$3, last_attempt_ip=$4
WHERE id=$1"#,
)
.bind(row.get::<Uuid, _>("id")).bind(pin_hash).bind(now).bind(source_ip)
.execute(conn)
.await?;
} else {
sqlx::query(
r#"INSERT INTO pos.pos_manager_pins
(id, employee_party_id, pin_hash, failed_attempts, locked_until, last_attempt_at, last_attempt_ip)
VALUES ($1,$2,$3,0,NULL,$4,$5)"#,
)
.bind(Uuid::new_v4()).bind(employee_party_id).bind(pin_hash).bind(now).bind(source_ip)
.execute(conn)
.await?;
}
Ok(())
}
pub async fn fetch_credential(
&self,
pool: &PgPool,
employee_party_id: Uuid,
) -> Result<Option<PinCredentialRow>, sqlx::Error> {
let row = org_scope::fetch_optional_row_scoped(
pool,
sqlx::query(
r#"SELECT id, pin_hash, failed_attempts, locked_until FROM pos.pos_manager_pins
WHERE employee_party_id=$1 AND (metadata->>'deleted_at') IS NULL"#,
)
.bind(employee_party_id),
)
.await?;
Ok(row.map(|r| PinCredentialRow {
id: r.get("id"),
pin_hash: r.get("pin_hash"),
failed_attempts: r.get("failed_attempts"),
locked_until: r.get("locked_until"),
}))
}
pub async fn any_live_pin_exists_on(
&self,
conn: &mut sqlx::PgConnection,
) -> Result<bool, sqlx::Error> {
let n: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM pos.pos_manager_pins WHERE (metadata->>'deleted_at') IS NULL",
)
.fetch_one(conn)
.await?;
Ok(n > 0)
}
pub async fn record_failure(
&self,
pool: &PgPool,
id: Uuid,
max_attempts: u32,
lockout_until: chrono::DateTime<chrono::Utc>,
now: chrono::DateTime<chrono::Utc>,
source_ip: Option<&str>,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
r#"UPDATE pos.pos_manager_pins SET
failed_attempts = failed_attempts + 1,
locked_until = CASE WHEN failed_attempts + 1 >= $2 THEN $3 ELSE locked_until END,
last_attempt_at = $4,
last_attempt_ip = $5
WHERE id=$1"#,
)
.bind(id).bind(max_attempts as i32).bind(lockout_until).bind(now).bind(source_ip),
)
.await?;
Ok(())
}
pub async fn record_success(
&self,
pool: &PgPool,
id: Uuid,
now: chrono::DateTime<chrono::Utc>,
source_ip: Option<&str>,
) -> Result<(), sqlx::Error> {
org_scope::execute_scoped(
pool,
sqlx::query(
"UPDATE pos.pos_manager_pins SET failed_attempts=0, locked_until=NULL, last_attempt_at=$2, last_attempt_ip=$3 WHERE id=$1",
)
.bind(id).bind(now).bind(source_ip),
)
.await?;
Ok(())
}
}
backbone_core::impl_crud_repository!(PosManagerPinRepository, PosManagerPin, soft_delete);