diff --git a/.sqlx/query-716d3c846ae1c0dcf53c8035b7c987e5d579cca262fa37afcd8b5b7fa0dea0bf.json b/.sqlx/query-716d3c846ae1c0dcf53c8035b7c987e5d579cca262fa37afcd8b5b7fa0dea0bf.json new file mode 100644 index 0000000..c1b8a1a --- /dev/null +++ b/.sqlx/query-716d3c846ae1c0dcf53c8035b7c987e5d579cca262fa37afcd8b5b7fa0dea0bf.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE Loans SET left_to_pay = left_to_pay - $3 WHERE uid = $1 AND chat_id = $2 AND repaid_at IS NULL", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "716d3c846ae1c0dcf53c8035b7c987e5d579cca262fa37afcd8b5b7fa0dea0bf" +} diff --git a/.sqlx/query-ec8e198184da36681b997c8a7d59ec350784e471158846cf152e6aa2589389dc.json b/.sqlx/query-ec8e198184da36681b997c8a7d59ec350784e471158846cf152e6aa2589389dc.json new file mode 100644 index 0000000..f069ea0 --- /dev/null +++ b/.sqlx/query-ec8e198184da36681b997c8a7d59ec350784e471158846cf152e6aa2589389dc.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT left_to_pay FROM Loans WHERE uid = $1 AND chat_id = $2 AND repaid_at IS NULL", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "left_to_pay", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "ec8e198184da36681b997c8a7d59ec350784e471158846cf152e6aa2589389dc" +} diff --git a/migrations/15_create-loans-table.sql b/migrations/15_create-loans-table.sql new file mode 100644 index 0000000..7f90ea6 --- /dev/null +++ b/migrations/15_create-loans-table.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS Loans ( + id serial PRIMARY KEY, + uid bigint NOT NULL REFERENCES Users(uid), + chat_id bigint NOT NULL REFERENCES Chats(id), + left_to_pay int NOT NULL CHECK ( left_to_pay >= 0 ), + created_at date NOT NULL DEFAULT current_date, + repaid_at date +); + +CREATE INDEX IF NOT EXISTS idx_loans_uid ON Loans(uid); diff --git a/src/repo/loans.rs b/src/repo/loans.rs new file mode 100644 index 0000000..bcd16e4 --- /dev/null +++ b/src/repo/loans.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; +use std::sync::Arc; +use teloxide::types::UserId; +use tokio::sync::RwLock; +use crate::config::FeatureToggles; +use crate::repo; +use crate::repo::{ChatIdKind, ChatIdPartiality}; + +#[derive(Clone)] +pub struct Loans { + pool: sqlx::Pool, + features: FeatureToggles, + chat_id_cache: Arc>>, + chats_repo: repo::Chats, +} + +impl Loans { + pub fn new(pool: sqlx::Pool, features: FeatureToggles) -> Self { + Self { + chats_repo: repo::Chats::new(pool.clone(), features), + pool, + features, + chat_id_cache: Arc::new(RwLock::new(HashMap::new())), + } + } + + pub async fn get_active_loan(&self, uid: UserId, chat_id: ChatIdPartiality) -> anyhow::Result { + let internal_chat_id = match self.get_internal_chat_id(chat_id.kind()).await? { + Some(id) => id, + None => return Ok(Default::default()) + }; + sqlx::query_scalar!("SELECT left_to_pay FROM loans WHERE uid = $1 AND chat_id = $2 AND repaid_at IS NULL", + uid.0 as i64, internal_chat_id) + .fetch_optional(&self.pool) + .await + .map(|maybe_loan| maybe_loan.map(|value| value as u32).unwrap_or_default()) + .map_err(|e| e.into()) + } + + pub async fn pay(&self, uid: UserId, chat_id: ChatIdPartiality, value: u32) -> anyhow::Result<()> { + let internal_chat_id = match self.get_internal_chat_id(chat_id.kind()).await? { + Some(id) => id, + None => return Ok(()) // TODO: check or logging? + }; + sqlx::query!("UPDATE Loans SET left_to_pay = left_to_pay - $3 WHERE uid = $1 AND chat_id = $2 AND repaid_at IS NULL", + uid.0 as i64, internal_chat_id, value as i64) + .execute(&self.pool) + .await + .map(|_| ()) + .map_err(|e| e.into()) + } + + async fn get_internal_chat_id(&self, chat_id: ChatIdKind) -> anyhow::Result> { + let maybe_internal_id = self.chat_id_cache + .read().await + .get(&chat_id).copied(); + let internal_id = match maybe_internal_id { + None => { + let maybe_id = self.chats_repo.get_chat(chat_id.clone()) + .await? + .map(|chat| chat.internal_id); + if let Some(id) = maybe_id { + self.chat_id_cache + .write().await + .insert(chat_id, id); + } + maybe_id + } + Some(id) => Some(id) + }; + Ok(internal_id) + } +} diff --git a/src/repo/mod.rs b/src/repo/mod.rs index 0287aae..e94ca70 100644 --- a/src/repo/mod.rs +++ b/src/repo/mod.rs @@ -3,6 +3,7 @@ mod dicks; mod chats; mod import; mod promo; +mod loans; #[cfg(test)] pub(crate) mod test; @@ -95,7 +96,7 @@ impl ChatIdFull { } } -#[derive(Debug, derive_more::Display, Clone)] +#[derive(Debug, derive_more::Display, Clone, Eq, PartialEq, Hash)] pub enum ChatIdKind { ID(ChatId), Instance(String)