doge 9 godzin temu
rodzic
commit
7b99efd258

+ 1 - 1
app/Console/Commands/BackfillShareCommissions.php

@@ -82,7 +82,7 @@ class BackfillShareCommissions extends Command
         $this->line('database: ' . DB::connection()->getDatabaseName());
         $this->line('database: ' . DB::connection()->getDatabaseName());
         $this->line('table_prefix: ' . DB::getTablePrefix());
         $this->line('table_prefix: ' . DB::getTablePrefix());
         $data = (new ShareCommissionDiagnostics())->inspect($range);
         $data = (new ShareCommissionDiagnostics())->inspect($range);
-        foreach (['recharge_rate', 'share_enabled', 'benefit_enabled', 'valid_member_min_deposit', 'valid_member_min_effective_bet', 'first_rebate_at', 'last_rebate_at'] as $key) {
+        foreach (['recharge_rate', 'share_enabled', 'valid_member_min_deposit', 'valid_member_min_effective_bet', 'first_rebate_at', 'last_rebate_at'] as $key) {
             $this->line($key . ': ' . ($data[$key] ?? '(无)'));
             $this->line($key . ': ' . ($data[$key] ?? '(无)'));
         }
         }
         $this->table(['检查项', '条数'], array_map(fn ($key, $count) => [$key, $count], array_keys($data['counts']), array_values($data['counts'])));
         $this->table(['检查项', '条数'], array_map(fn ($key, $count) => [$key, $count], array_keys($data['counts']), array_values($data['counts'])));

+ 1 - 1
app/Models/ShareSetting.php

@@ -6,7 +6,7 @@ class ShareSetting extends BaseModel
 {
 {
     protected $table = 'share_settings';
     protected $table = 'share_settings';
     protected $guarded = [];
     protected $guarded = [];
-    protected $hidden = ['indirect_bonus_rate'];
+    protected $hidden = ['indirect_bonus_rate', 'benefit_enabled'];
     protected $casts = [
     protected $casts = [
         'enabled' => 'integer', 'benefit_enabled' => 'integer',
         'enabled' => 'integer', 'benefit_enabled' => 'integer',
         'valid_member_min_deposit' => 'decimal:2', 'valid_member_min_effective_bet' => 'decimal:10',
         'valid_member_min_deposit' => 'decimal:2', 'valid_member_min_effective_bet' => 'decimal:10',

+ 0 - 1
app/Services/ShareCommissionDiagnostics.php

@@ -64,7 +64,6 @@ class ShareCommissionDiagnostics
             'table_prefix' => DB::getTablePrefix(),
             'table_prefix' => DB::getTablePrefix(),
             'recharge_rate' => $rate,
             'recharge_rate' => $rate,
             'share_enabled' => $setting->enabled ?? null,
             'share_enabled' => $setting->enabled ?? null,
-            'benefit_enabled' => $setting->benefit_enabled ?? null,
             'valid_member_min_deposit' => $setting->valid_member_min_deposit ?? null,
             'valid_member_min_deposit' => $setting->valid_member_min_deposit ?? null,
             'valid_member_min_effective_bet' => $setting->valid_member_min_effective_bet ?? null,
             'valid_member_min_effective_bet' => $setting->valid_member_min_effective_bet ?? null,
             'counts' => [
             'counts' => [

+ 9 - 11
app/Services/Sharing/ShareAdminService.php

@@ -26,7 +26,6 @@ class ShareAdminService
             'id' => $setting ? (int)$setting->id : null,
             'id' => $setting ? (int)$setting->id : null,
             'enabled' => (int)($setting->enabled ?? 0),
             'enabled' => (int)($setting->enabled ?? 0),
             'share_domain' => (string)($setting->share_domain ?? ''),
             'share_domain' => (string)($setting->share_domain ?? ''),
-            'benefit_enabled' => (int)($setting->benefit_enabled ?? 0),
             'valid_member_min_deposit' => $this->money($setting->valid_member_min_deposit ?? '20'),
             'valid_member_min_deposit' => $this->money($setting->valid_member_min_deposit ?? '20'),
             'valid_member_min_effective_bet' => isset($setting->valid_member_min_effective_bet) ? $this->money($setting->valid_member_min_effective_bet) : null,
             'valid_member_min_effective_bet' => isset($setting->valid_member_min_effective_bet) ? $this->money($setting->valid_member_min_effective_bet) : null,
             'activation_date' => $setting?->activation_date ?: null,
             'activation_date' => $setting?->activation_date ?: null,
@@ -39,6 +38,8 @@ class ShareAdminService
 
 
     public function saveSettings(array $input): array
     public function saveSettings(array $input): array
     {
     {
+        // The old UI switch is retired. Stale clients cannot use it to disable/enable settlement.
+        unset($input['benefit_enabled']);
         foreach (self::RETIRED as $key) {
         foreach (self::RETIRED as $key) {
             if (array_key_exists($key, $input)) $this->invalid($key, '旧分享参数已移除,请使用有效会员条件及分类返佣规则');
             if (array_key_exists($key, $input)) $this->invalid($key, '旧分享参数已移除,请使用有效会员条件及分类返佣规则');
         }
         }
@@ -48,22 +49,19 @@ class ShareAdminService
         $data = Validator::make($input, [
         $data = Validator::make($input, [
             'enabled' => ['required', Rule::in([0, 1])],
             'enabled' => ['required', Rule::in([0, 1])],
             'share_domain' => ['required', 'string', 'max:500'],
             'share_domain' => ['required', 'string', 'max:500'],
-            'benefit_enabled' => ['required', Rule::in([0, 1])],
             'valid_member_min_deposit' => ['required', 'regex:/^\d{1,14}(?:\.\d{1,2})?$/'],
             'valid_member_min_deposit' => ['required', 'regex:/^\d{1,14}(?:\.\d{1,2})?$/'],
-            'valid_member_min_effective_bet' => array_merge(['nullable', 'required_if:benefit_enabled,1'], self::MONEY),
+            'valid_member_min_effective_bet' => array_merge(['required'], self::MONEY),
         ])->validate();
         ])->validate();
         $data['share_domain'] = $this->origin($data['share_domain']);
         $data['share_domain'] = $this->origin($data['share_domain']);
+        $data['benefit_enabled'] = 1; // Complete, initialized configuration activates automatically tomorrow.
         return DB::transaction(function () use ($data) {
         return DB::transaction(function () use ($data) {
             $this->lockConfiguration();
             $this->lockConfiguration();
             $setting = ShareSetting::query()->lockForUpdate()->first() ?: new ShareSetting();
             $setting = ShareSetting::query()->lockForUpdate()->first() ?: new ShareSetting();
             $before = $setting->exists ? $setting->toArray() : [];
             $before = $setting->exists ? $setting->toArray() : [];
-            if ((int)$data['benefit_enabled'] === 1) {
-                if (!DB::table('share_system_state')->where('key', 'initialized_at')->exists()) {
-                    $this->invalid('benefit_enabled', '会员分享关系初始化完成后才可启用佣金');
-                }
-                if (!$setting->activation_date) $setting->activation_date = $this->tomorrow();
+            if (!DB::table('share_system_state')->where('key', 'initialized_at')->exists()) {
+                $this->invalid('initialized', '会员分享关系初始化完成后才可保存佣金配置');
             }
             }
-            // Blank nullable fields may clear unfinished setup, but can never enable it.
+            if (!$setting->activation_date) $setting->activation_date = $this->tomorrow();
             $setting->fill($data + OperationAuditService::actor());
             $setting->fill($data + OperationAuditService::actor());
             $setting->save();
             $setting->save();
             $policy = $this->writablePolicy();
             $policy = $this->writablePolicy();
@@ -232,9 +230,9 @@ class ShareAdminService
             : $query->where('effective_date', '<=', $this->today())->orderByDesc('effective_date')->first();
             : $query->where('effective_date', '<=', $this->today())->orderByDesc('effective_date')->first();
         if (!$row) return null;
         if (!$row) return null;
         $data = (array)$row;
         $data = (array)$row;
-        foreach (['id', 'enabled', 'benefit_enabled'] as $field) $data[$field] = (int)$data[$field];
+        foreach (['id', 'enabled'] as $field) $data[$field] = (int)$data[$field];
         foreach (['valid_member_min_deposit', 'valid_member_min_effective_bet'] as $field) $data[$field] = $data[$field] === null ? null : $this->money($data[$field]);
         foreach (['valid_member_min_deposit', 'valid_member_min_effective_bet'] as $field) $data[$field] = $data[$field] === null ? null : $this->money($data[$field]);
-        unset($data['indirect_bonus_rate']);
+        unset($data['indirect_bonus_rate'], $data['benefit_enabled']);
         $data['rules'] = json_decode($data['rules'], true, 512, JSON_THROW_ON_ERROR);
         $data['rules'] = json_decode($data['rules'], true, 512, JSON_THROW_ON_ERROR);
         foreach ($data['rules'] as &$rows) foreach ($rows as &$rule) {
         foreach ($data['rules'] as &$rows) foreach ($rows as &$rule) {
             $rule['min_valid_members'] = (int)$rule['min_valid_members'];
             $rule['min_valid_members'] = (int)$rule['min_valid_members'];

+ 11 - 21
docs/前端接口/分享赚钱/推广设定/接口.md

@@ -1,41 +1,31 @@
 # 分享赚钱 / 推广设定
 # 分享赚钱 / 推广设定
 
 
+本页沿用原接口地址,字段及完整响应以[新分享系统-后台返佣配置](../../新分享系统/后台返佣配置.md)为准。页面删除原“佣金/手续费减免设置开关”,金额门槛和比例输入框始终可编辑,不再做此开关联动。
+
 ## 后台读取和保存
 ## 后台读取和保存
 
 
 - `GET /admin/shareEarning/settings`
 - `GET /admin/shareEarning/settings`
 - `POST /admin/shareEarning/settings/save`
 - `POST /admin/shareEarning/settings/save`
 
 
-请求头:`Authorization: Bearer <admin_token>`。
+请求头:`Authorization: Bearer <admin_token>`。保存示例:
 
 
 ```json
 ```json
 {
 {
   "enabled": 1,
   "enabled": 1,
-  "share_domain": "https://show888.com/register",
-  "valid_member_min_deposit": 20,
-  "benefit_enabled": 1,
-  "non_seller_threshold": 200,
-  "tier_one_rate": 4,
-  "tier_two_rate": 6
+  "share_domain": "https://pc.example.com",
+  "valid_member_min_deposit": "20.00",
+  "valid_member_min_effective_bet": "100.00"
 }
 }
 ```
 ```
 
 
-| 字段 | 中文含义 |
-|---|---|
-| `enabled` | 分享赚钱开关,同时用于前台显示 |
-| `share_domain` | 分享注册链接域名/完整注册页地址 |
-| `valid_member_min_deposit` | 有效会员最低累计存款 |
-| `benefit_enabled` | 佣金/手续费减免设置开关 |
-| `non_seller_threshold` | 非卖家单笔订单阈值 |
-| `tier_one_rate` | 第一阶梯百分比 |
-| `tier_two_rate` | 第二阶梯百分比 |
+两项金额门槛每次保存均必填,允许明确填写 `0.00`。先完成分享关系初始化,再保存完整合法配置;计佣规则自动从次日北京时间生效。分类返佣比例通过独立的 `rateRules` 接口配置,详见上述完整文档。
 
 
-百分比范围均为 0~100。分享链接由后端拼接 `user_code=<当前会员邀请码>`。
-`share_domain` 可传 `show888.com/register` 或完整 `https://show888.com/register`,后端会补齐 HTTPS 并保存规范化地址。
+`enabled` 仅控制分享入口展示;`share_domain` 为 PC 网站源地址,不附加 `/register` 路径。原有单笔阈值、第一阶梯和第二阶梯参数已移除,不再提交。
 
 
-## APP 读取设置
+## 会员端读取
 
 
 `GET /api/share/config`
 `GET /api/share/config`
 
 
-无需登录,成功码 `code=1`。返回上述设置字段;APP 应在 `enabled=0` 时隐藏分享赚钱入口。
+允许匿名访问,成功码为 `code=1`;返回展示开关、PC 网站地址、生效日期及资格门槛,不返回佣金启停字段。
 
 
-登录会员通过 `GET /api/share/profile` 获取服务端生成的 `share_url`,不要在客户端自行拼数据库用户 ID。
+会员首页使用 `GET /api/share/home`,`GET /api/share/profile` 保留兼容;服务端生成推广链接 `{PC网站}/register?invitationCode={邀请码}`。会员字段与领取流程见[新分享系统-首页](../../新分享系统/首页.md)。首页的 `commission_enabled` 仅展示当日实际计佣状态,不能作为可编辑开关。

+ 2 - 2
docs/前端接口/新分享系统/下级投注.md

@@ -145,7 +145,7 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 | --- | --- |
 | --- | --- |
 | bet_amount | 可识别原注金额,包含未结算、取消或零有效注单 |
 | bet_amount | 可识别原注金额,包含未结算、取消或零有效注单 |
 | effective_amount | 历史真实有效投注;已结算且未作废,包含启动日前可用于资格累计的记录 |
 | effective_amount | 历史真实有效投注;已结算且未作废,包含启动日前可用于资格累计的记录 |
-| performance_amount | 可计佣业绩;仅计启动日起、原日佣金开关及必填条件有效且会员原日双达标的有效投注 |
+| performance_amount | 可计佣业绩;仅计启动日起、原日完整配置已生效且会员原日双达标的有效投注 |
 | net_profit | 已结算且未作废注单净盈亏,会员盈利为正、亏损为负 |
 | net_profit | 已结算且未作废注单净盈亏,会员盈利为正、亏损为负 |
 | bet_count | 原注记录数,包含未结算和取消记录 |
 | bet_count | 原注记录数,包含未结算和取消记录 |
 | effective_bet_count | 已结算、未作废且有效金额大于0的注单数 |
 | effective_bet_count | 已结算、未作废且有效金额大于0的注单数 |
@@ -154,4 +154,4 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 
 
 total为符合下级范围的会员人数;即使所选日期、分类和状态内没有下注,该下级仍可返回一行零值。summary包含表中八项指标,合计整个筛选下级范围。响应currency是所有金额的币种,接口只统计与当前钱包相同币种的注单。
 total为符合下级范围的会员人数;即使所选日期、分类和状态内没有下注,该下级仍可返回一行零值。summary包含表中八项指标,合计整个筛选下级范围。响应currency是所有金额的币种,接口只统计与当前钱包相同币种的注单。
 
 
-本接口按会员汇总,不返回逐注source、source_id、bet_at、status等旧明细字段。有效投注和计佣业绩分开展示:启动前有效投注可供资格累计,原日未达资格或资金开关未生效时不能成为performance_amount。原注金额也不能直接当作有效金额。筛选日期按原投注日,后绑定的上级不能看到或获得绑定前下注。
+本接口按会员汇总,不返回逐注source、source_id、bet_at、status等旧明细字段。有效投注和计佣业绩分开展示:启动前有效投注可供资格累计,原日未达资格或计佣配置尚未生效时不能成为performance_amount。原注金额也不能直接当作有效金额。筛选日期按原投注日,后绑定的上级不能看到或获得绑定前下注。

+ 10 - 15
docs/前端接口/新分享系统/后台返佣配置.md

@@ -11,16 +11,15 @@
 | id | 配置ID,未保存时为null |
 | id | 配置ID,未保存时为null |
 | enabled | 分享页面展示开关,0关闭、1开启;关闭展示不停止佣金计提 |
 | enabled | 分享页面展示开关,0关闭、1开启;关闭展示不停止佣金计提 |
 | share_domain | PC网站源地址,例如 `https://pc.example.com` |
 | share_domain | PC网站源地址,例如 `https://pc.example.com` |
-| benefit_enabled | 已保存的佣金开关目标值;当日实际状态以 current_policy 为准 |
 | valid_member_min_deposit | 已保存的累计成功充值最低门槛 |
 | valid_member_min_deposit | 已保存的累计成功充值最低门槛 |
-| valid_member_min_effective_bet | 已保存的累计全游戏有效投注最低门槛;未填为null |
-| activation_date | 新佣金首次启用日期,首次启用时定为次日,之后不重置 |
-| initialized | 分享会员关系是否已完成初始化;为false时不能启用佣金 |
+| valid_member_min_effective_bet | 已保存的累计全游戏有效投注最低门槛;历史未配置时可为null,保存时必填 |
+| activation_date | 新佣金首次生效日期,首次保存完整配置时定为次日,之后不重置 |
+| initialized | 分享会员关系是否已完成初始化;为false时须先完成初始化才能保存系统配置 |
 | effective_date | 当前待生效配置的日期,无待生效版本时为null |
 | effective_date | 当前待生效配置的日期,无待生效版本时为null |
 | current_policy | 今日已生效版本,没有版本时为null |
 | current_policy | 今日已生效版本,没有版本时为null |
 | pending_policy | 次日待生效版本,没有修改时为null |
 | pending_policy | 次日待生效版本,没有修改时为null |
 
 
-版本包含 `id`(版本ID)、`effective_date`(生效日)、`enabled`、`benefit_enabled`、两项有效会员门槛、`rules`及创建/更新时间。`rules`以分类标识为键,值为规则数组,每条包含 `min_valid_members`、`min_performance`、`rate`。下例为九类规则均已各调整为一档后的完整合法示例:
+版本包含 `id`(版本ID)、`effective_date`(生效日)、`enabled`、两项有效会员门槛、`rules`及创建/更新时间。`rules`以分类标识为键,值为规则数组,每条包含 `min_valid_members`、`min_performance`、`rate`。下例为九类规则均已各调整为一档后的完整合法示例:
 
 
 ```json
 ```json
 {
 {
@@ -31,7 +30,6 @@
     "id": 1,
     "id": 1,
     "enabled": 1,
     "enabled": 1,
     "share_domain": "https://pc.example.com",
     "share_domain": "https://pc.example.com",
-    "benefit_enabled": 1,
     "valid_member_min_deposit": "20.00",
     "valid_member_min_deposit": "20.00",
     "valid_member_min_effective_bet": "100.00",
     "valid_member_min_effective_bet": "100.00",
     "activation_date": "2026-10-25",
     "activation_date": "2026-10-25",
@@ -41,7 +39,6 @@
       "id": 2,
       "id": 2,
       "effective_date": "2026-10-25",
       "effective_date": "2026-10-25",
       "enabled": 1,
       "enabled": 1,
-      "benefit_enabled": 1,
       "valid_member_min_deposit": "20.00",
       "valid_member_min_deposit": "20.00",
       "valid_member_min_effective_bet": "100.00",
       "valid_member_min_effective_bet": "100.00",
       "rules": {
       "rules": {
@@ -116,7 +113,6 @@
       "id": 3,
       "id": 3,
       "effective_date": "2026-10-26",
       "effective_date": "2026-10-26",
       "enabled": 1,
       "enabled": 1,
-      "benefit_enabled": 1,
       "valid_member_min_deposit": "20.00",
       "valid_member_min_deposit": "20.00",
       "valid_member_min_effective_bet": "100.00",
       "valid_member_min_effective_bet": "100.00",
       "rules": {
       "rules": {
@@ -191,7 +187,7 @@
 }
 }
 ```
 ```
 
 
-顶层字段用于编辑表单回显。金额条件、佣金开关和分类规则保存后在次日00:00生效;展示开关和分享域名可立即供页面使用。展示开关仅控制入口显示,新佣金计提由benefit_enabled、必填条件及首次启用日决定。当日计算使用 `current_policy`。同日反复保存只更新同一个次日 `pending_policy`,不会改写已经生效的版本。
+顶层字段用于编辑表单回显。金额条件和分类规则保存后在次日北京时间 `00:00` 生效;展示开关和分享域名可立即供页面使用。新佣金是否可计提取决于完整配置、关系初始化、首次生效日及当日政策版本。表单中的充值门槛、有效投注门槛和分类比例始终可编辑,不依赖另一个启停选项。当日计算使用 `current_policy`。同日反复保存只更新同一个次日 `pending_policy`,不会改写已经生效的版本。
 
 
 ## 保存系统配置
 ## 保存系统配置
 
 
@@ -199,21 +195,20 @@
 
 
 | 参数 | 必填 | 说明 |
 | 参数 | 必填 | 说明 |
 | --- | --- | --- |
 | --- | --- | --- |
-| enabled | 是 | 0或1 |
+| enabled | 是 | 分享入口展示开关,0或1;不控制佣金计提 |
 | share_domain | 是 | PC网站源地址,最长500字符;可省略协议,默认补https;不可含路径、查询、片段、用户名密码 |
 | share_domain | 是 | PC网站源地址,最长500字符;可省略协议,默认补https;不可含路径、查询、片段、用户名密码 |
-| benefit_enabled | 是 | 0或1;开启前须 initialized=true |
 | valid_member_min_deposit | 是 | 非负十进制金额,最多14位整数、2位小数;超过2位小数会拒绝 |
 | valid_member_min_deposit | 是 | 非负十进制金额,最多14位整数、2位小数;超过2位小数会拒绝 |
-| valid_member_min_effective_bet | 开佣金时必填 | 非负十进制金额,最多14位整数、2位小数;关闭时可传null,不能依靠隐含默认值启用 |
+| valid_member_min_effective_bet | 是 | 非负十进制金额,最多14位整数、2位小数;不可省略或传null |
 
 
 ```json
 ```json
-{"enabled":1,"share_domain":"https://pc.example.com","benefit_enabled":1,"valid_member_min_deposit":"20.00","valid_member_min_effective_bet":"100.00"}
+{"enabled":1,"share_domain":"https://pc.example.com","valid_member_min_deposit":"20.00","valid_member_min_effective_bet":"100.00"}
 ```
 ```
 
 
 返回保存后的完整配置,结构同读取接口。不要把只读字段 `activation_date`、`effective_date`、`current_policy`、`pending_policy` 原样提交;接口会拒绝这些字段。旧参数 `non_seller_threshold`、`tier_one_rate`、`tier_two_rate` 已移除,提交会明确报错。
 返回保存后的完整配置,结构同读取接口。不要把只读字段 `activation_date`、`effective_date`、`current_policy`、`pending_policy` 原样提交;接口会拒绝这些字段。旧参数 `non_seller_threshold`、`tier_one_rate`、`tier_two_rate` 已移除,提交会明确报错。
 
 
-首次启用的 `activation_date` 固定为次日,不能指定过去日期;再停用、再启用也不重置。历史业务记录可用于查询和资格累计,首次启用日前不产生新分享佣金。
+保存完整且合法的系统配置前,须先完成分享会员关系初始化。保存成功后自动使新佣金从次日北京时间 `00:00` 起按已生效规则计提,无需另行提交启用操作。首次保存完整配置时,`activation_date` 固定为次日,不能指定过去日期,之后再次保存不重置。两项资格金额门槛每次保存均必填,允许明确填写 `0.00`,不可省略或传null。历史业务记录可用于查询和资格累计,首次生效日前不产生新分享佣金。
 
 
-已启用系统的本次计佣新规则自次日北京时间 `00:00` 起生效;已结算历史日保留原入账金额,后续补结算或退款按原投注日规则修正,不以新规则重写历史佣金。开启佣金仍需关系初始化完成并明确设置累计有效投注门槛;前端仅保留本页列出的配置项。
+现有配置不会仅因接口更新而立即开始计佣;完成并保存配置后,按次日待生效版本执行。本次计佣新规则也自次日北京时间 `00:00` 起生效。已结算历史日保留原入账金额,后续补结算或退款按原投注日规则修正,不以新规则重写历史佣金。会员首页和比例接口的 `commission_enabled` 只读,仅表示当日实际是否具备计佣条件,不能作为配置输入或编辑开关。
 
 
 ## 读取分类规则
 ## 读取分类规则
 
 

+ 2 - 2
docs/前端接口/新分享系统/我的数据.md

@@ -430,7 +430,7 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 | total_reward_amount | 返利、活动、新分享佣金、原充值返现四类金额之和 |
 | total_reward_amount | 返利、活动、新分享佣金、原充值返现四类金额之和 |
 | bet_amount | 可识别原注金额,包含未结算、取消或零有效注单 |
 | bet_amount | 可识别原注金额,包含未结算、取消或零有效注单 |
 | effective_amount | 历史真实有效投注;已结算且未作废,包含启动日前可用于资格累计的记录 |
 | effective_amount | 历史真实有效投注;已结算且未作废,包含启动日前可用于资格累计的记录 |
-| performance_amount | 可计佣业绩;仅计启动日起、原日佣金开关及必填条件有效且会员原日双达标的有效投注 |
+| performance_amount | 可计佣业绩;仅计首次生效日起、原日配置及政策版本满足计佣条件且会员原日双达标的有效投注 |
 | net_profit | 已结算且未作废注单净盈亏,会员盈利为正、亏损为负 |
 | net_profit | 已结算且未作废注单净盈亏,会员盈利为正、亏损为负 |
 | balance | 当前会员主钱包可用余额;历史区间也显示当前余额,不是区间末余额 |
 | balance | 当前会员主钱包可用余额;历史区间也显示当前余额,不是区间末余额 |
 | member_count | 当前统计下级总人数,不限其注册时间是否在所选期间 |
 | member_count | 当前统计下级总人数,不限其注册时间是否在所选期间 |
@@ -475,4 +475,4 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 | period_paid_amount | 本人在所选期间实际批准到账金额,按审核时间,全部分类 |
 | period_paid_amount | 本人在所选期间实际批准到账金额,按审核时间,全部分类 |
 | period_paid_count | 本人在所选期间已批准领取笔数,按审核时间 |
 | period_paid_count | 本人在所选期间已批准领取笔数,按审核时间 |
 
 
-本人佣金只有已日结部分进入账户;今日预估仅展示。显示入口开关enabled不影响计佣;新佣金依原日benefit_enabled、初始化、必填条件和启动日约束。所有业绩按当时真实祖先链归属,后绑定不追溯此前下注。
+本人佣金只有已日结部分进入账户;今日预估仅展示。显示入口开关enabled不影响计佣;新佣金依原日完整配置、关系初始化、首次生效日及政策版本约束。关系初始化完成并保存完整合法配置后,按次日生效版本计提新佣金;前端使用首页或比例接口只读的 `commission_enabled` 判断当日实际状态,不提交该字段。所有业绩按当时真实祖先链归属,后绑定不追溯此前下注。

+ 3 - 1
docs/前端接口/新分享系统/返佣比例.md

@@ -250,7 +250,7 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 | effective_date | 当前规则版本生效日,没有版本时为null |
 | effective_date | 当前规则版本生效日,没有版本时为null |
 | valid_member_min_deposit | 截至原投注日末累计成功充值最低门槛 |
 | valid_member_min_deposit | 截至原投注日末累计成功充值最低门槛 |
 | valid_member_min_effective_bet | 截至原投注日末累计全游戏有效投注最低门槛,未配置时为null |
 | valid_member_min_effective_bet | 截至原投注日末累计全游戏有效投注最低门槛,未配置时为null |
-| commission_enabled | 今日是否允许按已生效的benefit_enabled、必填参数、启动日及政策版本产生新佣金;入口展示开关enabled不影响此值,布尔值 |
+| commission_enabled | 今日完整配置、关系初始化、首次生效日及政策版本是否满足计佣条件;布尔值、只读,不作为配置输入,入口展示开关enabled不影响此值 |
 | commission_status | 今日比例与业绩仍为estimated,最终以日结为准 |
 | commission_status | 今日比例与业绩仍为estimated,最终以日结为准 |
 | updated_at | 当前规则版本更新时间,可能为null |
 | updated_at | 当前规则版本更新时间,可能为null |
 | data | 不传category返回九类;传category只返回该类 |
 | data | 不传category返回九类;传category只返回该类 |
@@ -275,3 +275,5 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 按现有默认档位,一名有效直属会员、100 万元直属分类业绩仍命中 `0.25%`。另一个计算示例:收款人比例为 `0.10%`、第 2 层合格业绩为 `20000.00`,则间接佣金为 `20000 × (0.10 / 100)² = 0.02`。全部间接业绩仍按原值合计展示,实际计佣须按各层分别计算。
 按现有默认档位,一名有效直属会员、100 万元直属分类业绩仍命中 `0.25%`。另一个计算示例:收款人比例为 `0.10%`、第 2 层合格业绩为 `20000.00`,则间接佣金为 `20000 × (0.10 / 100)² = 0.02`。全部间接业绩仍按原值合计展示,实际计佣须按各层分别计算。
 
 
 本次按层数相乘的新规则自次日北京时间 `00:00` 起生效;后台今天保存的金钱条件和比例也在次日 `00:00` 生效。本接口显示今日已生效版本。已结算历史佣金保留原入账金额,后续补结算或退款按原投注日规则修正,不能用本接口今日比例重算历史金额。今日预估不进入可领取金额,前端不要按本接口金额或比例自行生成领取申请金额。
 本次按层数相乘的新规则自次日北京时间 `00:00` 起生效;后台今天保存的金钱条件和比例也在次日 `00:00` 生效。本接口显示今日已生效版本。已结算历史佣金保留原入账金额,后续补结算或退款按原投注日规则修正,不能用本接口今日比例重算历史金额。今日预估不进入可领取金额,前端不要按本接口金额或比例自行生成领取申请金额。
+
+后台系统配置保存时,累计充值与累计有效投注门槛均必填非负金额,表单字段始终可编辑。完成关系初始化并保存完整合法配置后,新佣金自动从次日按生效版本计提;前端不增加计佣启停操作。`commission_enabled` 仅反映当日实际状态,不能用于修改配置。

+ 4 - 5
docs/前端接口/新分享系统/首页.md

@@ -58,8 +58,8 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
 | next_level.required_total | 达到下一级的累计佣金门槛,由各级增量相加 |
 | next_level.required_total | 达到下一级的累计佣金门槛,由各级增量相加 |
 | next_level.remaining_commission | 距下一等级仍需累计的佣金,最低0 |
 | next_level.remaining_commission | 距下一等级仍需累计的佣金,最低0 |
 | enabled | 分享入口展示开关,0隐藏、1显示;隐藏不停止计佣 |
 | enabled | 分享入口展示开关,0隐藏、1显示;隐藏不停止计佣 |
-| commission_enabled | 当前日期是否按已生效规则产生新佣金,布尔值 |
-| activation_date | 新佣金首次启用日;未启用时为null |
+| commission_enabled | 当日完整配置、关系初始化、首次生效日及政策版本是否满足计佣条件,布尔值;只读,不作为提交参数或开关 |
+| activation_date | 新佣金首次生效日;尚未保存完整配置时为null |
 | currency | 佣金与钱包的币种代码,例如CNY |
 | currency | 佣金与钱包的币种代码,例如CNY |
 | invitation_code / share_url | 本人邀请码、完整PC站分享链接;未配置域名时链接为空字符串 |
 | invitation_code / share_url | 本人邀请码、完整PC站分享链接;未配置域名时链接为空字符串 |
 | direct_members / team_members | 直属人数、全部下级人数;团队不含本人 |
 | direct_members / team_members | 直属人数、全部下级人数;团队不含本人 |
@@ -164,7 +164,6 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
   "msg": "",
   "msg": "",
   "data": {
   "data": {
     "enabled": 1,
     "enabled": 1,
-    "benefit_enabled": 1,
     "share_domain": "https://pc.example.com",
     "share_domain": "https://pc.example.com",
     "activation_date": "2026-10-01",
     "activation_date": "2026-10-01",
     "valid_member_min_deposit": "20.00",
     "valid_member_min_deposit": "20.00",
@@ -172,6 +171,6 @@ URL 相对会员网站 API 域名。请求头 `Authorization: <登录接口返
   }
   }
 }
 }
 ```
 ```
-`benefit_enabled` 和资格门槛读取当前已生效版本;后台今日保存的修改次日才反映于本接口。实际今日开佣金看home的 `commission_enabled`,今日金额条件及比例看 `/api/share/rates`。累计有效投注门槛未配置时为null,开启佣金前仍须明确设置。旧三级分享参数不再返回。已有 `/api/share/profile` 为首页兼容入口,前端新接入使用home。
+资格门槛读取当前已生效版本;后台今日保存的修改次日才反映于本接口。今日实际计佣条件是否满足,看home的只读字段 `commission_enabled`;今日金额条件及比例看 `/api/share/rates`。历史未配置的累计有效投注门槛可为null,后台每次保存时两项门槛均须明确填写非负金额。关系初始化完成并保存完整合法配置后,自次日北京时间 `00:00` 起按已生效版本计提新佣金。旧三级分享参数不再返回。已有 `/api/share/profile` 为首页兼容入口,前端新接入使用home。
 
 
-`enabled`仅控制入口显示,与资金开关独立;即使其为0,只要已生效的`benefit_enabled`为1且其他计佣条件满足,仍产生佣金。前端不得以隐藏入口推断计佣停止。
+`enabled`仅控制入口显示;即使其为0,只要当日配置、初始化、首次生效日及政策版本满足条件,仍产生佣金。前端不得以隐藏入口推断计佣停止,也不应把 `commission_enabled` 作为可编辑控件。

+ 101 - 9
tests/Integration/ShareAdminManagementTest.php

@@ -89,7 +89,7 @@ class ShareAdminManagementTest extends TestCase
 
 
     private function config(array $extra = []): array
     private function config(array $extra = []): array
     {
     {
-        return array_replace(['enabled' => 1, 'benefit_enabled' => 1, 'share_domain' => 'https://PC.example.com/',
+        return array_replace(['enabled' => 1, 'share_domain' => 'https://PC.example.com/',
             'valid_member_min_deposit' => '20.00', 'valid_member_min_effective_bet' => '100.00'], $extra);
             'valid_member_min_deposit' => '20.00', 'valid_member_min_effective_bet' => '100.00'], $extra);
     }
     }
 
 
@@ -104,22 +104,35 @@ class ShareAdminManagementTest extends TestCase
         catch (ValidationException $e) { $this->assertStringContainsString($contains, $e->validator->errors()->first()); }
         catch (ValidationException $e) { $this->assertStringContainsString($contains, $e->validator->errors()->first()); }
     }
     }
 
 
+    private function assertNoCommissionSwitch(array $settings): void
+    {
+        $this->assertArrayNotHasKey('benefit_enabled', $settings);
+        foreach (['current_policy', 'pending_policy'] as $key) {
+            if ($settings[$key] !== null) $this->assertArrayNotHasKey('benefit_enabled', $settings[$key]);
+        }
+    }
+
     public function test_explicit_setup_and_initializer_gate_enablement(): void
     public function test_explicit_setup_and_initializer_gate_enablement(): void
     {
     {
-        $this->assertNull($this->admin->settings()['activation_date']);
+        $initial = $this->admin->settings();
+        $this->assertNull($initial['activation_date']);
+        $this->assertNoCommissionSwitch($initial);
         $this->fails(fn () => $this->admin->saveSettings($this->config()), '初始化');
         $this->fails(fn () => $this->admin->saveSettings($this->config()), '初始化');
+        $this->fails(fn () => $this->admin->saveSettings($this->config(['benefit_enabled' => 0])), '初始化');
         $this->assertSame(0, DB::table('share_settings')->count());
         $this->assertSame(0, DB::table('share_settings')->count());
         $this->initialized();
         $this->initialized();
         $incomplete = $this->config(); unset($incomplete['valid_member_min_effective_bet']);
         $incomplete = $this->config(); unset($incomplete['valid_member_min_effective_bet']);
-        $this->fails(fn () => $this->admin->saveSettings($incomplete), 'required_if');
+        $this->fails(fn () => $this->admin->saveSettings($incomplete), 'required');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['tier_one_rate' => 4])), '已移除');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['tier_one_rate' => 4])), '已移除');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['activation_date' => '2020-01-01'])), '不可指定');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['activation_date' => '2020-01-01'])), '不可指定');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['share_domain' => 'https://pc.example/a'])), '源地址');
         $this->fails(fn () => $this->admin->saveSettings($this->config(['share_domain' => 'https://pc.example/a'])), '源地址');
         $data = $this->admin->saveSettings($this->config());
         $data = $this->admin->saveSettings($this->config());
         $this->assertSame('2026-10-25', $data['activation_date']);
         $this->assertSame('2026-10-25', $data['activation_date']);
         $this->assertSame('https://pc.example.com', $data['share_domain']);
         $this->assertSame('https://pc.example.com', $data['share_domain']);
-        $this->assertSame(0, $data['current_policy']['benefit_enabled']);
-        $this->assertSame(1, $data['pending_policy']['benefit_enabled']);
+        $this->assertNoCommissionSwitch($data);
+        $this->assertSame(0, (int) DB::table('share_policies')->where('id', $data['current_policy']['id'])->value('benefit_enabled'));
+        $this->assertSame(1, (int) DB::table('share_policies')->where('id', $data['pending_policy']['id'])->value('benefit_enabled'));
+        $this->assertSame(1, (int) DB::table('share_settings')->value('benefit_enabled'));
         $this->assertArrayNotHasKey('tier_one_rate', $data);
         $this->assertArrayNotHasKey('tier_one_rate', $data);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data['current_policy']);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data['current_policy']);
@@ -141,12 +154,87 @@ class ShareAdminManagementTest extends TestCase
         $this->admin->saveSettings($this->config(['benefit_enabled' => 0]));
         $this->admin->saveSettings($this->config(['benefit_enabled' => 0]));
         $this->admin->saveRateRules('third:2', [['min_valid_members' => 0, 'min_performance' => 10, 'rate' => '0.50']]);
         $this->admin->saveRateRules('third:2', [['min_valid_members' => 0, 'min_performance' => 10, 'rate' => '0.50']]);
         $this->assertSame($before, (array)DB::table('share_policies')->where('id', $pendingId)->first());
         $this->assertSame($before, (array)DB::table('share_policies')->where('id', $pendingId)->first());
-        $this->assertSame(0, $this->admin->settings()['pending_policy']['benefit_enabled']);
+        $settings = $this->admin->settings();
+        $this->assertNoCommissionSwitch($settings);
+        $this->assertSame(1, (int) DB::table('share_policies')->where('id', $settings['pending_policy']['id'])->value('benefit_enabled'));
+        $this->assertSame(1, (int) DB::table('share_policies')->where('id', $settings['current_policy']['id'])->value('benefit_enabled'));
         $this->admin->saveSettings($this->config());
         $this->admin->saveSettings($this->config());
         $this->assertSame('2026-10-25', $this->admin->settings()['activation_date']);
         $this->assertSame('2026-10-25', $this->admin->settings()['activation_date']);
         $this->assertSame(3, DB::table('share_policies')->count());
         $this->assertSame(3, DB::table('share_policies')->count());
     }
     }
 
 
+    public function test_both_member_thresholds_are_required_and_invalid_values_cannot_create_configuration(): void
+    {
+        $this->initialized();
+        foreach (['valid_member_min_deposit', 'valid_member_min_effective_bet'] as $field) {
+            foreach ([null, ''] as $value) {
+                $input = $this->config([$field => $value, 'benefit_enabled' => 0]);
+                try { $this->admin->saveSettings($input); $this->fail('Expected a required member threshold'); }
+                catch (ValidationException $e) {
+                    $this->assertArrayHasKey('Required', $e->validator->failed()[$field]);
+                    $this->assertArrayNotHasKey('RequiredIf', $e->validator->failed()[$field]);
+                }
+            }
+            $input = $this->config(['benefit_enabled' => 0]); unset($input[$field]);
+            try { $this->admin->saveSettings($input); $this->fail('Expected a missing member threshold rejection'); }
+            catch (ValidationException $e) { $this->assertArrayHasKey('Required', $e->validator->failed()[$field]); }
+            foreach ([-1, 'invalid', '20.001'] as $value) {
+                $this->fails(fn () => $this->admin->saveSettings($this->config([$field => $value, 'benefit_enabled' => 0])), 'regex');
+            }
+        }
+        $this->assertSame(0, DB::table('share_settings')->count());
+        $this->assertSame(1, DB::table('share_policies')->count());
+        $this->assertSame(0, DB::table('operation_audits')->count());
+    }
+
+    public function test_legacy_switch_input_is_ignored_and_cannot_disable_complete_configuration(): void
+    {
+        $this->initialized();
+        $past = (array) DB::table('share_policies')->where('effective_date', '1970-01-01')->first();
+        $legacyInputs = [[]];
+        foreach ([0, 1, '0', '1', null, 'obsolete', []] as $value) $legacyInputs[] = ['benefit_enabled' => $value];
+        foreach ($legacyInputs as $legacyInput) {
+            $request = Request::create('/admin/shareEarning/settings', 'POST', $this->config($legacyInput + ['enabled' => 0]));
+            $request->user = (object) ['id' => 8, 'username' => 'admin'];
+            $this->app->instance('request', $request);
+            $body = (new ShareEarning())->saveSettings($this->admin)->getData(true);
+            $this->assertSame(0, $body['code'], $body['msg']);
+            $this->assertNoCommissionSwitch($body['data']);
+            $this->assertSame(0, $body['data']['enabled']);
+            $this->assertSame('2026-10-25', $body['data']['activation_date']);
+            $this->assertSame(1, (int) DB::table('share_settings')->value('benefit_enabled'));
+            $this->assertSame(1, (int) DB::table('share_policies')->where('effective_date', '2026-10-25')->value('benefit_enabled'));
+        }
+        $this->assertSame(2, DB::table('share_policies')->count());
+        $this->assertSame($past, (array) DB::table('share_policies')->where('id', $past['id'])->first());
+        $this->assertArrayNotHasKey('benefit_enabled', ShareSetting::query()->firstOrFail()->toArray());
+        Carbon::setTestNow(Carbon::parse('2026-10-24 23:59:59', 'Asia/Shanghai'));
+        $this->assertSame($past['id'], $this->admin->settings()['current_policy']['id']);
+        Carbon::setTestNow(Carbon::parse('2026-10-25 00:00:00', 'Asia/Shanghai'));
+        $current = $this->admin->settings();
+        $this->assertNoCommissionSwitch($current);
+        $this->assertSame('2026-10-25', $current['current_policy']['effective_date']);
+        $this->assertSame(1, (int) DB::table('share_policies')->where('id', $current['current_policy']['id'])->value('benefit_enabled'));
+        $this->assertNull($current['pending_policy']);
+    }
+
+    public function test_existing_disabled_configuration_remains_unchanged_until_a_complete_save(): void
+    {
+        $id = DB::table('share_settings')->insertGetId(['benefit_enabled' => 0, 'share_domain' => 'https://legacy.example.test']);
+        $before = (array) DB::table('share_settings')->where('id', $id)->first();
+        $this->assertNoCommissionSwitch($this->admin->settings());
+        $this->assertArrayNotHasKey('benefit_enabled', ShareSetting::query()->findOrFail($id)->toArray());
+        $this->assertSame($before, (array) DB::table('share_settings')->where('id', $id)->first());
+        $this->fails(fn () => $this->admin->saveSettings($this->config(['benefit_enabled' => 1])), '初始化');
+        $this->assertSame($before, (array) DB::table('share_settings')->where('id', $id)->first());
+        $this->initialized();
+        $saved = $this->admin->saveSettings($this->config(['benefit_enabled' => 0]));
+        $this->assertNoCommissionSwitch($saved);
+        $this->assertSame(1, (int) DB::table('share_settings')->where('id', $id)->value('benefit_enabled'));
+        $this->assertSame('2026-10-25', $saved['activation_date']);
+        $this->assertSame(0, (int) DB::table('share_policies')->where('id', $saved['current_policy']['id'])->value('benefit_enabled'));
+    }
+
     public function test_retired_indirect_coefficient_is_rejected_even_when_null_without_saving(): void
     public function test_retired_indirect_coefficient_is_rejected_even_when_null_without_saving(): void
     {
     {
         $this->initialized();
         $this->initialized();
@@ -179,6 +267,7 @@ class ShareAdminManagementTest extends TestCase
         $model = ShareSetting::query()->firstOrFail();
         $model = ShareSetting::query()->firstOrFail();
         $this->assertArrayNotHasKey('indirect_bonus_rate', $model->getCasts());
         $this->assertArrayNotHasKey('indirect_bonus_rate', $model->getCasts());
         $this->assertArrayNotHasKey('indirect_bonus_rate', $model->toArray());
         $this->assertArrayNotHasKey('indirect_bonus_rate', $model->toArray());
+        $this->assertArrayNotHasKey('benefit_enabled', $model->toArray());
 
 
         $rules = $this->admin->saveRateRules('third:1', [['min_valid_members' => 0, 'min_performance' => 0, 'rate' => '10']]);
         $rules = $this->admin->saveRateRules('third:1', [['min_valid_members' => 0, 'min_performance' => 0, 'rate' => '10']]);
         $this->assertSame('10.0000', $rules['categories'][0]['pending_rules'][0]['rate']);
         $this->assertSame('10.0000', $rules['categories'][0]['pending_rules'][0]['rate']);
@@ -208,10 +297,11 @@ class ShareAdminManagementTest extends TestCase
             $t->id(); $t->date('stat_date'); $t->integer('status');
             $t->id(); $t->date('stat_date'); $t->integer('status');
         });
         });
         DB::table('config')->insert(['field' => 'recharge_rate', 'val' => '2.5000']);
         DB::table('config')->insert(['field' => 'recharge_rate', 'val' => '2.5000']);
-        DB::table('share_settings')->insert(['indirect_bonus_rate' => '30', 'valid_member_min_effective_bet' => '100']);
+        DB::table('share_settings')->insert(['benefit_enabled' => 1, 'indirect_bonus_rate' => '30', 'valid_member_min_effective_bet' => '100']);
         $before = (array) DB::table('share_settings')->first();
         $before = (array) DB::table('share_settings')->first();
         $data = (new ShareCommissionDiagnostics())->inspect(null);
         $data = (new ShareCommissionDiagnostics())->inspect(null);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data);
         $this->assertArrayNotHasKey('indirect_bonus_rate', $data);
+        $this->assertArrayNotHasKey('benefit_enabled', $data);
         $this->assertEquals('100', $data['valid_member_min_effective_bet']);
         $this->assertEquals('100', $data['valid_member_min_effective_bet']);
         $command = new BackfillShareCommissions();
         $command = new BackfillShareCommissions();
         $command->setLaravel($this->app);
         $command->setLaravel($this->app);
@@ -219,6 +309,7 @@ class ShareAdminManagementTest extends TestCase
         $tester->execute(['--diagnose' => true, '--all' => true]);
         $tester->execute(['--diagnose' => true, '--all' => true]);
         $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay());
         $this->assertSame(0, $tester->getStatusCode(), $tester->getDisplay());
         $this->assertStringNotContainsString('indirect_bonus_rate', $tester->getDisplay());
         $this->assertStringNotContainsString('indirect_bonus_rate', $tester->getDisplay());
+        $this->assertStringNotContainsString('benefit_enabled', $tester->getDisplay());
         $this->assertStringContainsString('valid_member_min_effective_bet: 100', $tester->getDisplay());
         $this->assertStringContainsString('valid_member_min_effective_bet: 100', $tester->getDisplay());
         $this->assertSame($before, (array) DB::table('share_settings')->first());
         $this->assertSame($before, (array) DB::table('share_settings')->first());
         $this->assertSame(0, DB::table('share_commission_records')->count());
         $this->assertSame(0, DB::table('share_commission_records')->count());
@@ -233,8 +324,9 @@ class ShareAdminManagementTest extends TestCase
         $this->assertSame(1, DB::table('share_policies')->count());
         $this->assertSame(1, DB::table('share_policies')->count());
         $saved = $this->admin->saveSettings($this->config(['enabled' => 0, 'valid_member_min_deposit' => '20.12']));
         $saved = $this->admin->saveSettings($this->config(['enabled' => 0, 'valid_member_min_deposit' => '20.12']));
         $this->assertSame(0, $saved['enabled']);
         $this->assertSame(0, $saved['enabled']);
-        $this->assertSame(1, $saved['benefit_enabled']);
-        $this->assertSame(1, $saved['pending_policy']['benefit_enabled']);
+        $this->assertNoCommissionSwitch($saved);
+        $this->assertSame(1, (int) DB::table('share_settings')->value('benefit_enabled'));
+        $this->assertSame(1, (int) DB::table('share_policies')->where('id', $saved['pending_policy']['id'])->value('benefit_enabled'));
         $this->assertSame('20.12', $saved['pending_policy']['valid_member_min_deposit']);
         $this->assertSame('20.12', $saved['pending_policy']['valid_member_min_deposit']);
         $this->assertSame('20.12', $saved['valid_member_min_deposit']);
         $this->assertSame('20.12', $saved['valid_member_min_deposit']);
         $this->assertSame('20.1200000000', bcadd((string)DB::table('share_policies')->where('effective_date', '2026-10-25')->value('valid_member_min_deposit'), '0', 10));
         $this->assertSame('20.1200000000', bcadd((string)DB::table('share_policies')->where('effective_date', '2026-10-25')->value('valid_member_min_deposit'), '0', 10));