Skip to content

✨ feat: Add topup billing history with admin manual completion - #1976

Merged
seefs001 merged 3 commits into
mainfrom
feature/invoicing
Oct 7, 2025
Merged

✨ feat: Add topup billing history with admin manual completion#1976
seefs001 merged 3 commits into
mainfrom
feature/invoicing

Conversation

@t0ng7u

@t0ng7u t0ng7u commented Oct 6, 2025

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added top-up history: searchable, paginated “Bills” view with status and payment method display.
    • New “Bills” button in recharge UI to open history; supports mobile-friendly modal.
    • Admins can manually mark pending top-ups as completed from the history view.
    • Expanded translations for billing across English, French, and Chinese.
  • Bug Fixes

    • Improved passkey support detection reliability in error scenarios.
    • Adjusted audio model price calculation for more accurate totals.

Apple\Apple added 3 commits October 7, 2025 00:22
Implement comprehensive topup billing system with user history viewing and admin management capabilities.

## Features Added

### Frontend
- Add topup history modal with paginated billing records
- Display order details: trade number, payment method, amount, money, status, create time
- Implement empty state with proper illustrations
- Add payment method column with localized display (Stripe, Alipay, WeChat)
- Add admin manual completion feature for pending orders
- Add Coins icon for recharge amount display
- Integrate "Bills" button in RechargeCard header
- Optimize code quality by using shared utility functions (isAdmin)
- Extract constants for status and payment method mappings
- Use React.useMemo for performance optimization

### Backend
- Create GET `/api/user/topup/self` endpoint for user topup history with pagination
- Create POST `/api/user/topup/complete` endpoint for admin manual order completion
- Add `payment_method` field to TopUp model for tracking payment types
- Implement `GetUserTopUps` method with proper pagination and ordering
- Implement `ManualCompleteTopUp` with transaction safety and row-level locking
- Add application-level mutex locks to prevent concurrent order processing
- Record payment method in Epay and Stripe payment flows
- Ensure idempotency and data consistency with proper error handling

### Internationalization
- Add i18n keys for Chinese (zh), English (en), and French (fr)
- Support for billing-related UI text and status messages

## Technical Improvements
- Use database transactions with FOR UPDATE row-level locking
- Implement sync.Map-based mutex for order-level concurrency control
- Proper error handling and user-friendly toast notifications
- Follow existing codebase patterns for empty states and modals
- Maintain code quality with extracted render functions and constants

## Files Changed
- Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go
- Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx
- i18n: web/src/i18n/locales/{zh,en,fr}.json
Allow administrators to view all platform topup orders and streamline admin-only routes.

Frontend
- TopupHistoryModal: dynamically switch endpoint by role
  - Admin → GET /api/user/topup (all orders)
  - Non-admin → GET /api/user/topup/self (own orders)
- Use shared utils `isAdmin()`; keep logic centralized and DRY
- Minor UI: set admin action button theme to outline for clarity

Backend
- model/topup.go: add GetAllTopUps(pageInfo) with pagination (ordered by id desc)
- controller/topup.go: add GetAllTopUps handler returning PageInfo response
- router/api-router.go:
  - Add admin route GET /api/user/topup (AdminAuth)
  - Move POST /api/user/topup/complete to adminRoute (keeps path stable, consolidates admin endpoints)

Security/Behavior
- Admin-only endpoints now reside under the admin route group with AdminAuth
- No behavior change for regular users; no schema changes

Affected files
- model/topup.go
- controller/topup.go
- router/api-router.go
- web/src/components/topup/modals/TopupHistoryModal.jsx
… user)

Enable searching topup records by trade_no across both admin-wide and user-only views.

Frontend
- TopupHistoryModal.jsx:
  - Add search input with prefix icon (IconSearch) to filter by order number
  - Send `keyword` query param to backend; works with both endpoints:
    - Admin: GET /api/user/topup?p=1&page_size=10&keyword=...
    - User:  GET /api/user/topup/self?p=1&page_size=10&keyword=...
  - Keep endpoint auto-switching based on role (isAdmin)
  - Minor UI polish: outlined admin action button; keep Coins icon for amount

Backend
- model/topup.go:
  - Add SearchUserTopUps(userId, keyword, pageInfo)
  - Add SearchAllTopUps(keyword, pageInfo)
  - Both support pagination and `trade_no LIKE %keyword%` filtering (ordered by id desc)
- controller/topup.go:
  - GetUserTopUps / GetAllTopUps accept optional `keyword` and route to search functions when present

Routes
- No new endpoints; search is enabled via `keyword` on existing:
  - GET /api/user/topup
  - GET /api/user/topup/self

Affected files
- model/topup.go
- controller/topup.go
- web/src/components/topup/modals/TopupHistoryModal.jsx
@coderabbitai

coderabbitai Bot commented Oct 6, 2025

Copy link
Copy Markdown
Contributor

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly summarizes the primary change by stating it adds top-up billing history along with admin manual completion, directly reflecting the main features introduced in the pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/invoicing

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 and usage tips.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controller/topup.go (1)

234-246: Critical: nil-pointer risk when Epay client is not configured.

If GetEpayClient() returns nil, code writes "fail" but continues and calls client.Verify, causing a panic. Return early after writing the response.

Apply this diff:

 if client == nil {
   log.Println("易支付回调失败 未找到配置信息")
   _, err := c.Writer.Write([]byte("fail"))
-  if err != nil {
-    log.Println("易支付回调写入失败")
-    return
-  }
+  if err != nil {
+    log.Println("易支付回调写入失败")
+  }
+  return
 }
🧹 Nitpick comments (7)
model/topup.go (6)

105-137: Optional: drop read-only transactions to reduce overhead.

Counting + listing are independent reads; wrapping in a transaction isn’t necessary and can add lock contention.


139-166: Same as above: consider removing transaction in GetAllTopUps.


168-200: SearchUserTopUps: transaction optional; LIKE pattern OK.

You can compute total and page in separate queries without a tx. Current approach is correct but can be simplified.


202-234: SearchAllTopUps: mirrors user search; transaction optional.


236-306: ManualCompleteTopUp: solid idempotence and row locking; minor GORM API tweak.

  • Good: FOR UPDATE + status checks + decimal math.
  • Replace raw query option with GORM clause locking for portability:
    // import "gorm.io/gorm/clause"
    tx.Clauses(clause.Locking{Strength: "UPDATE"}).
       Where(refCol+" = ?", tradeNo).
       First(topUp)
  • Both logger.LogQuota and logger.FormatQuota exist; consider standardizing on one API for quota formatting across the codebase.

14-23: Add composite and status indexes to TopUp model

GORM’s AutoMigrate already includes &TopUp{}, so PaymentMethod will be applied. To optimize common queries, add a composite index on (user_id, id DESC) and a single‐column index on status (via GORM tags in model/topup.go or a dedicated SQL migration).

controller/topup.go (1)

372-389: Admin middleware in place; no changes needed.

  • The route is already registered under adminRoute.Use(middleware.AdminAuth()).

Revisit lock cleanup strategy.

  • Deleting the orderLocks entry in UnlockOrder can break mutual exclusion when a new caller races between unlock and lock.
  • Consider an eviction approach (TTL, background GC) or accept the map’s retention rather than removing entries directly.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2397ec8 and a8c9b24.

📒 Files selected for processing (33)
  • controller/topup.go (2 hunks)
  • controller/topup_stripe.go (1 hunks)
  • model/topup.go (2 hunks)
  • router/api-router.go (2 hunks)
  • web/src/components/auth/LoginForm.jsx (2 hunks)
  • web/src/components/common/examples/ChannelKeyViewExample.jsx (3 hunks)
  • web/src/components/common/modals/SecureVerificationModal.jsx (8 hunks)
  • web/src/components/settings/PersonalSetting.jsx (2 hunks)
  • web/src/components/settings/SystemSetting.jsx (7 hunks)
  • web/src/components/settings/personal/cards/AccountManagement.jsx (2 hunks)
  • web/src/components/settings/personal/cards/NotificationSettings.jsx (2 hunks)
  • web/src/components/setup/components/steps/DatabaseStep.jsx (1 hunks)
  • web/src/components/table/channels/modals/EditTagModal.jsx (1 hunks)
  • web/src/components/table/channels/modals/ModelTestModal.jsx (2 hunks)
  • web/src/components/table/users/UsersColumnDefs.jsx (1 hunks)
  • web/src/components/table/users/modals/ResetPasskeyModal.jsx (1 hunks)
  • web/src/components/table/users/modals/ResetTwoFAModal.jsx (1 hunks)
  • web/src/components/topup/RechargeCard.jsx (5 hunks)
  • web/src/components/topup/index.jsx (5 hunks)
  • web/src/components/topup/modals/TopupHistoryModal.jsx (1 hunks)
  • web/src/constants/channel.constants.js (1 hunks)
  • web/src/helpers/passkey.js (6 hunks)
  • web/src/helpers/render.jsx (14 hunks)
  • web/src/helpers/secureApiCall.js (2 hunks)
  • web/src/hooks/channels/useChannelsData.jsx (1 hunks)
  • web/src/hooks/common/useSecureVerification.jsx (6 hunks)
  • web/src/hooks/users/useUsersData.jsx (1 hunks)
  • web/src/i18n/locales/en.json (1 hunks)
  • web/src/i18n/locales/fr.json (1 hunks)
  • web/src/i18n/locales/zh.json (1 hunks)
  • web/src/pages/Setting/Chat/SettingsChats.jsx (1 hunks)
  • web/src/pages/Setting/Operation/SettingsLog.jsx (3 hunks)
  • web/src/services/secureVerification.js (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (14)
web/src/components/topup/index.jsx (1)
web/src/components/topup/modals/TopupHistoryModal.jsx (1)
  • TopupHistoryModal (56-266)
router/api-router.go (2)
controller/topup.go (3)
  • GetUserTopUps (318-341)
  • GetAllTopUps (344-366)
  • AdminCompleteTopUp (373-389)
model/topup.go (2)
  • GetUserTopUps (105-137)
  • GetAllTopUps (140-166)
web/src/components/settings/personal/cards/AccountManagement.jsx (1)
web/src/components/settings/PersonalSetting.jsx (2)
  • passkeyDeleteLoading (75-75)
  • passkeyRegisterLoading (74-74)
web/src/components/table/channels/modals/ModelTestModal.jsx (1)
web/src/hooks/channels/useChannelsData.jsx (3)
  • testChannel (757-840)
  • currentTestChannel (80-80)
  • selectedEndpointType (87-87)
controller/topup.go (3)
model/topup.go (6)
  • GetUserTopUps (105-137)
  • TopUp (13-23)
  • SearchUserTopUps (169-200)
  • GetAllTopUps (140-166)
  • SearchAllTopUps (203-234)
  • ManualCompleteTopUp (237-306)
common/page_info.go (1)
  • GetPageQuery (41-82)
common/gin.go (3)
  • ApiError (95-100)
  • ApiSuccess (109-115)
  • ApiErrorMsg (102-107)
web/src/components/topup/modals/TopupHistoryModal.jsx (1)
web/src/helpers/utils.jsx (2)
  • isAdmin (35-40)
  • timestamp2string (192-218)
web/src/hooks/common/useSecureVerification.jsx (6)
web/src/components/common/examples/ChannelKeyViewExample.jsx (1)
  • useSecureVerification (38-56)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
  • useSecureVerification (223-251)
web/src/components/common/modals/SecureVerificationModal.jsx (2)
  • verificationMethods (62-62)
  • verificationState (63-63)
web/src/services/secureVerification.js (1)
  • SecureVerificationService (31-185)
web/src/helpers/utils.jsx (2)
  • showError (122-151)
  • showSuccess (157-159)
web/src/helpers/secureApiCall.js (2)
  • error (33-33)
  • isVerificationRequiredError (30-47)
web/src/components/settings/PersonalSetting.jsx (1)
web/src/helpers/passkey.js (5)
  • publicKey (55-62)
  • publicKey (90-93)
  • prepareCredentialCreationOptions (46-79)
  • credential (108-108)
  • buildRegistrationResult (105-126)
controller/topup_stripe.go (1)
common/constants.go (1)
  • TopUpStatusPending (200-200)
web/src/components/topup/RechargeCard.jsx (2)
web/src/components/topup/index.jsx (1)
  • amountLoading (69-69)
web/src/helpers/render.jsx (11)
  • getCurrencyConfig (929-955)
  • getCurrencyConfig (964-964)
  • getCurrencyConfig (1133-1133)
  • getCurrencyConfig (1392-1392)
  • getCurrencyConfig (1492-1492)
  • getCurrencyConfig (1689-1689)
  • getCurrencyConfig (1833-1833)
  • symbol (843-843)
  • symbol (913-913)
  • symbol (933-933)
  • symbol (977-977)
web/src/components/auth/LoginForm.jsx (1)
web/src/helpers/passkey.js (3)
  • prepareCredentialRequestOptions (81-103)
  • assertion (131-131)
  • buildAssertionResult (128-148)
model/topup.go (8)
controller/topup.go (2)
  • GetUserTopUps (318-341)
  • GetAllTopUps (344-366)
model/main.go (1)
  • DB (63-63)
common/database.go (1)
  • UsingPostgreSQL (10-10)
common/constants.go (3)
  • TopUpStatusSuccess (201-201)
  • TopUpStatusPending (200-200)
  • QuotaPerUnit (21-21)
common/utils.go (1)
  • GetTimestamp (272-274)
model/user.go (1)
  • User (19-48)
model/log.go (2)
  • RecordLog (78-94)
  • LogTypeTopup (43-43)
logger/logger.go (1)
  • FormatQuota (122-146)
web/src/services/secureVerification.js (1)
web/src/helpers/passkey.js (4)
  • isPasskeySupported (150-177)
  • publicKey (55-62)
  • publicKey (90-93)
  • prepareCredentialRequestOptions (81-103)
web/src/pages/Setting/Chat/SettingsChats.jsx (1)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
  • isEdit (122-122)
🔇 Additional comments (19)
web/src/components/common/examples/ChannelKeyViewExample.jsx (1)

72-74: Formatting tidy-up looks good.

Condensing the button props keeps things consistent without altering behavior. Nicely done.

web/src/components/table/channels/modals/EditTagModal.jsx (1)

122-134: LGTM! Formatting improvement enhances readability.

The multi-line array formatting makes the model list more readable and easier to maintain. Future additions or removals will also produce cleaner git diffs.

web/src/components/table/channels/modals/ModelTestModal.jsx (3)

70-73: LGTM! Formatting improves readability.

The multi-line formatting makes the long label string more readable.


75-78: LGTM! Consistent with the gemini option formatting.

The multi-line formatting improves readability and maintains consistency with the previous option.


175-181: LGTM! Multi-line formatting improves readability.

The expanded function call with trailing comma is more readable and will produce cleaner diffs if arguments are added or modified in the future.

web/src/components/settings/personal/cards/NotificationSettings.jsx (1)

624-626: LGTM! Minor i18n and formatting improvements.

The changes improve consistency in the Gotify configuration section:

  • Line 624-626: The validation message is now properly wrapped with t() for internationalization.
  • Line 683: The configuration list item is consolidated to match the formatting of the other list items.

Both changes follow i18n best practices and improve readability without introducing any functional changes.

Also applies to: 683-683

web/src/pages/Setting/Operation/SettingsLog.jsx (1)

233-244: Modal affordances remain intact.

Thanks for keeping the DatePicker helper text and destructive-action button aligned; the readability bump comes with no behavioral regressions.

web/src/helpers/render.jsx (2)

1180-1189: LGTM: Improved readability with multi-line formatting.

The reformatting of i18next.t template calls improves readability by breaking long strings across multiple lines. These changes maintain the same functionality while making the code easier to read and maintain.

Also applies to: 1317-1325, 1329-1337, 1597-1621, 1630-1638, 1776-1808


1525-1531: Ignore incorrect parameter removal suggestionrenderAudioModelPrice does not take an audioInputPrice parameter; it intentionally derives audio pricing from inputRatioPrice * audioRatio. No unused parameter to remove.

Likely an incorrect or invalid review comment.

web/src/components/settings/personal/cards/AccountManagement.jsx (1)

552-556: LGTM! Correct loading state selection.

The updated loading state logic now correctly displays the appropriate loading indicator based on the current action (delete vs. register), rather than showing loading if either operation is in progress. This prevents the button from incorrectly appearing busy when the opposite operation is loading.

web/src/components/settings/PersonalSetting.jsx (1)

215-217: LGTM! Robust fallback chain for passkey options.

The expanded fallback chain (data?.options || data?.publicKey || data) ensures the function can handle various response structures from the server, making the passkey registration more resilient.

controller/topup_stripe.go (1)

85-92: LGTM! Correctly populates PaymentMethod field.

The TopUp creation now includes the PaymentMethod field set to PaymentMethodStripe, which aligns with the PR's objective to add payment method tracking for billing history. The field population is consistent with the Stripe payment flow.

web/src/i18n/locales/en.json (1)

2249-2262: LGTM: new billing/top-up i18n keys align with the feature.

Keys read well and match the admin/manual-completion flow.

web/src/i18n/locales/fr.json (1)

2241-2254: LGTM: FR translations added for billing/top-up.

Terminology is consistent with EN (Bills/Order/Complete).

web/src/components/table/users/modals/ResetTwoFAModal.jsx (1)

32-37: LGTM: formatting-only change.

Multiline t() keeps spacing via {' '} and preserves logic.

controller/topup.go (3)

186-193: Persisting PaymentMethod on order creation is correct.

This enables downstream manual-completion logic to compute quota accurately.


318-341: LGTM: user self top-ups listing with optional keyword search.

Pagination and keyword flow are clear; consistent response via ApiSuccess.


344-366: LGTM: admin all top-ups listing with search.

Matches the self endpoint structure; good reuse of model functions.

web/src/hooks/common/useSecureVerification.jsx (1)

34-121: LGTM: clearer async flow, state handling, and helper wrappers.

  • startVerification/executeVerification now explicit async; good UX via loading/finally.
  • withVerification wrapper is handy; null-return contract is clear.

No action needed.

Also applies to: 124-171, 174-181, 189-204, 208-218, 227-245, 268-273

Comment on lines +86 to +104
setLoading(false);
}
};

useEffect(() => {
if (visible) {
loadTopups(page, pageSize);
}
}, [visible, page, pageSize, keyword]);

const handlePageChange = (currentPage) => {
setPage(currentPage);
};

const handlePageSizeChange = (currentPageSize) => {
setPageSize(currentPageSize);
setPage(1);
};

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.

⚠️ Potential issue | 🟠 Major

Reset pagination when applying a search keyword

Line [234]: when the search keyword changes we keep whatever page the user was previously on. If they were on page 5 and their new query only has one page of results, we end up requesting p=5 and render an empty table even though matches exist. Reset the page to 1 whenever the keyword updates so searches always start from the first page.

-  const handlePageSizeChange = (currentPageSize) => {
-    setPageSize(currentPageSize);
-    setPage(1);
-  };
+  const handlePageSizeChange = (currentPageSize) => {
+    setPageSize(currentPageSize);
+    setPage(1);
+  };
+
+  const handleKeywordChange = (value) => {
+    setKeyword(value);
+    setPage(1);
+  };
...
-        <Input
+        <Input
           prefix={<IconSearch />}
           placeholder={t('订单号')}
-          value={keyword}
-          onChange={setKeyword}
+          value={keyword}
+          onChange={handleKeywordChange}
           showClear
         />

Also applies to: 230-236

🤖 Prompt for AI Agents
In web/src/components/topup/modals/TopupHistoryModal.jsx around lines 86-104,
the component keeps the current page when the search keyword changes which can
request an out-of-range page and render an empty table; update the logic so that
when the keyword changes the page is reset to 1 before loading results — e.g.,
on keyword change call setPage(1) (or set page to 1 and then call loadTopups
with page=1) so searches always start from the first page; ensure this does not
create an infinite loop by either performing the reset inside a separate effect
that watches keyword only or by applying the reset and loading explicitly in the
same effect.

@seefs001
seefs001 merged commit ec590d1 into main Oct 7, 2025
1 check passed
@Calcium-Ion
Calcium-Ion deleted the feature/invoicing branch October 11, 2025 03:06
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 2026
✨ feat: Add topup billing history with admin manual completion
@coderabbitai coderabbitai Bot mentioned this pull request Jun 3, 2026
11 tasks
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.

2 participants