forked from blasthavers/blastmud
268 lines
9.2 KiB
Rust
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;
|