Skip to content

Commit

Permalink
Fix payout ratio at the moment of loan issuance
Browse files Browse the repository at this point in the history
  • Loading branch information
Leonid Kozarin committed Apr 30, 2024
1 parent c2e7529 commit c754b7b
Show file tree
Hide file tree
Showing 22 changed files with 263 additions and 169 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

This file was deleted.

This file was deleted.

3 changes: 2 additions & 1 deletion locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ commands:
description: "Take a loan if you have a deep cave"
debt: "Left to pay <b>%{debt} cm</b>"
confirmation:
text: "Your deep hole will be reset to zero, but each growth will be lowered by <b>%{payout_percentage}%</b> until all <b>%{debt} cm</b> is repaid."
text: "Your deep hole will be reset to zero, but each growth will be lowered by <b>%{payout_percentage}</b> until all <b>%{debt} cm</b> is repaid."
buttons:
agree: "I'm in!"
disagree: "Disagree"
callback:
success: "Perk «Where is my centimeters, Lebowski?» has been assigned successfully!"
refused: "The patient refused gender reassignment on credit."
payout_ratio_changed: "The payout rate has been changed since you sent the loan application. Please, invoke the command again."
errors:
positive_length: "I cannot make your little bro bigger. Only change your gender."
import:
Expand Down
3 changes: 2 additions & 1 deletion locales/ru.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,14 @@ commands:
description: "Возьми кредит, если у тебя глубокая пещера"
debt: "Осталось выплатить <b>%{debt} см</b>"
confirmation:
text: "Твоя пропасть будет обнулена, но размер каждого прироста снизится на <b>%{payout_percentage}%</b> до выплаты всех <b>%{debt} см</b>."
text: "Твоя пропасть будет обнулена, но размер каждого прироста снизится на <b>%{payout_percentage}</b> до выплаты всех <b>%{debt} см</b>."
buttons:
agree: "Согласен"
disagree: "Я пас"
callback:
success: "Перк «Где мои сантиметры, Лебовски?» успешно предоставлен!"
refused: "Пациент отказался от смены пола в кредит."
payout_ratio_changed: "С момента подачи заявления ставка выплаты изменилась. Пожалуйста, вызовите команду ещё раз."
errors:
positive_length: "Увеличение братюни недоступно: только смена пола."
import:
Expand Down
3 changes: 3 additions & 0 deletions migrations/16_add-loans-column-payout-ratio.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE Loans ADD COLUMN IF NOT EXISTS payout_ratio real NOT NULL DEFAULT 0.1
CHECK ( payout_ratio > 0.0 AND payout_ratio < 1.0 );
ALTER TABLE Loans ALTER COLUMN payout_ratio DROP DEFAULT;
3 changes: 3 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::help;
pub struct AppConfig {
pub features: FeatureToggles,
pub top_limit: u16,
pub loan_payout_ratio: f32,
}

#[derive(Clone)]
Expand Down Expand Up @@ -46,6 +47,7 @@ pub struct BattlesFeatureToggles {
impl AppConfig {
pub fn from_env() -> Self {
let top_limit = get_env_value_or_default("TOP_LIMIT", 10);
let loan_payout_ratio = get_env_value_or_default("LOAN_PAYOUT_COEF", 0.0);
let chats_merging = get_env_value_or_default("CHATS_MERGING_ENABLED", false);
let top_unlimited = get_env_value_or_default("TOP_UNLIMITED_ENABLED", false);
let check_acceptor_length = get_env_value_or_default("PVP_CHECK_ACCEPTOR_LENGTH", false);
Expand All @@ -58,6 +60,7 @@ impl AppConfig {
}
},
top_limit,
loan_payout_ratio,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/handlers/inline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ impl InlineCommand {
},
InlineCommand::Loan => {
metrics::CMD_LOAN_COUNTER.invoked.inline.inc();
loan::loan_impl(repos, from_refs, incr)
loan::loan_impl(repos, from_refs, config)
.await
.map(InlineResult::from)
}
Expand Down
106 changes: 72 additions & 34 deletions src/handlers/loan.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::str::{FromStr, Split};
use anyhow::anyhow;
use derive_more::Display;
use num_traits::Zero;
use rust_i18n::t;
use teloxide::Bot;
use teloxide::macros::BotCommands;
Expand All @@ -9,12 +10,12 @@ use teloxide::requests::Requester;
use teloxide::types::ReplyMarkup;
use callbacks::{EditMessageReqParamsKind, InvalidCallbackData};

use crate::{check_invoked_by_owner_and_get_answer_params, config, metrics, repo};
use crate::{check_invoked_by_owner_and_get_answer_params, metrics, repo};
use crate::config::AppConfig;
use crate::handlers::{CallbackButton, ensure_lang_code, FromRefs, HandlerImplResult, HandlerResult, reply_html, try_resolve_chat_id};
use crate::handlers::perks::LoanPayoutPerk;
use crate::handlers::utils::{callbacks, Incrementor};
use crate::handlers::utils::callbacks;
use crate::handlers::utils::callbacks::{CallbackDataWithPrefix, InvalidCallbackDataBuilder};
use crate::repo::ChatIdPartiality;
use crate::repo::{ChatIdPartiality, Loan};

#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
Expand All @@ -24,14 +25,14 @@ pub enum LoanCommands {
Borrow,
}

pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, incr: Incrementor) -> HandlerResult {
pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, config: AppConfig) -> HandlerResult {
metrics::CMD_LOAN_COUNTER.invoked.chat.inc();

let from = msg.from().ok_or(anyhow!("unexpected absence of a FROM field"))?;
let chat_id = msg.chat.id.into();
let from_refs = FromRefs(from, &chat_id);

let result = loan_impl(&repos, from_refs, incr).await?;
let result = loan_impl(&repos, from_refs, config).await?;
let markup = result.keyboard().map(ReplyMarkup::InlineKeyboard);

let mut request = reply_html(bot, msg, result.text());
Expand All @@ -41,33 +42,35 @@ pub async fn cmd_handler(bot: Bot, msg: Message, repos: repo::Repositories, incr
Ok(())
}

pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_>, incr: Incrementor) -> anyhow::Result<HandlerImplResult<LoanCallbackData>> {
pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_>, config: AppConfig) -> anyhow::Result<HandlerImplResult<LoanCallbackData>> {
let (from, chat_id_part) = (from_refs.0, from_refs.1);
let chat_id_kind = chat_id_part.kind();
let lang_code = ensure_lang_code(Some(from));

let active_loan = repos.loans.get_active_loan(from.id, &chat_id_kind).await?;
if active_loan > 0 {
let left_to_pay = t!("commands.loan.debt", locale = &lang_code, debt = active_loan);
let maybe_loan = repos.loans.get_active_loan(from.id, &chat_id_kind).await?;
if let Some(Loan { debt, .. }) = maybe_loan {
let left_to_pay = t!("commands.loan.debt", locale = &lang_code, debt = debt);
return Ok(HandlerImplResult::OnlyText(left_to_pay))
}

let payout_percentage = if let Some(payout_ratio) = incr.find_perk_config::<LoanPayoutPerk>() {
payout_ratio * 100.0
} else {
if config.loan_payout_ratio <= 0.0 || config.loan_payout_ratio >= 1.0 {
let err_text = t!("errors.feature_disabled", locale = &lang_code);
return Ok(HandlerImplResult::OnlyText(err_text))
};
}

let length = repos.dicks.fetch_length(from.id, &chat_id_kind).await?;
let res = if length < 0 {
let debt = length.unsigned_abs() as u16;
let payout_percentage = format!("{:.2}%", config.loan_payout_ratio * 100.0);

let btn_agree = CallbackButton::new(
t!("commands.loan.confirmation.buttons.agree", locale = &lang_code),
LoanCallbackData {
uid: from.id,
action: LoanCallbackAction::Confirmed { value: debt }
action: LoanCallbackAction::Confirmed {
value: debt,
payout_ratio: config.loan_payout_ratio
}
}
);
let btn_disagree = CallbackButton::new(
Expand All @@ -78,7 +81,8 @@ pub(crate) async fn loan_impl(repos: &repo::Repositories, from_refs: FromRefs<'_
}
);
HandlerImplResult::WithKeyboard {
text: t!("commands.loan.confirmation.text", locale = &lang_code, debt = debt, payout_percentage = payout_percentage),
text: t!("commands.loan.confirmation.text", locale = &lang_code,
debt = debt, payout_percentage = payout_percentage),
buttons: vec![btn_agree, btn_disagree]
}
} else {
Expand All @@ -94,13 +98,17 @@ pub fn callback_filter(query: CallbackQuery) -> bool {
}

pub async fn callback_handler(bot: Bot, query: CallbackQuery,
repos: repo::Repositories, config: config::AppConfig) -> HandlerResult {
repos: repo::Repositories, config: AppConfig) -> HandlerResult {
let data = LoanCallbackData::parse(&query)?;
let (answer, lang_code) = check_invoked_by_owner_and_get_answer_params!(bot, query, data.uid);
let (mut answer, lang_code) = check_invoked_by_owner_and_get_answer_params!(bot, query, data.uid);

let edit_msg_params = callbacks::get_params_for_message_edit(&query)?;

match data.action {
LoanCallbackAction::Confirmed { value } => {
LoanCallbackAction::Confirmed { .. } if config.loan_payout_ratio.is_zero() => {
answer.show_alert.replace(true);
answer.text.replace(t!("errors.feature_disabled", locale = &lang_code));
}
LoanCallbackAction::Confirmed { value, payout_ratio } if payout_ratio == config.loan_payout_ratio => {
metrics::CMD_LOAN_COUNTER.finished.inc();
let updated_text = t!("commands.loan.callback.success", locale = &lang_code);
match edit_msg_params {
Expand All @@ -126,6 +134,17 @@ pub async fn callback_handler(bot: Bot, query: CallbackQuery,
}
}
}
LoanCallbackAction::Confirmed { .. } => {
let updated_text = t!("commands.loan.callback.payout_ratio_changed", locale = &lang_code);
match edit_msg_params {
EditMessageReqParamsKind::Chat(chat_id, message_id) => {
bot.edit_message_text(chat_id, message_id, updated_text).await?;
}
EditMessageReqParamsKind::Inline { inline_message_id, .. } => {
bot.edit_message_text_inline(inline_message_id, updated_text).await?;
}
}
}
LoanCallbackAction::Refused => {
let updated_text = t!("commands.loan.callback.refused", locale = &lang_code);
match edit_msg_params {
Expand Down Expand Up @@ -163,10 +182,10 @@ pub(crate) struct LoanCallbackData {
}

#[derive(Display)]
#[cfg_attr(test, derive(PartialEq, Eq, Debug))]
#[cfg_attr(test, derive(PartialEq, Debug))]
pub(crate) enum LoanCallbackAction {
#[display("confirmed:{value}")]
Confirmed { value: u16 },
#[display("confirmed:{value}:{payout_ratio}")]
Confirmed { value: u16, payout_ratio: f32 },
#[display("refused")]
Refused
}
Expand All @@ -189,7 +208,15 @@ impl TryFrom<String> for LoanCallbackData {
let action = match action {
"confirmed" => {
let value = Self::parse_part(&mut parts, &err, "value")?;
LoanCallbackAction::Confirmed { value }
let payout_ratio = match Self::parse_part(&mut parts, &err, "payout_ratio") {
Ok(ratio) => ratio,
// for backward compatibility; zero ratio disables the loans completely,
// so this value is out of possible ones, thus either the "rate changed" or
// "feature disabled" message will always be sent.
Err(InvalidCallbackData::MissingPart { .. }) => 0.0,
Err(e) => return Err(e)
};
LoanCallbackAction::Confirmed { value, payout_ratio }
}
"refused" => LoanCallbackAction::Refused,
_ => return Err(err.split_err())
Expand Down Expand Up @@ -220,46 +247,57 @@ mod test {

#[test]
fn test_parse() {
let (uid, value) = get_test_params();
let [cd_confirmed, cd_refused] = get_strings(uid, value)
let (uid, value, payout_ratio) = get_test_params();
let [cd_confirmed, cd_refused] = get_strings(uid, value, payout_ratio)
.map(build_callback_query);
{
let lcd_confirmed = LoanCallbackData::parse(&cd_confirmed)
.expect("callback data for 'confirmed' must be parsed successfully");
assert_eq!(lcd_confirmed.uid, uid);
assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value });
assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value, payout_ratio });
}{
let lcd_refused = LoanCallbackData::parse(&cd_refused)
.expect("callback data for 'refused' must be parsed successfully");
assert_eq!(lcd_refused.uid, uid);
assert_eq!(lcd_refused.action, LoanCallbackAction::Refused)
}
}

#[test]
fn test_parse_old() {
let (uid, value, _) = get_test_params();
let cd_confirmed = build_callback_query(format!("loan:{uid}:confirmed:{value}"));

let lcd_confirmed = LoanCallbackData::parse(&cd_confirmed)
.expect("callback data for 'confirmed' must be parsed successfully");
assert_eq!(lcd_confirmed.uid, uid);
assert_eq!(lcd_confirmed.action, LoanCallbackAction::Confirmed { value, payout_ratio: 0.0 });
}

#[test]
fn test_serialize() {
let (uid, value) = get_test_params();
let (uid, value, payout_ratio) = get_test_params();
let lcd_confirmed = LoanCallbackData {
uid,
action: LoanCallbackAction::Confirmed { value }
action: LoanCallbackAction::Confirmed { value, payout_ratio }
};
let lcd_refused = LoanCallbackData {
uid,
action: LoanCallbackAction::Refused
};

let [expected_confirmed, expected_refused] = get_strings(uid, value);
let [expected_confirmed, expected_refused] = get_strings(uid, value, payout_ratio);

assert_eq!(lcd_confirmed.to_data_string(), expected_confirmed);
assert_eq!(lcd_refused.to_data_string(), expected_refused);
}

fn get_test_params() -> (UserId, u16) {
(UserId(123456), 10)
fn get_test_params() -> (UserId, u16, f32) {
(UserId(123456), 10, 0.1)
}

fn get_strings(uid: UserId, value: u16) -> [String; 2] {[
format!("loan:{uid}:confirmed:{value}"),
fn get_strings(uid: UserId, value: u16, payout_ratio: f32) -> [String; 2] {[
format!("loan:{uid}:confirmed:{value}:{payout_ratio}"),
format!("loan:{uid}:refused"),
]}

Expand Down
Loading

0 comments on commit c754b7b

Please sign in to comment.