blastmud/blastmud_game/src/message_handler/user_commands/pay.rs
2023-10-01 16:34:25 +11:00

268 lines
9.2 KiB
Rust

use super::{
corp::check_corp_perm, get_user_or_fail, parsing::parse_payment_request, user_error, UResult,
UserVerb, UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
corp::{Corp, CorpCommType, CorpId, CorpPermission},
user::{User, UserFlag},
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use log::info;
use mockall_double::double;
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum FinancialAccount {
NamedAccount(String),
Treasury, // Staff only source of unlimited funds
}
#[derive(Clone, PartialEq, Debug)]
pub enum ResolvedFinancialAccount {
CorpAccount((CorpId, Corp)),
UserAccount(User),
Treasury,
}
#[derive(Clone, PartialEq, Debug)]
pub struct PaymentRequest<AccDetails> {
pub from: AccDetails,
pub to: AccDetails,
pub amount: u64,
}
async fn resolve_account(
trans: &DBTrans,
inp: &FinancialAccount,
) -> UResult<ResolvedFinancialAccount> {
match inp {
FinancialAccount::Treasury => Ok(ResolvedFinancialAccount::Treasury),
FinancialAccount::NamedAccount(ref name) => {
match trans.find_corp_by_name(name.as_str()).await? {
Some(corp) => Ok(ResolvedFinancialAccount::CorpAccount(corp)),
None => match trans.find_by_username(name.as_str()).await? {
Some(user) => Ok(ResolvedFinancialAccount::UserAccount(user)),
None => user_error(format!(
"I couldn't find a user or corp matching \"{}\"",
name.as_str()
)),
},
}
}
}
}
async fn resolve_payment_request(
trans: &DBTrans,
inp: &PaymentRequest<FinancialAccount>,
) -> UResult<PaymentRequest<ResolvedFinancialAccount>> {
Ok(PaymentRequest {
from: resolve_account(trans, &inp.from).await?,
to: resolve_account(trans, &inp.to).await?,
amount: inp.amount.clone(),
})
}
async fn verify_access(
trans: &DBTrans,
user: &User,
inp: &PaymentRequest<ResolvedFinancialAccount>,
) -> UResult<()> {
if inp.from == inp.to {
user_error("That seems a bit like a money-go-round!".to_owned())?;
}
if inp.from == ResolvedFinancialAccount::Treasury
|| inp.to == ResolvedFinancialAccount::Treasury
{
if !user.user_flags.contains(&UserFlag::Staff) {
user_error("I'm sorry Dave, I can't let you do that!".to_owned())?;
}
// Treasury deliberately bypasses all other access checks, but
// even staff have to follow the rules for payments not involving
// treasury.
} else {
match inp.from {
ResolvedFinancialAccount::UserAccount(ref usr) => {
if usr.username != user.username {
user_error("Sadly, it appears the wristpad system won't let you access other people's funds.".to_owned())?;
}
}
ResolvedFinancialAccount::CorpAccount(ref corp) => {
match trans
.match_user_corp_by_name(&corp.1.name, &user.username)
.await?
{
None => user_error(format!("You're not even a member of {}!", &corp.1.name))?,
Some((_, _, mem)) => {
if !check_corp_perm(&CorpPermission::Finance, &mem) {
user_error(format!(
"You lack finance permissions in {}",
&corp.1.name
))?;
}
}
}
}
_ => user_error("Oops, can't verify you have access to pay".to_owned())?,
}
}
Ok(())
}
fn get_name(acc: &ResolvedFinancialAccount) -> String {
match acc {
ResolvedFinancialAccount::Treasury => "Treasury".to_owned(),
ResolvedFinancialAccount::UserAccount(ref usr) => usr.username.clone(),
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.name.clone(),
}
}
fn get_balance(acc: &ResolvedFinancialAccount) -> u64 {
match acc {
ResolvedFinancialAccount::Treasury => 10000000000,
ResolvedFinancialAccount::UserAccount(ref usr) => usr.credits,
ResolvedFinancialAccount::CorpAccount(ref corp) => corp.1.credits,
}
}
async fn modify_balance<F>(trans: &DBTrans, acc: &mut ResolvedFinancialAccount, f: F) -> DResult<()>
where
F: Fn(u64) -> u64,
{
match acc {
ResolvedFinancialAccount::Treasury => {}
ResolvedFinancialAccount::UserAccount(ref mut usr) => {
usr.credits = f(usr.credits);
trans.save_user_model(&usr).await?;
}
ResolvedFinancialAccount::CorpAccount(ref mut corp) => {
corp.1.credits = f(corp.1.credits);
trans.update_corp_details(&corp.0, &corp.1).await?;
}
}
Ok(())
}
fn involves_treasury(req: &PaymentRequest<ResolvedFinancialAccount>) -> bool {
req.from == ResolvedFinancialAccount::Treasury || req.to == ResolvedFinancialAccount::Treasury
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let remaining = remaining.trim();
let user = get_user_or_fail(ctx)?;
let req = match parse_payment_request(
remaining,
&FinancialAccount::NamedAccount(user.username.clone()),
) {
Err(msg) => user_error(msg.to_owned())?,
Ok(r) => r,
};
if req.amount == 0 || req.amount >= 10000000000000 {
user_error(
"The wristpad seems to require a number between $1 and $10 trillion.".to_owned(),
)?;
}
let mut req = resolve_payment_request(&ctx.trans, &req).await?;
verify_access(&ctx.trans, &user, &req).await?;
if get_balance(&req.from) < req.amount {
user_error(ansi!("The wristpad plays a sad sounding beep, and a red cross comes up, with the text: <red>DECLINED Insufficient Funds<reset>. [Hint: Try having rich parents, or not spending all your credits on smashed avo, and you might be able to afford to make your payments]").to_owned())?;
}
modify_balance(&ctx.trans, &mut req.to, |a| a + req.amount).await?;
modify_balance(&ctx.trans, &mut req.from, |a| a - req.amount).await?;
let mut notify_corps: Vec<(CorpId, String)> = vec![];
match req.from {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
match req.to {
ResolvedFinancialAccount::CorpAccount(ref c) => {
notify_corps.push((c.0.clone(), c.1.name.clone()))
}
_ => {}
}
for (corp_id, corp_name) in notify_corps {
ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Notice,
None,
&format!(
ansi!("<green>[{}] {} has transferred ${} from {} to {}.<reset>\n"),
&corp_name,
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
&get_name(&req.to),
),
)
.await?;
}
match req.to {
ResolvedFinancialAccount::UserAccount(ref other_usr)
if other_usr.username != user.username =>
{
if let Some((sess, _)) = ctx
.trans
.find_session_for_player(&other_usr.username.to_lowercase())
.await?
{
ctx.trans
.queue_for_session(
&sess,
Some(&format!(
ansi!("<green>{} has transferred ${} from {} to you.<reset>\n"),
if involves_treasury(&req) {
"A staff member"
} else {
&user.username
},
req.amount,
&get_name(&req.from),
)),
)
.await?;
}
}
_ => {}
}
if involves_treasury(&req) {
info!(
"{} has transferred ${} from {} to {}.",
&user.username,
req.amount,
&get_name(&req.from),
&get_name(&req.to),
);
}
ctx.trans.queue_for_session(&ctx.session,
Some(ansi!("Your wristpad displays a <green>green<reset> tick, indicating a successful transaction.\n"))).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;