mirror of
https://git.ngram.ca/OpenJam/rc-servers
synced 2026-08-23 23:08:52 +00:00
Allow promo codes to only be redeemed once
This commit is contained in:
@@ -544,6 +544,7 @@ impl <C: Clone + Send> super::ConfigProvider<C> for CubeConfig {
|
|||||||
bundle_id: val.bundle_id.to_owned().unwrap_or_else(|| key.to_owned()),
|
bundle_id: val.bundle_id.to_owned().unwrap_or_else(|| key.to_owned()),
|
||||||
promo_id: val.promo_id.to_owned().unwrap_or_else(|| key.to_owned()),
|
promo_id: val.promo_id.to_owned().unwrap_or_else(|| key.to_owned()),
|
||||||
is_serial: val.is_serial,
|
is_serial: val.is_serial,
|
||||||
|
is_repeatable: val.is_repeatable,
|
||||||
value: val.value,
|
value: val.value,
|
||||||
transaction: tx,
|
transaction: tx,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -498,6 +498,7 @@ pub struct PromoCode {
|
|||||||
pub bundle_id: String,
|
pub bundle_id: String,
|
||||||
pub promo_id: String,
|
pub promo_id: String,
|
||||||
pub is_serial: bool,
|
pub is_serial: bool,
|
||||||
|
pub is_repeatable: bool,
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
pub transaction: ShopAction,
|
pub transaction: ShopAction,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ pub struct ItemCode {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_serial: bool,
|
pub is_serial: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub is_repeatable: bool,
|
||||||
|
#[serde(default)]
|
||||||
pub value: f32,
|
pub value: f32,
|
||||||
pub gives: Vec<ItemPurchase>,
|
pub gives: Vec<ItemPurchase>,
|
||||||
}
|
}
|
||||||
@@ -389,6 +391,16 @@ pub fn default_codes() -> std::collections::HashMap<String, ItemCode> {
|
|||||||
bundle_id: None,
|
bundle_id: None,
|
||||||
promo_id: None,
|
promo_id: None,
|
||||||
is_serial: false,
|
is_serial: false,
|
||||||
|
is_repeatable: true,
|
||||||
|
value: 1.5,
|
||||||
|
gives: vec![]
|
||||||
|
});
|
||||||
|
map.insert("TEST-ONCE".to_owned(), ItemCode {
|
||||||
|
message: Some("Test passed".to_owned()),
|
||||||
|
bundle_id: None,
|
||||||
|
promo_id: None,
|
||||||
|
is_serial: false,
|
||||||
|
is_repeatable: false,
|
||||||
value: 1.5,
|
value: 1.5,
|
||||||
gives: vec![]
|
gives: vec![]
|
||||||
});
|
});
|
||||||
@@ -397,6 +409,7 @@ pub fn default_codes() -> std::collections::HashMap<String, ItemCode> {
|
|||||||
bundle_id: None,
|
bundle_id: None,
|
||||||
promo_id: None,
|
promo_id: None,
|
||||||
is_serial: false,
|
is_serial: false,
|
||||||
|
is_repeatable: false,
|
||||||
value: 1.5,
|
value: 1.5,
|
||||||
gives: vec![
|
gives: vec![
|
||||||
ItemPurchase::Experience {
|
ItemPurchase::Experience {
|
||||||
|
|||||||
@@ -1398,6 +1398,71 @@ impl <C: Clone + Send> super::User<C> for UserData {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn mark_code_redeemed(&self, code: String) -> Result<bool, polariton_server::operations::SimpleOpError> {
|
||||||
|
// TODO support serial (single global use) codes
|
||||||
|
// FIXME this should probably be a transaction
|
||||||
|
let codes_opt = self.db.user_aux_by_user_id_and_descriptor(self.account.id, oj_rc_database::schema::user_aux::Descriptor::RedeemedPromoCodes).await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to retrieve RedeemedPromoCodes (user_aux) for user_id {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
"Failed to retrieve RedeemedPromoCodes".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if let Some(codes_entity) = codes_opt {
|
||||||
|
match serde_json::from_str::<Vec<String>>(&codes_entity.data) {
|
||||||
|
Ok(mut codes) => {
|
||||||
|
if codes.contains(&code) {
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
codes.push(code);
|
||||||
|
let active_model = oj_rc_database::schema::user_aux::ActiveModel {
|
||||||
|
data: oj_rc_database::sea_orm::ActiveValue::Set(serde_json::to_string(&codes).unwrap()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
self.db.update_user_aux_by_user_id_and_descriptor(
|
||||||
|
active_model,
|
||||||
|
self.account.id,
|
||||||
|
oj_rc_database::schema::user_aux::Descriptor::RedeemedPromoCodes,
|
||||||
|
).await.map_err(|e| {
|
||||||
|
log::error!("Failed to update RedeemedPromoCodes (user_aux id {}) for user_id {}: {}", codes_entity.id, self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
"Failed to update RedeemedPromoCodes".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to parse RedeemedPromoCodes (user_aux id {}) for user_id {}: {}", self.account.id, codes_entity.id, e);
|
||||||
|
return Err(polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
"Failed to parse RedeemedPromoCodes JSON".to_owned(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// user_aux entry needs to be created
|
||||||
|
let new_codes = oj_rc_database::schema::user_aux::ActiveModel {
|
||||||
|
id: oj_rc_database::sea_orm::ActiveValue::NotSet,
|
||||||
|
user_id: oj_rc_database::sea_orm::ActiveValue::Set(self.account.id),
|
||||||
|
creation_time: oj_rc_database::sea_orm::ActiveValue::Set(chrono::Utc::now().timestamp()),
|
||||||
|
descriptor: oj_rc_database::sea_orm::ActiveValue::Set(oj_rc_database::schema::user_aux::Descriptor::RedeemedPromoCodes),
|
||||||
|
data: oj_rc_database::sea_orm::ActiveValue::Set(serde_json::to_string(&vec![code]).unwrap()),
|
||||||
|
};
|
||||||
|
self.db.insert_user_aux(vec![new_codes]).await
|
||||||
|
.map_err(|e| {
|
||||||
|
log::error!("Failed to insert RedeemedPromoCodes (user_aux) for user_id {}: {}", self.account.id, e);
|
||||||
|
polariton_server::operations::SimpleOpError::with_message(
|
||||||
|
crate::data::error_codes::WebServicesError::DatabaseError as i16,
|
||||||
|
"Failed to insert updated RedeemedPromoCodes".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct GameEventSetterImpl {
|
struct GameEventSetterImpl {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + Singlep
|
|||||||
fn current_game_event_setter(&self) -> Box<dyn GameEventSetter>;
|
fn current_game_event_setter(&self) -> Box<dyn GameEventSetter>;
|
||||||
async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result<PurchaseResult, polariton_server::operations::SimpleOpError>;
|
async fn apply_purchase(&self, action: &crate::persist::config::ShopAction) -> Result<PurchaseResult, polariton_server::operations::SimpleOpError>;
|
||||||
async fn currency_debit(&self, ty: CurrencyType, to_sub: u64) -> Result<(), polariton_server::operations::SimpleOpError>;
|
async fn currency_debit(&self, ty: CurrencyType, to_sub: u64) -> Result<(), polariton_server::operations::SimpleOpError>;
|
||||||
|
async fn mark_code_redeemed(&self, code: String) -> Result<bool, polariton_server::operations::SimpleOpError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|||||||
@@ -43,4 +43,5 @@ pub enum Descriptor {
|
|||||||
LastSeen, // u64, seconds since Unix epoch
|
LastSeen, // u64, seconds since Unix epoch
|
||||||
SubscribedChannels, // Vec<String>, JSON
|
SubscribedChannels, // Vec<String>, JSON
|
||||||
AvatarId, // u32, u32::MAX means custom avatar
|
AvatarId, // u32, u32::MAX means custom avatar
|
||||||
|
RedeemedPromoCodes, // Vec<String>, JSON
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,22 @@ impl SimpleOperation<()> for PromoCodeApplier {
|
|||||||
if let Some(Typed::Str(promo_code)) = params.remove(&CODE_NAME_PARAM_KEY) {
|
if let Some(Typed::Str(promo_code)) = params.remove(&CODE_NAME_PARAM_KEY) {
|
||||||
let user_info = user.user()?;
|
let user_info = user.user()?;
|
||||||
if let Some(code_info) = self.code_map.get(&promo_code.string) {
|
if let Some(code_info) = self.code_map.get(&promo_code.string) {
|
||||||
|
if !code_info.is_repeatable {
|
||||||
|
if !user_info.mark_code_redeemed(promo_code.string.clone()).await? {
|
||||||
|
params.insert(SUCCESS_PARAM_KEY, Typed::Bool(false));
|
||||||
|
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::AlreadyAwarded as _));
|
||||||
|
params.insert(IS_SERIAL_PARAM_KEY, Typed::Bool(false));
|
||||||
|
params.insert(VALUE_PARAM_KEY, Typed::Float(0.0));
|
||||||
|
params.insert(PROMO_ID_PARAM_KEY, Typed::Str(promo_code.clone()));
|
||||||
|
params.insert(CUBES_AWARDED_PARAM_KEY, Typed::Str("{}".into()));
|
||||||
|
params.insert(MSG_PARAM_KEY, Typed::Str("".into()));
|
||||||
|
params.insert(BUNDLE_ID_PARAM_KEY, Typed::Str(code_info.bundle_id.clone().into()));
|
||||||
|
params.insert(ROBOPASS_PARAM_KEY, Typed::Bool(false));
|
||||||
|
params.insert(PAID_CURRENCY_PARAM_KEY, Typed::Long(0));
|
||||||
|
log::debug!("Code \"{}\" not redeemed by {} (code already redeemed)", promo_code.string, user_info.public_id());
|
||||||
|
return Ok(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
let result = user_info.apply_purchase(&code_info.transaction).await?;
|
let result = user_info.apply_purchase(&code_info.transaction).await?;
|
||||||
params.insert(SUCCESS_PARAM_KEY, Typed::Bool(result.success));
|
params.insert(SUCCESS_PARAM_KEY, Typed::Bool(result.success));
|
||||||
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::Success as _));
|
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::Success as _));
|
||||||
|
|||||||
Reference in New Issue
Block a user