1
0
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:
NG (Graham)
2026-02-07 12:27:48 -05:00
parent 514db6fd02
commit 31d7dd462f
7 changed files with 98 additions and 0 deletions

View File

@@ -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()),
promo_id: val.promo_id.to_owned().unwrap_or_else(|| key.to_owned()),
is_serial: val.is_serial,
is_repeatable: val.is_repeatable,
value: val.value,
transaction: tx,
});

View File

@@ -498,6 +498,7 @@ pub struct PromoCode {
pub bundle_id: String,
pub promo_id: String,
pub is_serial: bool,
pub is_repeatable: bool,
pub value: f32,
pub transaction: ShopAction,
}

View File

@@ -213,6 +213,8 @@ pub struct ItemCode {
#[serde(default)]
pub is_serial: bool,
#[serde(default)]
pub is_repeatable: bool,
#[serde(default)]
pub value: f32,
pub gives: Vec<ItemPurchase>,
}
@@ -389,6 +391,16 @@ pub fn default_codes() -> std::collections::HashMap<String, ItemCode> {
bundle_id: None,
promo_id: None,
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,
gives: vec![]
});
@@ -397,6 +409,7 @@ pub fn default_codes() -> std::collections::HashMap<String, ItemCode> {
bundle_id: None,
promo_id: None,
is_serial: false,
is_repeatable: false,
value: 1.5,
gives: vec![
ItemPurchase::Experience {

View File

@@ -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 {

View File

@@ -85,6 +85,7 @@ pub trait User<C>: ChatUser + SocialUser + LobbyUser + MultiplayerUser + Singlep
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 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]

View File

@@ -43,4 +43,5 @@ pub enum Descriptor {
LastSeen, // u64, seconds since Unix epoch
SubscribedChannels, // Vec<String>, JSON
AvatarId, // u32, u32::MAX means custom avatar
RedeemedPromoCodes, // Vec<String>, JSON
}

View File

@@ -41,6 +41,22 @@ impl SimpleOperation<()> for PromoCodeApplier {
if let Some(Typed::Str(promo_code)) = params.remove(&CODE_NAME_PARAM_KEY) {
let user_info = user.user()?;
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?;
params.insert(SUCCESS_PARAM_KEY, Typed::Bool(result.success));
params.insert(RESULT_CODE_PARAM_KEY, Typed::Int(PromoResultCode::Success as _));