Skip to content

feat(ImprovePwAI3UX): admin per-user credit detail page with refund s… - #647

Merged
EmilFattakhov merged 18 commits into
mainfrom
ImprovePwAI3UX
Apr 2, 2026
Merged

feat(ImprovePwAI3UX): admin per-user credit detail page with refund s…#647
EmilFattakhov merged 18 commits into
mainfrom
ImprovePwAI3UX

Conversation

@EmilFattakhov

@EmilFattakhov EmilFattakhov commented Apr 1, 2026

Copy link
Copy Markdown
Member

…upport

  • Capture receipt.from (EVM wallet address) at intent confirmation time and store it in a new intents.from_address column (migration included)
  • Add purchased_credits.refunded_at column (migration included)
  • Repository: markAsRefunded() zeros remaining bytes + sets refunded_at; getByUserPublicId() joins purchased_credits with intents to return price + wallet data per batch
  • Use cases: getUserBatches (admin-only, by userPublicId) + refundBatch (idempotent)
  • API routes: GET /credits/batches/user/:userPublicId and POST /credits/batches/:id/refund
  • Frontend: AdminUserCredits component shows full purchase history per user with date, status, expiry, original/consumed/remaining bytes, AI3 paid, EVM wallet address, and a "Mark Refunded" button per batch
  • AllBatchesTable User column is now a clickable link to the new per-user page
  • New Next.js page: /[chain]/drive/admin/credits/[userPublicId]
  • Added ROUTES.adminUserCredits helper to @auto-drive/ui

…upport

- Capture receipt.from (EVM wallet address) at intent confirmation time and
  store it in a new intents.from_address column (migration included)
- Add purchased_credits.refunded + refunded_at columns (migration included)
- Repository: markAsRefunded() zeros remaining bytes + sets refunded/refunded_at;
  getByUserPublicId() joins purchased_credits with intents to return price +
  wallet data per batch
- Use cases: getUserBatches (admin-only, by userPublicId) + refundBatch (idempotent)
- API routes: GET /credits/batches/user/:userPublicId and POST /credits/batches/:id/refund
- Frontend: AdminUserCredits component shows full purchase history per user
  with date, status, expiry, original/consumed/remaining bytes, AI3 paid,
  EVM wallet address, and a "Mark Refunded" button per batch
- AllBatchesTable User column is now a clickable link to the new per-user page
- New Next.js page: /[chain]/drive/admin/credits/[userPublicId]
- Added ROUTES.adminUserCredits helper to @auto-drive/ui

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@netlify

netlify Bot commented Apr 1, 2026

Copy link
Copy Markdown

Deploy Preview for auto-drive-storage ready!

Name Link
🔨 Latest commit 268e8b0
🔍 Latest deploy log https://app.netlify.com/projects/auto-drive-storage/deploys/69ce7571f2c9e200070a5f42
😎 Deploy Preview https://deploy-preview-647--auto-drive-storage.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

EmilFattakhov and others added 4 commits April 1, 2026 11:02
refunded_at being non-null is already the canonical signal that a refund
occurred. The separate refunded boolean was redundant — remove it from the
migration, DB type, model, and frontend type. All refund-state checks now
read batch.refundedAt !== null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflict in apps/frontend/src/services/api.ts — kept all ToU,
Deletion admin methods from main alongside the new getUserCreditBatches
and refundCreditBatch methods from this branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… active

account.pendingUploadCredits = freeRemaining + purchasedRemaining, but
uploadLimit is only the free quota. This caused uploadUsed to go negative
(e.g. -75 MiB/100 MiB) and the progress bar to misrepresent usage.

Fixes:
- SideNavBar: creditSummary is always loaded for logged-in users, so
  purchasedBytesRemaining is now derived regardless of the feature flag.
  freeRemaining = pendingUploadCredits - purchasedBytesRemaining is passed
  as uploadPending so the progress bar and used/limit label track only the
  free allocation. The purchasedBytesRemaining prop is still gated by
  hasBuyCreditsFeature for the display section.
- AccountInformation: when the user has purchased credits, the primary "left"
  label now shows total available (free + purchased) — the figure that governs
  whether an upload will succeed. The right-side used/limit and progress bar
  continue to reflect the free allocation so the user can see both at a glance.
  Negative uploadUsed (free quota exhausted) renders as 0 in the text and
  100% in the bar, which is correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idation

- Replace raw MiB number input (which caused "0200" leading-zeros bug) with
  a text input + MB / GB / TB segmented unit toggle. Internal value stays in
  MiB; the display converts automatically when switching units.
- Remove editable AI3 amount field — dual coupled inputs were confusing and
  allowed inconsistent state. AI3 amount, USD equivalent, and Total are now
  read-only display rows derived from the storage amount.
- Add real-time cap validation: shows an amber warning with the maximum
  purchasable amount when the entered size would exceed the user's credit cap,
  and disables the Confirm Purchase button until the amount is valid.
- "After Purchase" summary now falls back to current balance when no amount
  is entered, instead of showing 0B.
- Unit conversion on toggle: switching MB→GB converts the current value so
  the underlying purchase size stays constant (e.g. 1024 MB → 1 GB).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@EmilFattakhov
EmilFattakhov marked this pull request as ready for review April 2, 2026 00:31
EmilFattakhov and others added 2 commits April 1, 2026 20:56
- Extract UNITS, MIB_PER_UNIT, bestUnit, mibToDisplay, sanitizeAmountInput,
  inputToMib, isCustomAmountOverCap, and computePaymentShannons into a new
  pure-function module (utils/purchaseCredits.ts) so they can be tested in
  isolation without a React environment.

- Fix handleUnitChange context staleness: now calls onContextChange after a
  unit switch so context.sizeMB stays in sync with the displayed value, and
  Step 3 always receives the correct payment amount.

- Add 52 unit tests covering constants, unit conversions, input sanitisation,
  cap validation, round-trip consistency, and the on-chain payment formula.

- Fix 6 react-hooks/exhaustive-deps lint warnings introduced by the
  origin/main merge (SessionEnsurer, TouAdmin ×4, web3 context).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntentAsConfirmed

Our paymentManager now passes `fromAddress: receipt.from` when calling
markIntentAsConfirmed, so the receipt mock needs a `from` field and the
toHaveBeenCalledWith assertion must include `fromAddress`.

Without this fix the CI backend unit tests fail because Jest's deep equality
check rejects { intentId, paymentAmount, fromAddress: undefined } against
the old expected shape { intentId, paymentAmount }.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@EmilFattakhov

Copy link
Copy Markdown
Member Author

In a nutshell, this PR resolves most of the issues that were found during the initial testing. The main three points are:

  • Added a new admin page where staff can look up any user's full purchase history. It shows each credit batch — how much was bought, how much is left, when it expires, how much AI3 was paid, and the EVM wallet address that made the payment. Admins can mark individual batches as refunded directly from this page.

  • Fixed the sidebar upload usage display. The sidebar was showing a negative number like "-75 MiB / 100 MiB" for users with purchased credits. Fixed it so: when the paid credits feature is off, it shows only free tier usage; when it's on, the first row shows your total available storage (free + purchased) and a second row shows your purchased credits separately.

  • Redesigned the "buy credits" custom amount input
    The old input had a bug where you could accidentally end up with numbers like "0200". It also forced you to manually calculate gigabytes in megabytes. Replaced it with a clean text input plus a MB / GB / TB toggle — type "2" and pick "GB", done. Added input sanitisation to block invalid characters, and a cap warning if you try to buy more than your account limit allows.

PurchasedCredit now requires refundedAt (nullable timestamp) after the
refund migration was added.  The makeCreditRow test fixture was missing
the field, causing ts-jest to fail with a type error at compile time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@EmilFattakhov

Copy link
Copy Markdown
Member Author

bugbot run

Comment thread apps/backend/src/core/users/credits.ts Outdated
Comment thread apps/frontend/src/utils/purchaseCredits.ts Outdated
Plain Error fell through handleError as a 500 instead of the intended 404.

Made-with: Cursor
…unded_at

Add `AND refunded_at IS NULL` to the UPDATE WHERE clause so repeated
calls don't overwrite the original refund timestamp.  A follow-up
SELECT distinguishes "not found" from "already refunded", letting
the use-case return ok() in both cases without losing audit data.

Made-with: Cursor
@EmilFattakhov

Copy link
Copy Markdown
Member Author

bugbot run

Comment thread apps/frontend/src/utils/purchaseCredits.ts Outdated
Comment thread apps/frontend/src/utils/purchaseCredits.ts Outdated
`!maxPurchasableBytes` treats 0n as falsy, allowing purchases when the
cap is fully exhausted. Switch to `=== null` to match isPackageOverCap.

Made-with: Cursor
…plication

isCustomAmountOverCap and isPackageOverCap both converted MiB to bytes
and compared against maxPurchasableBytes with divergent null-guard logic.
A single isMibOverCap in credits.ts now owns the conversion and comparison;
isPackageOverCap delegates to it, and isCustomAmountOverCap is a re-export.

Made-with: Cursor
@EmilFattakhov

Copy link
Copy Markdown
Member Author

bugbot run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

@jim-counter jim-counter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could we review the use of MB/MiB please?

Comment thread apps/frontend/__tests__/unit/utils/purchaseCredits.spec.ts
Comment thread apps/frontend/__tests__/unit/utils/purchaseCredits.spec.ts
Comment thread apps/frontend/__tests__/unit/utils/purchaseCredits.spec.ts
Comment thread apps/frontend/__tests__/unit/utils/purchaseCredits.spec.ts
Comment thread apps/frontend/src/components/views/AdminPanel/AdminUserCredits.tsx Outdated
Comment thread apps/frontend/src/utils/purchaseCredits.ts
…purchase flow

Address PR review feedback on the purchase-credits UI:

- Use familiar consumer labels (MB / GB / TB) throughout the unit toggle,
  package cards, and success screen, with binary multipliers under the hood
  (1 GB = 1,024 MiB, 1 TB = 1,048,576 MiB) — matching how Windows and this
  codebase count storage. purchaseCredits.ts, Step1, Step4, CreditCurrentPrice,
  and both spec files updated for consistency.

- Replace the hand-rolled SHANNONS_PER_AI3 BigInt arithmetic in
  AdminUserCredits with shannonsToAi3() from @autonomys/auto-utils — the
  canonical SDK converter already used across the Autonomys ecosystem.

- Correct internal comments in credits.spec.ts (byte values are MiB/GiB,
  not MB/GB); rename oneMiB → oneGiB to match its actual 1 GiB value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a small ⓘ icon next to the MB/GB/TB toggle on the custom amount
screen. Hovering reveals a tooltip that explains binary storage sizing
in plain language:

  - "We use binary units — like most storage hardware"
  - 1 GB = 1,024 MB (not 1,000)
  - 1 TB = 1,024 GB (not 1,000)

Users who don't care see only a subtle muted icon; curious users get
the full picture without cluttering the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EmilFattakhov and others added 2 commits April 2, 2026 09:55

@jim-counter jim-counter left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM!

@EmilFattakhov
EmilFattakhov merged commit 4ae289b into main Apr 2, 2026
7 checks passed
@EmilFattakhov
EmilFattakhov deleted the ImprovePwAI3UX branch April 2, 2026 14:13
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