Skip to content

Conversation

WcaleNieWolny
Copy link
Contributor

@WcaleNieWolny WcaleNieWolny commented Jan 29, 2025

fix #941

Summary by CodeRabbit

  • New Features

    • Introduced a comprehensive token management interface in organization settings to view, purchase, and track tokens with detailed history and usage analytics.
    • Enabled one-off token purchases with tiered pricing integrated via Stripe checkout.
    • Added new global localization strings for token purchasing, management, billing, and usage explanations across all supported languages.
    • Added new Vue components supporting token purchase flows and dialogs.
  • Improvements

    • Added a "tokens" tab in organization settings visible to users with super admin permissions.
    • Enhanced internationalization coverage for all token-related UI elements and billing messages.
  • Chores

    • Updated dependencies and build configuration to include SVG loader support.
    • Added rotating gradient background animation option to blur background component.

@WcaleNieWolny WcaleNieWolny marked this pull request as draft January 29, 2025 11:57
Copy link

coderabbitai bot commented Mar 29, 2025

Walkthrough

This change introduces a comprehensive credits (tokens) system for the Capgo platform, enabling organizations to purchase, manage, and view usage history for credits (tokens) applicable to MAU, storage, or bandwidth. The update adds database tables, migration scripts, backend Stripe integration, frontend Vue components, localization strings, and seed data for tiered pricing, supporting both one-off and recurring purchases, usage tracking, and detailed UI for credits management.

Changes

File(s) Change Summary
messages/*.json Added new localization keys for token purchasing, management, explanations, and UI across all supported languages.
package.json, vite.config.mts Added vite-svg-loader dependency and plugin configuration.
src/components.d.ts Registered new Vue components: BuyTokens, copy, Dashboard, KeyCreateDialog.
src/components/BuyTokens.vue New component for displaying and purchasing token packages.
src/layouts/settings.vue Dynamically adds/removes "tokens" tab for super_admin users in settings.
src/pages/settings/organization/Tokens.vue New page for managing, buying, and viewing token (credit) history and usage.
src/services/stripe.ts Added function to initiate one-off Stripe checkout for token purchases.
src/types/supabase.types.ts Added types for new tables: capgo_tokens_history, capgo_tokens_steps, and function get_tokens_history.
supabase/functions/_backend/private/stripe_checkout.ts Extended to support one-off token purchases and pass quantity to checkout.
supabase/functions/_backend/triggers/stripe_event.ts Handles payment_intent.succeeded to record token purchases in capgo_tokens_history.
supabase/functions/_backend/utils/stripe.ts Added createCheckoutForOneOff for tiered pricing; updated createCheckout for variable quantity.
supabase/functions/_backend/utils/supabase.types.ts Added types for capgo_tokens_history, capgo_tokens_steps, and get_tokens_history.
supabase/migrations/20250131091941_capgo_tokens.sql Migration: creates capgo_tokens_history, capgo_tokens_steps, and supporting functions for credits system.
supabase/seed.sql Seeds capgo_tokens_steps with tiered pricing data.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant Supabase
    participant Stripe
    participant DB

    User->>Frontend: Open Tokens page
    Frontend->>Supabase: Fetch token history, pricing steps
    Supabase->>DB: Query capgo_tokens_history, capgo_tokens_steps
    DB-->>Supabase: Return data
    Supabase-->>Frontend: Return data
    Frontend-->>User: Display tokens, history, buy options

    User->>Frontend: Click Buy Tokens
    Frontend->>Supabase: Request session (openCheckoutForOneOff)
    Supabase->>Stripe: Create checkout session (with tiered pricing, quantity)
    Stripe-->>Supabase: Return session URL
    Supabase-->>Frontend: Return URL
    Frontend->>User: Redirect to Stripe

    Stripe->>User: Complete payment
    Stripe->>Supabase: Send payment_intent.succeeded webhook
    Supabase->>DB: Insert into capgo_tokens_history
    Supabase-->>Stripe: Respond success
Loading

Assessment against linked issues

Objective Addressed Explanation
Implement credits (tokens) system for MAU, storage, bandwidth; allow any plan to use credits (#941)
Allow users to buy credits in various packages, with tiered pricing; support one-off and recurring
Store credit purchase/usage history and display monthly/annual history (#941)
Credits valid for 1 year, usage tracked, and history limited to one year (#941)

Assessment against linked issues: Out-of-scope changes

No out-of-scope changes found.

Suggested reviewers

  • riderx

Poem

In fields of code where tokens grow,
A rabbit hops with credits in tow.
Buy, spend, and track with ease,
Tiered prices flutter on the breeze.
History tallies, charts arise—
Capgo’s credits, a sweet surprise!
🐇💰✨

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

npm error code ERESOLVE
npm error ERESOLVE could not resolve
npm error
npm error While resolving: [email protected]
npm error Found: [email protected]
npm error node_modules/vite
npm error dev vite@"npm:rolldown-vite@latest" from the root project
npm error peer vite@"^5.2.0 || ^6" from @tailwindcss/[email protected]
npm error node_modules/@tailwindcss/vite
npm error 15 more (@vitejs/plugin-vue, @vitejs/plugin-vue-jsx, ...)
npm error
npm error Could not resolve dependency:
npm error peer vite@"^4.0.0 || ^5.0.0" from [email protected]
npm error node_modules/vite-plugin-vue-layouts
npm error dev vite-plugin-vue-layouts@"0.11.0" from the root project
npm error
npm error Conflicting peer dependency: [email protected]
npm error node_modules/vite
npm error peer vite@"^4.0.0 || ^5.0.0" from [email protected]
npm error node_modules/vite-plugin-vue-layouts
npm error dev vite-plugin-vue-layouts@"0.11.0" from the root project
npm error
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.
npm error
npm error
npm error For a full report see:
npm error /.npm/_logs/2025-06-01T05_05_08_263Z-eresolve-report.txt
npm error A complete log of this run can be found in: /.npm/_logs/2025-06-01T05_05_08_263Z-debug-0.log

✨ Finishing Touches
  • 📝 Generate Docstrings

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

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@riderx
Copy link
Contributor

riderx commented May 15, 2025

⚠️ Snyk checks are incomplete.

⚠️ security/snyk check encountered an error. (View Details)

@riderx riderx moved this to In Progress in Roadmap May 26, 2025
@riderx riderx added this to Roadmap May 26, 2025
@WcaleNieWolny WcaleNieWolny marked this pull request as ready for review May 30, 2025 15:29
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: 16

🔭 Outside diff range comments (1)
supabase/functions/_backend/utils/stripe.ts (1)

248-277: ⚠️ Potential issue

Add missing howMany parameter to function signature.

The function uses howMany on line 276 but it's not defined in the parameters.

-export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string) {
+export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string, howMany?: number) {
♻️ Duplicate comments (2)
messages/id.json (1)

123-125: Duplicate localization changes: please apply the same reviews as for messages/ja.json
The Indonesian file includes the same new keys. Refer to the feedback on messages/ja.json to ensure consistency, completeness across locales, and correct translations.

Also applies to: 165-167, 285-285, 294-294, 302-302, 304-304, 309-311, 324-324, 363-364, 408-408, 425-425, 516-516, 532-532, 546-546, 552-552, 555-555, 557-561, 563-563

messages/it.json (1)

123-125: Duplicate localization changes: please apply the same reviews as for messages/ja.json
The Italian file contains identical additions. Please ensure the keys and translations adhere to the same standards and address any typos identified in the Japanese locale.

Also applies to: 165-167, 285-285, 294-294, 302-302, 304-304, 309-311, 324-324, 363-364, 408-408, 425-425, 516-516, 532-532, 546-546, 552-552, 555-555, 557-561, 563-563

🧹 Nitpick comments (8)
src/components/BuyTokens.vue (1)

24-33: Consider improving accessibility and text formatting.

The heading structure and conditional rendering work well, but there are some considerations:

  1. Accessibility: Consider adding proper ARIA labels and semantic structure
  2. Text transformation: The .toLocaleLowerCase() call may not handle all languages correctly

Consider these improvements:

-        <h1 class="text-3xl">
+        <h1 class="text-3xl" role="heading" aria-level="1">
           {{ t('buy') }} {{ !custom ? amount : t('custom').toLocaleLowerCase() }}
         </h1>
-        <h2 class="text-xl">
+        <h2 class="text-xl" role="heading" aria-level="2">

For better internationalization, consider using a dedicated translation key instead of .toLocaleLowerCase():

-          {{ t('buy') }} {{ !custom ? amount : t('custom').toLocaleLowerCase() }}
+          {{ !custom ? t('buy-amount', { amount }) : t('buy-custom') }}
src/services/stripe.ts (1)

104-104: Add parameter validation for the howMany parameter.

Consider adding validation to ensure howMany is a positive number to prevent invalid requests to the backend.

export async function openCheckoutForOneOff(priceId: string, successUrl: string, cancelUrl: string, orgId: string, howMany: number) {
+  if (howMany <= 0) {
+    toast.error('Invalid quantity: must be a positive number')
+    return
+  }
supabase/functions/_backend/utils/stripe.ts (2)

285-287: Fix typo in function name.

-function toMilion(price: number) {
+function toMillion(price: number) {
  return `${price / 1000000}M`
}

Also update all usages of this function in the file.


308-314: Fix typo in variable name.

-    const formatedAmmount = 
+    const formattedAmount = 
      howManyInStep % 1000000 === 0 && howManyInStep > 0 ? 
-       toMilion(howManyInStep) : 
+       toMillion(howManyInStep) : 
        howManyInStep % 1000 === 0 && howManyInStep > 0 ? 
          `${howManyInStep / 1000}K` : 
          `${howManyInStep}`

Also update the usage on line 321:

-          name: `${formatedAmmount} tokens`,
+          name: `${formattedAmount} tokens`,
src/pages/settings/organization/Tokens.vue (2)

85-119: Optimize chart data processing for better performance.

The chart data processing in this watcher could be expensive as it processes a full year of data and runs on every tokensHistory change. Consider memoizing or debouncing this computation.

Consider this optimization:

-watch(tokensHistory, () => {
+const debouncedChartUpdate = debounce(() => {
   const oneYearAgoDate = new Date()
   // ... existing logic
+}, 300)
+
+watch(tokensHistory, debouncedChartUpdate)

Or use a computed property with proper dependency tracking for better performance.

🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 101-101: ESLint: 'd' is not modified in this loop (no-unmodified-loop-condition)


[error] 101-101: ESLint: 'today' is not modified in this loop (no-unmodified-loop-condition)


1-258: Consider component decomposition for better maintainability.

This component handles multiple responsibilities: data visualization, purchasing flow, and history management. Consider extracting some functionality into separate components or composables.

Potential improvements:

  • Extract chart logic into a separate composable (useTokensChart)
  • Create separate components for purchase flow and history view
  • Move price calculation logic to a separate utility or composable

This would improve testability, reusability, and maintainability of the code.

🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 101-101: ESLint: 'd' is not modified in this loop (no-unmodified-loop-condition)


[error] 101-101: ESLint: 'today' is not modified in this loop (no-unmodified-loop-condition)

supabase/migrations/20250131091941_capgo_tokens.sql (2)

1-9: Verify naming of the sum column
Using sum as a column name may shadow the SQL aggregate function. Consider renaming it to something like quantity or delta to avoid confusion.


36-81: get_tokens_history function is well implemented
The security-definer function checks admin rights, computes running totals, and limits to one year. Consider adding a composite index on (org_id, created_at) for even faster filtering and ordering.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c290371 and b584e12.

⛔ Files ignored due to path filters (3)
  • package-lock.json is excluded by !**/package-lock.json
  • src/assets/capgo-token-gold.svg is excluded by !**/*.svg
  • src/assets/mau.svg is excluded by !**/*.svg
📒 Files selected for processing (28)
  • messages/de.json (11 hunks)
  • messages/en.json (11 hunks)
  • messages/es.json (11 hunks)
  • messages/fr.json (11 hunks)
  • messages/id.json (11 hunks)
  • messages/it.json (11 hunks)
  • messages/ja.json (11 hunks)
  • messages/ko.json (11 hunks)
  • messages/pl.json (11 hunks)
  • messages/pt-br.json (11 hunks)
  • messages/ru.json (11 hunks)
  • messages/tr.json (11 hunks)
  • messages/vi.json (11 hunks)
  • messages/zh-cn.json (11 hunks)
  • package.json (2 hunks)
  • src/components.d.ts (2 hunks)
  • src/components/BuyTokens.vue (1 hunks)
  • src/layouts/settings.vue (2 hunks)
  • src/pages/settings/organization/Tokens.vue (1 hunks)
  • src/services/stripe.ts (1 hunks)
  • src/types/supabase.types.ts (2 hunks)
  • supabase/functions/_backend/private/stripe_checkout.ts (2 hunks)
  • supabase/functions/_backend/triggers/stripe_event.ts (2 hunks)
  • supabase/functions/_backend/utils/stripe.ts (3 hunks)
  • supabase/functions/_backend/utils/supabase.types.ts (2 hunks)
  • supabase/migrations/20250131091941_capgo_tokens.sql (1 hunks)
  • supabase/seed.sql (1 hunks)
  • vite.config.mts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
supabase/functions/_backend/private/stripe_checkout.ts (3)
supabase/functions/_backend/utils/loggin.ts (1)
  • cloudlog (3-17)
supabase/functions/_backend/utils/stripe.ts (2)
  • createCheckout (248-284)
  • createCheckoutForOneOff (289-357)
supabase/functions/_backend/utils/utils.ts (1)
  • getEnv (160-164)
supabase/functions/_backend/triggers/stripe_event.ts (1)
supabase/functions/_backend/utils/supabase.ts (1)
  • supabaseAdmin (60-69)
src/services/stripe.ts (1)
src/services/supabase.ts (1)
  • useSupabase (53-66)
🪛 Biome (1.9.4)
supabase/functions/_backend/utils/stripe.ts

[error] 6-6: Shouldn't redeclare 'supabaseAdmin'. Consider to delete it or rename it.

'supabaseAdmin' is defined here:

(lint/suspicious/noRedeclare)


[error] 303-303: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)

🪛 GitHub Actions: Run tests
supabase/functions/_backend/utils/stripe.ts

[error] 5-5: Uncaught SyntaxError: Identifier 'supabaseAdmin' has already been declared. This causes worker boot errors and prevents proper function execution.

🪛 GitHub Actions: autofix.ci
src/pages/settings/organization/Tokens.vue

[error] 101-101: ESLint: 'd' is not modified in this loop (no-unmodified-loop-condition)


[error] 101-101: ESLint: 'today' is not modified in this loop (no-unmodified-loop-condition)

🔇 Additional comments (55)
package.json (1)

167-167: LGTM! SVG loader dependency added correctly.

The vite-svg-loader dependency is properly added to support SVG loading capabilities for the new token management UI components.

messages/de.json (1)

123-563: Comprehensive German localization for token features.

The extensive set of German translations properly supports the new token purchasing and management functionality. The translations appear grammatically correct and contextually appropriate for the German market.

vite.config.mts (2)

11-11: SVG loader import added correctly.

The vite-svg-loader import is properly added to support SVG loading capabilities.


52-52: SVG loader plugin configured properly.

The svgLoader() plugin is correctly added to the plugins array to enable SVG loading throughout the application.

supabase/seed.sql (1)

535-548: Tiered pricing seed data looks well-structured.

The pricing tiers follow a logical volume-based discount model with decreasing unit costs for higher quantities. The ranges cover the full spectrum from small to enterprise-level purchases, providing good flexibility for different customer segments.

src/layouts/settings.vue (1)

2-2: Imports added correctly for token functionality.

The new imports for SVGAttributes, h function, and CurrencyIcon are properly added to support the tokens tab implementation.

Also applies to: 6-6, 8-8

src/components.d.ts (1)

6-6: LGTM! Proper component type declarations added.

The auto-generated TypeScript declarations correctly include the new components (BuyTokens, copy, Dashboard, KeyCreateDialog) introduced in this PR, enabling proper type support throughout the Vue application.

Also applies to: 18-18, 20-21, 29-29

messages/zh-cn.json (1)

123-125: LGTM! Comprehensive Chinese localization for token features.

The extensive additions provide proper Chinese translations for all token-related functionality including purchasing, management, history, and billing concepts. The translations appear contextually appropriate and follow consistent naming patterns.

Also applies to: 165-167, 285-285, 294-294, 302-302, 304-304, 309-311, 324-324, 363-365, 408-408, 424-424, 516-516, 532-532, 546-546, 551-551, 554-554, 557-563

supabase/functions/_backend/private/stripe_checkout.ts (4)

5-5: LGTM! Proper import added for one-off checkout functionality.

The import correctly adds the new createCheckoutForOneOff function needed for token purchases.


13-13: LGTM! Interface properly extended for token purchases.

The PortalData interface correctly adds:

  • 'one_off' option to the reccurence type for one-time token purchases
  • howMany: number field to specify token quantity

These additions align with the new token purchasing functionality.

Also applies to: 16-16


53-53: LGTM! Enhanced logging with context.

The addition of context: 'user' to the logging call provides better categorization for debugging and monitoring purposes.


54-56:

✅ Verification successful

Verify the conditional logic and URL construction.

The conditional logic correctly routes to different checkout functions based on the presence of howMany. However, there are a few considerations:

  1. The success URL construction for one-off checkouts includes a query parameter ?thankYou=true
  2. Both paths use different default success URLs

Let me verify that the referenced URLs and query parameters are properly handled:


🏁 Script executed:

#!/bin/bash
# Description: Check if the tokens page and thankYou parameter are properly handled in the frontend

# Search for the tokens page route and thankYou parameter handling
rg -A 5 -B 5 "thankYou" --type vue --type ts --type js
rg -A 3 "/dashboard/settings/organization/tokens" --type vue --type ts --type js

Length of output: 191


🏁 Script executed:

#!/bin/bash
# Re-verify thankYou parameter and tokens page route handling in the frontend

# Search for any occurrences of "thankYou" across common frontend file types
rg -g '*.vue' -g '*.ts' -g '*.js' "thankYou" -A5 -B5

# Search for the tokens page route in the same set of files
rg -g '*.vue' -g '*.ts' -g '*.js' "/dashboard/settings/organization/tokens" -A5 -B5

Length of output: 10412


Conditional Logic & URL Handling Verified
The one-off checkout success URL (…/dashboard/settings/organization/tokens?thankYou=true) is correctly surfaced in Tokens.vue via route.query.thankYou, and the recurring checkout default (…/app/usage) aligns with the existing usage page. No changes needed.

src/components/BuyTokens.vue (3)

6-11: LGTM! Well-typed component props.

The props are properly typed with TypeScript, covering all necessary data for the token purchase UI:

  • price and amount as numbers for pricing information
  • custom boolean for conditional rendering
  • icon string for dynamic icon rendering

13-15: LGTM! Clean event definition.

The component properly defines a single click event emission with correct TypeScript typing.


27-27: LGTM! Dynamic icon rendering implemented correctly.

The use of component :is="props.icon" properly enables dynamic icon rendering based on the passed prop, providing flexibility for different token types.

messages/en.json (1)

123-126: LGTM! Comprehensive localization support for token features.

The new localization keys provide comprehensive coverage for the token purchase and management features. The key names are descriptive and follow existing naming conventions. All additions are new keys without modifying existing functionality.

Also applies to: 165-167, 287-287, 296-296, 304-304, 309-312, 324-325, 364-366, 409-409, 424-424, 516-516, 531-531, 546-546, 551-551, 554-554, 557-563

messages/fr.json (1)

123-126: Excellent French localization for token features.

The French translations are comprehensive and natural. The key structure matches the English version perfectly. The correction in the encryption-related key improves accuracy.

Also applies to: 165-167, 285-285, 294-294, 302-302, 309-312, 324-324, 363-365, 408-408, 424-424, 516-516, 532-532, 546-546, 551-551, 554-554, 557-563

messages/ru.json (1)

123-126: Comprehensive Russian localization for token features.

The Russian translations provide complete coverage for all token-related features with consistent key structure across all language files. The translations maintain appropriate technical terminology while remaining accessible.

Also applies to: 165-167, 285-285, 294-294, 302-302, 309-312, 324-324, 363-365, 408-408, 424-424, 516-516, 532-532, 546-546, 551-551, 554-554, 557-563

messages/ko.json (1)

123-563: Comprehensive Korean localization for token features looks good.

The extensive addition of Korean localization keys for the new token management system is well-structured and appears to cover all necessary UI elements including:

  • Token purchasing and management
  • Usage history and explanations
  • MAU token functionality
  • Billing and limit increase messaging

The key naming follows consistent patterns with the existing localization structure.

src/types/supabase.types.ts (3)

288-322: capgo_tokens_history table definition looks well-structured.

The table definition includes:

  • Proper primary key (id)
  • Foreign key relationship to orgs table via org_id
  • Appropriate fields for tracking token transactions (sum, reason)
  • Standard timestamp fields with proper defaults
  • Complete Row/Insert/Update type definitions

The schema design follows established patterns in the codebase.


323-352: capgo_tokens_steps table definition is properly implemented.

The table effectively models tiered pricing steps with:

  • Price tier boundaries (step_min, step_max)
  • Stripe integration via price_id
  • Unit pricing information (price_per_unit)
  • Complete type definitions for all operations

The lack of foreign key relationships is appropriate as this appears to be a reference/configuration table.


1611-1622: get_tokens_history function definition is correctly typed.

The function signature properly:

  • Accepts organization ID parameter
  • Returns structured history data with running totals
  • Includes all necessary fields for token transaction tracking
  • Follows the established function definition pattern

The return type structure aligns well with typical usage history requirements.

messages/pl.json (1)

123-563: LGTM! Polish translations added for token features.

The localization entries properly support the new token purchase and management functionality.

supabase/functions/_backend/utils/supabase.types.ts (1)

288-352: LGTM! Well-structured database schema for token management.

The new tables and function properly support token purchase tracking and tiered pricing.

Also applies to: 1611-1622

messages/ja.json (6)

285-285: Approve generic UI term additions
These single-term keys (for, gb, history, how-many-tokens, or, see-your, show-full-history, sum, thank-you-for-money, this-will-allow-you-to, total-tokens) are straightforward UI strings that align with other locales. No issues detected.

Also applies to: 294-294, 302-302, 304-304, 425-425, 516-516, 532-532, 546-546, 552-552, 555-555, 563-563


309-311: Approve limit-increase description keys
The keys describing limit increases (increase-bandwidth-limit-by, increase-mau-limit-by, increase-storage-limit-by, it-will-increase-your-mau-by) are correctly added and follow naming conventions. Looks good.

Also applies to: 324-324


363-364: Approve token management page UI keys
The keys manage-your, manage-your-tokens-here, and no-tokens-history support the management overview UI. They are well-named and consistent.

Also applies to: 408-408


557-558: Approve token explanation and history summary keys
The keys tokens-cost, tokens-explanation-mau, tokens-history, and tokens-key-mau provide necessary information for the user. They align with the frontend UI needs.

Also applies to: 560-561


165-167:

✅ Verification successful

Consistent token management keys across locales
The keys capgo-tokens, capgo-tokens-mau, and capgo-tokens-mau-history support the token management page. Confirm they exist in every localization to maintain feature parity.


🏁 Script executed:

rg -L '"capgo-tokens"' messages/*.json

Length of output: 982


🏁 Script executed:

#!/bin/bash
echo "Missing capgo-tokens-mau:"
rg -L '"capgo-tokens-mau"' messages/*.json
echo ""
echo "Missing capgo-tokens-mau-history:"
rg -L '"capgo-tokens-mau-history"' messages/*.json

Length of output: 2437


All locales include token management keys
Verified that capgo-tokens, capgo-tokens-mau, and capgo-tokens-mau-history are present in every messages/*.json file—no missing entries found.


123-125:

✅ Verification successful

Validate purchase-related localization keys
Ensure that the new keys buy, buy-any-amount, and buy-tokens are present and consistent across all locale JSON files to prevent missing translations in the token purchase UI.


🏁 Script executed:

#!/bin/bash
# Verify presence of purchase keys in all locale files
rg -L '"buy"' messages/*.json

Length of output: 753



🏁 Script executed:

#!/bin/bash
# Verify presence of all purchase-related localization keys in every locale file
for key in buy buy-any-amount buy-tokens; do
  echo "=== Checking key: \"$key\" ==="
  missing=$(rg -L "\"$key\"" messages/*.json)
  if [ -n "$missing" ]; then
    echo "❌ Key \"$key\" is missing in:"
    echo "$missing"
  else
    echo "✔️ Key \"$key\" found in all files."
  fi
  echo
done

Length of output: 9818



🏁 Script executed:

#!/bin/bash
# Verify presence of purchase-related localization keys in all locale files using grep
for key in buy buy-any-amount buy-tokens; do
  echo "=== Checking key: \"$key\" ==="
  missing=$(grep -L "\"$key\"" messages/*.json)
  if [ -n "$missing" ]; then
    echo "❌ Missing \"$key\" in:"
    echo "$missing"
  else
    echo "✔️ \"$key\" present in all locale files."
  fi
  echo
done

Length of output: 1562


Purchase-related localization keys verified
All three keys—buy, buy-any-amount, and buy-tokens—have been confirmed in every locale JSON file. No missing translations detected.

messages/tr.json (1)

123-167: LGTM: Turkish localization additions look consistent and complete.

The Turkish translations for the new token system features are well-structured and follow the existing localization patterns. The translations appear grammatically correct and comprehensive for the token purchasing, management, and history features.

Also applies to: 285-285, 302-302, 304-304, 309-311, 324-324, 363-365, 408-408, 424-424, 516-516, 532-532, 546-546, 551-551, 554-554, 557-564

messages/es.json (1)

123-167: LGTM: Spanish localization additions are consistent and comprehensive.

The Spanish translations properly support the new token system features with grammatically correct translations that follow the existing localization patterns.

Also applies to: 285-285, 302-302, 304-304, 309-311, 324-324, 363-365, 408-408, 424-424, 516-516, 532-532, 546-546, 551-551, 554-554, 557-564

messages/pt-br.json (9)

123-125: Purchase-related localization keys look good
The new "buy", "buy-any-amount", and "buy-tokens" entries have accurate Portuguese translations that match the UI tone.


165-167: Capgo tokens labels are correctly added
The "capgo-tokens", "capgo-tokens-mau", and "capgo-tokens-mau-history" keys have clear and consistent Portuguese labels.


285-285: General UI term additions – verify consistency
Multiple common UI terms ("for", "gb", "history", "how-many-tokens", "or", "see-your", "show-full-history", "sum") were added. Please double-check they match other language files and styling guidelines.

Also applies to: 294-294, 303-304, 424-424, 516-516, 532-532, 547-547


309-311: Limit-increase message translations are accurate
The "increase-bandwidth-limit-by", "increase-mau-limit-by", and "increase-storage-limit-by" keys have clear, imperative Portuguese translations.


363-364: Management section translations – looks good
The "manage-your" and "manage-your-tokens-here" keys are translated consistently.


408-408: Empty history message is clear
The "no-tokens-history" translation communicates the empty-state appropriately.


551-551: Thank-you message translated correctly
The "thank-you-for-money" key has a polite and direct Portuguese equivalent.


555-555: Action prompt translation is fine
The "this-will-allow-you-to" message is clear and invites the user correctly.


557-561: Tokens overview and explanation keys
Most of the token-related strings ("tokens-cost", "tokens-explanation-mau", "tokens-explanation-message-mau", "tokens-history", "total-tokens") are well translated.
However, the "tokens-key-mau": "RUIM" translation appears incorrect—“ruim” means “bad” in Portuguese. Please verify and correct this label to match the intended MAU token key.

Also applies to: 563-563

messages/vi.json (9)

123-125: Purchase UI keys in Vietnamese are accurate
The "buy", "buy-any-amount", and "buy-tokens" translations clearly convey the purchase actions.


165-167: Capgo token labels added correctly
The "capgo-tokens", "capgo-tokens-mau", and "capgo-tokens-mau-history" keys have clear Vietnamese equivalents.


285-285: Common UI term additions – please verify
Several general UI terms ("for", "gb", "history", "how-many-tokens", "or", "see-your", "show-full-history", "sum") were introduced. Ensure they align with other languages and the UI style guide.

Also applies to: 294-294, 302-302, 304-304, 424-424, 516-516, 532-532, 546-546


309-311: Limit-increase keys are well translated
The "increase-bandwidth-limit-by", "increase-mau-limit-by", and "increase-storage-limit-by" strings accurately reflect the intended action.


363-364: Management prompts in Vietnamese are clear
The "manage-your" and "manage-your-tokens-here" entries read naturally.


408-408: Empty-state history message is fine
The "no-tokens-history" translation properly indicates an empty history.


551-551: Thank-you message translation looks good
The "thank-you-for-money" entry thanks the user appropriately in Vietnamese.


554-554: Feature introduction prompt
The "this-will-allow-you-to" translation is clear and inviting.


557-561: Tokens explanation and cost keys
The batch of new token-related strings ("tokens-cost", "tokens-explanation-mau", "tokens-explanation-message-mau", "tokens-history", "total-tokens") are generally well translated.
One anomaly: "tokens-key-mau": "MÀU" currently reads “color” in Vietnamese. Please verify that this key matches the intended MAU token abbreviation.

Also applies to: 563-563

supabase/migrations/20250131091941_capgo_tokens.sql (5)

11-27: Indexes and comments for capgo_tokens_history look solid
Great use of descriptive comments and individual indexes on org_id and created_at for performance.


94-119: Pricing tier comments, trigger, index, and read policy are correct
The strategy to enable RLS, grant public SELECT, and document the table is well structured.


121-150: get_extra_mau_for_org logic – confirm handling of negative entries
The function sums absolute values for 'MAU limit increased'. If refunds or negative adjustments exist, ABS may misrepresent the net. Please verify this behavior matches business requirements.


157-182: get_total_mau_tokens mirrors extra MAU logic
This one-year lookback is consistent. Same note applies regarding the use of ABS on negative adjustments—confirm expected.


184-189: Privilege grants for service_role are correct
Revoking public/anonymous access and granting only to service_role follows best security practices.

Comment on lines +37 to +40
<button @click="$emit('click')" class="bg-[#f8b324] text-white p-3 rounded-full aspect-square transform transition-transform hover:scale-120">
<CalculatorIcon v-if="custom" class="w-6 h-6 text-black" />
<ShoppingCartIcon v-else class="w-6 h-6 text-black" />
</button>
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider improving button accessibility and styling.

The button implementation has good interactivity but could benefit from accessibility improvements:

  1. Missing accessibility attributes: No ARIA labels or descriptions
  2. Color contrast: Ensure sufficient contrast for accessibility
  3. Focus management: Consider focus indicators

Apply these accessibility improvements:

-      <button @click="$emit('click')" class="bg-[#f8b324] text-white p-3 rounded-full aspect-square transform transition-transform hover:scale-120">
+      <button 
+        @click="$emit('click')" 
+        :aria-label="custom ? t('open-calculator') : t('buy-tokens')"
+        class="bg-[#f8b324] text-white p-3 rounded-full aspect-square transform transition-transform hover:scale-120 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#f8b324]"
+      >

Also consider using CSS custom properties instead of hardcoded colors for better maintainability.

📝 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
<button @click="$emit('click')" class="bg-[#f8b324] text-white p-3 rounded-full aspect-square transform transition-transform hover:scale-120">
<CalculatorIcon v-if="custom" class="w-6 h-6 text-black" />
<ShoppingCartIcon v-else class="w-6 h-6 text-black" />
</button>
<button
@click="$emit('click')"
:aria-label="custom ? t('open-calculator') : t('buy-tokens')"
class="bg-[#f8b324] text-white p-3 rounded-full aspect-square transform transition-transform hover:scale-120 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#f8b324]"
>
<CalculatorIcon v-if="custom" class="w-6 h-6 text-black" />
<ShoppingCartIcon v-else class="w-6 h-6 text-black" />
</button>
🤖 Prompt for AI Agents
In src/components/BuyTokens.vue around lines 37 to 40, the button lacks
accessibility features such as ARIA labels, sufficient color contrast, and
visible focus indicators. Add an appropriate aria-label attribute to the button
to describe its action, replace the hardcoded background color with a CSS custom
property for maintainability, ensure the text and background colors meet
contrast standards, and add a visible focus style (like an outline or
box-shadow) to improve keyboard navigation visibility.

Comment on lines 104 to 128
export async function openCheckoutForOneOff(priceId: string, successUrl: string, cancelUrl: string, orgId: string, howMany: number) {
// console.log('openCheckout')
const supabase = useSupabase()
const session = await supabase.auth.getSession()
if (!session)
return
try {
const resp = await supabase.functions.invoke('private/stripe_checkout', {
body: JSON.stringify({
priceId,
successUrl,
cancelUrl,
reccurence: 'one_off',
orgId,
howMany,
}),
})
if (!resp.error && resp.data && resp.data.url)
openBlank(resp.data.url)
}
catch (error) {
console.error(error)
toast.error('Cannot get your checkout')
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider refactoring to reduce code duplication.

The openCheckoutForOneOff function is nearly identical to the existing openCheckout function, creating significant code duplication. Consider refactoring both functions to use a shared implementation.

Here's a suggested refactor to eliminate duplication:

+interface CheckoutOptions {
+  priceId: string
+  successUrl: string
+  cancelUrl: string
+  orgId: string
+  recurrence: 'year' | 'month' | 'one_off'
+  howMany?: number
+}
+
+async function createCheckoutSession(options: CheckoutOptions) {
+  const supabase = useSupabase()
+  const session = await supabase.auth.getSession()
+  if (!session)
+    return
+  
+  try {
+    const payload: any = {
+      priceId: options.priceId,
+      successUrl: options.successUrl,
+      cancelUrl: options.cancelUrl,
+      reccurence: options.recurrence,
+      orgId: options.orgId,
+    }
+    
+    if (options.howMany !== undefined) {
+      payload.howMany = options.howMany
+    }
+    
+    const resp = await supabase.functions.invoke('private/stripe_checkout', {
+      body: JSON.stringify(payload),
+    })
+    if (!resp.error && resp.data && resp.data.url)
+      openBlank(resp.data.url)
+  }
+  catch (error) {
+    console.error(error)
+    toast.error('Cannot get your checkout')
+  }
+}

export async function openCheckout(priceId: string, successUrl: string, cancelUrl: string, isYear: boolean, orgId: string) {
-  const supabase = useSupabase()
-  const session = await supabase.auth.getSession()
-  if (!session)
-    return
-  try {
-    const resp = await supabase.functions.invoke('private/stripe_checkout', {
-      body: JSON.stringify({
-        priceId,
-        successUrl,
-        cancelUrl,
-        reccurence: isYear ? 'year' : 'month',
-        orgId,
-      }),
-    })
-    if (!resp.error && resp.data && resp.data.url)
-      openBlank(resp.data.url)
-  }
-  catch (error) {
-    console.error(error)
-    toast.error('Cannot get your checkout')
-  }
+  return createCheckoutSession({
+    priceId,
+    successUrl,
+    cancelUrl,
+    orgId,
+    recurrence: isYear ? 'year' : 'month'
+  })
}

export async function openCheckoutForOneOff(priceId: string, successUrl: string, cancelUrl: string, orgId: string, howMany: number) {
-  //   console.log('openCheckout')
-    const supabase = useSupabase()
-    const session = await supabase.auth.getSession()
-    if (!session)
-      return
-    try {
-      const resp = await supabase.functions.invoke('private/stripe_checkout', {
-        body: JSON.stringify({
-          priceId,
-          successUrl,
-          cancelUrl,
-          reccurence: 'one_off',
-          orgId,
-          howMany,
-        }),
-      })
-      if (!resp.error && resp.data && resp.data.url)
-        openBlank(resp.data.url)
-    }
-    catch (error) {
-      console.error(error)
-      toast.error('Cannot get your checkout')
-    }
+  return createCheckoutSession({
+    priceId,
+    successUrl,
+    cancelUrl,
+    orgId,
+    recurrence: 'one_off',
+    howMany
+  })
}
🤖 Prompt for AI Agents
In src/services/stripe.ts between lines 104 and 128, the openCheckoutForOneOff
function duplicates much of the logic found in the openCheckout function. To fix
this, create a shared helper function that accepts parameters for priceId,
successUrl, cancelUrl, orgId, howMany, and recurrence type. Then refactor both
openCheckoutForOneOff and openCheckout to call this helper with the appropriate
recurrence value, eliminating duplicated code and centralizing the checkout
invocation logic.

.single()

if (!org || orgError) {
console.log({ requestId: c.get('requestId'), context: 'no org found for payment intent', paymentIntent })
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Use consistent logging with cloudlog instead of console.log.

For consistency with the rest of the codebase and proper request tracking, replace console.log with cloudlog.

-          console.log({ requestId: c.get('requestId'), context: 'no org found for payment intent', paymentIntent })
+          cloudlog({ requestId: c.get('requestId'), context: 'no org found for payment intent', paymentIntent })
-          console.log({ requestId: c.get('requestId'), context: 'no howMany found for payment intent', paymentIntent })
+          cloudlog({ requestId: c.get('requestId'), context: 'no howMany found for payment intent', paymentIntent })
-          console.log({ requestId: c.get('requestId'), context: 'error inserting capgo_tokens_history', dbError })
+          cloudlog({ requestId: c.get('requestId'), context: 'error inserting capgo_tokens_history', dbError })

Also applies to: 50-50, 64-64

🤖 Prompt for AI Agents
In supabase/functions/_backend/triggers/stripe_event.ts at lines 43, 50, and 64,
replace all instances of console.log with cloudlog to maintain consistent
logging practices across the codebase and ensure proper request tracking. Update
the logging calls to use cloudlog with the same parameters currently passed to
console.log.

Comment on lines +47 to +53
const howMany = paymentIntent.metadata?.howMany
const parsedHowMany = Number.parseInt(howMany ?? '0')
if (!howMany || Number.isNaN(parsedHowMany)) {
console.log({ requestId: c.get('requestId'), context: 'no howMany found for payment intent', paymentIntent })
return c.json({ received: false })
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add validation for positive token quantity.

The current validation only checks if howMany is a valid number, but doesn't ensure it's positive.

        const howMany = paymentIntent.metadata?.howMany
        const parsedHowMany = Number.parseInt(howMany ?? '0')
-       if (!howMany || Number.isNaN(parsedHowMany)) {
+       if (!howMany || Number.isNaN(parsedHowMany) || parsedHowMany <= 0) {
-         console.log({ requestId: c.get('requestId'), context: 'no howMany found for payment intent', paymentIntent })
+         cloudlog({ requestId: c.get('requestId'), context: 'invalid howMany for payment intent', paymentIntent, howMany, parsedHowMany })
          return c.json({ received: false })
        }
📝 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 howMany = paymentIntent.metadata?.howMany
const parsedHowMany = Number.parseInt(howMany ?? '0')
if (!howMany || Number.isNaN(parsedHowMany)) {
console.log({ requestId: c.get('requestId'), context: 'no howMany found for payment intent', paymentIntent })
return c.json({ received: false })
}
const howMany = paymentIntent.metadata?.howMany
const parsedHowMany = Number.parseInt(howMany ?? '0')
if (!howMany || Number.isNaN(parsedHowMany) || parsedHowMany <= 0) {
cloudlog({
requestId: c.get('requestId'),
context: 'invalid howMany for payment intent',
paymentIntent,
howMany,
parsedHowMany
})
return c.json({ received: false })
}
🤖 Prompt for AI Agents
In supabase/functions/_backend/triggers/stripe_event.ts around lines 47 to 53,
the validation for the `howMany` value only checks if it is a valid number but
does not verify that it is positive. Update the condition to also check that
`parsedHowMany` is greater than zero. If `parsedHowMany` is zero or negative,
log the appropriate message and return the JSON response indicating the request
was not received.

<h2 class="text-base 2xl:text-lg font-semibold text-center">
{{ t('how-many-tokens') }}
</h2>
<input v-model="tokensToBuy" type="number" min="1" step="1" class="mt-2 bg-gray-700 rounded-full text-center text-white" @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy)))">
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add input validation and accessibility attributes.

The number input for token amount needs better validation and accessibility support.

Apply these improvements:

-<input v-model="tokensToBuy" type="number" min="1" step="1" class="mt-2 bg-gray-700 rounded-full text-center text-white" @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy)))">
+<input 
+  v-model="tokensToBuy" 
+  type="number" 
+  min="1" 
+  step="1" 
+  :aria-label="t('how-many-tokens')"
+  :placeholder="t('how-many-tokens')"
+  class="mt-2 bg-gray-700 rounded-full text-center text-white focus:ring-2 focus:ring-blue-500 focus:outline-none" 
+  @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy || 0)))"
+>
📝 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
<input v-model="tokensToBuy" type="number" min="1" step="1" class="mt-2 bg-gray-700 rounded-full text-center text-white" @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy)))">
<input
v-model="tokensToBuy"
type="number"
min="1"
step="1"
:aria-label="t('how-many-tokens')"
:placeholder="t('how-many-tokens')"
class="mt-2 bg-gray-700 rounded-full text-center text-white focus:ring-2 focus:ring-blue-500 focus:outline-none"
@input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy || 0)))"
>
🤖 Prompt for AI Agents
In src/pages/settings/organization/Tokens.vue at line 357, the number input for
tokensToBuy lacks proper validation and accessibility attributes. Enhance
validation by ensuring the input value is always a positive integer using
Math.max, Math.floor, and Math.abs as currently done, but also add attributes
like aria-label or aria-describedby for accessibility. Additionally, consider
adding input validation feedback or constraints such as required and pattern if
applicable to improve user experience and accessibility compliance.

Comment on lines +150 to +157
if (tokensHistoryValue.error) {
console.error('tokensHistory.error', tokensHistoryValue.error)
return
}
if (tokensStepsValue.error) {
console.error('tokensSteps.error', tokensStepsValue.error)
return
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve error handling for better user experience.

The current error handling only logs to console, which doesn't provide feedback to users when data loading fails.

Consider showing user-friendly error messages:

   if (tokensHistoryValue.error) {
-    console.error('tokensHistory.error', tokensHistoryValue.error)
+    console.error('tokensHistory.error', tokensHistoryValue.error)
+    displayStore.dialogOption = {
+      header: t('error'),
+      message: t('cannot-load-tokens-history'),
+      buttons: [{ text: t('ok'), role: 'cancel' }]
+    }
+    displayStore.showDialog = true
     return
   }
📝 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
if (tokensHistoryValue.error) {
console.error('tokensHistory.error', tokensHistoryValue.error)
return
}
if (tokensStepsValue.error) {
console.error('tokensSteps.error', tokensStepsValue.error)
return
}
if (tokensHistoryValue.error) {
console.error('tokensHistory.error', tokensHistoryValue.error)
displayStore.dialogOption = {
header: t('error'),
message: t('cannot-load-tokens-history'),
buttons: [{ text: t('ok'), role: 'cancel' }]
}
displayStore.showDialog = true
return
}
if (tokensStepsValue.error) {
console.error('tokensSteps.error', tokensStepsValue.error)
return
}
🤖 Prompt for AI Agents
In src/pages/settings/organization/Tokens.vue around lines 150 to 157, the error
handling only logs errors to the console without informing the user. Update the
code to display user-friendly error messages in the UI when
tokensHistoryValue.error or tokensStepsValue.error occurs, such as using a
notification component or rendering an error message element, so users are aware
of the data loading failure.

})

// Iterate through each day
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix ESLint loop condition errors.

The ESLint errors indicate issues with the loop condition variables. The loop variable d is being modified correctly with d.setDate(d.getDate() + 1), but the condition checking against today could be problematic since today is derived from oneYearAgo.

Apply this fix to address the loop condition issues:

-  const today = new Date(+oneYearAgo)
-  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)
-
-  const data = []
-  let lastToken = 0
-
-  const tokensChanges = tokensHistory.value.map((token) => {
-    return {
-      date: new Date(token.created_at),
-      change: token.sum,
-    }
-  })
-
-  // Iterate through each day
-  for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
+  const oneYearAgoDate = new Date()
+  oneYearAgoDate.setFullYear(oneYearAgoDate.getFullYear() - 1)
+  const today = new Date()
+
+  const data = []
+  let lastToken = 0
+
+  const tokensChanges = tokensHistory.value.map((token) => {
+    return {
+      date: new Date(token.created_at),
+      change: token.sum,
+    }
+  })
+
+  // Iterate through each day
+  for (const d = new Date(oneYearAgoDate); d <= today; d.setDate(d.getDate() + 1)) {
📝 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
for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
const oneYearAgoDate = new Date()
oneYearAgoDate.setFullYear(oneYearAgoDate.getFullYear() - 1)
const today = new Date()
const data = []
let lastToken = 0
const tokensChanges = tokensHistory.value.map((token) => {
return {
date: new Date(token.created_at),
change: token.sum,
}
})
// Iterate through each day
for (const d = new Date(oneYearAgoDate); d <= today; d.setDate(d.getDate() + 1)) {
// ... existing loop body ...
}
🧰 Tools
🪛 GitHub Actions: autofix.ci

[error] 101-101: ESLint: 'd' is not modified in this loop (no-unmodified-loop-condition)


[error] 101-101: ESLint: 'today' is not modified in this loop (no-unmodified-loop-condition)

🤖 Prompt for AI Agents
In src/pages/settings/organization/Tokens.vue at line 101, the for-loop
condition uses a Date object 'd' compared to 'today', which may cause ESLint
loop condition errors due to object reference comparison. To fix this, convert
both 'd' and 'today' to their numeric time values using getTime() in the loop
condition, ensuring a proper numeric comparison. This will resolve the ESLint
errors related to the loop condition.

Comment on lines +28 to +35
-- Create trigger for updating updated_at column
CREATE TRIGGER handle_updated_at
BEFORE UPDATE ON capgo_tokens_history
FOR EACH ROW
EXECUTE FUNCTION extensions.moddatetime('updated_at');

ALTER TABLE capgo_tokens_history ENABLE ROW LEVEL SECURITY;

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

RLS enabled without policies on history table
You’ve enabled row-level security on capgo_tokens_history but haven’t defined any SELECT/INSERT policies, which will block all access. Please add appropriate RLS policies for service roles or authenticated functions.

🤖 Prompt for AI Agents
In supabase/migrations/20250131091941_capgo_tokens.sql around lines 28 to 35,
row-level security (RLS) is enabled on the capgo_tokens_history table without
defining any RLS policies, which will block all access. To fix this, add
appropriate RLS policies that allow SELECT and INSERT operations for the service
roles or authenticated users as needed, ensuring the intended access is granted
while maintaining security.

Comment on lines 83 to 92
-- Create the capgo_tokens_steps table
CREATE TABLE IF NOT EXISTS capgo_tokens_steps (
id BIGSERIAL PRIMARY KEY,
step_min bigint NOT NULL,
step_max bigint NOT NULL,
price_per_unit FLOAT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT step_range_check CHECK (step_min < step_max)
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

capgo_tokens_steps table design – reconsider data type
Defining price_per_unit as FLOAT can lead to rounding issues. It’s safer to use NUMERIC(10,4) or DECIMAL for monetary values.

🤖 Prompt for AI Agents
In supabase/migrations/20250131091941_capgo_tokens.sql around lines 83 to 92,
the column price_per_unit is defined as FLOAT, which can cause rounding errors
for monetary values. Change the data type of price_per_unit to NUMERIC(10,4) or
DECIMAL to ensure precise storage of monetary amounts and avoid floating-point
inaccuracies.

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: 4

🔭 Outside diff range comments (1)
supabase/functions/_backend/utils/stripe.ts (1)

247-275: ⚠️ Potential issue

Fix undefined variable error in createCheckout function.

The function uses howMany at line 275 but it's not defined as a parameter.

Add the missing parameter to the function signature:

-export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string) {
+export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string, howMany?: number) {
♻️ Duplicate comments (6)
supabase/functions/_backend/utils/stripe.ts (3)

291-295: ⚠️ Potential issue

Add validation for empty pricing steps.

The while loop could run indefinitely if tokensSteps is empty or malformed.

  const { data: tokensSteps, error } = await supabaseAdmin(c).from('capgo_tokens_steps').select('*').order('step_min', { ascending: true })
  if (error)
    return { url: '' }
+
+ if (!tokensSteps || tokensSteps.length === 0) {
+   cloudlog({ requestId: c.get('requestId'), context: 'createCheckoutForOneOff', error: 'No pricing steps configured' })
+   return { url: '' }
+ }

298-329: ⚠️ Potential issue

Add bounds checking for array access in pricing loop.

The loop increments i without checking if it exceeds the array bounds.

  while (true) {
+   if (i >= tokensSteps.length) {
+     cloudlog({ requestId: c.get('requestId'), context: 'createCheckoutForOneOff', error: 'Pricing steps exhausted', howMany, stepsCount: tokensSteps.length })
+     return { url: '' }
+   }
    const step = tokensSteps[i]
🧰 Tools
🪛 Biome (1.9.4)

[error] 302-302: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)


302-302: ⚠️ Potential issue

Use BigInt or constant for large number literal to avoid precision loss.

The number 9223372036854775807 exceeds JavaScript's safe integer range and will lose precision.

Define a constant to represent "unlimited":

const UNLIMITED_TOKENS = 9223372036854775807n // Use BigInt

// Then in the comparison:
step.step_max !== Number(UNLIMITED_TOKENS)

Or use a more practical maximum:

-      step.step_max !== 9223372036854775807
+      step.step_max !== Number.MAX_SAFE_INTEGER
🧰 Tools
🪛 Biome (1.9.4)

[error] 302-302: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)

src/pages/settings/organization/Tokens.vue (3)

233-242: Strengthen Stripe checkout parameter validation and error handling

Both the client‐side helper and the serverless function need tighter guards to prevent invalid or malicious requests.

Run the following script to verify the current implementation of openCheckoutForOneOff:

#!/bin/bash
# Description: Check the implementation of openCheckoutForOneOff and related server-side validation

# Check the openCheckoutForOneOff implementation
echo "=== Checking openCheckoutForOneOff implementation ==="
rg -A 20 "function openCheckoutForOneOff" src/services/stripe.ts

# Check if the stripe_checkout serverless function validates parameters
echo -e "\n=== Checking stripe_checkout parameter validation ==="
rg -A 30 "stripe_checkout" supabase/functions/_backend/private/

158-165: 🛠️ Refactor suggestion

Improve error handling for better user experience.

The current error handling only logs to console, which doesn't provide feedback to users when data loading fails.

Consider showing user-friendly error messages:

   if (tokensHistoryValue.error) {
-    console.error('tokensHistory.error', tokensHistoryValue.error)
+    console.error('tokensHistory.error', tokensHistoryValue.error)
+    displayStore.dialogOption = {
+      header: t('error'),
+      message: t('cannot-load-tokens-history'),
+      buttons: [{ text: t('ok'), role: 'cancel' }]
+    }
+    displayStore.showDialog = true
     return
   }

389-389: 🛠️ Refactor suggestion

Add input validation and accessibility attributes.

The number input for token amount needs better validation and accessibility support.

Apply these improvements:

-<input v-model="tokensToBuy" type="number" min="1" step="1" class="mt-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-full text-center text-gray-900 dark:text-white" @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy)))">
+<input 
+  v-model="tokensToBuy" 
+  type="number" 
+  min="1" 
+  step="1" 
+  :aria-label="t('how-many-tokens')"
+  :placeholder="t('enter-token-amount')"
+  class="mt-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-full text-center text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 focus:outline-none" 
+  @input="tokensToBuy = Math.max(1, Math.floor(Math.abs(tokensToBuy || 0)))"
+>
🧹 Nitpick comments (2)
supabase/functions/_backend/utils/stripe.ts (1)

284-286: Fix typo in function name.

The function name has a spelling error.

-function toMilion(price: number) {
-  return `${price / 1000000}M`
-}
+function toMillion(price: number) {
+  return `${price / 1000000}M`
+}

Also update the function calls at lines 303-305:

-        ? `Price per token: ${step.price_per_unit} between ${toMilion(step.step_min)} and ${toMilion(step.step_max)}`
-        : `Price per token: ${step.price_per_unit} after ${toMilion(step.step_min)}`
-      : `Price per token: ${step.price_per_unit} up to ${toMilion(step.step_max)}`
+        ? `Price per token: ${step.price_per_unit} between ${toMillion(step.step_min)} and ${toMillion(step.step_max)}`
+        : `Price per token: ${step.price_per_unit} after ${toMillion(step.step_min)}`
+      : `Price per token: ${step.price_per_unit} up to ${toMillion(step.step_max)}`

And at line 309:

-        toMilion(howManyInStep) :
+        toMillion(howManyInStep) :
src/pages/settings/organization/Tokens.vue (1)

1-511: Consider decomposing this large component for better maintainability.

This 500+ line component handles many responsibilities. Consider splitting into:

  • A composable for token data fetching and calculations
  • Separate components for history display, purchase UI, and charts
  • Extract price calculation logic into a utility module

Example structure:

// composables/useTokens.ts
export function useTokens() {
  const tokensHistory = ref<...>([])
  const tokensSteps = ref<...>([])
  
  async function loadData() { ... }
  function computePrice(howMany: number) { ... }
  function computeTokens(euros: number) { ... }
  
  return { tokensHistory, tokensSteps, loadData, computePrice, computeTokens }
}

// components/TokensChart.vue
// components/TokensHistory.vue  
// components/TokensPurchase.vue
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b584e12 and 1754788.

⛔ Files ignored due to path filters (1)
  • src/assets/storage.svg is excluded by !**/*.svg
📒 Files selected for processing (21)
  • messages/de.json (11 hunks)
  • messages/en.json (16 hunks)
  • messages/es.json (11 hunks)
  • messages/fr.json (11 hunks)
  • messages/id.json (11 hunks)
  • messages/it.json (11 hunks)
  • messages/ja.json (11 hunks)
  • messages/ko.json (11 hunks)
  • messages/pl.json (11 hunks)
  • messages/pt-br.json (11 hunks)
  • messages/ru.json (11 hunks)
  • messages/tr.json (11 hunks)
  • messages/vi.json (11 hunks)
  • messages/zh-cn.json (11 hunks)
  • src/components.d.ts (1 hunks)
  • src/components/BlurBg.vue (2 hunks)
  • src/components/BuyTokens.vue (1 hunks)
  • src/layouts/settings.vue (2 hunks)
  • src/pages/settings/organization/Tokens.vue (1 hunks)
  • src/typed-router.d.ts (1 hunks)
  • supabase/functions/_backend/utils/stripe.ts (2 hunks)
✅ Files skipped from review due to trivial changes (2)
  • src/typed-router.d.ts
  • messages/it.json
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/layouts/settings.vue
  • src/components.d.ts
  • src/components/BuyTokens.vue
  • messages/ko.json
  • messages/pl.json
  • messages/fr.json
  • messages/es.json
  • messages/de.json
  • messages/en.json
  • messages/zh-cn.json
  • messages/ru.json
  • messages/ja.json
  • messages/id.json
  • messages/tr.json
  • messages/pt-br.json
  • messages/vi.json
🧰 Additional context used
🪛 Biome (1.9.4)
supabase/functions/_backend/utils/stripe.ts

[error] 302-302: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)

🔇 Additional comments (3)
src/components/BlurBg.vue (1)

87-106: Well-implemented CSS animations with proper Safari optimization!

The animation implementation includes good performance optimizations:

  • will-change for GPU acceleration
  • backface-visibility to prevent flickering
  • Proper vendor prefixes for Safari compatibility
  • Reasonable 8-second duration for subtle background effect
src/pages/settings/organization/Tokens.vue (2)

1-66: Good implementation of core Vue 3 features!

The component demonstrates several best practices:

  • Proper TypeScript typing with Database types
  • Clean use of Vue 3 composition API
  • Comprehensive internationalization support
  • Dark mode handling with useDark()
  • Well-structured imports and setup

94-109: ⚠️ Potential issue

Fix date initialization logic error.

The today variable is created from oneYearAgo before oneYearAgo is modified, resulting in both variables having the same initial value.

-  const oneYearAgo = new Date()
-  const today = new Date(+oneYearAgo)
-  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)
+  const today = new Date()
+  const oneYearAgo = new Date()
+  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1)

To fix the ESLint warnings, use numeric comparison:

-  for (let d = new Date(oneYearAgo); d <= today; d.setDate(d.getDate() + 1)) {
+  for (let d = new Date(oneYearAgo); d.getTime() <= today.getTime(); d.setDate(d.getDate() + 1)) {

Likely an incorrect or invalid review comment.

Comment on lines +22 to +24
// Extract colors from the gradient
const colors = ['#44ff9a', '#44b0ff', '#8b44ff', '#ff6644', '#ebff70']
const positions = ['-0.55%', '22.86%', '48.36%', '73.33%', '99.34%']
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract gradient colors dynamically to avoid maintenance issues.

The hardcoded colors and positions arrays won't update if the background prop changes. This could lead to inconsistencies between the static and rotating gradients.

Extract colors dynamically from the gradient prop:

-// Extract colors from the gradient
-const colors = ['#44ff9a', '#44b0ff', '#8b44ff', '#ff6644', '#ebff70']
-const positions = ['-0.55%', '22.86%', '48.36%', '73.33%', '99.34%']
+// Extract colors from the gradient
+const gradientColors = computed(() => {
+  const match = props.background.match(/linear-gradient\([^,]+,(.+)\)/)
+  if (!match) return { colors: [], positions: [] }
+  
+  const colorStops = match[1].split(',').map(s => s.trim())
+  const colors = []
+  const positions = []
+  
+  colorStops.forEach(stop => {
+    const parts = stop.split(' ')
+    colors.push(parts[0])
+    positions.push(parts[1] || '')
+  })
+  
+  return { colors, positions }
+})

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/BlurBg.vue around lines 22 to 24, the colors and positions
arrays are hardcoded, which can cause inconsistencies if the background prop
changes. To fix this, parse the background gradient string dynamically to
extract the color stops and their positions instead of using fixed arrays. This
ensures the colors and positions always reflect the current background prop
value.

Comment on lines +45 to +63
function injectKeyframes() {
// Remove existing keyframes if any
const existingStyle = document.getElementById('dynamic-blur-keyframes')
if (existingStyle) {
existingStyle.remove()
}

// Create and inject new keyframes
const style = document.createElement('style')
style.id = 'dynamic-blur-keyframes'
style.textContent = generateKeyframes()
document.head.appendChild(style)
}

onMounted(() => {
if (props.rotate) {
injectKeyframes()
}
})
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add cleanup on component unmount to prevent memory leaks.

The injected style element should be removed when the component unmounts to prevent accumulation of style elements.

Add cleanup logic:

+import { computed, onMounted, onUnmounted, ref } from 'vue'

 onMounted(() => {
   if (props.rotate) {
     injectKeyframes()
   }
 })
+
+onUnmounted(() => {
+  if (props.rotate) {
+    const existingStyle = document.getElementById('dynamic-blur-keyframes')
+    if (existingStyle) {
+      existingStyle.remove()
+    }
+  }
+})

Also update the import statement at line 2:

-import { computed, onMounted, ref } from 'vue'
+import { computed, onMounted, onUnmounted, ref } from 'vue'
📝 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
function injectKeyframes() {
// Remove existing keyframes if any
const existingStyle = document.getElementById('dynamic-blur-keyframes')
if (existingStyle) {
existingStyle.remove()
}
// Create and inject new keyframes
const style = document.createElement('style')
style.id = 'dynamic-blur-keyframes'
style.textContent = generateKeyframes()
document.head.appendChild(style)
}
onMounted(() => {
if (props.rotate) {
injectKeyframes()
}
})
import { computed, onMounted, onUnmounted, ref } from 'vue'
function injectKeyframes() {
// Remove existing keyframes if any
const existingStyle = document.getElementById('dynamic-blur-keyframes')
if (existingStyle) {
existingStyle.remove()
}
// Create and inject new keyframes
const style = document.createElement('style')
style.id = 'dynamic-blur-keyframes'
style.textContent = generateKeyframes()
document.head.appendChild(style)
}
onMounted(() => {
if (props.rotate) {
injectKeyframes()
}
})
onUnmounted(() => {
if (props.rotate) {
const existingStyle = document.getElementById('dynamic-blur-keyframes')
if (existingStyle) {
existingStyle.remove()
}
}
})
🤖 Prompt for AI Agents
In src/components/BlurBg.vue around lines 45 to 63, the injectKeyframes function
adds a style element to the document head but does not remove it on component
unmount, causing potential memory leaks. Add a cleanup function inside the
onUnmounted lifecycle hook to remove the injected style element with id
'dynamic-blur-keyframes'. Also, update the import statement at line 2 to include
onUnmounted from Vue to enable this cleanup.

Comment on lines 182 to 200
function computePrice(howMany: number) {
if (howMany > 0 && tokensSteps.value && tokensSteps.value.length > 0) {
let i = 0
let price = 0

while (true) {
const step = tokensSteps.value[i]
price += step.price_per_unit * (Math.min(howMany, step.step_max) - Math.max(0, step.step_min))
if (howMany >= step.step_min && howMany <= step.step_max) {
break
}
i++
}
return price
}
else {
return 0
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add bounds checking in computePrice function.

The while loop could access out-of-bounds array elements.

 function computePrice(howMany: number) {
   if (howMany > 0 && tokensSteps.value && tokensSteps.value.length > 0) {
     let i = 0
     let price = 0

     while (true) {
+      if (i >= tokensSteps.value.length) {
+        console.error('Pricing steps exhausted', { howMany, stepsCount: tokensSteps.value.length })
+        break
+      }
       const step = tokensSteps.value[i]
       price += step.price_per_unit * (Math.min(howMany, step.step_max) - Math.max(0, step.step_min))
       if (howMany >= step.step_min && howMany <= step.step_max) {
         break
       }
       i++
     }
     return price
   }
   else {
     return 0
   }
 }
📝 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
function computePrice(howMany: number) {
if (howMany > 0 && tokensSteps.value && tokensSteps.value.length > 0) {
let i = 0
let price = 0
while (true) {
const step = tokensSteps.value[i]
price += step.price_per_unit * (Math.min(howMany, step.step_max) - Math.max(0, step.step_min))
if (howMany >= step.step_min && howMany <= step.step_max) {
break
}
i++
}
return price
}
else {
return 0
}
}
function computePrice(howMany: number) {
if (howMany > 0 && tokensSteps.value && tokensSteps.value.length > 0) {
let i = 0
let price = 0
while (true) {
if (i >= tokensSteps.value.length) {
console.error('Pricing steps exhausted', { howMany, stepsCount: tokensSteps.value.length })
break
}
const step = tokensSteps.value[i]
price +=
step.price_per_unit *
(Math.min(howMany, step.step_max) - Math.max(0, step.step_min))
if (howMany >= step.step_min && howMany <= step.step_max) {
break
}
i++
}
return price
}
else {
return 0
}
}
🤖 Prompt for AI Agents
In src/pages/settings/organization/Tokens.vue between lines 182 and 200, the
computePrice function's while loop can access tokensSteps.value out of bounds.
Add a condition to check that the index i is less than tokensSteps.value.length
before accessing tokensSteps.value[i] to prevent runtime errors. Modify the loop
condition or add a break when i reaches the array length.

Comment on lines 233 to 242
async function buyTokens(howMany: number) {
const orgId = organizationStore.currentOrganization!.gid
if (!orgId) {
return
}
if (howMany < 1) {
return
}
openCheckoutForOneOff('', 'https://capgo.app', 'https://capgo.app', orgId, howMany)
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Improve buyTokens function implementation.

Several issues need attention:

  1. Hardcoded URLs should be dynamic
  2. Empty string as first parameter seems incorrect
  3. Missing error handling
 async function buyTokens(howMany: number) {
   const orgId = organizationStore.currentOrganization!.gid
   if (!orgId) {
+    console.error('No organization ID found')
     return
   }
   if (howMany < 1) {
+    console.error('Invalid token amount:', howMany)
     return
   }
-  openCheckoutForOneOff('', 'https://capgo.app', 'https://capgo.app', orgId, howMany)
+  
+  const baseUrl = window.location.origin
+  const successUrl = `${baseUrl}${router.currentRoute.value.path}?thankYou=true`
+  const cancelUrl = `${baseUrl}${router.currentRoute.value.path}`
+  
+  try {
+    await openCheckoutForOneOff(orgId, successUrl, cancelUrl, orgId, howMany)
+  } catch (error) {
+    console.error('Checkout failed:', error)
+    displayStore.dialogOption = {
+      header: t('error'),
+      message: t('checkout-failed'),
+      buttons: [{ text: t('ok'), role: 'cancel' }]
+    }
+    displayStore.showDialog = true
+  }
 }
📝 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
async function buyTokens(howMany: number) {
const orgId = organizationStore.currentOrganization!.gid
if (!orgId) {
return
}
if (howMany < 1) {
return
}
openCheckoutForOneOff('', 'https://capgo.app', 'https://capgo.app', orgId, howMany)
}
async function buyTokens(howMany: number) {
const orgId = organizationStore.currentOrganization!.gid
if (!orgId) {
console.error('No organization ID found')
return
}
if (howMany < 1) {
console.error('Invalid token amount:', howMany)
return
}
const baseUrl = window.location.origin
const successUrl = `${baseUrl}${router.currentRoute.value.path}?thankYou=true`
const cancelUrl = `${baseUrl}${router.currentRoute.value.path}`
try {
await openCheckoutForOneOff(orgId, successUrl, cancelUrl, orgId, howMany)
} catch (error) {
console.error('Checkout failed:', error)
displayStore.dialogOption = {
header: t('error'),
message: t('checkout-failed'),
buttons: [{ text: t('ok'), role: 'cancel' }]
}
displayStore.showDialog = true
}
}
🤖 Prompt for AI Agents
In src/pages/settings/organization/Tokens.vue around lines 233 to 242, the
buyTokens function has hardcoded URLs which should be replaced with dynamic
values from configuration or environment variables. The first parameter to
openCheckoutForOneOff is an empty string, which should be replaced with a
meaningful value such as a product or plan identifier. Add proper error handling
around the function to catch and handle any exceptions during the checkout
process to improve robustness.

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

♻️ Duplicate comments (9)
supabase/functions/_backend/utils/stripe.ts (3)

291-293: Add validation for empty pricing steps.

The while loop could run indefinitely if tokensSteps is empty or malformed.


304-304: Use BigInt or string for large number literal to avoid precision loss.

The number 9223372036854775807 exceeds JavaScript's safe integer range and will lose precision.

Also applies to: 310-310

🧰 Tools
🪛 Biome (1.9.4)

[error] 304-304: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)


298-337: Add bounds checking for array access in pricing loop.

The loop increments i without checking if it exceeds the array bounds.

🧰 Tools
🪛 Biome (1.9.4)

[error] 304-304: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)


[error] 310-310: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)

src/pages/settings/organization/Tokens.vue (4)

154-188: Fix ESLint loop condition errors.

The ESLint errors indicate issues with the loop condition variables. The loop variable d is being modified correctly with d.setDate(d.getDate() + 1), but the condition checking against today could be problematic since today is derived from oneYearAgo.


219-226: Improve error handling for better user experience.

The current error handling only logs to console, which doesn't provide feedback to users when data loading fails.


247-265: Add bounds checking in computePrice function.

The while loop could access out-of-bounds array elements.


298-307: Improve buyTokens function implementation.

Several issues need attention:

  1. Hardcoded URLs should be dynamic
  2. Empty string as first parameter seems incorrect
  3. Missing error handling
supabase/migrations/20250131091941_capgo_tokens.sql (2)

34-34: RLS enabled without policies on history table

You've enabled row-level security on capgo_tokens_history but haven't defined any SELECT/INSERT policies, which will block all access. Please add appropriate RLS policies for service roles or authenticated functions.


88-88: capgo_tokens_steps table design – reconsider data type

Defining price_per_unit as FLOAT can lead to rounding issues. It's safer to use NUMERIC(10,4) or DECIMAL for monetary values.

🧹 Nitpick comments (1)
supabase/functions/_backend/utils/stripe.ts (1)

284-286: Fix typo in function name.

The function name toMilion should be toMillion for correct spelling.

Apply this diff to fix the typo:

-function toMilion(price: number) {
+function toMillion(price: number) {

Remember to update the usage in lines 305 and 306 as well.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1754788 and 8c5e7d8.

⛔ Files ignored due to path filters (1)
  • src/assets/bandwidth.svg is excluded by !**/*.svg
📒 Files selected for processing (22)
  • messages/de.json (12 hunks)
  • messages/en.json (16 hunks)
  • messages/es.json (12 hunks)
  • messages/fr.json (12 hunks)
  • messages/id.json (12 hunks)
  • messages/it.json (12 hunks)
  • messages/ja.json (12 hunks)
  • messages/ko.json (12 hunks)
  • messages/pl.json (12 hunks)
  • messages/pt-br.json (12 hunks)
  • messages/ru.json (12 hunks)
  • messages/tr.json (12 hunks)
  • messages/vi.json (12 hunks)
  • messages/zh-cn.json (12 hunks)
  • src/components/BuyTokens.vue (1 hunks)
  • src/pages/settings/organization/Tokens.vue (1 hunks)
  • src/services/stripe.ts (2 hunks)
  • src/types/supabase.types.ts (13 hunks)
  • supabase/functions/_backend/private/stripe_checkout.ts (2 hunks)
  • supabase/functions/_backend/utils/stripe.ts (2 hunks)
  • supabase/migrations/20250131091941_capgo_tokens.sql (1 hunks)
  • supabase/seed.sql (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
  • supabase/seed.sql
  • messages/pl.json
  • supabase/functions/_backend/private/stripe_checkout.ts
  • messages/de.json
  • src/services/stripe.ts
  • src/components/BuyTokens.vue
  • messages/ko.json
  • messages/zh-cn.json
  • messages/fr.json
  • messages/ru.json
  • messages/en.json
  • messages/it.json
  • messages/ja.json
  • messages/pt-br.json
  • messages/vi.json
  • messages/tr.json
  • messages/es.json
  • messages/id.json
🧰 Additional context used
🧬 Code Graph Analysis (1)
src/types/supabase.types.ts (1)
supabase/functions/_backend/utils/supabase.types.ts (1)
  • Json (1-7)
🪛 Biome (1.9.4)
supabase/functions/_backend/utils/stripe.ts

[error] 304-304: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)


[error] 310-310: This number literal will lose precision at runtime.

The value at runtime will be 9223372036854776000

(lint/correctness/noPrecisionLoss)

🔇 Additional comments (11)
src/pages/settings/organization/Tokens.vue (1)

1-106: LGTM! Well-structured component setup.

The component setup with imports, reactive data structures, and store integrations is well-organized and follows Vue 3 composition API best practices.

supabase/migrations/20250131091941_capgo_tokens.sql (2)

37-81: LGTM! Well-implemented security function.

The get_tokens_history function properly implements access control checks and uses secure query patterns with running totals calculation.


123-190: LGTM! Secure utility functions with proper access control.

The MAU token aggregation functions are well-designed with appropriate security restrictions (service_role only) and clear documentation.

src/types/supabase.types.ts (8)

10-34: LGTM! GraphQL schema addition is well-structured.

The new graphql_public schema follows the existing patterns and provides proper typing for GraphQL endpoint integration. The function signature correctly handles typical GraphQL parameters (operationName, query, variables, extensions) with appropriate JSON typing.


313-377: LGTM! Token tables are well-designed.

The new token management tables follow proper database design principles:

  • capgo_tokens_history correctly tracks token transactions with org relationship
  • capgo_tokens_steps provides pricing configuration with appropriate numeric fields
  • Both tables have proper TypeScript typing with appropriate optional fields for Insert/Update operations
  • Foreign key relationship to orgs table is correctly defined

1037-1037: LGTM! Version field addition follows proper patterns.

The new version field in the plans table is correctly typed:

  • Required in Row type for data consistency
  • Optional in Insert/Update types allowing for defaults or server-side generation

1473-1476: LGTM! New function definitions are properly typed.

All new function signatures follow proper TypeScript patterns:

  • Token-related functions (get_extra_mau_for_org, get_tokens_history, get_total_mau_tokens) have appropriate parameter and return types
  • Database replication function (replicate_to_d1) correctly accepts JSON records and metadata
  • Reset functions properly parameterize operations with app_id for targeted functionality

Also applies to: 1613-1622, 1627-1630, 1931-1939, 1941-1941, 1945-1945


2195-2197: LGTM! Constants addition is consistent.

The new graphql_public constants section properly mirrors the schema definition and follows the existing pattern with an empty Enums object.


1-2278: Excellent implementation of comprehensive token management types.

This file successfully integrates new Capgo token management functionality while maintaining type safety and consistency. The additions include:

  • Well-structured database tables for token history and pricing
  • Proper function definitions for token operations
  • Consistent typing patterns throughout
  • Appropriate relationships and constraints

The implementation supports the full token billing system while preserving existing functionality.


610-610:

✅ Verification successful

Verify impact of making email field required.

The email field in the deleted_account Insert type is now required (no ? marker). This could be a breaking change if there's existing code that inserts records without providing an email value.


🏁 Script executed:

#!/bin/bash
# Description: Search for deleted_account inserts to verify if email is always provided
# Expected: Find all INSERT operations to check if they provide email values

# Search for direct table inserts
rg -A 5 -B 2 "deleted_account.*insert|insert.*deleted_account"

# Search for Supabase client inserts
rg -A 10 -B 2 "\.from\(['\"]deleted_account['\"].*\.insert"

Length of output: 130


🏁 Script executed:

#!/bin/bash
# Search for any references to deleted_account outside of the type definitions
rg -n "deleted_account" --glob '!src/types/supabase.types.ts'

# Specifically look for Supabase client inserts on deleted_account
rg -n "\.from\(['\"]deleted_account['\"]\).*\.insert" --glob '!src/types/supabase.types.ts'

Length of output: 1686


No breaking change for deleted_account email field

All inserts into deleted_account are confined to SQL migrations and seed files—and each of those always supplies an email. No Supabase client .from('deleted_account').insert(...) calls were found in the application code, so making the TypeScript Insert type’s email property required now matches the database schema without impacting runtime behavior.


2044-2044:

✅ Verification successful

Verify enum reordering impact.

The usage_mode enum values have been reordered. While this should be safe for string enums, verify that no application logic depends on the specific ordering of these values.


🏁 Script executed:

#!/bin/bash
# Description: Check if usage_mode enum ordering is used in application logic
# Expected: Find any code that might depend on enum value ordering

# Search for usage_mode enum usage
rg -A 5 -B 5 "usage_mode"

# Search for any array operations or ordering logic with these enum values
rg -A 3 -B 3 "(last_saved|5min|day|cycle).*\.(sort|indexOf|findIndex)"

Length of output: 1231


🏁 Script executed:

#!/bin/bash
# Search all TypeScript files for usage_mode references
rg -n "usage_mode" -t ts

# Look for any array ordering or index operations involving the enum values in TS code
rg -nE "(\"last_saved\"|\"5min\"|\"day\"|\"cycle\").*(sort|indexOf|findIndex)" -t ts

Length of output: 500


Please share the output so we can confirm if any logic relies on enum ordering.


🏁 Script executed:

#!/bin/bash
# Refined search for TypeScript references to usage_mode
rg -n "usage_mode" -g "*.ts"

# Refined search for any ordering operations (sort, indexOf, findIndex) on the enum’s string values
rg -nE "\"(last_saved|5min|day|cycle)\".*(sort|indexOf|findIndex)" -g "*.ts"

Length of output: 933


No enum ordering dependencies detected.

A search across all TypeScript files shows that usage_mode is only used in type definitions and array literals—there are no calls to .sort(), .indexOf(), or .findIndex() on these values. Since this is a string union, reordering the members will not introduce a breaking change.

@@ -272,7 +272,7 @@ export async function createCheckout(c: Context, customerId: string, reccurence:
line_items: [
{
price: prices.priceId,
quantity: 1,
quantity: howMany ?? 1,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add missing parameter to function signature.

The howMany parameter is used in the function body but is not defined in the function signature, which will cause a TypeScript error.

Apply this diff to fix the function signature:

-export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string) {
+export async function createCheckout(c: Context, customerId: string, reccurence: string, planId: string, successUrl: string, cancelUrl: string, clientReferenceId?: string, howMany?: number) {
📝 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
quantity: howMany ?? 1,
export async function createCheckout(
c: Context,
customerId: string,
reccurence: string,
planId: string,
successUrl: string,
cancelUrl: string,
clientReferenceId?: string,
howMany?: number
) {
🤖 Prompt for AI Agents
In supabase/functions/_backend/utils/stripe.ts at line 275, the parameter
'howMany' is used but missing from the function signature, causing a TypeScript
error. Update the function signature to include 'howMany' as a parameter with
the appropriate type to ensure it is defined and can be used within the function
body.

@riderx
Copy link
Contributor

riderx commented Jun 5, 2025

@WcaleNieWolny i think we will go for credits instead of token

@riderx
Copy link
Contributor

riderx commented Jun 6, 2025

Another thing
CleanShot 2025-06-06 at 10 35 57
In expo they do have the same price for all plan but lower plan have higher price first, they sub the plan number to the price to now the tier we should do the same

@riderx
Copy link
Contributor

riderx commented Jun 6, 2025

To me all plan should give credits, no more different price structure.
That means we could should allow to not pay any plan and just buy some credit for tiny apps

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Allow any plan to have credits
2 participants