Skip to content

feat(referral): add payment-based referral commission system - #3288

Closed
0-don wants to merge 1 commit into
QuantumNous:mainfrom
0-don:feat/payment-based-referral
Closed

feat(referral): add payment-based referral commission system#3288
0-don wants to merge 1 commit into
QuantumNous:mainfrom
0-don:feat/payment-based-referral

Conversation

@0-don

@0-don 0-don commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

Inviters earn a configurable percentage of every recharge their referred users make, replacing the one-time registration bonus. Commissions apply to all payment paths (Stripe, Creem, Epay, manual top-up) and subscription purchases, with per-user rate overrides and a max-recharges cap. The legacy QuotaForInviter / QuotaForInvitee bonuses auto-disable when the commission system is active.

The core logic lives in model.CreditReferralCommission() which runs inside a single DB transaction with a composite unique index (invitee_id, top_up_id, payment_method) for idempotency. Each payment handler calls it after crediting quota. A new GET /api/user/aff/commissions endpoint returns paginated commission history, and GET /api/user/aff/invited returns invited users with per-user totals. The frontend adds an admin override field (referral_commission_percent) on the user edit drawer so a single inviter can run on a different rate from the global default.

Scope note: the original classic-frontend revamp of InvitationCard (invitee + commission tabs) was dropped. The new web/default frontend uses affiliate-rewards-card.tsx with a different shape; reworking that UI is left for a follow-up PR.

🚀 变更类型 / Type of change

  • ✨ 新功能 (New feature)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 Issues 与 PRs,确认不是重复提交。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

Backend: go build -o /dev/null . passes, all payment paths exercised locally.

Frontend: cd web/default && bun run build passes.

Functional verification:

  • Enable referral commissions in admin settings, create user B via user A's invite link
  • User B recharges via Stripe / Creem / Epay / manual top-up: user A receives commission credited to aff_quota
  • Subscription purchase by user B also credits user A
  • Max-recharges cap stops further commissions after the limit is hit
  • Per-user referral_commission_percent override takes precedence over the global rate
  • GET /api/user/aff/commissions returns paginated commission history; GET /api/user/aff/invited returns invited users with totals
  • Legacy QuotaForInviter / QuotaForInvitee are skipped when the commission system is enabled
  • Duplicate webhook deliveries (same topup_id + payment_method + invitee_id) do not double-credit thanks to the composite unique index

@coderabbitai

coderabbitai Bot commented Mar 17, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a referral commission system: new global options, DB model and migration, per-user override, transactional commission-crediting on successful top-ups/subscriptions (best-effort logging on failures), API to list user commissions, and UI/settings/i18n updates.

Changes

Cohort / File(s) Summary
Constants & Options
common/constants.go, model/option.go
Add three global option variables and wire option initialization and runtime updates to keep ReferralCommissionEnabled, ReferralCommissionPercent, and ReferralCommissionMaxRecharges in sync.
DB Migration & Model
model/main.go, model/referral_commission.go
Introduce ReferralCommission GORM model and include it in AutoMigrate; add paginated GetUserReferralCommissions with user join.
Referral Business Logic
model/user.go, model/topup.go, model/subscription.go
Add per-user ReferralCommissionPercent, implement CreditReferralCommission(...) (idempotent, transactional, max-count guard), and call it from multiple top-up/subscription completion paths; log failures without altering primary success flows.
Controllers & Routes
controller/topup.go, controller/user.go, router/api-router.go
Call CreditReferralCommission from payment notification flows; add GetReferralCommissions handler and GET /api/user/self/aff/commissions route.
Admin & Settings UI
web/src/pages/Setting/Operation/SettingsCreditLimit.jsx, web/src/components/settings/OperationSetting.jsx
Expose enable flag, percent, and max-recharges in admin settings; participate in existing load/save option flow.
User Edit UI
web/src/components/table/users/modals/EditUserModal.jsx
Add nullable per-user referral percent input (uses null to mean fallback to global default) and include in update payload.
Invitation & History UI
web/src/components/topup/InvitationCard.jsx, web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
InvitationCard now fetches/displays commission history and uses internal i18n; removed tooltip behavior from DETAILS ellipsis fallback.
I18n
web/src/i18n/locales/... (en, fr, ja, ru, vi, zh-CN, zh-TW)
Add translation keys/values for referral/commission UI across multiple locales.

Sequence Diagram(s)

sequenceDiagram
    participant Client as User
    participant Payment as PaymentService
    participant TopUp as TopUpHandler
    participant Referral as ReferralProcessor
    participant DB as Database

    Client->>Payment: complete payment
    Payment->>TopUp: notify success (topUpId, userId, amount, method)
    TopUp->>Referral: CreditReferralCommission(userId, amount, method, topUpId)

    activate Referral
    Referral->>DB: lookup inviter, inviter override, existing commission count
    DB-->>Referral: inviter record + counts

    alt enabled & within limits
        Referral->>DB: insert ReferralCommission record
        Referral->>DB: update inviter aff_quota / aff_history (atomic)
        DB-->>Referral: OK
        Referral-->>TopUp: credited (logged)
    else disabled or exceeded or idempotent
        Referral-->>TopUp: skipped (logged)
    end
    deactivate Referral

    TopUp-->>Client: complete response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • seefs001
  • Calcium-Ion
  • creamlike1024

Poem

🐰 I hopped from coin to friendly face,
A tiny share in every trace.
When recharges sing and ledgers grow,
I nudge a crumb where invites flow.
Hop—small rewards make gardens glow.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(referral): add payment-based referral commission system' accurately and clearly describes the main change: implementing a new payment-based referral commission system to replace the one-time registration bonus.
Linked Issues check ✅ Passed The pull request comprehensively addresses issue #128's coding requirements: implementing percentage-based ongoing commissions tied to invitees' payments (via CreditReferralCommission), enforcing max-recharge caps, supporting per-user rate overrides, integrating across all payment paths, and providing commission history via API.
Out of Scope Changes check ✅ Passed All changes align with the payment-based referral commission feature scope: backend infrastructure (models, database, core logic), API integration points, global/per-user configuration, frontend admin settings, user commission history display, and i18n localization for the new feature.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (3)
controller/topup.go (1)

309-312: Include richer context in commission failure logs.

This path is good; adding topup_id/trade_no to the error log will make reconciliation much easier.

📝 Suggested logging improvement
-			if err := model.CreditReferralCommission(topUp.UserId, topUp.Money, "epay", topUp.Id); err != nil {
-				log.Printf("用户 %d 返佣失败: %v", topUp.UserId, err)
+			if err := model.CreditReferralCommission(topUp.UserId, topUp.Money, "epay", topUp.Id); err != nil {
+				log.Printf("返佣失败 user_id=%d topup_id=%d trade_no=%s payment_method=%s err=%v",
+					topUp.UserId, topUp.Id, topUp.TradeNo, topUp.PaymentMethod, err)
 			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup.go` around lines 309 - 312, The current error log for
model.CreditReferralCommission(topUp.UserId, topUp.Money, "epay", topUp.Id)
lacks contextual identifiers; update the log call to include topUp.Id (topup_id)
and the payment trade number (e.g., topUp.TradeNo or equivalent field) alongside
topUp.UserId and the error so that failures contain user_id, topup_id and
trade_no for easier reconciliation when CreditReferralCommission fails.
web/src/i18n/locales/zh-TW.json (1)

21-21: Consider terminology consistency in zh-TW translation.

At Line 21, 返佣設置 is understandable, but this file mostly uses 設定. Using 返佣設定 would keep wording consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/zh-TW.json` at line 21, The translation key "返佣设置"
currently maps to the value "返佣設置"; change the value to "返佣設定" to match the
project's existing zh-TW terminology pattern (use "設定" instead of "設置") so the
key "返佣设置" -> "返佣設定" is consistent with other entries.
web/src/i18n/locales/zh-CN.json (1)

9-22: Run i18n sync/lint after adding these keys.

Please run the repo i18n pipeline to ensure cross-locale/key consistency for these new entries.

As per coding guidelines: web/src/i18n/**/*.json should use the i18n CLI tools: bun run i18n:extract, bun run i18n:sync, bun run i18n:lint.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/zh-CN.json` around lines 9 - 22, You added new i18n keys
(e.g., "保存返佣设置", "充值金额", "启用充值返佣", "返佣比例覆盖", "邀请充值返佣设置") but haven't run the
repo i18n pipeline; run the i18n CLI commands to extract, sync, and lint so all
locales and keys stay consistent — execute `bun run i18n:extract`, then `bun run
i18n:sync`, and finally `bun run i18n:lint`, fix any reported mismatches/missing
translations across web/src/i18n/**/*.json, and commit the updated synced locale
files.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model/option.go`:
- Around line 404-407: The ReferralCommission cases currently ignore parse
errors and silently accept invalid input; update the "ReferralCommissionPercent"
and "ReferralCommissionMaxRecharges" handling so you parse with
strconv.ParseFloat and strconv.Atoi, check the returned error and only assign to
common.ReferralCommissionPercent and common.ReferralCommissionMaxRecharges on
success, and validate ranges (e.g. percent >= 0 && percent <= 100, maxRecharges
>= 0); on parse/validation failure return or log an error (do not silently
ignore) so invalid option values are rejected or reported.

In `@model/referral_commission.go`:
- Around line 20-28: GetUserReferralCommissions currently returns the entire
joined history which will grow unbounded; change its signature to accept
pagination parameters (limit, offset) and apply .Limit(limit).Offset(offset) to
the GORM chain used (the DB.Table(...) query) so the endpoint returns only a
page of results, and ensure callers (e.g. the component that requests 10 rows)
pass those values; additionally add a DB index to optimize newest-first lookups
by inviter (e.g. an index on referral_commissions(inviter_id, id) or inviter_id
with id DESC) to match the inviter_id + newest-first access pattern.

In `@model/subscription.go`:
- Around line 571-574: The current call to CreditReferralCommission in
subscription.go can silently lose commission on transient failure; change the
flow so commission credit participates in the same lifecycle as order completion
or is persisted for durable retry: either (A) modify the order completion code
path that calls CreditReferralCommission to pass a DB transaction/context (e.g.,
use an existing tx in the function that finalizes the order) and call a new
CreditReferralCommissionTx(tx, userId, amount, method, 0) that returns an error
so transaction rollback can occur on failure, or (B) if you cannot make it
transactional, replace the direct call with a durable enqueue step that writes a
RetryCommission record (or calls EnqueueCommissionRetry with userId, amount,
paymentMethod, orderId) before returning success and ensure failed
CreditReferralCommission attempts create that retry record; also propagate
errors up (or persist the retry) instead of only logging so retries will run
later. Ensure you update the existing call site
(CreditReferralCommission(logUserId,...)) and any caller error-handling to
reflect the new transactional or retry semantics.

In `@model/topup.go`:
- Around line 104-107: The current CreditReferralCommission(topUp.UserId,
topUp.Money, "stripe", topUp.Id) call is executed after the top-up transaction
commits and failures are only logged, which can permanently lose commissions;
move referral crediting into the same DB transaction or create a durable outbox
job within the transaction. Concretely: either call CreditReferralCommission (or
a transactional variant e.g. CreditReferralCommissionTx) while using the same DB
transaction and propagate errors to cause the top-up tx to rollback, or insert a
ReferralCommissionOutbox record (including topUp.Id, inviterId, amount, source)
inside the top-up transaction and remove the post-commit best-effort caller,
then implement a reliable background worker that reads the outbox and performs
CreditReferralCommission idempotently; apply this change for the usages at
CreditReferralCommission(topUp.UserId...) and the analogous calls noted around
lines 315-318 and 391-394, ensure idempotency and proper error handling/logging.

In `@web/src/components/topup/InvitationCard.jsx`:
- Around line 49-56: The data fetch in the useEffect
(API.get('/api/user/aff/commissions')) only uses .then/.finally so any rejection
or non-success payload is indistinguishable from an empty result; add an error
state (e.g., commissionsError via useState) and attach a .catch handler (or
check for !res.data.success in the .then) to set commissionsError with a message
and clear/set loading appropriately; update setCommissions only on success and
ensure the UI uses commissionsError to show a load-failure message instead of
the empty-state text when the fetch fails.

In `@web/src/i18n/locales/ja.json`:
- Line 15: The Japanese translation for the key
"开启后,被邀请用户充值时,邀请人可获得充值金额的一定比例作为返佣" lost the "percentage/proportion" meaning;
update the value so it explicitly conveys "a certain proportion/percentage of
the recharge amount" (e.g. use wording like "チャージ金額の一定割合をコミッションとして受け取ります") to
preserve the original intent.

In `@web/src/i18n/locales/ru.json`:
- Around line 16-29: Add the missing translation key "留空使用全局默认值" to the Russian
locale JSON so the call t('留空使用全局默认值') from EditUserModal.jsx shows a proper
Russian string; update the ru.json block (alongside keys like "返佣比例覆盖" and
"返佣记录") with a suitable Russian translation for "留空使用全局默认值" (e.g., "Оставьте
пустым для использования глобального значения") to ensure the component uses the
localized text instead of the Chinese fallback.

In `@web/src/pages/Setting/Operation/SettingsCreditLimit.jsx`:
- Around line 198-256: The form adds
ReferralCommissionEnabled/ReferralCommissionPercent/ReferralCommissionMaxRecharges
but does not prevent the legacy one-time bonuses
(QuotaForInviter/QuotaForInvitee) from also being applied; update the settings
flow so only one reward path is active: either retire/hide the legacy
QuotaForInviter/QuotaForInvitee controls when ReferralCommissionEnabled is true,
or add logic in the registration/reward path (referencing model/user.go
functions that award QuotaForInviter and QuotaForInvitee) to check
ReferralCommissionEnabled and skip the legacy bonus when the commission flow is
enabled; ensure the UI (SettingsCreditLimit.jsx handlers/onSubmit and the Form
fields ReferralCommissionEnabled) and the server-side award code
(QuotaForInviter/QuotaForInvitee grant points) use the same flag so referrals
are not double-rewarded.

---

Nitpick comments:
In `@controller/topup.go`:
- Around line 309-312: The current error log for
model.CreditReferralCommission(topUp.UserId, topUp.Money, "epay", topUp.Id)
lacks contextual identifiers; update the log call to include topUp.Id (topup_id)
and the payment trade number (e.g., topUp.TradeNo or equivalent field) alongside
topUp.UserId and the error so that failures contain user_id, topup_id and
trade_no for easier reconciliation when CreditReferralCommission fails.

In `@web/src/i18n/locales/zh-CN.json`:
- Around line 9-22: You added new i18n keys (e.g., "保存返佣设置", "充值金额", "启用充值返佣",
"返佣比例覆盖", "邀请充值返佣设置") but haven't run the repo i18n pipeline; run the i18n CLI
commands to extract, sync, and lint so all locales and keys stay consistent —
execute `bun run i18n:extract`, then `bun run i18n:sync`, and finally `bun run
i18n:lint`, fix any reported mismatches/missing translations across
web/src/i18n/**/*.json, and commit the updated synced locale files.

In `@web/src/i18n/locales/zh-TW.json`:
- Line 21: The translation key "返佣设置" currently maps to the value "返佣設置"; change
the value to "返佣設定" to match the project's existing zh-TW terminology pattern
(use "設定" instead of "設置") so the key "返佣设置" -> "返佣設定" is consistent with other
entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f734a723-23f3-48d5-9d6c-a884ad1d0674

📥 Commits

Reviewing files that changed from the base of the PR and between 620e066 and f2d05f7.

📒 Files selected for processing (22)
  • common/constants.go
  • controller/topup.go
  • controller/user.go
  • model/main.go
  • model/option.go
  • model/referral_commission.go
  • model/subscription.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • web/src/components/settings/OperationSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/components/topup/InvitationCard.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
💤 Files with no reviewable changes (1)
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx

Comment thread model/option.go Outdated
Comment thread model/referral_commission.go Outdated
Comment thread model/subscription.go
Comment thread model/topup.go
Comment thread model/user.go
Comment thread web/src/components/topup/InvitationCard.jsx Outdated
Comment thread web/src/i18n/locales/ja.json Outdated
Comment thread web/src/i18n/locales/ru.json Outdated
Comment thread web/src/pages/Setting/Operation/SettingsCreditLimit.jsx Outdated
@0-don
0-don force-pushed the feat/payment-based-referral branch from f2d05f7 to ee2b9ab Compare March 17, 2026 14:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (4)
model/referral_commission.go (1)

20-27: ⚠️ Potential issue | 🟠 Major

Paginate commission history queries before they grow unbounded.

GetUserReferralCommissions still returns all rows for an inviter. This will degrade query cost and response size over time.

💡 Proposed patch
-func GetUserReferralCommissions(inviterId int) ([]*ReferralCommissionWithUser, error) {
+func GetUserReferralCommissions(inviterId int, limit int, offset int) ([]*ReferralCommissionWithUser, error) {
 	var commissions []*ReferralCommissionWithUser
+	if limit <= 0 || limit > 100 {
+		limit = 10
+	}
+	if offset < 0 {
+		offset = 0
+	}
 	err := DB.Table("referral_commissions").
 		Select("referral_commissions.*, users.username as invitee_username").
 		Joins("LEFT JOIN users ON users.id = referral_commissions.invitee_id").
 		Where("referral_commissions.inviter_id = ?", inviterId).
 		Order("referral_commissions.id desc").
+		Limit(limit).
+		Offset(offset).
 		Find(&commissions).Error
 	return commissions, err
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/referral_commission.go` around lines 20 - 27,
GetUserReferralCommissions currently returns all rows for an inviter which will
grow unbounded; change the function signature (GetUserReferralCommissions) to
accept pagination parameters (e.g., limit and offset or page and pageSize),
enforce a sensible default/max limit, and apply GORM's Limit(...) and
Offset(...) to the DB.Table("referral_commissions") query before
Find(&commissions); optionally also return total count (using Model/Count) if
callers need pagination metadata. Ensure parameter names and any defaults are
validated to avoid zero/negative values.
web/src/components/topup/InvitationCard.jsx (1)

49-56: ⚠️ Potential issue | 🟡 Minor

Differentiate load failures from empty commission history.

If /api/user/aff/commissions fails, users currently see 暂无返佣记录, which misrepresents a fetch error as “no data”.

💡 Proposed patch
   const [commissions, setCommissions] = useState([]);
   const [commissionsLoading, setCommissionsLoading] = useState(false);
+  const [commissionsError, setCommissionsError] = useState('');
 
   useEffect(() => {
     setCommissionsLoading(true);
+    setCommissionsError('');
     API.get('/api/user/aff/commissions')
       .then((res) => {
-        if (res.data.success) setCommissions(res.data.data || []);
+        if (res?.data?.success) {
+          setCommissions(res.data.data || []);
+        } else {
+          setCommissionsError(t('加载失败,请重试'));
+        }
       })
+      .catch(() => setCommissionsError(t('加载失败,请重试')))
       .finally(() => setCommissionsLoading(false));
-  }, []);
+  }, [t]);
@@
               empty={
                 <Text type='tertiary' className='text-sm'>
-                  {t('暂无返佣记录')}
+                  {commissionsError || t('暂无返佣记录')}
                 </Text>
               }

Also applies to: 290-293

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/InvitationCard.jsx` around lines 49 - 56, The
current useEffect calling API.get('/api/user/aff/commissions') conflates a fetch
failure with an empty result — add an error state (e.g. commissionsError via
useState) and set it true in the catch handler of the promise returned by
API.get; only clear commissionsError on a successful response, and keep the
existing finally to setCommissionsLoading(false); update rendering logic that
currently shows "暂无返佣记录" to check commissionsError first and display an error
message when true, otherwise show the empty message when commissions is an empty
array; apply the same change for the duplicate block around the component lines
referenced (the other API.get('/api/user/aff/commissions') usage).
model/user.go (1)

345-405: ⚠️ Potential issue | 🔴 Critical

Make referral crediting transactional and idempotent.

The max-count check, commission insert, and inviter quota increment are still separate operations on DB. Concurrent/replayed callbacks can over-credit, and partial failure can leave audit rows diverging from inviter balance.

💡 Proposed patch (transaction + duplicate guard)
 func CreditReferralCommission(userId int, rechargeAmount float64, paymentMethod string, topUpId int) error {
 	if !common.ReferralCommissionEnabled || rechargeAmount <= 0 {
 		return nil
 	}
-
-	user, err := GetUserById(userId, true)
-	if err != nil || user.InviterId == 0 {
-		return err
-	}
+	return DB.Transaction(func(tx *gorm.DB) error {
+		var invitee User
+		if err := tx.Select("id", "inviter_id").First(&invitee, "id = ?", userId).Error; err != nil {
+			return err
+		}
+		if invitee.InviterId == 0 {
+			return nil
+		}
 
-	// Accurate count: count commission records not all top-ups, so max cap only applies to actual commission events
-	if common.ReferralCommissionMaxRecharges > 0 {
-		var count int64
-		if err := DB.Model(&ReferralCommission{}).Where("invitee_id = ?", userId).Count(&count).Error; err != nil {
-			return err
+		if common.ReferralCommissionMaxRecharges > 0 {
+			var count int64
+			if err := tx.Model(&ReferralCommission{}).Where("invitee_id = ?", userId).Count(&count).Error; err != nil {
+				return err
+			}
+			if int(count) >= common.ReferralCommissionMaxRecharges {
+				return nil
+			}
 		}
-		if int(count) >= common.ReferralCommissionMaxRecharges {
-			return nil
+
+		var inviter User
+		if err := tx.Select("id", "referral_commission_percent").First(&inviter, "id = ?", invitee.InviterId).Error; err != nil {
+			return err
 		}
-	}
-
-	// Per-inviter rate override: use inviter's custom rate if set, otherwise fall back to global
-	inviter, err := GetUserById(user.InviterId, true)
-	if err != nil {
-		return err
-	}
 
-	rate := common.ReferralCommissionPercent
-	if inviter.ReferralCommissionPercent != nil {
-		rate = *inviter.ReferralCommissionPercent
-	}
-	if rate <= 0 || rate > 100 {
-		return nil
-	}
+		rate := common.ReferralCommissionPercent
+		if inviter.ReferralCommissionPercent != nil {
+			rate = *inviter.ReferralCommissionPercent
+		}
+		if rate <= 0 || rate > 100 {
+			return nil
+		}
 
-	commission := int(rechargeAmount * (rate / 100) * common.QuotaPerUnit)
-	if commission <= 0 {
-		return nil
-	}
+		commission := int(rechargeAmount * (rate / 100) * common.QuotaPerUnit)
+		if commission <= 0 {
+			return nil
+		}
 
-	// Record commission event for full audit trail
-	if err := DB.Create(&ReferralCommission{
-		InviterId:       user.InviterId,
-		InviteeId:       userId,
-		TopUpId:         topUpId,
-		RechargeAmount:  rechargeAmount,
-		CommissionQuota: commission,
-		CommissionRate:  rate,
-		PaymentMethod:   paymentMethod,
-	}).Error; err != nil {
-		return err
-	}
+		if topUpId > 0 {
+			var existing int64
+			if err := tx.Model(&ReferralCommission{}).
+				Where("inviter_id = ? AND invitee_id = ? AND top_up_id = ?", invitee.InviterId, userId, topUpId).
+				Count(&existing).Error; err != nil {
+				return err
+			}
+			if existing > 0 {
+				return nil
+			}
+		}
 
-	// Atomically update inviter's aff_quota to prevent race conditions under concurrent recharges
-	result := DB.Model(&User{}).Where("id = ?", user.InviterId).Updates(map[string]interface{}{
-		"aff_quota":   gorm.Expr("aff_quota + ?", commission),
-		"aff_history": gorm.Expr("aff_history + ?", commission),
-	})
-	if result.Error != nil {
-		return result.Error
-	}
+		if err := tx.Create(&ReferralCommission{
+			InviterId:       invitee.InviterId,
+			InviteeId:       userId,
+			TopUpId:         topUpId,
+			RechargeAmount:  rechargeAmount,
+			CommissionQuota: commission,
+			CommissionRate:  rate,
+			PaymentMethod:   paymentMethod,
+		}).Error; err != nil {
+			return err
+		}
 
-	if result.RowsAffected > 0 {
-		RecordLog(user.InviterId, LogTypeSystem, fmt.Sprintf("邀请用户充值返佣 %s (%.1f%% of $%.2f)", logger.LogQuota(commission), rate, rechargeAmount))
-	}
-	return nil
+		return tx.Model(&User{}).Where("id = ?", invitee.InviterId).Updates(map[string]interface{}{
+			"aff_quota":   gorm.Expr("aff_quota + ?", commission),
+			"aff_history": gorm.Expr("aff_history + ?", commission),
+		}).Error
+	})
 }
web/src/i18n/locales/ru.json (1)

16-29: ⚠️ Potential issue | 🟡 Minor

Add the missing placeholder translation key.

t('留空使用全局默认值') is still missing in the Russian locale, so users will see Chinese fallback text in the referral override UI.

💡 Proposed patch
     "返佣记录": "История вознаграждений",
     "返佣设置": "Настройки комиссии",
     "邀请充值返佣设置": "Настройки реферального вознаграждения",
+    "留空使用全局默认值": "Оставьте пустым, чтобы использовать глобальное значение",
     ",当前无生效订阅,将自动使用钱包": ", нет активной подписки, автоматически будет использоваться кошелек.",

As per coding guidelines, frontend translation files in web/src/i18n/locales/{lang}.json must use Chinese source strings as keys for strings used in components.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/i18n/locales/ru.json` around lines 16 - 29, Add the missing
translation key "留空使用全局默认值" to the Russian locale JSON and set its value to a
natural Russian string like "Оставьте пустым для использования глобального
значения по умолчанию" so the referral override UI no longer falls back to
Chinese; update the same locales/ru.json resource where other referral strings
(e.g., "返佣比例覆盖", "邀请充值返佣设置") are defined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@model/referral_commission.go`:
- Around line 20-27: GetUserReferralCommissions currently returns all rows for
an inviter which will grow unbounded; change the function signature
(GetUserReferralCommissions) to accept pagination parameters (e.g., limit and
offset or page and pageSize), enforce a sensible default/max limit, and apply
GORM's Limit(...) and Offset(...) to the DB.Table("referral_commissions") query
before Find(&commissions); optionally also return total count (using
Model/Count) if callers need pagination metadata. Ensure parameter names and any
defaults are validated to avoid zero/negative values.

In `@web/src/components/topup/InvitationCard.jsx`:
- Around line 49-56: The current useEffect calling
API.get('/api/user/aff/commissions') conflates a fetch failure with an empty
result — add an error state (e.g. commissionsError via useState) and set it true
in the catch handler of the promise returned by API.get; only clear
commissionsError on a successful response, and keep the existing finally to
setCommissionsLoading(false); update rendering logic that currently shows
"暂无返佣记录" to check commissionsError first and display an error message when true,
otherwise show the empty message when commissions is an empty array; apply the
same change for the duplicate block around the component lines referenced (the
other API.get('/api/user/aff/commissions') usage).

In `@web/src/i18n/locales/ru.json`:
- Around line 16-29: Add the missing translation key "留空使用全局默认值" to the Russian
locale JSON and set its value to a natural Russian string like "Оставьте пустым
для использования глобального значения по умолчанию" so the referral override UI
no longer falls back to Chinese; update the same locales/ru.json resource where
other referral strings (e.g., "返佣比例覆盖", "邀请充值返佣设置") are defined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 62a2057a-c3fe-47eb-80b3-21b0f061084a

📥 Commits

Reviewing files that changed from the base of the PR and between f2d05f7 and ee2b9ab.

📒 Files selected for processing (22)
  • common/constants.go
  • controller/topup.go
  • controller/user.go
  • model/main.go
  • model/option.go
  • model/referral_commission.go
  • model/subscription.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • web/src/components/settings/OperationSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/components/topup/InvitationCard.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
💤 Files with no reviewable changes (1)
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
🚧 Files skipped from review as they are similar to previous changes (12)
  • model/main.go
  • controller/topup.go
  • model/option.go
  • web/src/i18n/locales/en.json
  • model/subscription.go
  • web/src/i18n/locales/ja.json
  • controller/user.go
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • common/constants.go
  • web/src/i18n/locales/vi.json

@0-don
0-don force-pushed the feat/payment-based-referral branch 2 times, most recently from eca8842 to 84413d0 Compare March 19, 2026 15:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
model/user.go (1)

388-395: ⚠️ Potential issue | 🔴 Critical

Critical: Idempotency check is broken for subscriptions and lacks invitee scope.

Two issues with this check:

  1. Missing invitee_id in WHERE clause: The query checks if ANY record exists with the given topUpId and paymentMethod, not specific to this invitee. Per the AI summary, subscriptions pass topUpId=0. After the first subscription from any user inserts a record (0, "subscription"), all subsequent subscription commissions globally will be incorrectly skipped.

  2. TOCTOU race without unique constraint: Per the ReferralCommission struct definition (context snippet 1), there's no compound unique index on (top_up_id, payment_method, invitee_id). Concurrent requests can both pass the COUNT check before either inserts, creating duplicates.

Proposed fix

1. Fix the idempotency query to scope by invitee:

 // Idempotency: skip if this topup already credited a commission
 var existing int64
-if err := tx.Model(&ReferralCommission{}).Where("top_up_id = ? AND payment_method = ?", topUpId, paymentMethod).Count(&existing).Error; err != nil {
+if err := tx.Model(&ReferralCommission{}).Where("top_up_id = ? AND payment_method = ? AND invitee_id = ?", topUpId, paymentMethod, userId).Count(&existing).Error; err != nil {
     return err
 }

2. Add a unique constraint in model/referral_commission.go:

 type ReferralCommission struct {
 	Id              int     `json:"id"               gorm:"primaryKey"`
-	InviterId       int     `json:"inviter_id"       gorm:"index"`
-	InviteeId       int     `json:"invitee_id"       gorm:"index"`
-	TopUpId         int     `json:"top_up_id"`
+	InviterId       int     `json:"inviter_id"       gorm:"index"`
+	InviteeId       int     `json:"invitee_id"       gorm:"index;uniqueIndex:idx_commission_idempotency,priority:1"`
+	TopUpId         int     `json:"top_up_id"        gorm:"uniqueIndex:idx_commission_idempotency,priority:2"`
 	RechargeAmount  float64 `json:"recharge_amount"`
 	CommissionQuota int     `json:"commission_quota"`
 	CommissionRate  float64 `json:"commission_rate"`
-	PaymentMethod   string  `json:"payment_method"   gorm:"type:varchar(50)"`
+	PaymentMethod   string  `json:"payment_method"   gorm:"type:varchar(50);uniqueIndex:idx_commission_idempotency,priority:3"`
 	CreatedAt       int64   `json:"created_at"       gorm:"autoCreateTime"`
 }

With a unique constraint, the transaction can rely on the INSERT failing for duplicates, providing true idempotency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/user.go` around lines 388 - 395, The idempotency check currently
queries ReferralCommission only by top_up_id and payment_method, causing global
skips for subscriptions (top_up_id=0) and allowing TOCTOU races; update the
WHERE in the transaction that counts existing commissions to include invitee_id
(e.g., add "AND invitee_id = ?") so the check is scoped to the invitee, then add
a DB-level unique constraint on the ReferralCommission model for the compound
key (top_up_id, payment_method, invitee_id) in model/referral_commission.go (use
your ORM's uniqueIndex/tag or migration to create the unique index), and finally
make the insert/commit path tolerant of unique-constraint violations (catch
duplicate-key error on insert and treat it as a no-op) so concurrent requests
cannot create duplicate commissions.
model/option.go (1)

450-455: ⚠️ Potential issue | 🟠 Major

Still reject invalid referral commission values here.

An out-of-range ReferralCommissionPercent is silently ignored, which leaves common.OptionMap holding a value the runtime is not actually using. ReferralCommissionMaxRecharges still ignores parse errors and negatives, and CreditReferralCommission() treats any value <= 0 as unlimited, so a bad admin input can silently remove the cap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/option.go` around lines 450 - 455, The code must reject invalid
referral values instead of silently ignoring them: in the
ReferralCommissionPercent branch validate strconv.ParseFloat(value, 64)
succeeded and v is within [0,100]; if not, do not set
common.ReferralCommissionPercent and remove the key from common.OptionMap (or
leave stored admin value out) and surface/log the parse/validation error.
Likewise for ReferralCommissionMaxRecharges, parse with strconv.Atoi, require a
non-negative integer (reject negatives and parse failures), and on invalid input
do not set common.ReferralCommissionMaxRecharges and remove the key from
common.OptionMap (or return/log the error) so CreditReferralCommission() won’t
treat bad values as unlimited. Use the existing symbols
ReferralCommissionPercent, ReferralCommissionMaxRecharges, common.OptionMap and
CreditReferralCommission() to locate the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/components/topup/InvitationCard.jsx`:
- Around line 51-53: The code currently sets commissions to the entire paged
envelope returned by API.get('/api/user/aff/commissions') which is an object {
page, page_size, total, items }; update the API response handler so
setCommissions receives the row array instead: use res.data.data.items (falling
back to [] if missing) when calling setCommissions; modify the promise callback
around the API.get(...) / then((res) => { ... }) to check res.data.success and
call setCommissions(res.data.data?.items || []) so the history table receives
the correct rows.

---

Duplicate comments:
In `@model/option.go`:
- Around line 450-455: The code must reject invalid referral values instead of
silently ignoring them: in the ReferralCommissionPercent branch validate
strconv.ParseFloat(value, 64) succeeded and v is within [0,100]; if not, do not
set common.ReferralCommissionPercent and remove the key from common.OptionMap
(or leave stored admin value out) and surface/log the parse/validation error.
Likewise for ReferralCommissionMaxRecharges, parse with strconv.Atoi, require a
non-negative integer (reject negatives and parse failures), and on invalid input
do not set common.ReferralCommissionMaxRecharges and remove the key from
common.OptionMap (or return/log the error) so CreditReferralCommission() won’t
treat bad values as unlimited. Use the existing symbols
ReferralCommissionPercent, ReferralCommissionMaxRecharges, common.OptionMap and
CreditReferralCommission() to locate the changes.

In `@model/user.go`:
- Around line 388-395: The idempotency check currently queries
ReferralCommission only by top_up_id and payment_method, causing global skips
for subscriptions (top_up_id=0) and allowing TOCTOU races; update the WHERE in
the transaction that counts existing commissions to include invitee_id (e.g.,
add "AND invitee_id = ?") so the check is scoped to the invitee, then add a
DB-level unique constraint on the ReferralCommission model for the compound key
(top_up_id, payment_method, invitee_id) in model/referral_commission.go (use
your ORM's uniqueIndex/tag or migration to create the unique index), and finally
make the insert/commit path tolerant of unique-constraint violations (catch
duplicate-key error on insert and treat it as a no-op) so concurrent requests
cannot create duplicate commissions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 53873d77-95f8-4f13-9bde-c763c595a214

📥 Commits

Reviewing files that changed from the base of the PR and between ee2b9ab and 84413d0.

📒 Files selected for processing (22)
  • common/constants.go
  • controller/topup.go
  • controller/user.go
  • model/main.go
  • model/option.go
  • model/referral_commission.go
  • model/subscription.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • web/src/components/settings/OperationSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/components/topup/InvitationCard.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
💤 Files with no reviewable changes (1)
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
✅ Files skipped from review due to trivial changes (6)
  • common/constants.go
  • controller/topup.go
  • web/src/components/settings/OperationSetting.jsx
  • model/main.go
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • router/api-router.go
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • model/subscription.go
  • controller/user.go
  • model/topup.go
  • model/referral_commission.go

Comment thread web/src/components/topup/InvitationCard.jsx Outdated
@0-don
0-don force-pushed the feat/payment-based-referral branch from 84413d0 to 4c5d1a6 Compare March 21, 2026 00:06
@0-don

0-don commented Mar 21, 2026

Copy link
Copy Markdown
Contributor Author

Fixed in the latest push: the Japanese translation now includes the percentage/proportion meaning (チャージ金額の一定割合をコミッションとして).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model/main.go`:
- Line 283: Add the ReferralCommission model to the fast migration list so fast
and standard migrations stay in sync: update migrateDBFast() to include
&ReferralCommission{} alongside the other models (matching the addition already
made in migrateDB()), ensuring all three supported DB backends (SQLite, MySQL,
PostgreSQL) will create the referral_commissions table when using the fast path.

In `@model/user.go`:
- Around line 376-425: The code currently always calls RecordLog after
DB.Transaction even when the transaction returned early (max-cap or idempotent
skip) and no commission was credited; fix by tracking whether a commission was
actually created/updated inside the transaction (e.g. declare a local boolean
variable credited = false before calling DB.Transaction and set credited = true
right after the successful tx.Create / Updates calls inside the Transaction
closure), then after DB.Transaction only call RecordLog(user.InviterId, ...)
when credited is true and err == nil; reference DB.Transaction,
ReferralCommission creation (tx.Create), inviter aff_quota update
(tx.Model(&User{}).Updates) and RecordLog to locate where to set and check the
flag.

In `@web/src/i18n/locales/zh-TW.json`:
- Around line 9-22: Add matching English entries for the new referral commission
translation keys used in SettingsCreditLimit.jsx (e.g., "邀请充值返佣设置", "保存返佣设置",
"充值金额", "启用充值返佣", "暂无返佣记录", "最大返佣次数", "获得额度", "被邀请用户充值时,邀请人获得的返佣比例",
"被邀请用户前N次充值给予返佣,0表示不限次数", "返佣比例", "返佣比例覆盖", "返佣记录", "返佣设置") to the English
locale file so t('...') in SettingsCreditLimit.jsx returns proper English
strings instead of Chinese fallbacks; ensure each key from the zh-TW diff has an
equivalent English phrase in the en.json file using the same key names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23456e03-8e64-4827-8d9a-a2ece6ce4408

📥 Commits

Reviewing files that changed from the base of the PR and between 84413d0 and 4c5d1a6.

📒 Files selected for processing (22)
  • common/constants.go
  • controller/topup.go
  • controller/user.go
  • model/main.go
  • model/option.go
  • model/referral_commission.go
  • model/subscription.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • web/src/components/settings/OperationSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/components/topup/InvitationCard.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
💤 Files with no reviewable changes (1)
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
✅ Files skipped from review due to trivial changes (5)
  • web/src/components/settings/OperationSetting.jsx
  • common/constants.go
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/i18n/locales/zh-CN.json
  • web/src/components/topup/InvitationCard.jsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • router/api-router.go
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
  • controller/user.go
  • model/subscription.go
  • model/topup.go
  • model/referral_commission.go

Comment thread model/main.go
Comment thread model/user.go Outdated
Comment thread web/src/i18n/locales/zh-TW.json Outdated
@0-don
0-don force-pushed the feat/payment-based-referral branch from 4c5d1a6 to 1640050 Compare March 21, 2026 00:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
model/option.go (1)

450-457: ⚠️ Potential issue | 🟠 Major

Reject invalid commission settings instead of silently ignoring them.

This branch still leaves err nil on parse/range failures. Because UpdateOption() saves first and common.OptionMap[key] = value already ran, inputs like ReferralCommissionPercent=abc or ReferralCommissionMaxRecharges=-1 will be persisted and echoed back to the UI while the runtime keeps the old in-memory value. Please validate before persisting, or return an error here and move the save after validation so stored and effective settings cannot diverge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/option.go` around lines 450 - 457, The code silently ignores
parse/range errors for ReferralCommissionPercent and
ReferralCommissionMaxRecharges causing invalid values to be persisted in
common.OptionMap while runtime values stay unchanged; change validation so
parsing and range checks happen before writing to common.OptionMap or make the
setter return an error on invalid input (i.e., in UpdateOption() validate value
for the key using strconv.ParseFloat/ParseInt and range checks for
common.ReferralCommissionPercent and common.ReferralCommissionMaxRecharges, and
if validation fails return an error instead of falling through), then only
update common.OptionMap and assign to
common.ReferralCommissionPercent/common.ReferralCommissionMaxRecharges after
successful validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@model/user.go`:
- Around line 389-407: The dedupe key currently uses topUpId (tuple invitee_id,
top_up_id, payment_method) which breaks subscription commissions because
CreditReferralCommission is called with topUpId=0 for subscriptions; update the
idempotency key to accept and store a source-specific payment identifier (e.g.,
subscriptionPaymentId or sourceId) instead of using topUpId for subscription
flows: change the ReferralCommission struct field(s) and the unique index logic
in model/referral_commission.go to include the new source identifier (or make
top_up_id nullable and use source_id), update CreditReferralCommission to accept
and pass that source-specific id from model/subscription.go (instead of 0), and
add a migration to alter the unique constraint and schema accordingly so each
subscription charge has its own unique commission key.
- Around line 619-624: Edit() currently writes referral_commission_percent
(newUser.ReferralCommissionPercent) directly to the DB allowing invalid values;
validate and sanitize this field before saving by enforcing a sensible range
(e.g. 0–100) or treating out-of-range values as NULL/unspecified so
CreditReferralCommission() won't skip payouts; update the Edit() update payload
to clamp or nil-out invalid referral_commission_percent values and ensure any
helper/validation logic is centralized (referencing the Edit() method and the
referral_commission_percent field used by CreditReferralCommission()).

---

Duplicate comments:
In `@model/option.go`:
- Around line 450-457: The code silently ignores parse/range errors for
ReferralCommissionPercent and ReferralCommissionMaxRecharges causing invalid
values to be persisted in common.OptionMap while runtime values stay unchanged;
change validation so parsing and range checks happen before writing to
common.OptionMap or make the setter return an error on invalid input (i.e., in
UpdateOption() validate value for the key using strconv.ParseFloat/ParseInt and
range checks for common.ReferralCommissionPercent and
common.ReferralCommissionMaxRecharges, and if validation fails return an error
instead of falling through), then only update common.OptionMap and assign to
common.ReferralCommissionPercent/common.ReferralCommissionMaxRecharges after
successful validation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7cc0a2f1-67c2-4f07-9f89-8714a6353b79

📥 Commits

Reviewing files that changed from the base of the PR and between 4c5d1a6 and 1640050.

📒 Files selected for processing (22)
  • common/constants.go
  • controller/topup.go
  • controller/user.go
  • model/main.go
  • model/option.go
  • model/referral_commission.go
  • model/subscription.go
  • model/topup.go
  • model/user.go
  • router/api-router.go
  • web/src/components/settings/OperationSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/components/topup/InvitationCard.jsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/pages/Setting/Operation/SettingsCreditLimit.jsx
💤 Files with no reviewable changes (1)
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
✅ Files skipped from review due to trivial changes (5)
  • model/main.go
  • common/constants.go
  • web/src/components/table/users/modals/EditUserModal.jsx
  • web/src/i18n/locales/zh-CN.json
  • web/src/i18n/locales/zh-TW.json
🚧 Files skipped from review as they are similar to previous changes (7)
  • model/subscription.go
  • controller/topup.go
  • controller/user.go
  • model/topup.go
  • web/src/components/settings/OperationSetting.jsx
  • model/referral_commission.go
  • web/src/components/topup/InvitationCard.jsx

Comment thread model/user.go
Comment thread model/user.go
@0-don
0-don force-pushed the feat/payment-based-referral branch 10 times, most recently from 76611fd to 02f44f4 Compare March 27, 2026 15:09
@ghost

This comment was marked as spam.

@0-don
0-don force-pushed the feat/payment-based-referral branch 5 times, most recently from df9253c to b2c0ee7 Compare April 4, 2026 14:09
@0-don
0-don force-pushed the feat/payment-based-referral branch 2 times, most recently from 81e07b5 to 4606807 Compare April 9, 2026 09:22
@0-don
0-don force-pushed the feat/payment-based-referral branch 5 times, most recently from 61e18ef to 4405341 Compare May 13, 2026 17:40
@0-don
0-don force-pushed the feat/payment-based-referral branch 4 times, most recently from 173e795 to 1d13c81 Compare May 23, 2026 23:51
@0-don
0-don force-pushed the feat/payment-based-referral branch 3 times, most recently from 7fca8ae to ed6a0e0 Compare May 27, 2026 20:24
@0-don
0-don force-pushed the feat/payment-based-referral branch 5 times, most recently from 3211d2c to e5bfa0b Compare June 7, 2026 14:51
@0-don
0-don force-pushed the feat/payment-based-referral branch 2 times, most recently from 0d02aa6 to d42ed73 Compare June 14, 2026 16:39
@0-don
0-don force-pushed the feat/payment-based-referral branch from d42ed73 to 6daa887 Compare June 18, 2026 20:56
@0-don
0-don force-pushed the feat/payment-based-referral branch 3 times, most recently from 245a7fa to 3ed3e0a Compare July 3, 2026 23:39
@0-don
0-don force-pushed the feat/payment-based-referral branch 2 times, most recently from be2b439 to c155a8d Compare July 9, 2026 20:35
Adds a per-user referral_commission_percent override field (admin-managed)
that overrides the global referral commission percent for a single user.
When null, the global default applies.

Backend: model.User.referral_commission_percent column, controller wiring,
referral_commission table for audit trail, and ratio-aware payment hooks.

Frontend: adds the override field to the users edit drawer in web/default.
The classic-frontend version of this PR also shipped a redesigned
InvitationCard with invitee + commission tabs; that piece was dropped
because the new default frontend now uses affiliate-rewards-card.tsx with
a different shape.
@0-don
0-don force-pushed the feat/payment-based-referral branch from c155a8d to 9dcf1be Compare July 22, 2026 17:14
@0-don

0-don commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Closing: no longer maintaining these against upstream.

@0-don 0-don closed this Jul 22, 2026
@0-don
0-don deleted the feat/payment-based-referral branch July 22, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

邀请赠送的额度,在消费后解锁

1 participant