feat: add invite code validity controls
- Add invite code starts/expires fields across contracts, API, Spacetime bindings, and admin UI - Enforce pending/expired invite code redemption behavior and expose admin status - Add admin write-operation confirmation guard and documentation - Add invite code contract/runtime tests
This commit is contained in:
@@ -89,8 +89,8 @@ pub fn build_runtime_tracking_event_input(
|
||||
) -> Result<RuntimeTrackingEventInput, RuntimeProfileFieldError> {
|
||||
let event_id = normalize_required_string(event_id)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTrackingEventId)?;
|
||||
let event_key =
|
||||
normalize_required_string(event_key).ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
|
||||
let event_key = normalize_required_string(event_key)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
|
||||
let scope_id = normalize_required_string(scope_id)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTrackingScopeId)?;
|
||||
let metadata_json = normalize_tracking_metadata_json(metadata_json)?;
|
||||
@@ -151,8 +151,12 @@ pub fn build_runtime_profile_task_config_admin_upsert_input(
|
||||
let task_id = normalize_profile_task_id(task_id)?;
|
||||
let title =
|
||||
normalize_required_string(title).ok_or(RuntimeProfileFieldError::MissingTaskTitle)?;
|
||||
let event_key =
|
||||
normalize_required_string(event_key).ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
|
||||
let event_key = normalize_required_string(event_key)
|
||||
.ok_or(RuntimeProfileFieldError::MissingTaskEventKey)?;
|
||||
// 中文注释:个人任务首版只按用户维度累计,避免 site/work/module 误复用用户桶。
|
||||
if scope_kind != RuntimeTrackingScopeKind::User {
|
||||
return Err(RuntimeProfileFieldError::UnsupportedProfileTaskScopeKind);
|
||||
}
|
||||
if threshold == 0 {
|
||||
return Err(RuntimeProfileFieldError::InvalidTaskThreshold);
|
||||
}
|
||||
@@ -326,17 +330,25 @@ pub fn build_runtime_profile_invite_code_admin_upsert_input(
|
||||
admin_user_id: String,
|
||||
invite_code: String,
|
||||
metadata_json: String,
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
updated_at_micros: i64,
|
||||
) -> Result<RuntimeProfileInviteCodeAdminUpsertInput, RuntimeProfileFieldError> {
|
||||
let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?;
|
||||
let invite_code =
|
||||
normalize_invite_code(invite_code).ok_or(RuntimeProfileFieldError::MissingInviteCode)?;
|
||||
let metadata_json = normalize_invite_code_metadata_json(metadata_json)?;
|
||||
crate::commands::validate_runtime_profile_invite_code_validity_window(
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
)?;
|
||||
|
||||
Ok(RuntimeProfileInviteCodeAdminUpsertInput {
|
||||
admin_user_id,
|
||||
invite_code,
|
||||
metadata_json,
|
||||
starts_at_micros,
|
||||
expires_at_micros,
|
||||
updated_at_micros,
|
||||
})
|
||||
}
|
||||
@@ -639,6 +651,40 @@ pub fn normalize_invite_code_metadata_json(
|
||||
serde_json::to_string(&parsed).map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata)
|
||||
}
|
||||
|
||||
pub fn validate_runtime_profile_invite_code_validity_window(
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
) -> Result<(), RuntimeProfileFieldError> {
|
||||
if matches!((starts_at_micros, expires_at_micros), (Some(starts_at), Some(expires_at)) if starts_at > expires_at)
|
||||
{
|
||||
return Err(RuntimeProfileFieldError::InvalidInviteCodeValidityWindow);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_runtime_profile_invite_code_status(
|
||||
starts_at_micros: Option<i64>,
|
||||
expires_at_micros: Option<i64>,
|
||||
now_micros: i64,
|
||||
) -> RuntimeProfileInviteCodeStatus {
|
||||
if starts_at_micros
|
||||
.map(|starts_at| now_micros < starts_at)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return RuntimeProfileInviteCodeStatus::Pending;
|
||||
}
|
||||
|
||||
if expires_at_micros
|
||||
.map(|expires_at| now_micros >= expires_at)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return RuntimeProfileInviteCodeStatus::Expired;
|
||||
}
|
||||
|
||||
RuntimeProfileInviteCodeStatus::Active
|
||||
}
|
||||
|
||||
fn normalize_tracking_metadata_json(value: String) -> Result<String, RuntimeProfileFieldError> {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
||||
Reference in New Issue
Block a user