feat(ImprovePwAI3UX): admin per-user credit detail page with refund s… - #647
Conversation
…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>
✅ Deploy Preview for auto-drive-storage ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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>
- 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>
|
In a nutshell, this PR resolves most of the issues that were found during the initial testing. The main three points are:
|
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>
|
bugbot run |
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
…mula) Made-with: Cursor
|
bugbot run |
`!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
|
bugbot run |
jim-counter
left a comment
There was a problem hiding this comment.
Could we review the use of MB/MiB please?
…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>
7236d98 to
377c55d
Compare
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>
7af580e to
3f24725
Compare
Co-authored-by: Jim Counter <jimcounter@hotmail.com>
…upport