Skip to content

Conversation

@rostikmanko
Copy link
Contributor

@rostikmanko rostikmanko commented Jan 23, 2026

  • Added a new webhook endpoint for handling Stripe events related to extra usage purchases.
  • Updated middleware to include the new webhook path for authentication.
  • Introduced components for managing extra usage settings, including dialogs for buying extra usage, adjusting spending limits, and configuring auto-reload options.
  • Enhanced user customization to support extra usage settings.
  • Updated rate limiting logic to incorporate extra usage balance checks, allowing users to utilize extra credits when limits are exceeded.
  • Added backend logic for managing extra usage credits and settings in the Convex database.

This update enhances the user experience by providing flexible options for managing extra usage and integrating with Stripe for payments.

Summary by CodeRabbit

  • New Features

    • Extra-usage system: buy and manage credits with dialogs for purchase, auto-reload, spending limits, and turn-off flows
    • Stripe webhook and checkout flow to grant purchased credits
  • Improvements

    • Usage UI shows balance, progress, spending limit, auto-reload and payment-method status; billing actions gated for admins
    • Rate limiting now integrates extra-usage balances to allow continued usage and supports idempotent refunds
  • Chores

    • Added STRIPE_EXTRA_USAGE_WEBHOOK_SECRET example environment variable

✏️ Tip: You can customize this high-level summary in your review settings.

- Added a new webhook endpoint for handling Stripe events related to extra usage purchases.
- Updated middleware to include the new webhook path for authentication.
- Introduced components for managing extra usage settings, including dialogs for buying extra usage, adjusting spending limits, and configuring auto-reload options.
- Enhanced user customization to support extra usage settings.
- Updated rate limiting logic to incorporate extra usage balance checks, allowing users to utilize extra credits when limits are exceeded.
- Added backend logic for managing extra usage credits and settings in the Convex database.

This update enhances the user experience by providing flexible options for managing extra usage and integrating with Stripe for payments.
@vercel
Copy link

vercel bot commented Jan 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
hackerai Ready Ready Preview, Comment Jan 24, 2026 1:54am

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

📝 Walkthrough

Walkthrough

Adds a prepaid “Extra Usage” system: Stripe checkout + webhook, Convex points-based accounting and actions (add/deduct/refund/auto-reload), new frontend dialogs/UI and Account/Usage wiring, schema and user customization flag, and rate-limit integration consuming prepaid balance before enforcing buckets.

Changes

Cohort / File(s) Summary
Configuration & Webhooks
.\env.local.example, app/api/extra-usage/webhook/route.ts, middleware.ts
Env var STRIPE_EXTRA_USAGE_WEBHOOK_SECRET added to example; new POST webhook handler validates Stripe signature, processes checkout.session.completed, and calls Convex addCredits; webhook route added to unauthenticated paths.
Database Schema & Customization
convex/schema.ts, convex/userCustomization.ts, types/user.ts
New extra_usage table and processed_webhooks table; extra_usage_enabled added to user_customization and propagated in getters and types.
Convex Backend: Balance & Actions
convex/extraUsage.ts, convex/extraUsageActions.ts
New points-per-dollar conversion utilities and full set of Convex mutations/queries (add/deduct/refund/update settings) plus actions integrating Stripe/WorkOS: payment status, checkout session, billing portal, and deductWithAutoReload.
Frontend: Extra Usage UI & Dialogs
app/components/ExtraUsageSection.tsx, app/components/extra-usage/*, app/components/extra-usage/index.ts, app/components/AccountTab.tsx, app/components/UsageTab.tsx, components/ui/sonner.tsx
New ExtraUsageSection and four dialogs (Buy, TurnOff, AdjustSpendingLimit, AutoReload); AccountTab/UsageTab wired to show/manage extra usage with admin gating; minor Toaster positioning tweak.
Lib: Extra Usage Utilities & Types
lib/extra-usage.ts, types/chat.ts, types/user.ts
New EXTRA_USAGE_MULTIPLIER, points↔dollars helpers, getExtraUsageBalance, deductFromBalance, refundToBalance; new ExtraUsageConfig type and extended RateLimitInfo fields for points tracking.
Rate Limiting Integration
lib/rate-limit/index.ts, lib/rate-limit/sliding-window.ts, lib/rate-limit/token-bucket.ts, lib/rate-limit/refund.ts, lib/api/chat-handler.ts
Rate-limit signatures extended to accept ExtraUsageConfig; token-bucket consults/deducts prepaid balance (with auto-reload) when buckets would overflow; added UsageRefundTracker and refund flows; chat handler fetches/forwards extraUsageConfig.
Billing Portal Action
lib/actions/billing-portal.ts
Server action now returns billing portal URL string instead of performing a Next.js redirect.

Sequence Diagram(s)

sequenceDiagram
    participant User as Client (Browser)
    participant App as Next.js App
    participant Convex as Convex Backend
    participant Stripe as Stripe

    User->>App: Click "Buy Extra Usage"
    App->>Convex: Convex action createPurchaseSession(amount)
    Convex-->>App: { checkoutSessionUrl }
    App->>User: Redirect to Stripe Checkout
    User->>Stripe: Complete payment
    Stripe->>App: POST /api/extra-usage/webhook (checkout.session.completed)
    App->>App: Validate signature (STRIPE_EXTRA_USAGE_WEBHOOK_SECRET)
    App->>Convex: addCredits(userId, amountDollars, idempotencyKey=event.id)
    Convex-->>App: { newBalance, alreadyProcessed? }
    App-->>Stripe: 200 OK
Loading
sequenceDiagram
    participant Client as Chat Client
    participant RateLimiter as Token Bucket
    participant ExtraLib as Extra Usage Lib
    participant Convex as Convex Backend
    participant Stripe as Stripe

    Client->>RateLimiter: Request tokens for chat
    RateLimiter->>RateLimiter: Check bucket capacity
    alt Bucket would overflow
        RateLimiter->>ExtraLib: getExtraUsageBalance(userId)
        ExtraLib->>Convex: read balance/settings
        Convex-->>ExtraLib: { balancePoints, autoReloadEnabled }
        alt Sufficient balance (or auto-reload succeeds)
            RateLimiter->>ExtraLib: deductFromBalance(pointsNeeded)
            ExtraLib->>Convex: deductPoints (may trigger auto-reload & Stripe charge)
            Convex-->>ExtraLib: { success, newBalance }
            ExtraLib-->>RateLimiter: Deduction success
            RateLimiter-->>Client: Allow request
        else Insufficient balance
            RateLimiter-->>Client: Deny request (rate limit)
        end
    else Within bucket limits
        RateLimiter-->>Client: Allow request
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Poem

🐰
I hopped into code to stash some dimes,
Stripe jingled bright for extra times,
Buckets sip credits, dialogs bloom light,
Auto-reload hums into the night,
The rabbit nods — balance looks just right.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ 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 and concisely describes the main feature being implemented: extra usage functionality with Stripe payment integration, which aligns with the substantial changes across multiple files.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

Added saved_payment_method_options to createPurchaseSession so users can
select their existing card instead of re-entering payment details each time.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (1)
convex/userCustomization.ts (1)

90-107: Preserve extra_usage_enabled when the caller omits it.

Line 103-106 currently defaults to false on partial updates, which will silently disable extra usage whenever users save other fields. Preserve the existing value when args.extra_usage_enabled is undefined.

🐛 Proposed fix
-        extra_usage_enabled:
-          args.extra_usage_enabled !== undefined
-            ? args.extra_usage_enabled
-            : false, // Default to disabled
+        extra_usage_enabled:
+          args.extra_usage_enabled !== undefined
+            ? args.extra_usage_enabled
+            : existing?.extra_usage_enabled ?? false, // Preserve existing
🤖 Fix all issues with AI agents
In `@app/api/extra-usage/webhook/route.ts`:
- Around line 54-66: In the checkout.session.completed handler, guard against
unpaid/processing payments by checking session.payment_status === "paid" before
crediting the user: if not "paid", return early (do not proceed to compute
amountDollars or fulfill the purchase). Update the logic around the session
variable (and the fulfillment function you call—e.g., fulfillExtraUsagePurchase
or similar) to require this check, and also make the fulfillment idempotent
and/or add handling for the checkout.session.async_payment_succeeded event so
delayed payments are credited when they actually succeed.
- Around line 78-84: The webhook handler currently calls the Convex mutation
api.extraUsage.addCredits without deduplicating Stripe events; update the
mutation to accept and persist the Stripe event.id (e.g., parameter eventId) and
implement an idempotency check in api.extraUsage.addCredits that queries/creates
a processed_events (or events) record with a unique constraint on eventId—if the
eventId already exists, the mutation should return without altering balance,
otherwise insert the eventId and proceed to add credits (amountDollars) to the
userId; also ensure the route passes the Stripe event.id into the mutation call.

In `@app/components/extra-usage/AutoReloadDialog.tsx`:
- Around line 113-117: In the AutoReloadDialog component update the branding in
the agreement paragraph: replace the literal "Anthropic" string with the correct
product/company name "HackerAI" (or reference a centralized BRAND_NAME constant
if one exists) so the paragraph rendered by AutoReloadDialog.tsx displays the
proper brand; ensure any tests or snapshots referencing that text are updated
accordingly.
- Around line 133-146: The Cancel button in AutoReloadDialog calls handleTurnOff
when isEnabled is false; change it to just close the dialog by calling the
dialog close handler instead: add an onOpenChange (or close) prop to the
component (update ContentProps to include onOpenChange: (open: boolean) =>
void), accept that prop in AutoReloadDialog, and replace the Cancel button's
onClick={handleTurnOff} with onClick={() => onOpenChange(false)} (or the
provided close function) when rendering the "Turn on" UI path; keep
handleTurnOff for the turn-off flow and ensure parent passes onOpenChange into
the content component.

In `@app/components/ExtraUsageSection.tsx`:
- Around line 241-252: The UI currently hardcodes the reset date in
ExtraUsageSection by computing the first of next month; instead, read the
backend-provided reset date from extraUsageSettings (e.g.,
extraUsageSettings.monthly_reset_date) when available and display that formatted
value, falling back to the existing new Date(...) computation only if
extraUsageSettings.monthly_reset_date is null/undefined; update the JSX near the
Resets {...} line to prefer the backend field and ensure you parse/format the
date with toLocaleDateString("en-US", { month: "short", day: "numeric" }) for
consistent display.
- Around line 63-65: The call to getPaymentStatus in ExtraUsageSection.tsx
passes no arguments, sending undefined to a Convex action that expects args: {};
update the invocation to call getPaymentStatus with an explicit empty object
(i.e., getPaymentStatus({})) so it matches the action signature and the pattern
used elsewhere (e.g., BuyExtraUsageDialog.tsx).

In `@convex/extraUsage.ts`:
- Around line 1-116: Both addCredits and deductPoints accept unvalidated numbers
(args.amountDollars / args.amountPoints) which allows negative or NaN values;
add runtime guards at the start of each handler to ensure the provided amount is
a finite positive number (e.g., use Number.isFinite(...) and > 0) before
proceeding (for addCredits check args.amountDollars, for deductPoints check
args.amountPoints), and if validation fails throw a clear error (e.g., throw new
Error("Invalid amount: must be a finite positive number")) so dollarsToPoints
and balance math cannot be abused.

In `@convex/extraUsageActions.ts`:
- Around line 283-299: The unit_amount passed to Stripe must be an integer cents
value but currently amountCents is computed as args.amountDollars * 100 which
can produce fractional cents; update the calculation of amountCents (used when
creating the session in checkout.sessions.create) to round to the nearest
integer (e.g., use Math.round on args.amountDollars * 100) so unit_amount is
always an integer before being passed to Stripe.

In `@convex/schema.ts`:
- Around line 117-129: The schema comment says "all monetary values stored in
POINTS" but the extra_usage table defines a field named
auto_reload_amount_dollars which is inconsistent and confusing; update the
schema to be explicit by either renaming auto_reload_amount_dollars to
auto_reload_amount_points (and keep it as v.number() representing POINTS) or, if
the value is truly in dollars, change the comment to list which fields are in
dollars and which are in points and rename the field to auto_reload_amount_cents
or document units clearly; make the change around the extra_usage table and
specifically reference auto_reload_amount_dollars in your edits and update the
surrounding comment to remove ambiguity.

In `@lib/extra-usage.ts`:
- Around line 84-90: The early-return when pointsUsed <= 0 returns a misleading
newBalanceDollars: 0; instead, return the actual current balance or omit the
field. Update the branch in the function containing pointsUsed so it returns
newBalanceDollars: currentBalanceDollars (or balanceDollars if that variable
exists in scope) or remove the newBalanceDollars property and make it optional
in the function's return type/interface; ensure callers handle the optional
field if you choose that route.

In `@lib/rate-limit/token-bucket.ts`:
- Around line 162-186: The current extra-usage path computes different
sessionDeduct and weeklyDeduct values (sessionDeduct/weeklyDeduct) which can
over-drain the larger bucket when extraUsage covers the shortfall; change the
logic inside the block that handles deductResult.success (the code that calls
weekly.limiter.limit and session.limiter.limit) to compute a single commonDeduct
= Math.min(estimatedCost, sessionCheck.remaining, weeklyCheck.remaining) and use
that same commonDeduct for both limiter.limit calls so neither bucket is
over-deducted; adjust any variable names accordingly near deductFromBalance,
pointsNeeded, estimatedCost, sessionCheck.remaining, weeklyCheck.remaining,
sessionDeduct, and weeklyDeduct.
🧹 Nitpick comments (7)
app/components/RateLimitWarning.tsx (1)

59-62: Simplify the redundant ternary.

Since bucketType is typed as "session" | "weekly", the ternary data.bucketType === "session" ? "session" : "weekly" just returns the same value. You can use data.bucketType directly.

Suggested simplification
-  const limitType = data.bucketType === "session" ? "session" : "weekly";
+  const limitType = data.bucketType;
app/components/extra-usage/AdjustSpendingLimitDialog.tsx (2)

56-65: Input value includes the $ prefix, which may cause cursor positioning issues.

The input value is set as $${inputValue}, embedding the dollar sign in the controlled value. This can cause awkward cursor behavior when users try to edit the beginning of the input. Consider using a separate visual prefix element (like in AutoReloadDialog lines 76-79) instead.

♻️ Suggested approach using a positioned prefix
-        <div>
-          <Input
-            type="text"
-            value={`$${inputValue}`}
-            onChange={(e) => {
-              const val = e.target.value.replace(/[^0-9.]/g, "");
-              setInputValue(val);
-            }}
-            className="w-full"
-            aria-label="Monthly spending limit"
-          />
+        <div className="relative">
+          <span className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground">
+            $
+          </span>
+          <Input
+            type="text"
+            value={inputValue}
+            onChange={(e) => {
+              const val = e.target.value.replace(/[^0-9.]/g, "");
+              setInputValue(val);
+            }}
+            className="w-full pl-7"
+            aria-label="Monthly spending limit"
+          />

43-44: Validation allows multiple decimal points (e.g., "10.5.3").

The regex on line 60 permits multiple . characters, and parseFloat will parse "10.5.3" as 10.5, silently ignoring the rest. Consider either sanitizing to allow only one decimal point or adding explicit validation.

♻️ Suggested fix to handle multiple decimals
  const handleSetLimit = async () => {
    const limit = parseFloat(inputValue);
-   if (isNaN(limit) || limit < 0) return;
+   // Also reject if input has multiple dots
+   if (isNaN(limit) || limit < 0 || (inputValue.match(/\./g) || []).length > 1) return;
    await onSave(limit);
  };
app/components/AccountTab.tsx (1)

31-39: Consider adding error logging for debugging failed admin status fetches.

The catch block silently sets isTeamAdmin to false on any error. While this is a safe fallback, it may hide connectivity or API issues. Consider logging the error for debugging purposes.

♻️ Suggested enhancement
    if (subscription === "team") {
      fetch("/api/team/members")
        .then((res) => res.json())
        .then((data) => setIsTeamAdmin(data.isAdmin ?? false))
-       .catch(() => setIsTeamAdmin(false));
+       .catch((error) => {
+         console.error("Failed to fetch team admin status:", error);
+         setIsTeamAdmin(false);
+       });
    }
app/components/extra-usage/BuyExtraUsageDialog.tsx (1)

105-116: Normalize amount to 2 decimals before purchase.

This avoids fractional-cent inconsistencies when the backend converts dollars to cents.

💡 Suggested tweak
-  const parsedAmount = parseFloat(removeCommas(purchaseAmount) || "0");
+  const parsedAmount = parseFloat(removeCommas(purchaseAmount) || "0");
+  const normalizedAmount = Math.round(parsedAmount * 100) / 100;

-  const isValidAmount =
-    !isNaN(parsedAmount) && parsedAmount >= 5 && parsedAmount <= MAX_AMOUNT;
+  const isValidAmount =
+    !isNaN(normalizedAmount) &&
+    normalizedAmount >= 5 &&
+    normalizedAmount <= MAX_AMOUNT;

   const handlePurchase = async () => {
     if (!isValidAmount) return;
-    await onPurchase(parsedAmount);
+    await onPurchase(normalizedAmount);
   };
lib/extra-usage.ts (1)

1-4: Consider marking this module server-only to prevent client bundling.

This file reads CONVEX_SERVICE_ROLE_KEY and uses server-only Convex calls. A server-only guard helps prevent accidental client imports.

🔒 Suggested guard
+import "server-only";
 import { POINTS_PER_DOLLAR } from "@/lib/rate-limit/token-bucket";
 import { ConvexHttpClient } from "convex/browser";
 import { api } from "@/convex/_generated/api";
convex/extraUsageActions.ts (1)

403-426: Consider internalAction for backend-only deductWithAutoReload.

Line 403-426 is service-key gated and invoked from backend rate-limit logic; exposing it as a public action broadens the API surface unnecessarily. Prefer internalAction and internal.* references. As per coding guidelines, private functions should use internal actions.

Comment on lines +54 to +66
case "checkout.session.completed": {
const session = event.data.object as Stripe.Checkout.Session;
// Only process extra usage purchases
if (session.metadata?.type !== "extra_usage_purchase") {
return NextResponse.json({ received: true });
}

const userId = session.metadata.userId;
// Support both new (amountDollars) and old (amountCents) metadata formats
const amountDollars = session.metadata.amountDollars
? parseFloat(session.metadata.amountDollars)
: parseInt(session.metadata.amountCents, 10) / 100;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Stripe checkout.session.completed payment_status unpaid async payment methods when does event fire

💡 Result:

checkout.session.completed fires when the customer finishes the Stripe Checkout flow (i.e., reaches “complete”/success in the hosted UI), not necessarily when the funds are available.

  • For instant payment methods (cards, etc.), the Session will usually arrive with payment_status: "paid" and you can fulfill then. (docs.stripe.com)
  • For delayed / asynchronous payment methods (e.g., ACH debit/bank transfer types), Stripe can still send checkout.session.completed with payment_status: "unpaid" because the customer completed Checkout but the payment outcome/funds aren’t finalized yet. You should wait to fulfill until you later receive:
    • checkout.session.async_payment_succeeded (then fulfill), or
    • checkout.session.async_payment_failed (ask for another payment method). (docs.stripe.com)

Practical rule: Treat checkout.session.completed as “Checkout finished,” and gate fulfillment on session.payment_status == "paid" (or on async_payment_succeeded for delayed methods). (docs.stripe.com)

Citations:


🌐 Web query:

Stripe webhook async_payment_succeeded vs checkout.session.completed event timing

💡 Result:

  • checkout.session.completed is sent when the customer finishes the Checkout flow (they reach “success” in Checkout). For instant payment methods (cards, wallets), this typically happens at the same time the payment succeeds. (docs.stripe.com)
  • For delayed/asynchronous payment methods (for example bank debits/transfers), Checkout can complete while the payment is still processing. In that case, Stripe later sends checkout.session.async_payment_succeeded (or checkout.session.async_payment_failed) when the payment outcome is known. (docs.stripe.com)

Practical timing expectation

  • Instant methods: you usually get only checkout.session.completed (no async event). (docs.stripe.com)
  • Delayed methods: you get checkout.session.completed first, then (minutes → days later) checkout.session.async_payment_succeeded or ...failed. (docs.stripe.com)

Citations:


🌐 Web query:

Stripe checkout session fulfillment best practices payment not completed yet

💡 Result:

If a Checkout Session “completed” but the payment isn’t completed/final yet, the key is to fulfill based on server-side webhook events + status fields, not the redirect.

Best practices (Stripe-recommended)

  1. Fulfill only from webhooks (not success_url / client callbacks).
    Clients can close tabs or be spoofed; use asynchronous server events to trigger fulfillment. [4]

  2. In your fulfillment function, always re-fetch the Session and gate on payment_status.
    Stripe explicitly exposes checkout.session.payment_status (paid, unpaid, no_payment_required) and says you can use it to decide when to fulfill. [2]
    Stripe’s fulfillment guide recommends your server-side fulfill_checkout(session_id):

    • handles duplicate calls (idempotent),
    • retrieves the Session (expand line_items),
    • checks payment_status,
    • fulfills,
    • records fulfillment status. [1]
  3. Handle delayed/asynchronous payment methods explicitly.
    Some methods (ACH/bank transfers, etc.) can finish later: Checkout can complete while funds aren’t available yet. Stripe notes these generate:

    • checkout.session.async_payment_succeeded (fulfill then)
    • checkout.session.async_payment_failed (prompt for new payment) [1][3]
      Practical rule:
    • On checkout.session.completed: fulfill only if payment_status == "paid".
    • If payment_status == "unpaid": wait for checkout.session.async_payment_succeeded. [1][2][3]
  4. Track session expiration / abandonment.
    Sessions can expire; Stripe emits checkout.session.expired so you can stop waiting and optionally create a new Session/recovery flow. [3][5]

  5. Make fulfillment idempotent (must).
    Webhooks can be delivered more than once or concurrently; record “fulfilled” by checkout.session.id (and/or the underlying PaymentIntent/charge) and no-op on repeats. [1]

Minimal event set to listen for (most Checkout integrations)

  • checkout.session.completed → fulfill only if payment_status=paid [1][2]
  • checkout.session.async_payment_succeeded → fulfill (delayed methods) [1][3]
  • checkout.session.async_payment_failed → mark failed / ask customer to retry [3]
  • checkout.session.expired → mark abandoned / trigger recovery flow [3][5]

Sources: Stripe “Fulfill orders” guide [1], Checkout Session API fields (payment_status) [2], Stripe event recommendations for async/expired sessions [3], Stripe webhook-first guidance for fulfillment [4], expired-session recovery/abandoned carts [5].


Guard against unpaid orders before crediting.

checkout.session.completed fires when the customer finishes Checkout, but the payment may not be finalized yet—especially for async payment methods (ACH, bank transfers). Stripe can send this event with payment_status: "unpaid" while the payment is still processing. Without a payment status check, the code will credit users for orders that may later fail or remain unpaid.

Add a payment_status guard:

✅ Suggested guard
     case "checkout.session.completed": {
       const session = event.data.object as Stripe.Checkout.Session;
+      if (session.payment_status !== "paid") {
+        console.warn(
+          "[Extra Usage Webhook] Session not paid yet:",
+          session.id,
+        );
+        return NextResponse.json({ received: true });
+      }
       // Only process extra usage purchases
       if (session.metadata?.type !== "extra_usage_purchase") {
         return NextResponse.json({ received: true });
       }

Also consider:

  • Listen for checkout.session.async_payment_succeeded to handle delayed payment completions
  • Make fulfillment idempotent to safely handle webhook retries
🤖 Prompt for AI Agents
In `@app/api/extra-usage/webhook/route.ts` around lines 54 - 66, In the
checkout.session.completed handler, guard against unpaid/processing payments by
checking session.payment_status === "paid" before crediting the user: if not
"paid", return early (do not proceed to compute amountDollars or fulfill the
purchase). Update the logic around the session variable (and the fulfillment
function you call—e.g., fulfillExtraUsagePurchase or similar) to require this
check, and also make the fulfillment idempotent and/or add handling for the
checkout.session.async_payment_succeeded event so delayed payments are credited
when they actually succeed.

Comment on lines +63 to +65
// Check if user has a valid payment method before enabling
const paymentStatus = await getPaymentStatus();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find getPaymentStatus definition and calls
echo "=== getPaymentStatus calls in codebase ==="
rg "getPaymentStatus" --type=ts --type=tsx -B 2 -A 2

echo -e "\n=== Searching for action definition in convex ==="
rg "getPaymentStatus" convex --type=ts --type=tsx -B 5 -A 5

Repository: hackerai-tech/hackerai

Length of output: 232


🏁 Script executed:

#!/bin/bash
# Find getPaymentStatus definition and calls
echo "=== getPaymentStatus calls in codebase ==="
rg "getPaymentStatus" -A 2 -B 2

echo -e "\n=== Check Convex action files ==="
fd "." convex --type f --extension ts --extension js

Repository: hackerai-tech/hackerai

Length of output: 3248


🏁 Script executed:

#!/bin/bash
# Get the full getPaymentStatus action definition
cat -n convex/extraUsageActions.ts | head -50

Repository: hackerai-tech/hackerai

Length of output: 1945


🏁 Script executed:

#!/bin/bash
# Find the getPaymentStatus action definition
rg -n "export const getPaymentStatus" convex/extraUsageActions.ts -A 20

Repository: hackerai-tech/hackerai

Length of output: 745


Pass explicit args to getPaymentStatus (Convex action signature).

The action is defined with args: {}. Calling without arguments passes undefined instead of an empty object, which is inconsistent with the validator and the pattern used elsewhere (e.g., BuyExtraUsageDialog.tsx calls it with {}). Use await getPaymentStatus({}) for consistency and correctness.

✅ Suggested fix
-      const paymentStatus = await getPaymentStatus();
+      const paymentStatus = await getPaymentStatus({});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Check if user has a valid payment method before enabling
const paymentStatus = await getPaymentStatus();
// Check if user has a valid payment method before enabling
const paymentStatus = await getPaymentStatus({});
🤖 Prompt for AI Agents
In `@app/components/ExtraUsageSection.tsx` around lines 63 - 65, The call to
getPaymentStatus in ExtraUsageSection.tsx passes no arguments, sending undefined
to a Convex action that expects args: {}; update the invocation to call
getPaymentStatus with an explicit empty object (i.e., getPaymentStatus({})) so
it matches the action signature and the pattern used elsewhere (e.g.,
BuyExtraUsageDialog.tsx).

Comment on lines +283 to +299
const stripe = getStripe();
const amountCents = args.amountDollars * 100;

const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: "payment",
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "HackerAI Extra Usage Credits",
description: `$${args.amountDollars} in extra usage credits`,
},
unit_amount: amountCents,
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate and examine the file
find . -name "extraUsageActions.ts" -type f

Repository: hackerai-tech/hackerai

Length of output: 95


🏁 Script executed:

# Read the relevant section of the file around lines 283-299
cat -n convex/extraUsageActions.ts | sed -n '270,310p'

Repository: hackerai-tech/hackerai

Length of output: 1679


🏁 Script executed:

# Get the full function context to understand args and visibility
cat -n convex/extraUsageActions.ts | sed -n '260,320p'

Repository: hackerai-tech/hackerai

Length of output: 2376


🏁 Script executed:

# Check the beginning of the file for function definition and args validator
head -n 50 convex/extraUsageActions.ts

Repository: hackerai-tech/hackerai

Length of output: 1595


🏁 Script executed:

# Find the function that contains the code around line 283-299
cat -n convex/extraUsageActions.ts | sed -n '240,265p'

Repository: hackerai-tech/hackerai

Length of output: 1095


🏁 Script executed:

# Get the function signature and args validator
cat -n convex/extraUsageActions.ts | sed -n '220,260p'

Repository: hackerai-tech/hackerai

Length of output: 1603


🏁 Script executed:

# Check if amountDollars is validated as integer or number in args
rg "amountDollars" convex/extraUsageActions.ts -B 5 -A 2

Repository: hackerai-tech/hackerai

Length of output: 2080


🌐 Web query:

Stripe checkout sessions unit_amount integer requirement cents

💡 Result:

In Stripe Checkout Sessions, line_items[].price_data.unit_amount must be an integer in the currency’s smallest unit—for USD that means cents (e.g., $20.00 → 2000) and it must be non‑negative. One of unit_amount or unit_amount_decimal is required (and you can’t set both). [1]

If you need fractional minor units, use unit_amount_decimal (a decimal value in cents with up to 12 decimal places). [1]

Most Stripe SDKs enforce this the same way (e.g., Java’s setUnitAmount(Long) is “integer in cents (or local equivalent)”). [3]


Round unit_amount to ensure it's an integer cent value for Stripe.

Line 284 calculates amountCents = args.amountDollars * 100. Since amountDollars is validated as v.number(), it can accept decimal values (e.g., 5.75). Floating-point arithmetic may produce fractional cents, but Stripe's unit_amount requires an integer. Use Math.round() to ensure an integer:

Proposed fix
-      const amountCents = args.amountDollars * 100;
+      const amountCents = Math.round(args.amountDollars * 100);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const stripe = getStripe();
const amountCents = args.amountDollars * 100;
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: "payment",
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "HackerAI Extra Usage Credits",
description: `$${args.amountDollars} in extra usage credits`,
},
unit_amount: amountCents,
},
const stripe = getStripe();
const amountCents = Math.round(args.amountDollars * 100);
const session = await stripe.checkout.sessions.create({
customer: stripeCustomerId,
mode: "payment",
payment_method_types: ["card"],
line_items: [
{
price_data: {
currency: "usd",
product_data: {
name: "HackerAI Extra Usage Credits",
description: `$${args.amountDollars} in extra usage credits`,
},
unit_amount: amountCents,
},
🤖 Prompt for AI Agents
In `@convex/extraUsageActions.ts` around lines 283 - 299, The unit_amount passed
to Stripe must be an integer cents value but currently amountCents is computed
as args.amountDollars * 100 which can produce fractional cents; update the
calculation of amountCents (used when creating the session in
checkout.sessions.create) to round to the nearest integer (e.g., use Math.round
on args.amountDollars * 100) so unit_amount is always an integer before being
passed to Stripe.

rostikmanko and others added 2 commits January 23, 2026 09:28
- Add monthlyCapExceeded check in deductPoints mutation before allowing deduction
- Propagate monthlyCapExceeded flag through deductWithAutoReload action and deductFromBalance
- Show specific error message when monthly cap exceeded, recommending user increase limit in Settings
- Previously, hitting the monthly cap would still allow extra usage to be deducted

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ents

- Enhanced error handling in AccountTab to provide clearer feedback when failing to open the billing portal.
- Introduced a new function in ExtraUsageSection to determine color classes based on usage percentage, improving visual feedback on usage levels.
- Updated various dialogs in the extra usage section to ensure consistent formatting and clarity in user messages.

These changes aim to enhance user experience by providing better error messages and visual cues related to usage and spending limits.
@coderabbitai
Copy link

coderabbitai bot commented Jan 23, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/hackerai-tech/hackerai/issues/comments/3787548040","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- This is an auto-generated comment: failure by coderabbit.ai -->\n\n> [!CAUTION]\n> ## Review failed\n> \n> The head commit changed during the review from a7d622e9828b08dc14b0031ee4ab7c3e8cc11170 to e2a82d790b070b80c2bd12e522da967ccc3347ca.\n\n<!-- end of auto-generated comment: failure by coderabbit.ai -->\n\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nAdds an \"Extra Usage\" paid credits system: Stripe-backed purchase flow and webhook, Convex-backed points accounting with auto-reload, frontend dialogs and UI, and rate-limit logic that checks/deduces prepaid balance before enforcing standard limits.\n\n## Changes\n\n| Cohort / File(s) | Summary |\n|---|---|\n| **Configuration & Webhooks** <br> `.\\env.local.example`, `app/api/extra-usage/webhook/route.ts`, `middleware.ts` | New STRIPE_EXTRA_USAGE_WEBHOOK_SECRET config; webhook POST handler validates Stripe signatures, processes checkout.session.completed events, extracts user/amount metadata and calls Convex mutation to add credits; route added to unauthenticated paths. |\n| **Database Schema & Customization** <br> `convex/schema.ts`, `convex/userCustomization.ts`, `types/user.ts` | Adds `extra_usage` table with points, auto-reload and monthly tracking; adds `extra_usage_enabled` to user_customization and UserCustomization type; updated customization getters/save to include flag. |\n| **Convex Backend: Balance & Actions** <br> `convex/extraUsage.ts`, `convex/extraUsageActions.ts` | New points-based accounting (POINTS_PER_DOLLAR) with add/deduct mutations, balance/settings queries, update settings mutation, and actions integrating Stripe/WorkOS: payment status, purchase session, billing portal, and deductWithAutoReload logic. |\n| **Frontend: Extra Usage UI & Dialogs** <br> `app/components/ExtraUsageSection.tsx`, `app/components/extra-usage/*`, `app/components/extra-usage/index.ts`, `app/components/AccountTab.tsx`, `app/components/UsageTab.tsx`, `components/ui/sonner.tsx` | New ExtraUsageSection and four dialog components (Buy, TurnOff, AdjustSpendingLimit, AutoReload); AccountTab/UsageTab updated to surface flows and admin checks; toast positioning adjusted. |\n| **Lib: Extra Usage Utilities & Types** <br> `lib/extra-usage.ts`, `types/chat.ts` | New helper module (get/deduct balance, pointsToDollars), exported EXTRA_USAGE_MULTIPLIER and types; adds ExtraUsageConfig type. |\n| **Rate Limiting Integration** <br> `lib/rate-limit/index.ts`, `lib/rate-limit/sliding-window.ts`, `lib/rate-limit/token-bucket.ts`, `lib/api/chat-handler.ts` | Extended checkRateLimit/checkAgentRateLimit/deductAgentUsage signatures to accept ExtraUsageConfig; token-bucket now consults/deducts prepaid balance (auto-reload support) before enforcing buckets; chat handler fetches/passes extraUsageConfig. |\n| **Misc UI Messaging** <br> `app/components/RateLimitWarning.tsx` | Updated wording to use \"session usage\" when appropriate for session buckets. |\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant User as Client (Browser)\n    participant App as Next.js App\n    participant Convex as Convex Backend\n    participant Stripe as Stripe\n\n    User->>App: Click \"Buy Extra Usage\"\n    App->>Convex: createPurchaseSession(amount) (action)\n    Convex-->>App: { checkoutSessionUrl }\n    App->>Stripe: Redirect to Checkout (session URL)\n    User->>Stripe: Complete payment\n    Stripe->>App: POST /api/extra-usage/webhook (checkout.session.completed)\n    App->>App: Validate Stripe signature (STRIPE_EXTRA_USAGE_WEBHOOK_SECRET)\n    App->>Convex: addCredits(userId, amountDollars) (mutation)\n    Convex-->>App: { newBalance }\n    App-->>Stripe: 200 OK\n```\n\n```mermaid\nsequenceDiagram\n    participant Client as Client (chat request)\n    participant RateLimiter as Rate Limiter (token-bucket)\n    participant ExtraUsageLib as Extra Usage lib\n    participant Convex as Convex Backend\n\n    Client->>RateLimiter: Start chat request (tokens needed)\n    RateLimiter->>RateLimiter: Check bucket capacity\n    alt Bucket would overflow\n        RateLimiter->>ExtraUsageLib: getExtraUsageBalance(userId)\n        ExtraUsageLib->>Convex: read balance/settings\n        Convex-->>ExtraUsageLib: { balancePoints, autoReloadEnabled }\n        alt Sufficient balance or auto-reload succeeds\n            RateLimiter->>ExtraUsageLib: deductFromBalance(pointsNeeded)\n            ExtraUsageLib->>Convex: deductPoints (may trigger auto-reload)\n            Convex-->>ExtraUsageLib: { success, newBalance }\n            ExtraUsageLib-->>RateLimiter: Deduction success\n            RateLimiter-->>Client: Allow request\n        else Insufficient balance\n            RateLimiter-->>Client: Deny request (rate limit)\n        end\n    else Within bucket limits\n        RateLimiter-->>Client: Allow request\n    end\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~75 minutes\n\n## Possibly related PRs\n\n- **PR `#177`**: Overlaps token-bucket rate-limit changes (checkAgentRateLimit/deductAgentUsage) and related logic.\n- **PR `#52`**: Modifies `lib/api/chat-handler.ts`, intersecting with this PR's chat handler integrations for extra usage.\n- **PR `#21`**: Related Convex user customization/schema work touching `convex/userCustomization` and schema additions.\n\n## Poem\n\n> 🐰  \n> I hopped in code to add some coins,  \n> Stripe chimed in with shiny loins,  \n> When buckets burst and chats would sigh,  \n> Extra credits let them fly,  \n> Toasts and dialogs, all set to play — hooray!\n\n</details>\n\n<!-- walkthrough_end -->\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 2 | ❌ 1</summary>\n\n<details>\n<summary>❌ Failed checks (1 warning)</summary>\n\n|     Check name     | Status     | Explanation                                                                           | Resolution                                                                         |\n| :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning | Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |\n\n</details>\n<details>\n<summary>✅ Passed checks (2 passed)</summary>\n\n|     Check name    | Status   | Explanation                                                                                                                                                                              |\n| :---------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                                              |\n|    Title check    | ✅ Passed | The title accurately and concisely summarizes the main objective of the PR: implementing an extra usage feature with Stripe integration. All major changes align with this core purpose. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKPR1AGxJcAks25ebBi4kCQAHrhUkNiIaKSQAGYk1NgUJJAA7uqwkADKkfDc6fDBJERU4vhYABS2kGYAjAAcAEwAlJCQBgCqNgAyXLC4uNyIHAD040TZ2AIaTMzjsGgMANbK8GA0DLBLK+tU8OPc2B4e480tnQYAgniw+BRcFPiI4qvMmKv417n4qQx0gIqBgdlwktRxuFImgwDE4ulAEmEMGcpBCwMwYMgnxKv1wKTG/CKWEAKASMNLUOiQFoABhaADYwDSGmAWgBmaA0mkcFoATg4NIA7AAtIwAYWWGFIyBKkUU2EBtA4BigN1oSnoaEg5AyeQKRUyJAED3wq1CGFo3HwssgBEgktoXlCUmCyASj1CESi8PiJwoOzQiGkGhVkG63FolPozHg6q8GWc6TtJQYHmwSltsHSGSNJrN3GoOXdfDQ93Y8AY1HgVRDqvVVO6PgU/iq7DdHs+GDiJSInph0VipC4tHgaA8+CI7b4Amwsh7fe9g5IABp0LQhDFxFL7MSR9uPPAY7hEKvMPQmBgEvAiKl56WCGA0uO0PR8NxKhhELXIABRCJkDUB0oRhNxYeAAC8qyqW0fjeR50jPexUgSFZ0mhRcEXsEhhh7L9Q3DSMaHoCp0gPI953HaYGBgxgszWBctR9IEx0xaR0AtdAznwXULwcfwP34BIGPJOh1GQDIsywMixPQNJPUBOg6G/PwAhIIIiMgAR9gAyBKIrGjO0w9DGKXESR2Pdj6CDHCpRlLBcCzSAxSqKQwkgQiVEDEgQwMKxUitIMuF4fAJFjdIEi8MJ4AEJ1jLhUzDNIdTLL1ChCnSAtZGS2Uygqat7J+MhJUBIC+HCIo0rIQEfIsJyWCPbFpCXZA+M+FwjDrQDYikWgAH1MvU3q2AcxRerfD9kDtBgKRoPz/WWINcia/L7FgtAesYZxaGQRN7AeDIsFxfI0oNCVRG+PBTy4rJtxiShJtgkgvDEdjPSQLdewG9hGpG4jC2AhzMEgNIwHYSh50rCh6CUfF4A8PCVTAQwDBMKAdPwIT70IUhyBI896vYIK+EEEQxEkNi5AUJQqFUdQtB0fQUfAKA4FQVAgax4gyGUDSFnU540F1VrnHkSmmGplQ1E0bRdCRowmdMAwNDICQNHHSsPGVsI0H8LxlQAIkNgxapuHwudxqMkKcFxBNozBpQ6yA1UArUdVS9LHyey2VfgF4MGSiRnFHGL0lyaAbB8Kwf16n8AA1w5uXrulyG4AHFo4AdR/AAhAAJAB5fOAGletyH8xRsH9oAAXhSrVUyDhI523DB8BoTNqFXeAhJiedjvS1cHOkdJA7SlQvBah5Tk1eGfgEBD63oO0h7qjBXMgAARQNYAEfAtvNUK/YDoPx+DR2ADlCqit4KJKNi0mYEK6AAbn4DAPHkF9zPyhHzEsG4PA0DylUB6mZ0hKAbsAz8ttyqPA0h6E4MV9LlnENIR21RL52ylNINof9IAAFlMDd2kCEAAYnDdINwuwf3ApQIwfQ77IADNgpUkAADULRxhMiMD+G+nxeaKHSGkUKJBdQkASMWTgBDRKOAMIbfWHVFZoG4NwcYyijhxSYuMHMxp8CmnGC8PA3ljwGyNibM2OMeZUmFjbDGWCHahmdjtbUojIDn2hBoIQyBDHtwdF4EsIQrD5zDpANR3ANFelhFonReYaLBUBIgZA/cDQxL0WaEgLoLLFmEkxHgAIFpnygAANTHLGSkST9TpEQNeLsuBUjpF7tuLUqTTRYWmthRILxmAH19lUY+Y8Q6IFfpQF4FBkD/BCFkBykAAAsXIUBCRjIk+cHoSiBwPLQb8NgSAAEdsC+zYmHCOUcY7xxsInZOadM45wLsXUu5dK7QGGRQUZ4lsiQAAKzzO7tqNuChLzXnqZs0MzlPyRAVBZZJaFMmQFClqN4J1vItNWF+Xi4KxA/hhYhPxbEqgf1oudCZGggzLJrAsVSGkMltm/KQj0T4MmYBCCS6pIDIDVGGi+agaANC4FkAaauArID62Mr1Ji/V8leX1m0VcxkxDIDuhQHwmoOI63+METpLAfqcvxGyhwKi4EyDbjkVV2Bggb3wGcZwO0VWP1NbgMUbZEiPH4YgNo34SkbPKaVJVddbXBGea8zI7y5k0jfigNepTozYW1dy0MPg16mjYqC9ezA8BQRqOorWMJuhLi0OqMUaRzKupol/MyMlskrwVauRpvYxT53PkUuO9ybBFJ8GKaONh859GjkXH8ABNb8uczwT2xGmgSIzHgyHkJRaYTSOJpDqRQDA84vmhuXmlIgpA+BQuBthSqeEoDbMXdAgAUrketu7EBWk/GxEoI5KwfSQgwBJ4yyovMeKuKZxqVEvF4KOduudoDQCsPYfEdSmGCIPZAeNqZ0xsSUDOIgUJ318BnfObJ1SiC1PqSgRJ2BpCriWdU7cF4rxEC7hGjZWqPKng4qmsDK0UJw3qVBwhsptDQKUChU4IQ0hXpAekOxABvXdgJyasPBekAAvk6vgrcMBgH4TsecVLXT8BLAkIBT6X0JFODwF4L6ew+TwQAoB6bQErwgR4Zw5mYFhCtBQeBfBEEHmoig+AaDQz4Owg8TUi8uAAANYGOfQIgWQoJEimrJtBIJYdqhpB2VwdxERtl7JIW0AL4bEgUMgAF5Rqj1FQkifFBE2jcxpIMRM4xiAAt4MIcupIbxIDkKdFQscshaEUHoYw+xdAuBsN5LyLhNIeF8MtuLIRGSPNiIkXArg3mRyyPkYosARh8vjHJa2V04wbjPrVbgaAKgeWIDCKYhR5jzZWKso4Nq8g7HMIcYekgARULIC46WQBkBtkrBCIeBzkz3ldjYPQP7BrZP2jSUMr7yQXqoFbrpKoW6UAtkc1SQM6B3LiI+5oLonUHqBhCK3cQV4H0/11VUXGHREKICmTsCmcMDzbn+2OdA0XDoo5CAkLpGP3s8ZLdqHWaODzo/i6JNIYhoD4GzgznsVg4Fjl2x+N1cbggvFoAqNirtXE5SXSzjIVAVHAV06CAShaDkS6lzLqUcvHMs8BiEDW8MwEuN1D9la1P9rOIIATsNE6KBbOe9ZhJ7lzchFyXYlevApv/BkFbz68uPDI/+0G6ZK83Zu6qGALSQZ6ARW4q/LU6sWdm/Fwdy3ZxZcJ8iyblaqA7r0AkpQJMjk1AV6Z1X1Ab5ubAqgBKe2bEV7S7b72G3+JE8FE3cBXEK8rBoCyt9IMbONXdKqGKVzqxq5CeqB0auehd0jlL5LofjOiCj7HNvmTdpV/r83yX0QZfj+V9tx4KT34nG2mSMwMAL4YxYCSLgHYBHKiFKSsDAerBER/EjFgE4BjKoU8CRe/CGKob+KoYvACcGbcbJVvE/bETABEdSN/dUa1cNIBahJPOBcHH/EoT2azDSCERdTXDicqM8bxL2DSRsIA/SL9CZexecFeYLARFscgdVTDbDNIYzY2f+QBHmMnZeRyKzGzCaOzf7KkBBWYVzc0cQVBRAC+H4FzCsIrFQ2gUGYIdQeQUQlIPjXrTZNlXbJgO1Q7AQZsa9b6AQ3dHEaBFeWINgDQXBSQghIhRrMhHLNrGhOhAwBhcgJhSUUgVhNhBoGZLhNkUbcQfhKkCbXdERGbSRebGRZgORI2RGVbAwdbTbYQ48cYP8bNJcJaNnY7U7Ao87f+CxbmPGK2W7W2B7Tzd/LXXUb7F6Mo76KoqgHNBEWogSaoAQugKnEIUo6ArbCo4YtAUY0gcY/KeojQGARyQY9VLIKw5NcISANLfdQjMdH+WjTUNnUBRKdIQAHAJjIBwERABcAkSGSAYJPHDVgz3CIHBGwmU1uiDD4AYFAhjEggEkQgeNyWsg+ihwqmqRvhIxiOkALw4jWVNDvCuPBy+hELAxiFXD9ADCDCwlJQwAuM0jjx4AT02J8BCA8NhmgWfG+NAy9QrQnCIBP3xIlWI3IxSm6nnGhNwjJIvDlCd3dFSBDzHAnEJGgFSAwHzgkSWJWJIA3lHEolXGzlnEVKXBVMlJ5LVA3DeFyF3B7AYSPB1LVJSjuAIG2UZPNInE2OcjknnmWFClWVBDTCUEJDIHHhU0iUePiDxXkC/Qx3WVjB4Dn2SmGl81flI19l/17GPTAAxiEjhQlMolfjvzJm3Cmjoguk5zUPmi8ih2wAjCrCaTwEICfD3mVSsmNP3EPHUFhVHE0m0g4kdyhzSAtAwN7C1A4ODK1E9OmkKHECkHDKoHKGUWNRVUgD6GSCXWxHgl0hKDNHtyJDIEmhbwpOZw8FfhHCvWs2bh7J3AAnnGCnKCahbIoFXCvRPPrIamFJeHhlXC0ms1BBXDrgrJoOrOZPAxRIb19nnBHF1OcIWNARxTGRSkZP5LAzPn8NMxkNZTkPAVEGsygXGSEimNfGc3UOQVMO0IvlcUws0LMMCy1LGMQKqEy1xEfnVydDyxUQ23mPKMQEqMiSVLWJrGPDCEy2DIsIYNyzRV/DYpqIoowC4G30gF30gDPVjg0B/ECHYEywhPszgSpFTKCxUpCxEzItWNEsgCkwCx8kiLYi6LiLZEFEFGG0aJWzWwYp2IqJsEpFNPUAzmcGXSlHqLOzgpaItg0hsTuyEi6J0IMHjWdwIHWAUxnDWA6QTCXXnAxFBFgFXGkgOz5XSCCuXyFUjDhlkH1hon1mZXyjysbywCivWFSoNFQAKuWiqDyuqCNUoCyCJIfg42QH1hzBIFWA/ilQdIEzS2CA/kIyagRFYKSE7IYHnFyRSuQFbl1EHLSnniFUKugiYjypKDeGSFfCEn1myvxVWpSmqpJP9Mmw8J7DWrBU2tth2u0D2tMhapKDOvBzT0z06x+HuvctP0oEBHVUrCDE/QQCdBXles9lOpIy8l+V1DSDGqqjYkOpZSwCmobNwHOo2pfCut2vkERqPH1gkJNmkLQpoksxQsULJzsSIrUKQTczwo82CswQyrtEwuOBwqpq0PkAULQp8mAE7GITeF6ivC8EMALDWARE8UQCqBRnGG5qCL5ooW6yiOsP6waC4QaBSMPHG0EUyOm1CFm0c1yMW3yOWyKNstUXspYqVMcM8usou0sTaP8s6KROCqe0fg2iJu40+36N+w5yy3NqOy4tflmrfnxVB0c3lUWhgpSjuh/AQJei501Q9sIMAmDvzL4B0pIA4owGUlV3lEVH+RQLIM7OphWSElTvTvB3ky2E/yQgEEQCHPfHOL2m4nnEbJKqrprpOgEhAEFX1hoB1hxq8xKA9GLH4Q+kYtYG+i/nUHd0MQ4jT1cRLr0qYn9p+Hpp+H4OvkfSmonH0g9A8leIAIQEwPHF1HnlkGQOdwvDzrQK7LSg8vljgvxtsyQsx0gVszJs0qczyUpuIvwtDEvnIDlpMqRLiI+WVtVrSPxgzGES1vERyOkX1usqNpKLsqYrbCKxhBK1IB23XE3CNNvKIGctwDtKIEtuW2ttaMtjtvuwdqMB6Jd0YAPHYDAGqQzA9rTInBAvKKdmwcNLrPwaRqIZT2NSrvsvB2FPYE2LgHAVVPYZWEBHfHGWJB/PfK1HAsJrZKdC71JNekDHC2oiqFyHWnSrHA8C0jWFPARxfHQ2sx5MQiBnGnyhZ2BJeW+hSoke2KqBoHVQevEFKVoRlAshKBgKUcyqcc7NwAIfNUtQgugj9VwHxJeFCk9NtAyB+Az0/C4GsgxwwEcHBmohSvUywjD3fiRqUmgwwCCdiGXXEDBJWgRUKBmsz2ybYDSmogDCoDEHunkmexCFoDblXFDMIjYnLvICICrFHKadyfDRgLJL3NPg3KqUMdZwEhbqgu3DdIGbcfSA4ILs6ZUc2soFXHmuHPynMYmZaambwH03EXgDCCpH7PcgtVQvsBqVXCzA8Aqg/wiDJNwBSaWZWhnGGFZWqCWhCDtFNRSroFXBBePItFviPA6GDNWd7DeC9Shueg0lxABYICkhUCel/nvrMyUKfvZtfowvftUOwq/vc08ygD/CMOrzZ0C31Jwd4YiekaICoqwBotOHSHopNpQe200SXCwYNNwFwdhalDZd1PqNqwiJ61MsVoaE4W4QMF4VSPVsgam0Ip1qkQW3gCW0KIgGKLmKENQaFdKytPwBtOrKIZIbMWaMu1tpuxFntv72CvjTlHV2D16PoY82CCYbCmhx+w4e+ktetZfFta4upJCGCkSc1wXMjETyAsolEaqDI1vHLIfCrLRoFNskEeSZ+DOf0kCbwGQGqAcj4weA8BrPQFiY6G8dVIgipFju6WClGGjcxyvHlp1zIJRfbnLdgErYtRrZNWCA6ELo2hbZAmcfVXWXww+ILDGTYgVQuZCGggcAECPCFOMfGQwAMakAHaHercidQo+NHcIceatTdWgyZRLP+0ml+Zoopi8lfEOkQB/C7BDlYRbu9K/cHkcndDbmAkQE9xgFlMEkxg4n3ZIFfhbtmb/aclYkT0QhlPnOgl2hA+4gzpvc+I9PjaDEXcpBIMoDILDaehtfZdBU8YdwFdBeWBCBxWQA4M22qQmKzBfAOcxzbuObgNk26SvCem2leaeg+ZoC+briuKp2nP13y2AjI9tPZY7l43QIgss0U5bs0Y7b2OXe4DfnzmJD72wS7kQD6GrJ7CFNSDCdhTHHw30zfAs3cdKGCE2L8ipffq4Hk4o+lbvrxsJdkNXvkOJoJrfvpYpo0Opcduayi3BP81y084jfZc5eyzopNZcMFeKy0Xi9oEjZO0y0mPJdoD8Nqnqx5uCNa2oQ63COMuiP7ziOaBpCstVbGwEU1ayO1tgb1YNYUUQdS9ArQaoAwZIHGA1NkFTpy4aNIYdZtooeddsUCuoduCIKydcVTD9dwADZYZhxo9NfVRG7G8U+yQJIWl9P7FyXaSLU2LpbB3hR7CdH0Nado8gD2+EoRFta2KkeAp2Ygvrg8YX1mBEee+qNe8o9+/VVXJuLslIJZz7fCg9Ejy5IQlifxIjO+ijMUB/LxMgrM+3Bh6/EgCLhIFFhIBdOrAgpTDw+VFDFpQoBdRSgGa9Xh8LKJPPfzd/0gAABIPkUpPg3J2fBteRlx+fDmkAXtDzw1Gd0hf9Ja0A3I/coMVT9y59nEDxRzKGyXTvTJEICAx93JbPs9VD7JHIwZC1a39saU/iswmFLPvpsTaSfN0e+2Ygw1YmmytQ0RZ959gh8gCReTQOSBsgAZHJup0j95qgEraBkqCcZkOgPQtRbjIizRKYoUXj7GqhX5ye4NiCfxzJNI8BsWlO1zoEtRsDTyq9lrDosAfX8Q5gQUZp42S/29n9iT4a2U88MggoUfgghp7fCvXeRJKRIDT8E8lojrEI1lI0vVKw6d6BbefpfNohr1tMmpB1h02Ijvwa0m++qg5pCT3z7n4OvArJw6Vnsfex1nI1Te7VX5MOMhkB4llocfw6xPJlJI8kmejNQwrAEmwpQ7KAwAUIJq24G8GUjfKfNZi05WMjT3TQF8V4bTeIFkDOCaQEIKiBhj3gCLsZ1qhfDbOOCJLOl1opPTMIYiIA5BI8iYXbpqRe6kAiGq4YsLFSZKaNgmiEKoPpzICGdSAPkVzuF3c6hhAeIxbUuyxMwP0iWAXZCi/SUIhdVKWFT+uF2po0s3EhFArt/U/ixcAsvA5YvwN1JJc+Wo9NLhUXNaYM1BSpcbrK2K6BESEzWEIhV06wAMauLCfrOyDZBWVDaRrY2joL676ChuqHOUgqUoHKl2WdrJok7B8pXZ2iLrKhm6xoZLcfWq3Rhsw3SCsMRGXg+UgkH27AVqA6AZBjtz0EZdhWSQnwUDyoH+Co273ENuqi+7OIaKLOARvc3Y7UxDm0gWuh+DJJagfmhALFtBEA5AIXOv6OyF8UEyKMw+eiLwJgGlR6cDOSJNlKo2XjqN+hZAJRqMNXxps4ybKHRhFjSajCx+JnU/myiRbzDNmiHN8onnaFYBUwrwAfPIXZbC9YgX7TIADVIin9X4keNKB0WOG2gN0W6XdqCivA09I+88Q/kKi8EQc8qHofWF4KLoJANAkIvKvr1fa4ZTOljKUM0Nngh5rh/w9XDfV7BIt9hHBZ/pkDcobl0hx6cETklMgIDE8UeGFLpkcxZg+AuSE+mfX15Lkjw1qaMHPmI4vISya7KUNaCgJrx7o5mfYSI1QBEV0ceQlIb4NtZ31f6rie7p6H+xcAxRqQlNsbiXxCjK+mQ3QSxQ8HjBFREoooSdmlG6hZRbhPivUi4BuERMuogoX4OAoyZcQvXZiv1yiS5DZSyQpUfaS4ryxTBDWcwS1koRWCqu8rIBv1g+RNBGuarNWi10mxtcYGc2OBvqwNqGslEGo9wTkNKx3pwgx2LymQ18rWJZuAVXrO6yzpeshmMo5mnKLBz2iUxjo7URmLCDHZwcYpPgMm3Yam1VwIMAQnwSzD8ducQ8X2JeiKBkxRy3LCeMqBZiuj8hfA4HrqR4EUDrRRDRxNwzFast+GAg1UBWXDbZcBBqrFYDkAl5rhtoPOUPBWJCxTsZII4jKIWBlSfscCvEHJqpx+BJ0wERJFsZOEyorw6xl5J8LjSkJ+dEKIg5+qhVJYniP6xo2QZFyu4hYv4fWXLFaKnGFDNBmVC8blg0A6iJx4o+cYlyyzaDTaTowbuMDrHHZastLd+geJgmqC5x8Em0ZREyxTtkJhlYbpRPUHTiaJ2Eh0WazTGYNCJx4YiUJWTzQTWEAWZljwzwZStWJdE+UHRVQnCTlxok1cYhNxA4TaOWoziUN24k1ZQwkE2YioKy5ENaJ3OeidJI3HkcEuCkrAEpKyEqT0GWidSSYMsAlcgiFg8ru1msFyt5aCrdhIkRVYRjwGVMaMdAx1Z60ExCDFwQYFwn7JxgYtDALjACHeVHWM3a2AWKCpGBtkztKkETUQCrACAunZMpkynbPUFMj8VvOkElwE5gIAUSelUAohb0GAq4SsDWkgDu9XglUjAF/3s40QF04HQMpSVY75QuA+sXeICy/wbohgxVbIDwTsYSJrI/1HLEVJyx4DXSfAd6vKkSq1c2U2UsAN9S0z3NspEHayNe0kbSD9IpUjakCUe6mi5IdeLsNHSIivxupH40oLrnJHNSBIJMe/GXUzxzSnQqAapLrG7geZ6AlMMCDZATKORcpHSNtpQF5Q/inYQg/zs7hJbiCyWoXSljIK0I01dCR0hgIYUkGASSarKHMHJBor/SlIdWMwU1j9FOwAxXWNyYA1q79YGuYAFWk13VZRjNa2rDrnkRCmKwLwrkJ0UqSzFW0pu5DPyvmNdbYJgqtDN2AcTciAkpAfAZCekN5nhB+ZuaCyKuT+wKU1MWoK0LKEQBZ4X2LZV8iVDCwbVukwZXplE31kPhdZamZWWMnroZ9vilPXvNb1BDyA3mcJLgGikZQsj/k8soBFOFAyPAm6WAW2RZFqD5wfA58aALkF6hRwbAvUDeF2j6A3A7AtcBoDSGXBcgaQowi2ZezGSS45ceslKOHMQCS4T2VqaIOIAPDaFvw2cFiKAJuKRkziICZUJ0CgBfwC0okY8NUDlkVgSABPWQNWkBJKpTwsTSuWMg6BIxIAImHUA3ONneBtQjgeeHwBkxwQMRl/NTLiDLkpROpcVbMo5BLKDNAZjckqLiEtmnsQwHczHF61wDFzXQfcygKFEBBDyR5lAMeVvPvk8jjw08vfCJgcDPomoq4eeWfJIAPzjwIC0RAvNYiTyPi61bABIgrBrdSEpqQ8TJmDKPxggsAfFDCDWDzh2UHjHBbIF6g3ku+ZcwjEQo/i9Q+M2EXqIMyk5WQ0A/+eQHTjWBByEFSCiat9GNzbR65rZWgOMFjpOd6AxxGmu3N0CNTsIqdGBW+Wp4NzoqFoJ+RQBfmDzCe78xVL3xnkiYXysCguR8V0VvkIFHxX9of1PDGTGSH7U+BHxwDWkTJtAOAEe2y76KAA/OYrsWMlHF0gKtrQGMXuKrW9im4BPP0X6UDAN8tEPPRBmIAJKM8wxYCDgX+LNxVihDljE3FeLMOx7VxYksCXBKrZlC7BR/DFDKI4FbihcgUtkDisL2Vs0MCCxhIL9Bmo6WAmJTCWSLj5lISJTCWqCpL7FySw/qUu6WeLB23i4diUuyWMkgl+2UZWUociFLilrijoGC1LLtwN5CIOqS5ChmAUQldoXeY3jkj5ZXMp8a/nezBztIyyR5N2CKlyTsAbYPyIjB/ygBbwdUT7RPDrjSaEgKQh4xCPrnUAD4r4MIUVJrw4gKpeowJOCKCWgHV8J4r8O6FOgBWUBeoYZOsWxGyQrtxwpoEslDidk6cT5vUQkYeBIQ6x22caJQHhVkBqJ7gxHCch+C4D08aALUZ+QPMgDrBZAfsmFdOys4rswopKxsX7E8aCLTGEVV8BVAFEgp1lIdJAT8xIBzDL5VcxCLvN4jvR2AuCvQrQpUUZRxcSAd3CqgPBYZ82XhFlTQGYCABMAgegRUs8CocqlhE7DiAGACMSRkSQyqyg1cGuDHOSi8A0BDm+irYIQF3lxK0ce2O1PyQNVqRWeVC+QHgtWDmd/k6bGmE6E5jZsbFiEYRXyvGACqdIsjC8gWGGAkd8WvnBCp4QAmIzSayM3GWF1wroy5B3mX6GRMEluFBKXcs3L3P7mvzCeGTAoIiO9SsI6mHa89nAq4BNNV5GWbCcrLCCqyRaPE9sdhFlLzgAsc86BWAv7UrzgIBlLzD3xrWBY61ICHpnQAhTGLlFqioeW2oxEaKlUR6qNee2MWLrmAg6rQSOrHXsCJ1u6IkVKAkVQAAsgCl9FwF3gWpkgGAWrDfICygLF5l65edesoD/rJFgG+dYvL7WgbV5EGt9ZwpJwoK0FhIb9cML/Wrrq1AkjdaRMEoRLfBsiwEPIoEX7qB5h60DMes7VnqpQQ6xSXeuMgCzH1+8j6q9DekvQv0r63LH6tg0DrwNLSt9X6pA18aKACG3LKYpgnobf1YmvLBYurK9LJNQw6TQJtywDLqy6SnxSUqvXwaVNsmjxepqGUZLfFP8xAC4u038aANaml8BMrtRaa4N/GqAFWvn44bxNeGrdVItwAdLcI2+W9estHWMa1ZNWSdc+t7D+8h4cmU4Inlj5YB2NAOByFxoCw8b9F5m0Tbpqs20AFNrCKTZgBk3paNNIy1xSlty1ybrNuS09mZvs2paANWCmZbICKXcA7NImmTTVuIWVLeNS61LY5rXUuaNKyeQSm0poBebbIXSkrRlpvF0AKt2WrRnlsM2abCtlWsZdWRs1mp5tIm/JbVvq2NaOtdG8yQxt8FETgt067cIBsi12T7J5Msrv6JcmBj3JwY9hGyBpBhjvJzXdIhrSgYcy4xuca8LAG5nFE9t1oxXD/EFmTcgh8U0WYlPFkOIpZriaAGlVyCNCE2PLf2SrIC0IhAdICBsZrLUjlhtwO6RCBnEeCrBgkBs6KqfMUWXElC2SSEglDwJJRxGZTPocgGszgR5AuQDeEXHDST1fG0A7JHjo4gE6KARO3IGSU9nAQ8AcMSer8qfWVRRyO6UFQQGabQYN4fs2fmjz4V8Szh7YcUjLL+ZtzHY7vTvmK1xKEgN42ESgL/lvRCQV494SSDastgrsFoIZC/julV099DtB83sAtA97qQnNigSPm8BmRklw+34U5bNAR4j94aXAbuV6i1A7ozoawHguXyxJckTuGEeIOdzEj9MJ+j6G3eWFJy8cWeu8VDUHvBq9A+gGZKdUunmbN8VoZegpkDD9wh66+g/M/B4Aj19SnIdfZxDukH7WAy+NVLAGXuvLHLHMd4GLXXVQKJ42+NEA/Pfj2pLLLxnvO3r9Chy0rNcFKxCIyLL0V7j01e7cmGH6D16sAje0MBqAhQZxsgWXLgKbrvnOIbyE1ATpqBd52Jd5U7EyMBD9Wfp3kqfMgljH4T6QM9ynRkn3x3Q/IK2wy6tjtCJnYReqjwPcF6mDJpq6MrcgtbujRp+rCmMJfEkB1MLGNw17w4CBzE/IJrwyS+mZjupegrwEs87DSGXOpLulM+1nT1K9MxhYQVFjK5lc+TAUEp2FZJF4DOCax+57Qw6DEi8ESTuxKqj0t5T5A9YkcWcousngwaUBjiPNUKLgMztkBgAG2vjQ5JUl9bfRgyNwKwE2GZV097o6xUMGiAF1C71DaAFnVoaqaNs/GkAaw8En0O7F3kRhkw4TxAIMNggSqb8GiChRigQSH81hHxgtRSAKkiKECGCrCN99XDuQdTKM2XQ1MYmakVeSB0KCUFSogR7CFCm93sBfdNbNEKbrdr3zDdxR09ZjiAQW695lew3g0kBImrO22OUg5GTXUwwbqUGUPSQCy6FHggQUSgEPWILJkmGA+iQ4vuSiIQWNLUBUC+kikwUfIP4FDMIYtA4E2YusbHaUH/LTJyormRsmwpRSrgbwW0VQsKomhkkEUEKIFE+vA4gdlEyK2PogOkMlD+ak2Bhru1XjrwxGEQSgmWFML57K+HEJA9GBQMIw81BNYlkF2Ank1UZ5ayXZF2KPrrcsBupfd73AxJd3jAlPzfer6NXEiJWG5zSoN6M78Ck7eyillmxMBZ/tVE9HZ+EJNdbsNJJ5vXHlb0Um/1VJnLDSdxOo7SA9Jr8DxKJPo8etZ+sQBfochZcsT3J2k8xP5MEmhT/hByb6MsHXaaZPnSwKbomot8bAPLYKtVwVrsIPk5lJwUmL+24ma63Y7lCYiFmg7pu4OjouEIlmOxnYaOOhj/pZxTbssgnBiACoRC9QJNS8ACcCvl1gQ0j9kU+JnU9Ya5NQdDS5aZEhVVImARQQGZjUBIIqbFwpDjPFTAX9QTNdPIODHlsWVl7F3PMNW8P2DoYPMkB/EmmHlQL6+o1AAvIgIE6QHZIyFLtnc3eT5Zf0Y8duB6drmyB9hbsRM7hnDRKBbmqZ0qBmZhnwUoThamE0jJAkUssZ39DGVT2rN+YNQuG/5WKkDNJdRzAWEMyCQgjpoeK2QejRabpyfAiJ1wG+aZyBOBZjzYK08x+Eyyjmi10ES83yNHWWm1I1pmrHeckVsCl5GlXc0uADPjbWEqsAc9UFVhTbt80qJSovEJqORZRf5z4ARSNHliUEsgHc1QD9OkB3zp8YdVeatO3mug95/AI+fQCaZgIR59M6GfBVvnbQp8FTSBf7WuIPzi5/KC0oA3xmEQw4a5uQEcJeBqgImYFbGBpXEp21RAbfFwcXl5m9Z0lkTfJeLM0L7FkF6xdJdgvwWlNmARC/4o0uMleo4BozUpddA6WJ91COCxoFUttANhFZYy9WVxWxN6FyW2FBoF0t2WOthl6ZcQpBXKILLJiTy95fsujCWt1CshbgGCuEgYL1l8/KrHCvraArtCmK4MyssfhErMljEX5YG10BcVUiJK75dGFSY3UdY6oANJIWSXaA+sVcAAG19YNV/WAAF0MsuhGgAqMcgjmSLqAe7vinZpo5USXZSc5pHkAMX4VsYQyjYMNPxF6Q4Yl7RA38kfbda8YrrjZTCm4mFUIRl8+GeB32s7TIsvMRDqdOPYuGgEN2AOdkg3hkoAl0gFpZuGDCf1IwmiMHxzSUAdrCu181PTfFyrzDG1EHL9kaOxGvr4Zx5c0m7OtG+cdiFCPDG8iWHsI71igJ9bDPQCA6sCElL6b3NQW2UU2+todAsgsbVCwgd6TMc2r+N3xQfAgCbxoxQ3AEfBH4LDaJIt0VAQYZzgjdwBI2UbzF/KCRvJ0Q1WccjCyHdZIAPX/hT1jDfjZQAWRUQfsrFcgBFti2qQEt39VLaoMNHibpMOJnTcfR2gmb2YV/qzfEZU9nj4+IZekDRCBy2UaILmyefDMnHEbgJbm99aqB83BVow0ymscdDAcaklhKXcDOdyy2xygubaTSOQFts+z6QaoOBWqBiAwgMfPgDHbjungKAk4P+V7f8Sx3cA8dzHLwzDRUGqsyuKAAREtg9sWcu9QMEGGvXKrCmWNiC4GayzB9oYXKUub+jiBEdGyuIPjNehJRf7pkvOT7HO01w6rWNdoA/nQCEVjhmbr/InHZzjbAovRv4/NQ51EFASlzcJ1cxF0djImetb1p23bbPPXW7IyXdKltf3u7X00x2YU1ZF9v8V8rgkvex9YPssWA68t1zeBf9P7mssMkWW/hbQCEXRbgZzKw4w8C2WELDl3iciZY22g0q0QVaSwnBzvrDGtti+yxdFy76VLkWvywC2dxl2k2Ld55QLbftgWCL2N6xeef7tY4+cfie5dImrUXSGkDZwLDbfPug3D7qIHJiIVvs4ZlpoQfiMPNz6/ZCbDR+wNeddjcRcOjBkh//bIdftpL4DyB2uoYf1KowzDx20/dQf5RMsqjT23aACzgUuAqw6iFnZztSURMkIzYiuqZPz9lH99tR5zdYeo2PwbtgCEpVTucOmUYjoh0obYjSOAHSt6CxoAUfX3nmWGP2yo6Ij2OUHbD5x48AUWCrMs0DjC+I91DEPFbQDzy8E+scO9uHckOx6ifUfI3n7vNuJwIu0er8gSQDGiPo4qeGOwsEWExzvn/mQALH+lOyQER9EUzVTYRGmQaY8nxFlWyRFmZGNe2tcApnM+Bs4MVgHgBAYSI4AGHW7gV9rgQ02GDuOuOn5uEQlXNGeDx6pk8VOv0rkgjVj7T5i8g2Tnk4hH1WIU6A8fOFTpfDrwx1eo7QSpAYH/8gBNvspA5ysrMbHMdUC1LtywP7nSw3sPlLQvljeURQP2Q3tUlGzWI9oETsBAI3WiiNUdt2LKJVFK5vwP4ZwPigXQy6WcdiFdkxZdtYBlpz5S5gHSns/AJ2bEPkyQAee9gW6laQEmOdnvQQV4XOKVa3UaFar6Aju9J1Bb7s5AMDJQd0I1LOXmhrFSjb8CRCZGHGxwwJWgitCpdAL5GDEJUoy9LmV3b0dHIgSQOXq5lHKNAAhteJ9Lbg/V3+WKkIiI56QGA34Vyh7qZ01SInMEn1gtFTqJBrGY5+vK9cjU5S+AezvjEwihkcZgYtrpGniKde3C5h1OzCIge4PsxA4cMU+FdCPpIFTC2TaAZ6XCTtxpq34G/RChWhEvTITAN4A011C0CtoCt3wVq7HsUHcANwHGA49WWrQFATWWRvtghhAwFV1MEkZhD9VQZDp0+4UiUHnY0R3nOQNgI/Bti5s3x7KNSI8FG5QWOgU7Yl8U/Q4cQCw4h6d0u5oKWwIocQCzEMMKYll4mw8dg9uHCDvR4qxPfAR6BweSlbIgbC5Yc9Mg0Pb6jmgektJIBwHuyXANehr0wjaGDw4ZscxVCHqvPxEi5NSOoEfQkQwA+TWKh9T9nzxiwSYE9yB9JfXiHAm82ejxBBdjn1oN1Ei9kl6YHQEUldDakSqgB00HaqF0+3JEo86x1MdOBFNAMHolBudAkO10gNPrDWmAFABzJK/pdavZQKq5BR2uQ/nqOIYpgSFmtgAQml785hGTxdZQSDUcUgsCRWsi6oKa867XJ4w5PmBYjjxrkgAQyS7ISP3vYOV9NQFsZr1Xfzy+uSJszDQjcHoON+npBepB00XG7ONB7SAmejXTlJGtUAVSfyn2w+6ury944kIRntAeNDAUlwRVXUYmm4HRaeACVgvJr0L+F5sWReeX7dE5qEEWuJe8AyX9cjKlrcguXF7VqntF1qaGfXXgkuT02/YBKlMsqroWzW6A9eeAUN4KBDRF7fAQHiork4a8G3V3yf4fngL6Bda/NulSYX0eTYo3bcdrLMqUr+U3K+JpPwFGJLzt4+ITJ9vKX9qzfPS9AJAs839r0uCW9hGova3poSV9Zl0AyvZeFL3t+2/vf+AeAY75V41dLhGXdX2j2WK/qYva8xYmM/Y5kVgLbvmiodfl3pZTsAsMzvCUxACyjDTFOZxedLo8zrIcjCqHyMD+wtf1IXTRwS7lmBf9e8uSdZttzgCwAABcYKT5qyjDfX9b7jotS8LLADQb9Xr6ff68+fwSW7nV0vEHb/BiB4bmgIh8jeIQ5PK0O1zVHO2dPLtVMtUzNf6czJHBYAekL9qMAo+PByzuKfafWdhDNnzp7Z86u9Z0NkJ+v1SQ2NjaxhU9JkTCH+kAGYiapCdZAHHAThJwU46cXqPgm6B9BoAkcPoD4B/B2AfZ6qLXr81lE64AB0gLgND+x+IRC3YgVF9sgcCAJPf1ciXbygZbyeTNFc/RdUDLkpa2gKWob2KqBtQ9npJcseyEuDLT0mSC00njn+RdUTUXsP6o92qIAV/rAXSJACQGADJ/4XAAH2XlnA98eti3qVDhdvlIp2EOpamRlm5xhg3ANfGtxz9yfSEXSLv3l5o08ky5711hCJv79tSlkw/tP7gAz/SAeM0/mlw28pIlzUyWoHXWk2FfsiyC1n/41m1LN8YeMP2SENfqQpAZ08OBW1WMf/bJHIBWhQXXGAddJjDTA+Md3VY1YgJIB1tL0QAO/B3rZAB10vDcHEGkRXbg3xdcfFnFl8G3Yr23duScNyNF8zbZVVVRyKPG1NivWX2osPHHaGINSzVvzdJmwdSFzVlPR+gXMxBYtWXMtPHC3AksLUQP+QUWQYwp8zkC5D99o4QP2D9Q/cPxsAtBW32slAtXiXcQxERQXj9UIUikI0wFDQOihUfbQMNEpAgwMBBAsa/1v8s/XAFMDZnA3yFMifKQLB8qgQLDLli/K2VL8TNcvyK1sJTQIG4mIRk3kE9AxHwa9PAgp081jAxeW78u1WS3P9B/IMBH84g8f0n8PAPQCcDzA8dQ0k3Aoig8CxKXLB389/GHwP9KNKNWP8c8AIIH8wIVILsCwFTP3v8cglwI0klTC7SckrtHp3V87tAZyVpntZ7yWt2ZbIjjFOuRMW65QpFHwQ8UqAiRGtDfHMRCE1eQsRdMULSnzIwGPGB0qpvnEPWy9zPSN2UdoQdAiDNBbbpn+8EQRlwq11g68BpQOwDWgFRa4fWARBggOqx4NVgNr2CAzPAhjZcJHR3C7M9jar369bg4mHC1bCTKQ6ACvI4xuBMpL4Mjc4cX4OMZ/gnIFE8QXb8HwRv3B5mBJ1IaAXysOpcRHRZncFEP69g7NzxLBUSQmx/UssTz3SplgVOw/5F7WGT/FUDImmED1PEtU08CmbT0RNt7JRya98nGkz2CCGBIMP9KFZQych6OfBEER7vGL2KDcgWYAe98odwE444vcBle8KvT8Aq01tc4NIBLgpP0BCyMJpzqDL/YAFhCjweNHdBsgoILMCZgpGjmCJzW82qAEwN5BGhLmaR01cQXTLEI5SQvwmVM3gXoPpl2EFoAGCdfYZ18kMid7VGDVrcYN18DAaYMpBpfI8EikNkHsDAAbocjwWDhZXMWuwTrM3zOsVIXGWuCEyWBynZ9YRn2Z8+6EiT5UqQDFyiCsAZRyoZzoaENWBTQxsjtAHPEICJCyMK4P1Cbgz/h1cevKiTrcxfbAAl8cyRsObcWwl/jmFVvGUJ+CQgfWC5cSACsLDAGzEkLN1mxNgOxCBICSSUAPAc+EFwaITsN7D5YbJy3MYJaoHkwSAXBFPDQnMQiM9VHVzT613NKEJhCQvI8GFDKgjtRnCivaILlDovH8IwAlQq8ie94vNUIO9K/WuCzltQhlxq89Q60UB9dAPfAv8h/E0LfD1Ac0PwBLQxSTjCpfWYMQBkwqUFTC70biDCCSkAZC8A1Edx2SgmIP+3dCqfGtU2DHcapyhCJwtCMcCrQ2ZxtDEw/CMd9CItMJIjFTSE0EDVPVkOgQNPUCXECdPR2HzCoJFQULD9JTVAZ8mfNKkAtsI60PjC8IgiKIAiIi0AEj8gqLn086wvkKYdKLSRX890PXDSfCwUN4KbDJwySlCxdGNlAqDe/aUIAiuAP8IVCqgICI28hgsCJS8IIyAFzlJKJCJSDh/ScIwjsglTXO9KASyIoJBKF8ObC2I+yKMcnI5bxFDCvHjllD5QmUO8iQI1UK283vdcgCioIo8KIBuw+CJBdDQ5CNSDwoy8EwjeJIpUQFWOe8MidTIqAHMj4IQLGgcWI9gEnCEg1yMyifI0CIKj1Q11Gfg0vDLy6iRHHqM+C2I/qIyj1vPKKjA/Iv71Ki2gcaP9C7BdhAaAtfGZDAYNWZa0jDdWLmSmdiiHCJIAEw9QCZ8dvc1WipNAG0xB1VnY32zCNnFYMXEuoEfWTp+3dPVpDZ0Jl1f5bPJGigM0IMIAUhtzFTRmjcAOyK69umAcLlMYI/r3KjBwwjzPAu4DChxt7mT/XQBs1fiFAQ5PFxBfQXWV/W5wdZNIALAwyDAzQ9FyfZTF4ZAyMChhc+O6LeC8efODrD5jJqF0wk2CgN45GyDsTnYY9OjDZFR3bJiaNswC8wRpVwsqg6RpqdPiEheFYGPDQHALhTW4u4PV24hiCIQ2DJfjWYiL0X+CsD3EZfedGHhQWfFWvlJFHwHV409YoAaYOwnGwfdE3OyCVjkNdgGvJ8QC0H3guIxsjl52zQgSw4WyC52ggJIfWMZjLVFJmngkBfgHlkKAU1DNioAK72CAlSezzVdhbHsLKi4I5GOJDUYmN0r5/nLKw8BnIQQ1BipVQ8SliHcZRBWAzCeoVv1QsUdEAJ0cAKGqQQ4TKlLi3QX2A7cZ6V/inZqQ+f3Pl0Y6V1sZk3azBDgozS3ypA3YJOj9lmfV60+jvo8KFrDSkXlC40ZIkIELCtg8KG5xSw5SKhc+6G+WXjb5CFF38WAVF1Lki/fADgVMqTeOCDnRBEGXDtkTsxn4WmE5xggzVMtxCBePSmAfgQoaqUIjtY/TEk9kWSsFmxq2J+I/EthKUCo5oQNcIu8VNVKSfh6AccB/jfuP41d8qzQTj9knwA6PDIHIPCBvlcgTWRJlzwRV1OBKQdUILjQWVJmeYpQO7kfimcQsH9iDeZ+LmFSfNlBLZcAcYCO88ABFnGlLmBBO0jf46z2xd7MM8GrDmaf/grjMCWsNvC/bMtnUQFlVJj2xWARQCI5u42MgG9fPCGNzIPgqGKSjlHV+x8c4YuiK7D04+GMZdY4/eLEAtExON0SJHN+1KikYkxNRC8EQQSZCV7PGWC52QiSKpYJA+r0MipEu+xMiBQ8cN6i5o5yNksBo6y3cjsogCNyiVQ5aJGjwIyrXsiSo1OPsTDE68GSD6gsKLYiIo6Uzopzoy6LYTwqMgFujyqW82RAYYxz3H0849+wziyMT0Nc91wmlEkTbHAJPjiW3UgA/CXIhaOpU8gKJMyiYkzb1+90mBbSWZsAMcBWjhkrUJ+wxkjwHzgfvT7yKiRkuxOMS0kvvy4Bqo4fwkBrQWgCwi/8bk3yTZgopMioLVGAx4lIAcpIkd2w0YwStE8N0IB8PQqBMoAvnelllEp4xCHF1a5cRSiiULe7i2BYHJOiMCKo+iKR8ywlSJyDPYwpJujS4oiTMTXTGfnLEPkxE3IIQ6S7wbdD45gFRcAsVcACxvAs+P0UFI7pCUir4wbnBSNI20KOSSk05JqwjKIMQDD4iNkC8kZkZmR8lMEiMPa5Ptb7RjCYwOMFEREwDMMOssw0ITm43ozqCpB9YOZ1yDMGZFDyon6boHPgbgboGgBc4H8Bjk20G4GgAfwDeHjl1U3OCSNppPsDwY4HXPUBNLYHxHQwPQfAIU88eTBAao+AIoMcZ6PGGHvwqQXEAfILUL1wkcPPSAJENb6BkLnNhIlkLXsRAjey5Cf6Wj0Nc3WQ8IK4maLxNZo/EljBpTbtOlIGCRsUMNZStWI6KCl1rRBmZ8Nsejn5Snoo6xejTfEVPOs3TdF3LFrA9IFXjgyVs22hzRHGxVsRhV5kDBUXFxVxt9LDAFGEktK2XbSLw7bTcY2YCHyt8K0kn1gdfXA5z59o1QFCgQqvAsAlZQZDKHLF8A7gkuYHsX0hvdsyFSIKZuAsqDhtdleGz9S4Zf8REig0tkKkCy1Fmm5DtnYY1QgUTALHkiuTOilzSFnQkxRgpaEhBloBaAwCFpVgEWi8RxaYAGl5lfL9JIBNo2In6wntVNJZS2ZNlNjEowk6LNMjAXNIJ8Hog60LTBU5YOSl3o8tNcQrrL00hlHMeQEFdpXWVMccebL80ekE/QdLv5f0KGXkBUAD5W6lEIT4CJ5+AI8CIhZzY9OZDAuUSPQoL0+EyvSw0uoIqh8/HrSYz34YjMiR/HQM0m1O0pLirTcsaJycctHJ9N5YUMwEjfTvRUri6DVfHoNplbBCDO2iFrIYL8kRg9lIQzJnJMWRhUYc0C2pizNZ3xgx6WQKoAhYMWTFhBEGmClh6YWWEMBbMvmHUAMzRAA0ssiAqxRYQsRmAVgoAekBIB6QR7TQB6QGLJaAEgNkESyPkKVVUAPkAQHpBeQQUDQBeQTOQaBBQBgD5AmgJoC0h6QBkF8yos6kBaAGAHLNoA6sgQA+QVgQUA+RBQKVV5BKwFoAZASAGZGKzeQGkBmRRAQUFoABAQUASAaQBIEFBqsmrICyYrWMGCyIwgqx0hGYWzKjxu+VO1FsjjYLPCyQgSLKEwWlfWCQBbAbOHVh1gWgGchnM7+QBt9YcECnsVwI7Mw5p4M7OotVgWwDuzEgB7OXAjspAHzgo42MBJVPs/Wx+zOgHaljBdTDAHNQGAfuFsh49VYE+zJMUHKyoIc01HcBcALwHhzEc6OMeywckcFoBIc03U8iMALHPuy4bZHP1hGcC7J8A8MaQGOhPsw2ApzrMN4HhzmgwBEQBPs+qz4tZ5bnLByjjfcLYAGconNnCsc5HM6Awcx3g5yAPHHLFzxc4VHsxXydNAZz4c+wH9cUzYC0ERHKWmFwAWjBAGIFEPKlETw7aYUSgsNAOq15yhUJ9gZzpPKUHNzxcuXJDlZ0McHhyBcpcP6kjmaywUR7cqTFlzDs+3L5zcyV3IZz0cp0COM7c/3IKpjdbHPwxZcsHP2M8CD8GDzHILQjjVn0QXyehP4NsiqBtTDPKFSm2avQ8J+AEmyHFBMK3UchbALgCx11IMfVnjXicJ2DJQDUoCpV1iWGUTxPgIQA9AMqUpF1VgyByFQBBPJdKE8zhM3NjzLcwRGty3KM6lHz9YcXG880gYHO+yLc/WEdyuPfOMDzBcBnJTylw7nJ9zucv3Ijz+cjfP6loc3vzqh5ZG+OnzJchfPJyl8+PNqQiqY/OotT8pgHPz4gVABmQZkDQA/yAAUluFg4odKdjkFenUOlqDA5FF8nFMcyaAaQDQC5Bv8kfKXyIjNMETz+pDODSh24XpltVZLIOU5daw5ADuVD5ENTtBYgcQEQAm4c+ifhxyZvCcV4C/3LHylACfI91w8+3OXyN0VfJdyj8rKifysCr3PFypMFpRasmcgnFsBhcgCIZyPkD5CSASAXLMqySANkBaA0AFoCaA5kBIAYA2QAQBpAGAWgF5BaAKbPpA0AQUDZAJC+rJpA2QNACFB6QWgEqyusrQpaBaAD5FoBBQGkF5A2QPrIEAZkOLJmRw8ynKEKbAEPLdyhUNABmQGACbJpAPkYbJaABATQtoBUsnaJpA4snLOCLEs0wpmQysmkAEBeQEgEe1EswUH6ymgWgGmyHC59G0LzKD5HpB7CpoESyEgekC8KMC3v2chX8kgFkMnpDE38L98vnN5NfBAU2OxPstorlzteMcD08riT7PpBp8ooMQAJTWABPzuCrgBVpvc2XMpyzAtoJ6Kl8/oo8BBiiaE+y2QUYtwKJiqYoxEpcyAGSJvclpT4K+CmrI2zmme6x2zILegH0AgAA -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/hackerai-tech/hackerai/issues/comments/3787548040","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Fri, 23 Jan 2026 14:30:59 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"701B:1DB1E7:7F50D1:241AD4B:69738623","x-xss-protection":"0"},"data":{"message":"Requires authentication","documentation_url":"https://docs.github.com/rest","status":"401"}}}

- Add flex-wrap to Current Balance Row to prevent auto-reload text overlapping with Buy button on mobile
- Allow setting unlimited monthly spending limit by accepting null in mutation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@convex/extraUsage.ts`:
- Around line 12-20: The points↔dollars conversion in pointsToDollars currently
does a raw division and must match the rounding/multiplier behavior used in
lib/extra-usage.ts; update pointsToDollars (and/or dollarsToPoints) to use the
same cents-multiplier and rounding logic as the shared helper in
lib/extra-usage.ts (or import and call that helper directly) so balances, caps
and UI displays remain consistent—reference POINTS_PER_DOLLAR, dollarsToPoints
and pointsToDollars when making the change.
- Around line 132-143: The check that rejects a deduction when monthlyCapPoints
is exceeded currently returns insufficientFunds: true which is misleading;
update the return in the monthly cap branch (the block using monthlyCapPoints,
monthlySpentPoints, args.amountPoints) to set insufficientFunds: false while
keeping monthlyCapExceeded: true and returning
newBalancePoints/currentBalancePoints and newBalanceDollars via
pointsToDollars(currentBalancePoints) so callers know the failure is due to cap
not lack of funds.
- Around line 206-216: The code uses truthy checks that drop valid zero values
for thresholds/caps; update the checks in the return object so that you test
explicitly for null/undefined (e.g., use settings?.auto_reload_threshold_points
!= null or typeof settings?.auto_reload_threshold_points !== 'undefined')
instead of simple truthy checks when computing autoReloadThresholdDollars and
any similar fields (also fix the analogous logic referenced around lines
258-264); keep using pointsToDollars when the value exists (including 0) and
only return undefined when the original is null or undefined, and ensure flags
like customization?.extra_usage_enabled and settings?.auto_reload_enabled remain
boolean defaults as intended.
♻️ Duplicate comments (1)
convex/extraUsage.ts (1)

30-43: Validate positive finite amounts in addCredits/deductPoints (already flagged).

Same issue as previous review: amountDollars / amountPoints accept negative or NaN values, enabling balance manipulation.

Also applies to: 76-112

rostikmanko and others added 2 commits January 23, 2026 15:12
… components

- Updated the redirectToBillingPortal function to return the URL instead of redirecting directly, allowing for better handling of the redirection in the UI.
- Modified the AutoReloadDialog to reflect the correct branding by changing "Anthropic" to "HackerAI".
- Removed redundant purchase confirmation text in the BuyExtraUsageDialog to streamline the user interface.

These changes improve the user experience by ensuring accurate branding and enhancing the functionality of the billing portal redirection.
- Add refundPoints mutation to Convex for extra usage balance refunds
- Add refundToBalance function to call Convex mutation
- Add refundBucketTokens to restore Redis token bucket credits
- Add refundUsage to orchestrate both bucket and extra usage refunds
- Add UsageRefundTracker class for idempotent refund handling
- Track pointsDeducted and extraUsagePointsDeducted in RateLimitInfo
- Integrate refund into chat-handler's onError and catch block

When a chat request fails after credits are deducted, both token bucket
credits (session/weekly) and extra usage balance are now refunded.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@app/components/extra-usage/BuyExtraUsageDialog.tsx`:
- Around line 151-158: parsedAmount can be NaN when the input contains only
non-numeric characters, causing parsedAmount.toFixed(2) to render "NaN"; in
BuyExtraUsageDialog fix this by deriving a safe display value (e.g., const
displayAmount = Number.isFinite(parsedAmount) ? parsedAmount : 0) and use
displayAmount with formatWithCommas(...displayAmount.toFixed(2)) in the "Extra
usage" and "Total due" spans so the UI shows "$0.00" instead of "$NaN".
- Around line 133-137: The onChange handler currently strips non-digits and dots
but allows multiple decimals (e.g., "1.2.3"); after generating the initial
cleaned string (the current val from e.target.value.replace(/[^0-9.]/g, "")),
remove any extra decimal points by keeping only the first '.' (e.g., locate the
first '.' and strip all subsequent '.' characters) before calling
setPurchaseAmount so the displayed input never contains more than one decimal
point; update the onChange in BuyExtraUsageDialog to perform this extra-dot
removal step.
- Around line 87-103: The payment method cache is cleared in
handleEditPaymentMethod but the component stays mounted so useEffect won't
re-run when the user returns from the Stripe portal; add a visibility/focus
listener to re-fetch the payment method (or call the existing fetch function
used in the component's useEffect) when document.visibilityState becomes
"visible" or window receives focus, and clean up the listeners on unmount;
reference handleEditPaymentMethod to keep the existing setPaymentMethod(null)
behavior and call the component's payment-fetch function (or setPaymentMethod
with a fresh value) from the visibility/focus handler so the UI updates without
closing the dialog.

In `@convex/extraUsage.ts`:
- Around line 362-380: Validate that args.autoReloadThresholdDollars,
args.autoReloadAmountDollars, and (when not null) args.monthlyCapDollars are
finite positive numbers before converting or assigning them: for each field
check typeof value === "number", Number.isFinite(value) and value > 0, and if
any check fails throw a clear error (e.g., RangeError or a ValidationError) so
invalid negatives/NaN/Infinity are rejected; keep the monthlyCapDollars null
handling (null means unlimited) and only call dollarsToPoints for values that
pass the checks, updating updateData as before.

In `@lib/api/chat-handler.ts`:
- Around line 186-203: When extra usage is enabled you must always populate
extraUsageConfig (even if balance is zero) instead of only when balanceDollars >
0; in the extra-usage block in chat-handler.ts where you call
getExtraUsageBalance(userId), set extraUsageConfig = { enabled: true,
hasBalance: (balanceInfo?.balanceDollars ?? 0) > 0, balanceDollars:
balanceInfo?.balanceDollars ?? 0 } so downstream checks in token-bucket.ts
(which inspect extraUsageConfig?.enabled and extraUsageConfig.hasBalance) see
the correct enabled state and can perform auto-reload logic.

In `@lib/extra-usage.ts`:
- Around line 93-102: The no-op branch in refundToBalance currently returns
success:true and newBalanceDollars:0 which can incorrectly overwrite an actual
balance; change the <=0 branch to either (A) retrieve and return the current
balance in the RefundBalanceResult (call your existing balance getter for the
user) or (B) omit/return undefined for newBalanceDollars so callers don't
overwrite state—update refundToBalance and the RefundBalanceResult usage
accordingly.

In `@lib/rate-limit/refund.ts`:
- Around line 42-61: The refund() method sets hasRefunded before calling
refundUsage, which prevents retries if refundUsage fails; change refund() to
defer setting hasRefunded until after a successful refund and add an in‑flight
guard (e.g., a refundPromise property) so concurrent calls await the same
promise: check hasRefunded and refundPromise at the start, if refundPromise
exists return/await it, create and assign refundPromise when starting
refundUsage, await refundUsage, on success set hasRefunded = true and clear
refundPromise, and on failure clear refundPromise (do not set hasRefunded) so
retries remain possible; reference refund(), hasRefunded, hasDeductions(),
refundUsage, and add refundPromise (or similar) as the unique symbols to modify.
♻️ Duplicate comments (10)
app/components/extra-usage/AutoReloadDialog.tsx (1)

133-146: Cancel button incorrectly calls handleTurnOff when enabling auto-reload.

When isEnabled is false, the user is trying to turn ON auto-reload. The Cancel button should simply close the dialog, not execute the turn-off logic. This requires passing onOpenChange to the content component.

🐛 Proposed fix

Update ContentProps to include onOpenChange:

-type ContentProps = Omit<AutoReloadDialogProps, "open" | "onOpenChange">;
+type ContentProps = Omit<AutoReloadDialogProps, "open">;

Update the content component signature and Cancel button:

 const AutoReloadDialogContent = ({
   onSave,
   onTurnOff,
   isLoading,
   isEnabled,
   currentThresholdDollars,
   currentAmountDollars,
+  onOpenChange,
 }: ContentProps) => {
        ) : (
          <>
            <Button
              variant="outline"
-             onClick={handleTurnOff}
+             onClick={() => onOpenChange(false)}
              disabled={isLoading}
            >
              Cancel
            </Button>

And pass onOpenChange in the parent:

          <AutoReloadDialogContent
            onSave={onSave}
            onTurnOff={onTurnOff}
            isLoading={isLoading}
            isEnabled={isEnabled}
            currentThresholdDollars={currentThresholdDollars}
            currentAmountDollars={currentAmountDollars}
+           onOpenChange={onOpenChange}
          />
lib/extra-usage.ts (1)

136-147: Duplicate: no‑op deduction still returns $0 balance.

Same issue as earlier feedback on this file—consider applying the previously suggested fix here as well.

lib/rate-limit/token-bucket.ts (1)

177-210: Over-deduction issue in extra usage path persists.

The previous review flagged that sessionDeduct and weeklyDeduct are computed independently (Lines 180-181), causing the bucket with more capacity to be over-drained when extra usage covers the shortfall. This issue remains unaddressed.

convex/extraUsage.ts (7)

12-20: Points↔dollars conversion mismatch with lib/extra-usage.ts remains unaddressed.

The previous review noted that lib/extra-usage.ts applies a multiplier and rounds to cents, while this file does raw division. This can cause inconsistent balance displays between backend and frontend.


39-42: Missing input validation for amountDollars.

The previous review flagged that args.amountDollars should be validated to ensure it's a positive finite number before processing. Negative or NaN values could corrupt the balance.


89-91: Missing input validation for amountPoints.

The previous review flagged that args.amountPoints should be validated to ensure it's a positive finite number before processing.


136-144: insufficientFunds: true is misleading when only monthly cap is exceeded.

The previous review noted that when the monthly cap blocks a deduction but the user has sufficient balance, insufficientFunds should be false to avoid confusing callers.


278-281: Truthy check drops valid zero threshold values.

The previous review noted that using thresholdPoints ? ... drops valid zero values. Use explicit null/undefined checks instead.


322-328: Same truthy check issue for threshold and cap values.

The previous review's note about truthy checks also applies here—settings.auto_reload_threshold_points and settings.monthly_cap_points should use explicit null/undefined checks.


185-194: Extend validation to check for finite numbers.

The <= 0 check (Line 188) catches negative values but not NaN or Infinity. Add Number.isFinite(args.amountPoints) to prevent corrupted balance data.

Proposed fix
   handler: async (ctx, args) => {
     validateServiceKey(args.serviceKey);
 
-    if (args.amountPoints <= 0) {
+    if (!Number.isFinite(args.amountPoints) || args.amountPoints <= 0) {
       return {
         success: true,
         newBalancePoints: 0,
         newBalanceDollars: 0,
       };
     }
🧹 Nitpick comments (5)
app/components/extra-usage/AutoReloadDialog.tsx (1)

45-54: Consider adding validation for non-positive values.

parseFloat accepts negative numbers, so a user could potentially bypass the HTML min attributes (e.g., by typing directly). While backend validation should catch this, adding client-side validation would improve UX.

✨ Optional enhancement
  const handleSubmit = async () => {
    const thresholdDollars = parseFloat(threshold);
    const amountDollars = parseFloat(amount);

-   if (isNaN(thresholdDollars) || isNaN(amountDollars)) {
+   if (
+     isNaN(thresholdDollars) ||
+     isNaN(amountDollars) ||
+     thresholdDollars < 1 ||
+     amountDollars < 5
+   ) {
      return;
    }

    await onSave(thresholdDollars, amountDollars);
  };
app/components/extra-usage/BuyExtraUsageDialog.tsx (2)

69-85: Consider showing an error state if payment method fetch fails.

If getPaymentStatus fails, the component falls back to showing "Link by Stripe" which could mislead users who have a saved payment method. Consider adding an error state or a retry mechanism.


45-49: onClose prop is passed but never used.

BuyExtraUsageDialogContent accepts onClose in its props (line 48, 54) but never calls it. Either remove the unused prop or use it where appropriate (e.g., after successful purchase).

Option 1: Remove unused prop
 type ContentProps = {
   onPurchase: (amountDollars: number) => Promise<void>;
   isLoading: boolean;
-  onClose: () => void;
 };

 const BuyExtraUsageDialogContent = ({
   onPurchase,
   isLoading,
-  onClose,
 }: ContentProps) => {
         <BuyExtraUsageDialogContent
           onPurchase={onPurchase}
           isLoading={isLoading}
-          onClose={() => onOpenChange(false)}
         />

Also applies to: 204-227

lib/rate-limit/token-bucket.ts (1)

356-363: Consider logging failed extra usage deductions for observability.

When deductFromBalance fails in the post-request path (Line 362), the error is silently swallowed. While this is intentional per the comment at Line 365, consider adding a warning log for failed deductions to aid debugging and track potential revenue leakage.

Suggested improvement
     // Deduct remainder from extra usage if enabled
     if (
       fromExtraUsage > 0 &&
       extraUsageConfig?.enabled &&
       extraUsageConfig.hasBalance
     ) {
-      await deductFromBalance(userId, fromExtraUsage);
+      const result = await deductFromBalance(userId, fromExtraUsage);
+      if (!result.success) {
+        console.warn(`Failed to deduct ${fromExtraUsage} points from extra usage for user ${userId}`);
+      }
     }
convex/extraUsage.ts (1)

385-390: Minor: updated_at is set twice.

updateData already contains updated_at: Date.now() (Line 359), but Line 389 sets it again when inserting. The spread order ensures the second value wins, so it's functionally correct but redundant.

Cleaner approach
     } else {
       await ctx.db.insert("extra_usage", {
         user_id: identity.subject,
         balance_points: 0,
         ...updateData,
-        updated_at: Date.now(),
       });
     }

- Updated the BuyExtraUsageDialog to ensure only whole dollar values are accepted and displayed.
- Simplified number formatting by removing decimal handling and ensuring consistent display of purchase amounts.
- Enhanced createPurchaseSession action to validate that the amount is a whole dollar value.
- Adjusted extra usage configuration to include auto-reload status, allowing usage even with a $0 balance.

These changes enhance user experience by enforcing stricter input validation and improving the clarity of displayed amounts.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/components/extra-usage/BuyExtraUsageDialog.tsx`:
- Around line 87-93: The call to window.open in BuyExtraUsageDialog (after
createBillingPortalSession returns result.url) is vulnerable to
reverse-tabnabbing; update the window.open invocation to include the
"noopener,noreferrer" feature string (or alternatively open via an anchor with
rel="noopener noreferrer") so the new tab cannot access window.opener and cannot
navigate the original page.
♻️ Duplicate comments (2)
app/components/extra-usage/BuyExtraUsageDialog.tsx (1)

85-95: Payment method won’t refresh when returning from the billing portal.

setPaymentMethod(null) clears the cache, but the component remains mounted and useEffect won’t re-run on return. Add a focus/visibility listener (or re-fetch on tab focus) so the UI updates without closing the dialog.

lib/rate-limit/token-bucket.ts (1)

180-213: Over-deduction from weekly bucket when session is tighter.

The past review concern about over-deducting from buckets is still present. When extra usage covers the shortfall, the code deducts different amounts from session vs weekly buckets (lines 183-188), which over-drains the bucket with more remaining capacity.

Example scenario:

  • estimatedCost = 100
  • sessionCheck.remaining = 30, weeklyCheck.remaining = 80
  • sessionDeduct = 30, weeklyDeduct = 80
  • pointsNeeded = 70 (from extra usage)

The weekly bucket loses 80 points when it should only lose 30 (matching the session). The total deducted (30 + 80 + 70 = 180) exceeds the actual cost (100) in terms of bucket drain.

🐛 Proposed fix: Deduct the shared minimum from both buckets
         if (deductResult.success) {
           // Balance deducted - deduct what we can from buckets and continue
-          // Deduct only the remaining capacity from each bucket (don't go negative)
-          const sessionDeduct = Math.min(estimatedCost, sessionCheck.remaining);
-          const weeklyDeduct = Math.min(estimatedCost, weeklyCheck.remaining);
+          // Deduct the same amount from both buckets (the minimum available)
+          const bucketDeduct = Math.min(
+            estimatedCost,
+            sessionCheck.remaining,
+            weeklyCheck.remaining,
+          );

           const [weeklyResult, sessionResult] = await Promise.all([
-            weekly.limiter.limit(weekly.key, { rate: weeklyDeduct }),
-            session.limiter.limit(session.key, { rate: sessionDeduct }),
+            weekly.limiter.limit(weekly.key, { rate: bucketDeduct }),
+            session.limiter.limit(session.key, { rate: bucketDeduct }),
           ]);
🧹 Nitpick comments (1)
lib/rate-limit/refund.ts (1)

42-63: Refund retry logic improved; minor race window remains.

The previous issue with hasRefunded being set before the refund call has been addressed - it's now set only after success (line 58), allowing retries on transient failures.

However, if two concurrent calls to refund() occur before the first completes, both could pass the hasRefunded check and execute refundUsage simultaneously. This is unlikely in practice but could be guarded with an in-flight promise pattern.

🔧 Optional: Add in-flight guard for concurrent safety
 export class UsageRefundTracker {
   private pointsDeducted = 0;
   private extraUsagePointsDeducted = 0;
   private userId: string | undefined;
   private subscription: SubscriptionTier | undefined;
   private hasRefunded = false;
+  private refundInFlight: Promise<void> | undefined;

   // ... other methods ...

   async refund(): Promise<void> {
     if (this.hasRefunded || !this.hasDeductions()) {
       return;
     }

     if (!this.userId || !this.subscription) {
       return;
     }

+    // Return existing promise if refund is already in flight
+    if (this.refundInFlight) {
+      return this.refundInFlight;
+    }
+
+    this.refundInFlight = this.executeRefund();
+    return this.refundInFlight;
+  }
+
+  private async executeRefund(): Promise<void> {
     try {
       await refundUsage(
         this.userId,
         this.subscription,
         this.pointsDeducted,
         this.extraUsagePointsDeducted,
       );
       this.hasRefunded = true;
     } catch (error) {
       console.error("Failed to refund usage:", error);
+      this.refundInFlight = undefined; // Allow retry
       // Flag stays false, allowing retry on transient failures
     }
   }
 }

- Add webhook idempotency using Stripe event ID to prevent double-crediting
- Fix AutoReloadDialog Cancel button calling onTurnOff (now uses onCancel)
- Add positive amount validation in addCredits mutation
- Clarify schema comment: dollars vs points for auto_reload_amount_dollars
- Add noOp field to indicate no-op returns in deduct/refund functions
- Add noopener,noreferrer to window.open for security
- Fix bucket deduction to only deduct subscription contribution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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