From afc2f0349775fad7488449e0a66b64d1d21f3ca4 Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 23 Jun 2026 17:42:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=8E=E5=8F=B0=E8=B4=A6?= =?UTF-8?q?=E5=8F=B7=E5=88=9D=E5=A7=8B=E6=B3=A5=E7=82=B9=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 profile_wallet_config 表和后台读写接口 注册赠送泥点改为读取后台配置并保留默认 100 后台新增账号配置页并同步数据契约文档 --- apps/admin-web/src/api/adminApiClient.ts | 23 +++ apps/admin-web/src/api/adminApiTypes.ts | 13 ++ apps/admin-web/src/app/AdminApp.tsx | 14 ++ apps/admin-web/src/app/AdminShell.tsx | 2 + apps/admin-web/src/app/adminRoutes.ts | 2 + .../pages/AdminProfileWalletConfigPage.tsx | 174 ++++++++++++++++++ ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 8 +- ...发运维】本地开发验证与生产运维-2026-05-15.md | 1 + .../crates/api-server/src/modules/admin.rs | 18 +- .../crates/api-server/src/runtime_profile.rs | 78 +++++++- .../crates/module-runtime/src/application.rs | 22 +++ .../crates/module-runtime/src/commands.rs | 23 +++ server-rs/crates/module-runtime/src/domain.rs | 46 +++++ server-rs/crates/module-runtime/src/errors.rs | 4 + .../crates/shared-contracts/src/runtime.rs | 17 ++ server-rs/crates/spacetime-client/src/lib.rs | 6 +- .../crates/spacetime-client/src/mapper.rs | 1 + .../src/mapper/runtime_profile.rs | 51 +++++ .../spacetime-client/src/module_bindings.rs | 40 ++++ ...min_get_profile_wallet_config_procedure.rs | 59 ++++++ ..._upsert_profile_wallet_config_procedure.rs | 62 +++++++ .../profile_wallet_config_table.rs | 161 ++++++++++++++++ .../profile_wallet_config_type.rs | 64 +++++++ ...file_wallet_config_admin_get_input_type.rs | 15 ++ ...llet_config_admin_procedure_result_type.rs | 19 ++ ...e_wallet_config_admin_upsert_input_type.rs | 17 ++ ...ime_profile_wallet_config_snapshot_type.rs | 20 ++ .../crates/spacetime-client/src/runtime.rs | 54 ++++++ .../crates/spacetime-module/src/migration.rs | 1 + .../spacetime-module/src/runtime/profile.rs | 132 ++++++++++++- 30 files changed, 1132 insertions(+), 15 deletions(-) create mode 100644 apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_table.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_get_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_procedure_result_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_upsert_input_type.rs create mode 100644 server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_snapshot_type.rs diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index dcd5340d4..ce5ee137d 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -20,6 +20,7 @@ import type { AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest, AdminUpsertProfileTaskConfigRequest, + AdminUpsertProfileWalletConfigRequest, AdminUpsertPublicWorkInteractionConfigRequest, AdminWorkVisibilityListResponse, ApiErrorEnvelope, @@ -34,6 +35,7 @@ import type { ProfileRedeemCodeAdminResponse, ProfileTaskConfigAdminListResponse, ProfileTaskConfigAdminResponse, + ProfileWalletConfigAdminResponse, } from './adminApiTypes'; const API_RESPONSE_ENVELOPE_HEADER = 'x-genarrative-response-envelope'; @@ -359,6 +361,27 @@ export function disableProfileTaskConfig( ); } +export function getProfileWalletConfig(token: string) { + return request( + '/admin/api/profile/wallet-config', + { token }, + ); +} + +export function upsertProfileWalletConfig( + token: string, + payload: AdminUpsertProfileWalletConfigRequest, +) { + return request( + '/admin/api/profile/wallet-config', + { + method: 'POST', + token, + body: payload, + }, + ); +} + export function listProfileRechargeProducts(token: string) { return request( '/admin/api/profile/recharge-products', diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index c876dcc40..efbab8b92 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -325,6 +325,10 @@ export interface AdminUpsertProfileRechargeProductRequest { sortOrder: number; } +export interface AdminUpsertProfileWalletConfigRequest { + initialMudPoints: number; +} + export interface ProfileRedeemCodeAdminResponse { code: string; mode: ProfileRedeemCodeMode; @@ -401,6 +405,15 @@ export interface ProfileRechargeProductConfigAdminListResponse { entries: ProfileRechargeProductConfigAdminResponse[]; } +export interface ProfileWalletConfigAdminResponse { + configId: string; + initialMudPoints: number; + createdBy: string; + createdAt: string; + updatedBy: string; + updatedAt: string; +} + export interface AdminTrackingEventEntryPayload { eventId: string; eventKey: string; diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 8a30a7afd..d4fa1d3d2 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -12,6 +12,7 @@ import type { ProfileRechargeProductConfigAdminResponse, ProfileRedeemCodeAdminResponse, ProfileTaskConfigAdminResponse, + ProfileWalletConfigAdminResponse, } from '../api/adminApiTypes'; import { clearStoredAdminToken, @@ -25,6 +26,7 @@ import {AdminInviteCodePage} from '../pages/AdminInviteCodePage'; import {AdminLoginPage} from '../pages/AdminLoginPage'; import {AdminEditorGenerationPricingPage} from '../pages/AdminEditorGenerationPricingPage'; import {AdminOverviewPage} from '../pages/AdminOverviewPage'; +import {AdminProfileWalletConfigPage} from '../pages/AdminProfileWalletConfigPage'; import {AdminRechargeProductPage} from '../pages/AdminRechargeProductPage'; import {AdminRedeemCodePage} from '../pages/AdminRedeemCodePage'; import {AdminTaskConfigPage} from '../pages/AdminTaskConfigPage'; @@ -51,6 +53,8 @@ export function AdminApp() { useState(null); const [taskConfigResult, setTaskConfigResult] = useState(null); + const [profileWalletConfigResult, setProfileWalletConfigResult] = + useState(null); const [rechargeProductResult, setRechargeProductResult] = useState(null); @@ -61,6 +65,7 @@ export function AdminApp() { setRedeemResult(null); setInviteResult(null); setTaskConfigResult(null); + setProfileWalletConfigResult(null); setRechargeProductResult(null); setStatus('guest'); setLoginNotice(message); @@ -131,6 +136,7 @@ export function AdminApp() { setRedeemResult(null); setInviteResult(null); setTaskConfigResult(null); + setProfileWalletConfigResult(null); setRechargeProductResult(null); setLoginNotice(''); setStatus('authenticated'); @@ -228,6 +234,14 @@ export function AdminApp() { onResultChange={setTaskConfigResult} /> ) : null} + {routeId === 'profile-wallet' ? ( + + ) : null} {routeId === 'recharge-products' ? ( void; + onResultChange: (result: ProfileWalletConfigAdminResponse) => void; +} + +export function AdminProfileWalletConfigPage({ + token, + result, + onUnauthorized, + onResultChange, +}: AdminProfileWalletConfigPageProps) { + const [initialMudPoints, setInitialMudPoints] = useState('100'); + const [isLoading, setIsLoading] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [loadErrorMessage, setLoadErrorMessage] = useState(''); + const [errorMessage, setErrorMessage] = useState(''); + const {confirmWrite, confirmDialog} = useAdminWriteConfirm(); + + useEffect(() => { + void refreshConfig(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [token]); + + async function refreshConfig() { + setIsLoading(true); + setLoadErrorMessage(''); + try { + const response = await getProfileWalletConfig(token); + onResultChange(response); + setInitialMudPoints(String(response.initialMudPoints)); + } catch (error: unknown) { + handlePageError(error, onUnauthorized, setLoadErrorMessage); + } finally { + setIsLoading(false); + } + } + + async function handleSave(event: FormEvent) { + event.preventDefault(); + if (isSaving) { + return; + } + + const normalizedInitialMudPoints = parsePositiveInteger(initialMudPoints); + if (!normalizedInitialMudPoints) { + setErrorMessage('账号初始泥点数必须是大于 0 的整数'); + return; + } + + setErrorMessage(''); + const confirmed = await confirmWrite({ + action: '保存账号配置', + target: `${normalizedInitialMudPoints}泥点`, + }); + if (!confirmed) { + return; + } + + setIsSaving(true); + try { + const response = await upsertProfileWalletConfig(token, { + initialMudPoints: normalizedInitialMudPoints, + }); + onResultChange(response); + setInitialMudPoints(String(response.initialMudPoints)); + } catch (error: unknown) { + handlePageError(error, onUnauthorized, setErrorMessage); + } finally { + setIsSaving(false); + } + } + + return ( +
+
+
+

账号配置

+

泥点钱包

+
+ +
+ + {loadErrorMessage ? ( +
+ {loadErrorMessage} +
+ ) : null} + +
+
+ + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} + + +
+ +
+
+

当前配置

+ {result?.configId ?? '-'} +
+ {result ? ( +
+
+
初始泥点
+
{result.initialMudPoints}
+
+
+
更新人
+
{result.updatedBy || '-'}
+
+
+
更新
+
{result.updatedAt}
+
+
+ ) : ( +
+ {isLoading ? '加载中' : '暂无记录'} +
+ )} +
+
+ {confirmDialog} +
+ ); +} + +function parsePositiveInteger(value: string) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; +} diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index b89419150..ec532c794 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -198,7 +198,7 @@ npm run check:server-rs-ddd ## 用户钱包与编辑器生成扣费契约 -1. 新用户账号完成注册并成功同步正式认证表后,注册赠送固定为 `100` 泥点,流水原因仍使用 `new_user_registration_reward`,流水 ID 继续保持幂等,重复发放请求不得叠加余额。 +1. 新用户账号完成注册并成功同步正式认证表后,注册赠送金额读取 `profile_wallet_config.initial_mud_points`;后台通过 `/admin/api/profile/wallet-config` 维护“账号初始泥点数”。未写入配置时默认仍为 `100` 泥点。流水原因仍使用 `new_user_registration_reward`,流水 ID 继续保持幂等,重复发放请求不得叠加余额。 2. 编辑器画板所有会调用外部生成 provider 的入口都不从前端请求接收 `priceMudPoints`;实际扣费真相以后端运行时模型定价配置为准,前端按钮泥点只作为展示。 3. 编辑器图片生成 / 图片修改 / 图标 spritesheet / UI 设计图提取素材 / 视频 / 角色动作 / 音效 / 背景音乐必须在后端计算模型价格后使用 `execute_billable_asset_operation_with_cost` 预扣泥点;预扣失败必须 fail-closed,不得继续提交 VectorEngine、Ark、Suno 或 Vidu 上游任务。 4. 音频生成的编辑器链路虽然任务提交和结果发布分离,仍必须把提交时后端计算出的模型价格写入 `AudioAssetBindingTarget.billing_points_cost`,最终发布落资产时按该价格扣费;创作音频目标未提供该字段时才使用旧的创作音频固定成本。 @@ -680,6 +680,12 @@ npm run check:server-rs-ddd - Rust 结构体:`ProfileWalletLedger` - 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +### `profile_wallet_config` + +- Rust 结构体:`ProfileWalletConfig` +- 源码:`server-rs/crates/spacetime-module/src/runtime/profile.rs` +- 作用:账号钱包全局配置真相源,当前维护新账号注册初始泥点数;表为空时业务回退 `100` 泥点。 + ### `public_work_like` - Rust 结构体:`PublicWorkLike` diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 6cffa142c..d9df1461e 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -497,6 +497,7 @@ cargo test -p platform-auth --manifest-path server-rs/Cargo.toml aliyun_send_sms - `profile_task_progress` - `profile_task_reward_claim` - `profile_wallet_ledger` +- `profile_wallet_config` 个人任务首版 scope 仅支持 `user`。每日登录任务按北京时间自然日 0 点重置;用户已登录并停留在“我的”页跨日时,前端需要先非阻断调用 refresh session 以写入新业务日 `daily_login`,再请求 `/api/profile/tasks` 刷新任务中心。认证成功后的 `daily_login` 必须通过 `SpacetimeClient::record_daily_login_tracking_event(...)` 调用 SpacetimeDB 专用 `record_daily_login_tracking_event_and_return` procedure,由数据库事务时间生成当日幂等事件并推进任务进度;不要改回普通 `record_tracking_event_after_success`、tracking outbox 或旧 `profile.login.daily` 事件键。后台、RPG、大鱼吃小鱼、Visual Novel、Story、Combat 等特定链路按 tracking 中间件排除规则处理;作品游玩统一使用 `work_play_start`。 diff --git a/server-rs/crates/api-server/src/modules/admin.rs b/server-rs/crates/api-server/src/modules/admin.rs index 58f5629c4..425d29b3a 100644 --- a/server-rs/crates/api-server/src/modules/admin.rs +++ b/server-rs/crates/api-server/src/modules/admin.rs @@ -14,10 +14,11 @@ use crate::{ }, runtime_profile::{ admin_disable_profile_redeem_code, admin_disable_profile_task_config, - admin_list_profile_invite_codes, admin_list_profile_recharge_products, - admin_list_profile_redeem_codes, admin_list_profile_task_configs, - admin_upsert_profile_invite_code, admin_upsert_profile_recharge_product, - admin_upsert_profile_redeem_code, admin_upsert_profile_task_config, + admin_get_profile_wallet_config, admin_list_profile_invite_codes, + admin_list_profile_recharge_products, admin_list_profile_redeem_codes, + admin_list_profile_task_configs, admin_upsert_profile_invite_code, + admin_upsert_profile_recharge_product, admin_upsert_profile_redeem_code, + admin_upsert_profile_task_config, admin_upsert_profile_wallet_config, }, state::AppState, }; @@ -145,6 +146,15 @@ pub fn router(state: AppState) -> Router { middleware::from_fn_with_state(state.clone(), require_admin_auth), ), ) + .route( + "/admin/api/profile/wallet-config", + get(admin_get_profile_wallet_config) + .post(admin_upsert_profile_wallet_config) + .route_layer(middleware::from_fn_with_state( + state.clone(), + require_admin_auth, + )), + ) .route( "/admin/api/profile/recharge-products", get(admin_list_profile_recharge_products) diff --git a/server-rs/crates/api-server/src/runtime_profile.rs b/server-rs/crates/api-server/src/runtime_profile.rs index 65d67d45e..067046e47 100644 --- a/server-rs/crates/api-server/src/runtime_profile.rs +++ b/server-rs/crates/api-server/src/runtime_profile.rs @@ -35,8 +35,8 @@ use shared_contracts::runtime::{ ANALYTICS_GRANULARITY_WEEK, ANALYTICS_GRANULARITY_YEAR, AdminDisableProfileRedeemCodeRequest, AdminDisableProfileTaskConfigRequest, AdminUpsertProfileInviteCodeRequest, AdminUpsertProfileRechargeProductRequest, AdminUpsertProfileRedeemCodeRequest, - AdminUpsertProfileTaskConfigRequest, AnalyticsBucketMetricResponse, - AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse, + AdminUpsertProfileTaskConfigRequest, AdminUpsertProfileWalletConfigRequest, + AnalyticsBucketMetricResponse, AnalyticsMetricQueryResponse, ClaimProfileTaskRewardResponse, ConfirmWechatProfileRechargeOrderResponse, CreateProfileRechargeOrderRequest, CreateProfileRechargeOrderResponse, PROFILE_FEEDBACK_STATUS_OPEN, PROFILE_MEMBERSHIP_TIER_MONTH, PROFILE_MEMBERSHIP_TIER_NORMAL, PROFILE_MEMBERSHIP_TIER_SEASON, @@ -62,11 +62,12 @@ use shared_contracts::runtime::{ ProfileRedeemCodeAdminResponse, ProfileReferralInviteCenterResponse, ProfileReferralInvitedUserResponse, ProfileTaskCenterResponse, ProfileTaskConfigAdminListResponse, ProfileTaskConfigAdminResponse, ProfileTaskItemResponse, - ProfileWalletLedgerEntryResponse, ProfileWalletLedgerResponse, - RedeemProfileReferralInviteCodeRequest, RedeemProfileReferralInviteCodeResponse, - RedeemProfileRewardCodeRequest, RedeemProfileRewardCodeResponse, SubmitProfileFeedbackRequest, - SubmitProfileFeedbackResponse, TRACKING_SCOPE_KIND_MODULE, TRACKING_SCOPE_KIND_SITE, - TRACKING_SCOPE_KIND_USER, TRACKING_SCOPE_KIND_WORK, WechatMiniProgramPaymentParamsResponse, + ProfileWalletConfigAdminResponse, ProfileWalletLedgerEntryResponse, + ProfileWalletLedgerResponse, RedeemProfileReferralInviteCodeRequest, + RedeemProfileReferralInviteCodeResponse, RedeemProfileRewardCodeRequest, + RedeemProfileRewardCodeResponse, SubmitProfileFeedbackRequest, SubmitProfileFeedbackResponse, + TRACKING_SCOPE_KIND_MODULE, TRACKING_SCOPE_KIND_SITE, TRACKING_SCOPE_KIND_USER, + TRACKING_SCOPE_KIND_WORK, WechatMiniProgramPaymentParamsResponse, WechatMiniProgramVirtualPayParamsResponse, WechatProfileRechargeOrderDoneEvent, WechatProfileRechargeOrderErrorEvent, }; @@ -819,6 +820,56 @@ pub async fn admin_disable_profile_task_config( )) } +pub async fn admin_get_profile_wallet_config( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, +) -> Result, Response> { + let record = state + .spacetime_client() + .admin_get_profile_wallet_config(admin.session().subject.clone()) + .await + .map_err(|error| { + runtime_profile_error_response( + &request_context, + map_runtime_profile_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + build_profile_wallet_config_admin_response(record), + )) +} + +pub async fn admin_upsert_profile_wallet_config( + State(state): State, + Extension(request_context): Extension, + Extension(admin): Extension, + Json(payload): Json, +) -> Result, Response> { + let updated_at_micros = OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000; + let record = state + .spacetime_client() + .admin_upsert_profile_wallet_config( + admin.session().subject.clone(), + payload.initial_mud_points, + updated_at_micros as i64, + ) + .await + .map_err(|error| { + runtime_profile_error_response( + &request_context, + map_runtime_profile_client_error(error), + ) + })?; + + Ok(json_success_body( + Some(&request_context), + build_profile_wallet_config_admin_response(record), + )) +} + pub async fn admin_list_profile_recharge_products( State(state): State, Extension(request_context): Extension, @@ -1770,6 +1821,19 @@ fn build_profile_recharge_product_config_admin_response( } } +fn build_profile_wallet_config_admin_response( + record: module_runtime::RuntimeProfileWalletConfigRecord, +) -> ProfileWalletConfigAdminResponse { + ProfileWalletConfigAdminResponse { + config_id: record.config_id, + initial_mud_points: record.initial_mud_points, + created_by: record.created_by, + created_at: record.created_at, + updated_by: record.updated_by, + updated_at: record.updated_at, + } +} + fn normalize_admin_invite_code_metadata(metadata: Option) -> Result { let metadata = match metadata { Some(Value::Null) | None => json!({}), diff --git a/server-rs/crates/module-runtime/src/application.rs b/server-rs/crates/module-runtime/src/application.rs index 2c819e4b8..cca5263e4 100644 --- a/server-rs/crates/module-runtime/src/application.rs +++ b/server-rs/crates/module-runtime/src/application.rs @@ -842,6 +842,28 @@ pub fn build_runtime_profile_wallet_ledger_entry_record( } } +pub fn build_runtime_profile_wallet_config_record( + snapshot: RuntimeProfileWalletConfigSnapshot, +) -> RuntimeProfileWalletConfigRecord { + let format_optional_audit_time = |micros: i64| { + if micros > 0 { + format_utc_micros(micros) + } else { + "-".to_string() + } + }; + RuntimeProfileWalletConfigRecord { + config_id: snapshot.config_id, + initial_mud_points: snapshot.initial_mud_points, + created_by: snapshot.created_by, + created_at: format_optional_audit_time(snapshot.created_at_micros), + created_at_micros: snapshot.created_at_micros, + updated_by: snapshot.updated_by, + updated_at: format_optional_audit_time(snapshot.updated_at_micros), + updated_at_micros: snapshot.updated_at_micros, + } +} + pub fn build_runtime_profile_recharge_center_record( snapshot: RuntimeProfileRechargeCenterSnapshot, ) -> RuntimeProfileRechargeCenterRecord { diff --git a/server-rs/crates/module-runtime/src/commands.rs b/server-rs/crates/module-runtime/src/commands.rs index c9361cff7..9afe3b9b3 100644 --- a/server-rs/crates/module-runtime/src/commands.rs +++ b/server-rs/crates/module-runtime/src/commands.rs @@ -78,6 +78,29 @@ pub fn build_runtime_profile_wallet_ledger_list_input( Ok(RuntimeProfileWalletLedgerListInput { user_id }) } +pub fn build_runtime_profile_wallet_config_admin_get_input( + admin_user_id: String, +) -> Result { + let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?; + Ok(RuntimeProfileWalletConfigAdminGetInput { admin_user_id }) +} + +pub fn build_runtime_profile_wallet_config_admin_upsert_input( + admin_user_id: String, + initial_mud_points: u64, + updated_at_micros: i64, +) -> Result { + let admin_user_id = normalize_runtime_profile_user_id(admin_user_id)?; + if initial_mud_points == 0 || initial_mud_points > i64::MAX as u64 { + return Err(RuntimeProfileFieldError::InvalidInitialWalletPoints); + } + Ok(RuntimeProfileWalletConfigAdminUpsertInput { + admin_user_id, + initial_mud_points, + updated_at_micros, + }) +} + pub fn build_runtime_tracking_event_input( event_id: String, event_key: String, diff --git a/server-rs/crates/module-runtime/src/domain.rs b/server-rs/crates/module-runtime/src/domain.rs index cc1e0fdac..35158b0e2 100644 --- a/server-rs/crates/module-runtime/src/domain.rs +++ b/server-rs/crates/module-runtime/src/domain.rs @@ -15,6 +15,7 @@ pub const DEFAULT_BROWSE_HISTORY_AUTHOR_DISPLAY_NAME: &str = "玩家"; pub const MAX_BROWSE_HISTORY_BATCH_SIZE: usize = 100; pub const PROFILE_WALLET_LEDGER_LIST_LIMIT: usize = 50; pub const PROFILE_NEW_USER_INITIAL_WALLET_POINTS: u64 = 100; +pub const PROFILE_WALLET_CONFIG_GLOBAL_ID: &str = "profile_wallet"; pub const PROFILE_REFERRAL_REWARD_POINTS: u64 = 30; pub const PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT: u32 = 10; pub const PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON: &str = "{}"; @@ -643,6 +644,39 @@ pub struct RuntimeProfileDashboardGetInput { pub user_id: String, } +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileWalletConfigSnapshot { + pub config_id: String, + pub initial_mud_points: u64, + pub created_by: String, + pub created_at_micros: i64, + pub updated_by: String, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileWalletConfigAdminGetInput { + pub admin_user_id: String, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileWalletConfigAdminUpsertInput { + pub admin_user_id: String, + pub initial_mud_points: u64, + pub updated_at_micros: i64, +} + +#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct RuntimeProfileWalletConfigAdminProcedureResult { + pub ok: bool, + pub record: Option, + pub error_message: Option, +} + #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum RuntimeProfileFeedbackStatus { @@ -1682,6 +1716,18 @@ pub struct RuntimeProfileRechargeProductConfigRecord { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq)] +pub struct RuntimeProfileWalletConfigRecord { + pub config_id: String, + pub initial_mud_points: u64, + pub created_by: String, + pub created_at: String, + pub created_at_micros: i64, + pub updated_by: String, + pub updated_at: String, + pub updated_at_micros: i64, +} + #[derive(Clone, Debug, PartialEq)] pub struct RuntimeProfileMembershipBenefitRecord { pub benefit_name: String, diff --git a/server-rs/crates/module-runtime/src/errors.rs b/server-rs/crates/module-runtime/src/errors.rs index 7ef3f3fa9..a9222c0a7 100644 --- a/server-rs/crates/module-runtime/src/errors.rs +++ b/server-rs/crates/module-runtime/src/errors.rs @@ -46,6 +46,7 @@ pub enum RuntimeProfileFieldError { MissingUserId, MissingLedgerId, InvalidWalletAmount, + InvalidInitialWalletPoints, WalletAmountOverflow, WalletBalanceOverflow, InsufficientWalletBalance, @@ -111,6 +112,9 @@ impl std::fmt::Display for RuntimeProfileFieldError { Self::MissingUserId => f.write_str("profile.user_id 不能为空"), Self::MissingLedgerId => f.write_str("profile.wallet_ledger_id 不能为空"), Self::InvalidWalletAmount => f.write_str("profile.wallet_amount 必须大于 0"), + Self::InvalidInitialWalletPoints => { + f.write_str("profile_wallet_config.initial_mud_points 必须大于 0") + } Self::WalletAmountOverflow => f.write_str("profile.wallet_amount 超出上限"), Self::WalletBalanceOverflow => f.write_str("profile.wallet_balance 超出上限"), Self::InsufficientWalletBalance => f.write_str("泥点余额不足"), diff --git a/server-rs/crates/shared-contracts/src/runtime.rs b/server-rs/crates/shared-contracts/src/runtime.rs index 294df1bfb..6294df060 100644 --- a/server-rs/crates/shared-contracts/src/runtime.rs +++ b/server-rs/crates/shared-contracts/src/runtime.rs @@ -514,6 +514,17 @@ pub struct ProfileRechargeProductConfigAdminListResponse { pub entries: Vec, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct ProfileWalletConfigAdminResponse { + pub config_id: String, + pub initial_mud_points: u64, + pub created_by: String, + pub created_at: String, + pub updated_by: String, + pub updated_at: String, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AnalyticsMetricQueryRequest { @@ -577,6 +588,12 @@ pub struct AdminUpsertProfileRechargeProductRequest { pub sort_order: Option, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct AdminUpsertProfileWalletConfigRequest { + pub initial_mud_points: u64, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct AdminDisableProfileTaskConfigRequest { diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index 1e21cd6c9..ad2d5c9b3 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -204,7 +204,7 @@ use module_runtime::{ RuntimeProfileRedeemCodeRecord, RuntimeProfileRewardCodeRedeemRecord, RuntimeProfileSaveArchiveRecord, RuntimeProfileTaskCenterRecord, RuntimeProfileTaskClaimRecord, RuntimeProfileTaskConfigRecord, RuntimeProfileTaskCycle as DomainRuntimeProfileTaskCycle, - RuntimeProfileTaskStatus as DomainRuntimeProfileTaskStatus, + RuntimeProfileTaskStatus as DomainRuntimeProfileTaskStatus, RuntimeProfileWalletConfigRecord, RuntimeProfileWalletLedgerEntryRecord, RuntimeReferralInviteCenterRecord, RuntimeReferralRedeemRecord, RuntimeSettingsRecord, RuntimeSnapshotRecord, RuntimeTrackingScopeKind as DomainRuntimeTrackingScopeKind, build_analytics_metric_query_input, @@ -234,7 +234,9 @@ use module_runtime::{ build_runtime_profile_task_config_admin_list_input, build_runtime_profile_task_config_admin_upsert_input, build_runtime_profile_task_config_record, build_runtime_profile_wallet_adjustment_input, - build_runtime_profile_wallet_ledger_entry_record, + build_runtime_profile_wallet_config_admin_get_input, + build_runtime_profile_wallet_config_admin_upsert_input, + build_runtime_profile_wallet_config_record, build_runtime_profile_wallet_ledger_entry_record, build_runtime_profile_wallet_ledger_list_input, build_runtime_referral_invite_center_get_input, build_runtime_referral_invite_center_record, build_runtime_referral_redeem_input, build_runtime_referral_redeem_record, build_runtime_setting_get_input, diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 0a852bc9d..6d8fb2ca1 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -279,6 +279,7 @@ pub(crate) use self::runtime_profile::{ map_runtime_profile_task_config_admin_list_procedure_result, map_runtime_profile_task_config_admin_procedure_result, map_runtime_profile_wallet_adjustment_procedure_result, + map_runtime_profile_wallet_config_admin_procedure_result, map_runtime_profile_wallet_ledger_procedure_result, map_runtime_referral_invite_center_procedure_result, map_runtime_referral_redeem_procedure_result, diff --git a/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs b/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs index 3a94396b9..a206a0e88 100644 --- a/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs +++ b/server-rs/crates/spacetime-client/src/mapper/runtime_profile.rs @@ -18,6 +18,28 @@ impl From } } +impl From + for RuntimeProfileWalletConfigAdminGetInput +{ + fn from(input: module_runtime::RuntimeProfileWalletConfigAdminGetInput) -> Self { + Self { + admin_user_id: input.admin_user_id, + } + } +} + +impl From + for RuntimeProfileWalletConfigAdminUpsertInput +{ + fn from(input: module_runtime::RuntimeProfileWalletConfigAdminUpsertInput) -> Self { + Self { + admin_user_id: input.admin_user_id, + initial_mud_points: input.initial_mud_points, + updated_at_micros: input.updated_at_micros, + } + } +} + impl From for RuntimeProfileWalletAdjustmentInput { @@ -577,6 +599,22 @@ pub(crate) fn map_runtime_profile_task_config_admin_procedure_result( )) } +pub(crate) fn map_runtime_profile_wallet_config_admin_procedure_result( + result: RuntimeProfileWalletConfigAdminProcedureResult, +) -> Result { + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + + let snapshot = result + .record + .ok_or_else(|| SpacetimeClientError::missing_snapshot("profile wallet config 快照"))?; + + Ok(build_runtime_profile_wallet_config_record( + map_runtime_profile_wallet_config_snapshot(snapshot), + )) +} + pub(crate) fn map_runtime_profile_recharge_product_admin_list_procedure_result( result: RuntimeProfileRechargeProductAdminListProcedureResult, ) -> Result, SpacetimeClientError> { @@ -778,6 +816,19 @@ pub(crate) fn map_runtime_profile_wallet_ledger_entry_snapshot( } } +pub(crate) fn map_runtime_profile_wallet_config_snapshot( + snapshot: RuntimeProfileWalletConfigSnapshot, +) -> module_runtime::RuntimeProfileWalletConfigSnapshot { + module_runtime::RuntimeProfileWalletConfigSnapshot { + config_id: snapshot.config_id, + initial_mud_points: snapshot.initial_mud_points, + created_by: snapshot.created_by, + created_at_micros: snapshot.created_at_micros, + updated_by: snapshot.updated_by, + updated_at_micros: snapshot.updated_at_micros, + } +} + pub(crate) fn map_runtime_profile_recharge_center_snapshot( snapshot: RuntimeProfileRechargeCenterSnapshot, ) -> module_runtime::RuntimeProfileRechargeCenterSnapshot { diff --git a/server-rs/crates/spacetime-client/src/module_bindings.rs b/server-rs/crates/spacetime-client/src/module_bindings.rs index 3c60f4497..8eaba5357 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings.rs @@ -10,6 +10,7 @@ pub mod accept_quest_reducer; pub mod acknowledge_quest_completion_reducer; pub mod admin_disable_profile_redeem_code_procedure; pub mod admin_disable_profile_task_config_procedure; +pub mod admin_get_profile_wallet_config_procedure; pub mod admin_list_profile_invite_codes_procedure; pub mod admin_list_profile_recharge_products_procedure; pub mod admin_list_profile_redeem_codes_procedure; @@ -20,6 +21,7 @@ pub mod admin_upsert_profile_invite_code_procedure; pub mod admin_upsert_profile_recharge_product_procedure; pub mod admin_upsert_profile_redeem_code_procedure; pub mod admin_upsert_profile_task_config_procedure; +pub mod admin_upsert_profile_wallet_config_procedure; pub mod admin_work_visibility_list_input_type; pub mod admin_work_visibility_list_procedure_result_type; pub mod admin_work_visibility_procedure_result_type; @@ -670,6 +672,8 @@ pub mod profile_task_progress_table; pub mod profile_task_progress_type; pub mod profile_task_reward_claim_table; pub mod profile_task_reward_claim_type; +pub mod profile_wallet_config_table; +pub mod profile_wallet_config_type; pub mod profile_wallet_ledger_table; pub mod profile_wallet_ledger_type; pub mod public_work_detail_entry_table; @@ -975,6 +979,10 @@ pub mod runtime_profile_task_item_snapshot_type; pub mod runtime_profile_task_status_type; pub mod runtime_profile_wallet_adjustment_input_type; pub mod runtime_profile_wallet_adjustment_procedure_result_type; +pub mod runtime_profile_wallet_config_admin_get_input_type; +pub mod runtime_profile_wallet_config_admin_procedure_result_type; +pub mod runtime_profile_wallet_config_admin_upsert_input_type; +pub mod runtime_profile_wallet_config_snapshot_type; pub mod runtime_profile_wallet_ledger_entry_snapshot_type; pub mod runtime_profile_wallet_ledger_list_input_type; pub mod runtime_profile_wallet_ledger_procedure_result_type; @@ -1219,6 +1227,7 @@ pub use accept_quest_reducer::accept_quest; pub use acknowledge_quest_completion_reducer::acknowledge_quest_completion; pub use admin_disable_profile_redeem_code_procedure::admin_disable_profile_redeem_code; pub use admin_disable_profile_task_config_procedure::admin_disable_profile_task_config; +pub use admin_get_profile_wallet_config_procedure::admin_get_profile_wallet_config; pub use admin_list_profile_invite_codes_procedure::admin_list_profile_invite_codes; pub use admin_list_profile_recharge_products_procedure::admin_list_profile_recharge_products; pub use admin_list_profile_redeem_codes_procedure::admin_list_profile_redeem_codes; @@ -1229,6 +1238,7 @@ pub use admin_upsert_profile_invite_code_procedure::admin_upsert_profile_invite_ pub use admin_upsert_profile_recharge_product_procedure::admin_upsert_profile_recharge_product; pub use admin_upsert_profile_redeem_code_procedure::admin_upsert_profile_redeem_code; pub use admin_upsert_profile_task_config_procedure::admin_upsert_profile_task_config; +pub use admin_upsert_profile_wallet_config_procedure::admin_upsert_profile_wallet_config; pub use admin_work_visibility_list_input_type::AdminWorkVisibilityListInput; pub use admin_work_visibility_list_procedure_result_type::AdminWorkVisibilityListProcedureResult; pub use admin_work_visibility_procedure_result_type::AdminWorkVisibilityProcedureResult; @@ -1879,6 +1889,8 @@ pub use profile_task_progress_table::*; pub use profile_task_progress_type::ProfileTaskProgress; pub use profile_task_reward_claim_table::*; pub use profile_task_reward_claim_type::ProfileTaskRewardClaim; +pub use profile_wallet_config_table::*; +pub use profile_wallet_config_type::ProfileWalletConfig; pub use profile_wallet_ledger_table::*; pub use profile_wallet_ledger_type::ProfileWalletLedger; pub use public_work_detail_entry_table::*; @@ -2184,6 +2196,10 @@ pub use runtime_profile_task_item_snapshot_type::RuntimeProfileTaskItemSnapshot; pub use runtime_profile_task_status_type::RuntimeProfileTaskStatus; pub use runtime_profile_wallet_adjustment_input_type::RuntimeProfileWalletAdjustmentInput; pub use runtime_profile_wallet_adjustment_procedure_result_type::RuntimeProfileWalletAdjustmentProcedureResult; +pub use runtime_profile_wallet_config_admin_get_input_type::RuntimeProfileWalletConfigAdminGetInput; +pub use runtime_profile_wallet_config_admin_procedure_result_type::RuntimeProfileWalletConfigAdminProcedureResult; +pub use runtime_profile_wallet_config_admin_upsert_input_type::RuntimeProfileWalletConfigAdminUpsertInput; +pub use runtime_profile_wallet_config_snapshot_type::RuntimeProfileWalletConfigSnapshot; pub use runtime_profile_wallet_ledger_entry_snapshot_type::RuntimeProfileWalletLedgerEntrySnapshot; pub use runtime_profile_wallet_ledger_list_input_type::RuntimeProfileWalletLedgerListInput; pub use runtime_profile_wallet_ledger_procedure_result_type::RuntimeProfileWalletLedgerProcedureResult; @@ -2771,6 +2787,7 @@ pub struct DbUpdate { profile_task_config: __sdk::TableUpdate, profile_task_progress: __sdk::TableUpdate, profile_task_reward_claim: __sdk::TableUpdate, + profile_wallet_config: __sdk::TableUpdate, profile_wallet_ledger: __sdk::TableUpdate, public_work_detail_entry: __sdk::TableUpdate, public_work_gallery_entry: __sdk::TableUpdate, @@ -3064,6 +3081,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "profile_task_reward_claim" => db_update.profile_task_reward_claim.append( profile_task_reward_claim_table::parse_table_update(table_update)?, ), + "profile_wallet_config" => db_update.profile_wallet_config.append( + profile_wallet_config_table::parse_table_update(table_update)?, + ), "profile_wallet_ledger" => db_update.profile_wallet_ledger.append( profile_wallet_ledger_table::parse_table_update(table_update)?, ), @@ -3612,6 +3632,12 @@ impl __sdk::DbUpdate for DbUpdate { &self.profile_task_reward_claim, ) .with_updates_by_pk(|row| &row.claim_id); + diff.profile_wallet_config = cache + .apply_diff_to_table::( + "profile_wallet_config", + &self.profile_wallet_config, + ) + .with_updates_by_pk(|row| &row.config_id); diff.profile_wallet_ledger = cache .apply_diff_to_table::( "profile_wallet_ledger", @@ -4100,6 +4126,9 @@ impl __sdk::DbUpdate for DbUpdate { "profile_task_reward_claim" => db_update .profile_task_reward_claim .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "profile_wallet_config" => db_update + .profile_wallet_config + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "profile_wallet_ledger" => db_update .profile_wallet_ledger .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -4488,6 +4517,9 @@ impl __sdk::DbUpdate for DbUpdate { "profile_task_reward_claim" => db_update .profile_task_reward_claim .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "profile_wallet_config" => db_update + .profile_wallet_config + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "profile_wallet_ledger" => db_update .profile_wallet_ledger .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -4730,6 +4762,7 @@ pub struct AppliedDiff<'r> { profile_task_config: __sdk::TableAppliedDiff<'r, ProfileTaskConfig>, profile_task_progress: __sdk::TableAppliedDiff<'r, ProfileTaskProgress>, profile_task_reward_claim: __sdk::TableAppliedDiff<'r, ProfileTaskRewardClaim>, + profile_wallet_config: __sdk::TableAppliedDiff<'r, ProfileWalletConfig>, profile_wallet_ledger: __sdk::TableAppliedDiff<'r, ProfileWalletLedger>, public_work_detail_entry: __sdk::TableAppliedDiff<'r, PublicWorkDetailEntry>, public_work_gallery_entry: __sdk::TableAppliedDiff<'r, PublicWorkGalleryEntry>, @@ -5157,6 +5190,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.profile_task_reward_claim, event, ); + callbacks.invoke_table_row_callbacks::( + "profile_wallet_config", + &self.profile_wallet_config, + event, + ); callbacks.invoke_table_row_callbacks::( "profile_wallet_ledger", &self.profile_wallet_ledger, @@ -6134,6 +6172,7 @@ impl __sdk::SpacetimeModule for RemoteModule { profile_task_config_table::register_table(client_cache); profile_task_progress_table::register_table(client_cache); profile_task_reward_claim_table::register_table(client_cache); + profile_wallet_config_table::register_table(client_cache); profile_wallet_ledger_table::register_table(client_cache); public_work_detail_entry_table::register_table(client_cache); public_work_gallery_entry_table::register_table(client_cache); @@ -6261,6 +6300,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "profile_task_config", "profile_task_progress", "profile_task_reward_claim", + "profile_wallet_config", "profile_wallet_ledger", "public_work_detail_entry", "public_work_gallery_entry", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs new file mode 100644 index 000000000..c7c836f1b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_get_profile_wallet_config_procedure.rs @@ -0,0 +1,59 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::runtime_profile_wallet_config_admin_get_input_type::RuntimeProfileWalletConfigAdminGetInput; +use super::runtime_profile_wallet_config_admin_procedure_result_type::RuntimeProfileWalletConfigAdminProcedureResult; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct AdminGetProfileWalletConfigArgs { + pub input: RuntimeProfileWalletConfigAdminGetInput, +} + +impl __sdk::InModule for AdminGetProfileWalletConfigArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `admin_get_profile_wallet_config`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait admin_get_profile_wallet_config { + fn admin_get_profile_wallet_config(&self, input: RuntimeProfileWalletConfigAdminGetInput) { + self.admin_get_profile_wallet_config_then(input, |_, _| {}); + } + + fn admin_get_profile_wallet_config_then( + &self, + input: RuntimeProfileWalletConfigAdminGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl admin_get_profile_wallet_config for super::RemoteProcedures { + fn admin_get_profile_wallet_config_then( + &self, + input: RuntimeProfileWalletConfigAdminGetInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, RuntimeProfileWalletConfigAdminProcedureResult>( + "admin_get_profile_wallet_config", + AdminGetProfileWalletConfigArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs new file mode 100644 index 000000000..b87b6506d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_wallet_config_procedure.rs @@ -0,0 +1,62 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::runtime_profile_wallet_config_admin_procedure_result_type::RuntimeProfileWalletConfigAdminProcedureResult; +use super::runtime_profile_wallet_config_admin_upsert_input_type::RuntimeProfileWalletConfigAdminUpsertInput; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +struct AdminUpsertProfileWalletConfigArgs { + pub input: RuntimeProfileWalletConfigAdminUpsertInput, +} + +impl __sdk::InModule for AdminUpsertProfileWalletConfigArgs { + type Module = super::RemoteModule; +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the procedure `admin_upsert_profile_wallet_config`. +/// +/// Implemented for [`super::RemoteProcedures`]. +pub trait admin_upsert_profile_wallet_config { + fn admin_upsert_profile_wallet_config( + &self, + input: RuntimeProfileWalletConfigAdminUpsertInput, + ) { + self.admin_upsert_profile_wallet_config_then(input, |_, _| {}); + } + + fn admin_upsert_profile_wallet_config_then( + &self, + input: RuntimeProfileWalletConfigAdminUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ); +} + +impl admin_upsert_profile_wallet_config for super::RemoteProcedures { + fn admin_upsert_profile_wallet_config_then( + &self, + input: RuntimeProfileWalletConfigAdminUpsertInput, + + __callback: impl FnOnce( + &super::ProcedureEventContext, + Result, + ) + Send + + 'static, + ) { + self.imp + .invoke_procedure_with_callback::<_, RuntimeProfileWalletConfigAdminProcedureResult>( + "admin_upsert_profile_wallet_config", + AdminUpsertProfileWalletConfigArgs { input }, + __callback, + ); + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_table.rs new file mode 100644 index 000000000..57cfdc85f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_table.rs @@ -0,0 +1,161 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use super::profile_wallet_config_type::ProfileWalletConfig; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `profile_wallet_config`. +/// +/// Obtain a handle from the [`ProfileWalletConfigTableAccess::profile_wallet_config`] method on [`super::RemoteTables`], +/// like `ctx.db.profile_wallet_config()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_wallet_config().on_insert(...)`. +pub struct ProfileWalletConfigTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `profile_wallet_config`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait ProfileWalletConfigTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`ProfileWalletConfigTableHandle`], which mediates access to the table `profile_wallet_config`. + fn profile_wallet_config(&self) -> ProfileWalletConfigTableHandle<'_>; +} + +impl ProfileWalletConfigTableAccess for super::RemoteTables { + fn profile_wallet_config(&self) -> ProfileWalletConfigTableHandle<'_> { + ProfileWalletConfigTableHandle { + imp: self + .imp + .get_table::("profile_wallet_config"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct ProfileWalletConfigInsertCallbackId(__sdk::CallbackId); +pub struct ProfileWalletConfigDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for ProfileWalletConfigTableHandle<'ctx> { + type Row = ProfileWalletConfig; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = ProfileWalletConfigInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileWalletConfigInsertCallbackId { + ProfileWalletConfigInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: ProfileWalletConfigInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = ProfileWalletConfigDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> ProfileWalletConfigDeleteCallbackId { + ProfileWalletConfigDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: ProfileWalletConfigDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +pub struct ProfileWalletConfigUpdateCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::TableWithPrimaryKey for ProfileWalletConfigTableHandle<'ctx> { + type UpdateCallbackId = ProfileWalletConfigUpdateCallbackId; + + fn on_update( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row, &Self::Row) + Send + 'static, + ) -> ProfileWalletConfigUpdateCallbackId { + ProfileWalletConfigUpdateCallbackId(self.imp.on_update(Box::new(callback))) + } + + fn remove_on_update(&self, callback: ProfileWalletConfigUpdateCallbackId) { + self.imp.remove_on_update(callback.0) + } +} + +/// Access to the `config_id` unique index on the table `profile_wallet_config`, +/// which allows point queries on the field of the same name +/// via the [`ProfileWalletConfigConfigIdUnique::find`] method. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.profile_wallet_config().config_id().find(...)`. +pub struct ProfileWalletConfigConfigIdUnique<'ctx> { + imp: __sdk::UniqueConstraintHandle, + phantom: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +impl<'ctx> ProfileWalletConfigTableHandle<'ctx> { + /// Get a handle on the `config_id` unique index on the table `profile_wallet_config`. + pub fn config_id(&self) -> ProfileWalletConfigConfigIdUnique<'ctx> { + ProfileWalletConfigConfigIdUnique { + imp: self.imp.get_unique_constraint::("config_id"), + phantom: std::marker::PhantomData, + } + } +} + +impl<'ctx> ProfileWalletConfigConfigIdUnique<'ctx> { + /// Find the subscribed row whose `config_id` column value is equal to `col_val`, + /// if such a row is present in the client cache. + pub fn find(&self, col_val: &String) -> Option { + self.imp.find(col_val) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("profile_wallet_config"); + _table.add_unique_constraint::("config_id", |row| &row.config_id); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `ProfileWalletConfig`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait profile_wallet_configQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `ProfileWalletConfig`. + fn profile_wallet_config(&self) -> __sdk::__query_builder::Table; +} + +impl profile_wallet_configQueryTableAccess for __sdk::QueryTableAccessor { + fn profile_wallet_config(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("profile_wallet_config") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_type.rs new file mode 100644 index 000000000..39a293d5e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/profile_wallet_config_type.rs @@ -0,0 +1,64 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct ProfileWalletConfig { + pub config_id: String, + pub initial_mud_points: u64, + pub created_by: String, + pub created_at: __sdk::Timestamp, + pub updated_by: String, + pub updated_at: __sdk::Timestamp, +} + +impl __sdk::InModule for ProfileWalletConfig { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `ProfileWalletConfig`. +/// +/// Provides typed access to columns for query building. +pub struct ProfileWalletConfigCols { + pub config_id: __sdk::__query_builder::Col, + pub initial_mud_points: __sdk::__query_builder::Col, + pub created_by: __sdk::__query_builder::Col, + pub created_at: __sdk::__query_builder::Col, + pub updated_by: __sdk::__query_builder::Col, + pub updated_at: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for ProfileWalletConfig { + type Cols = ProfileWalletConfigCols; + fn cols(table_name: &'static str) -> Self::Cols { + ProfileWalletConfigCols { + config_id: __sdk::__query_builder::Col::new(table_name, "config_id"), + initial_mud_points: __sdk::__query_builder::Col::new(table_name, "initial_mud_points"), + created_by: __sdk::__query_builder::Col::new(table_name, "created_by"), + created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), + updated_by: __sdk::__query_builder::Col::new(table_name, "updated_by"), + updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + } + } +} + +/// Indexed column accessor struct for the table `ProfileWalletConfig`. +/// +/// Provides typed access to indexed columns for query building. +pub struct ProfileWalletConfigIxCols { + pub config_id: __sdk::__query_builder::IxCol, +} + +impl __sdk::__query_builder::HasIxCols for ProfileWalletConfig { + type IxCols = ProfileWalletConfigIxCols; + fn ix_cols(table_name: &'static str) -> Self::IxCols { + ProfileWalletConfigIxCols { + config_id: __sdk::__query_builder::IxCol::new(table_name, "config_id"), + } + } +} + +impl __sdk::__query_builder::CanBeLookupTable for ProfileWalletConfig {} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_get_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_get_input_type.rs new file mode 100644 index 000000000..b027c7359 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_get_input_type.rs @@ -0,0 +1,15 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeProfileWalletConfigAdminGetInput { + pub admin_user_id: String, +} + +impl __sdk::InModule for RuntimeProfileWalletConfigAdminGetInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_procedure_result_type.rs new file mode 100644 index 000000000..28d1f11c8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_procedure_result_type.rs @@ -0,0 +1,19 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +use super::runtime_profile_wallet_config_snapshot_type::RuntimeProfileWalletConfigSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeProfileWalletConfigAdminProcedureResult { + pub ok: bool, + pub record: Option, + pub error_message: Option, +} + +impl __sdk::InModule for RuntimeProfileWalletConfigAdminProcedureResult { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_upsert_input_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_upsert_input_type.rs new file mode 100644 index 000000000..2b967e875 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_admin_upsert_input_type.rs @@ -0,0 +1,17 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeProfileWalletConfigAdminUpsertInput { + pub admin_user_id: String, + pub initial_mud_points: u64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for RuntimeProfileWalletConfigAdminUpsertInput { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_snapshot_type.rs new file mode 100644 index 000000000..c68000f8f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_config_snapshot_type.rs @@ -0,0 +1,20 @@ +// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE +// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. + +#![allow(unused, clippy::all)] +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct RuntimeProfileWalletConfigSnapshot { + pub config_id: String, + pub initial_mud_points: u64, + pub created_by: String, + pub created_at_micros: i64, + pub updated_by: String, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for RuntimeProfileWalletConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/runtime.rs b/server-rs/crates/spacetime-client/src/runtime.rs index 4ef8de576..0eed3f271 100644 --- a/server-rs/crates/spacetime-client/src/runtime.rs +++ b/server-rs/crates/spacetime-client/src/runtime.rs @@ -913,6 +913,60 @@ impl SpacetimeClient { .await } + pub async fn admin_get_profile_wallet_config( + &self, + admin_user_id: String, + ) -> Result { + let procedure_input = build_runtime_profile_wallet_config_admin_get_input(admin_user_id) + .map_err(SpacetimeClientError::validation_failed)? + .into(); + + self.call_after_connect( + "admin_get_profile_wallet_config", + move |connection, sender| { + connection + .procedures() + .admin_get_profile_wallet_config_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_runtime_profile_wallet_config_admin_procedure_result); + send_once(&sender, mapped); + }); + }, + ) + .await + } + + pub async fn admin_upsert_profile_wallet_config( + &self, + admin_user_id: String, + initial_mud_points: u64, + updated_at_micros: i64, + ) -> Result { + let procedure_input = build_runtime_profile_wallet_config_admin_upsert_input( + admin_user_id, + initial_mud_points, + updated_at_micros, + ) + .map_err(SpacetimeClientError::validation_failed)? + .into(); + + self.call_after_connect( + "admin_upsert_profile_wallet_config", + move |connection, sender| { + connection + .procedures() + .admin_upsert_profile_wallet_config_then(procedure_input, move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_runtime_profile_wallet_config_admin_procedure_result); + send_once(&sender, mapped); + }); + }, + ) + .await + } + pub async fn admin_list_profile_recharge_products( &self, admin_user_id: String, diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index 19eb41724..ef3c77d51 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -191,6 +191,7 @@ macro_rules! migration_tables { user_browse_history, profile_dashboard_state, profile_wallet_ledger, + profile_wallet_config, analytics_date_dimension, tracking_event, tracking_daily_stat, diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index 73595d0d2..81bed7882 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -38,6 +38,18 @@ pub struct ProfileWalletLedger { pub(crate) created_at: Timestamp, } +#[spacetimedb::table(accessor = profile_wallet_config)] +#[derive(Clone)] +pub struct ProfileWalletConfig { + #[primary_key] + pub(crate) config_id: String, + pub(crate) initial_mud_points: u64, + pub(crate) created_by: String, + pub(crate) created_at: Timestamp, + pub(crate) updated_by: String, + pub(crate) updated_at: Timestamp, +} + #[spacetimedb::table( accessor = tracking_event, index(accessor = by_tracking_event_event_key, btree(columns = [event_key])), @@ -705,6 +717,44 @@ pub fn admin_disable_profile_task_config( } } +#[spacetimedb::procedure] +pub fn admin_get_profile_wallet_config( + ctx: &mut ProcedureContext, + input: RuntimeProfileWalletConfigAdminGetInput, +) -> RuntimeProfileWalletConfigAdminProcedureResult { + match ctx.try_with_tx(|tx| get_profile_wallet_config_snapshot(tx, input.clone())) { + Ok(record) => RuntimeProfileWalletConfigAdminProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => RuntimeProfileWalletConfigAdminProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + +#[spacetimedb::procedure] +pub fn admin_upsert_profile_wallet_config( + ctx: &mut ProcedureContext, + input: RuntimeProfileWalletConfigAdminUpsertInput, +) -> RuntimeProfileWalletConfigAdminProcedureResult { + match ctx.try_with_tx(|tx| upsert_profile_wallet_config_record(tx, input.clone())) { + Ok(record) => RuntimeProfileWalletConfigAdminProcedureResult { + ok: true, + record: Some(record), + error_message: None, + }, + Err(message) => RuntimeProfileWalletConfigAdminProcedureResult { + ok: false, + record: None, + error_message: Some(message), + }, + } +} + #[spacetimedb::procedure] pub fn admin_list_profile_recharge_products( ctx: &mut ProcedureContext, @@ -2818,6 +2868,28 @@ fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 { .unwrap_or(0) } +fn build_profile_wallet_config_snapshot( + ctx: &ReducerContext, +) -> RuntimeProfileWalletConfigSnapshot { + ctx.db + .profile_wallet_config() + .config_id() + .find(&PROFILE_WALLET_CONFIG_GLOBAL_ID.to_string()) + .map(|row| build_profile_wallet_config_snapshot_from_row(&row)) + .unwrap_or_else(|| RuntimeProfileWalletConfigSnapshot { + config_id: PROFILE_WALLET_CONFIG_GLOBAL_ID.to_string(), + initial_mud_points: PROFILE_NEW_USER_INITIAL_WALLET_POINTS, + created_by: String::new(), + created_at_micros: 0, + updated_by: String::new(), + updated_at_micros: 0, + }) +} + +fn profile_new_user_initial_wallet_points(ctx: &ReducerContext) -> u64 { + build_profile_wallet_config_snapshot(ctx).initial_mud_points +} + fn build_new_user_registration_wallet_ledger_id(user_id: &str) -> String { format!("{PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX}:{user_id}") } @@ -2839,7 +2911,7 @@ fn grant_new_user_registration_wallet_reward_tx( apply_profile_wallet_delta( ctx, &validated_input.user_id, - PROFILE_NEW_USER_INITIAL_WALLET_POINTS, + profile_new_user_initial_wallet_points(ctx), RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward, &ledger_id, ctx.timestamp, @@ -3022,6 +3094,51 @@ fn list_profile_task_config_snapshots( Ok(entries) } +fn get_profile_wallet_config_snapshot( + ctx: &ReducerContext, + input: RuntimeProfileWalletConfigAdminGetInput, +) -> Result { + let _validated_input = build_runtime_profile_wallet_config_admin_get_input(input.admin_user_id) + .map_err(|error| error.to_string())?; + Ok(build_profile_wallet_config_snapshot(ctx)) +} + +fn upsert_profile_wallet_config_record( + ctx: &ReducerContext, + input: RuntimeProfileWalletConfigAdminUpsertInput, +) -> Result { + let validated_input = build_runtime_profile_wallet_config_admin_upsert_input( + input.admin_user_id, + input.initial_mud_points, + input.updated_at_micros, + ) + .map_err(|error| error.to_string())?; + let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros); + let config_id = PROFILE_WALLET_CONFIG_GLOBAL_ID.to_string(); + let existing = ctx.db.profile_wallet_config().config_id().find(&config_id); + if let Some(row) = existing.as_ref() { + ctx.db + .profile_wallet_config() + .config_id() + .delete(&row.config_id); + } + let inserted = ctx.db.profile_wallet_config().insert(ProfileWalletConfig { + config_id, + initial_mud_points: validated_input.initial_mud_points, + created_by: existing + .as_ref() + .map(|row| row.created_by.clone()) + .unwrap_or_else(|| validated_input.admin_user_id.clone()), + created_at: existing + .as_ref() + .map(|row| row.created_at) + .unwrap_or(updated_at), + updated_by: validated_input.admin_user_id, + updated_at, + }); + Ok(build_profile_wallet_config_snapshot_from_row(&inserted)) +} + fn list_profile_recharge_product_config_snapshots( ctx: &ReducerContext, input: RuntimeProfileRechargeProductAdminListInput, @@ -4042,6 +4159,19 @@ fn build_profile_wallet_ledger_snapshot_from_row( } } +fn build_profile_wallet_config_snapshot_from_row( + row: &ProfileWalletConfig, +) -> RuntimeProfileWalletConfigSnapshot { + RuntimeProfileWalletConfigSnapshot { + config_id: row.config_id.clone(), + initial_mud_points: row.initial_mud_points, + created_by: row.created_by.clone(), + created_at_micros: row.created_at.to_micros_since_unix_epoch(), + updated_by: row.updated_by.clone(), + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + } +} + fn build_profile_task_config_snapshot_from_row( row: &ProfileTaskConfig, ) -> RuntimeProfileTaskConfigSnapshot {