Skip to content

Conversation

@nikochiko
Copy link
Member

@nikochiko nikochiko commented Oct 28, 2025

  • disable auto recharge / addons for new non-enterprise users
  • wip: add standard plan

Q/A checklist

  • I have tested my UI changes on mobile and they look acceptable
  • I have tested changes to the workflows in both the API and the UI
  • I have done a code review of my changes and looked at each line of the diff + the references of each function I have changed
  • My changes have not increased the import time of the server
How to check import time?

time python -c 'import server'

You can visualize this using tuna:

python3 -X importtime -c 'import server' 2> out.log && tuna out.log

To measure import time for a specific library:

$ time python -c 'import pandas'

________________________________________________________
Executed in    1.15 secs    fish           external
   usr time    2.22 secs   86.00 micros    2.22 secs
   sys time    0.72 secs  613.00 micros    0.72 secs

To reduce import times, import libraries that take a long time inside the functions that use them instead of at the top of the file:

def my_function():
    import pandas as pd
    ...

Legal Boilerplate

Look, I get it. The entity doing business as “Gooey.AI” and/or “Dara.network” was incorporated in the State of Delaware in 2020 as Dara Network Inc. and is gonna need some rights from me in order to utilize my contributions in this PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Dara Network Inc can use, modify, copy, and redistribute my contributions, under its choice of terms.


Note

Adds a new tiered Standard plan with Stripe integration, overhauls billing UI/flows, records subscription amounts, and restricts credit topups/auto‑recharge to eligible workspaces.

  • Plans & Pricing
    • Introduce PricingPlan.STANDARD with multiple PricingTiers and tier-aware helpers (get_active_*, get_default_tier).
    • Stripe product/price lookup and line item creation updated to support tiered products; PricingPlan.from_sub accepts None.
  • Billing UI/Flows (daras_ai_v2/billing.py)
    • New plan rendering: compact/full-width layouts, tier selector (Stripe), and tier-aware pricing/captions.
    • Upgrade/downgrade flows updated to handle tier changes; Stripe metadata includes subscription key; subscription creation supports tiers.
    • Current plan panel shows next invoice badge; plan list renamed to "Plans" and hides STANDARD on team workspaces.
    • Addon section and Auto‑recharge section hidden unless workspace allow_credit_topups().
  • Models & Migrations
    • Subscription: add amount, charged_amount; default auto_recharge_enabled=False; add get_tier().
    • Migrations update choices (including Standard) and new fields; large usage_costs model name choices refresh.
  • Webhooks/Stripe/PayPal
    • Use metadata key for plan resolution; fallback to product lookup; set subscription with amount and charged_amount.
    • Stripe invoice paid/retry handling unchanged but now passes plan via metadata.
  • Auto‑recharge
    • Gate attempt via workspace.allow_credit_topups(); compute budgets from plan if needed; precision tweak for Stripe per-credit pricing.
  • Workspace policy (workspaces/models.py)
    • Add allow_credit_topups() to permit topups only for Enterprise or workspaces with prior topups.
  • Tasks/Scripts
    • Adjust save_stripe_default_payment_method to new set_workspace_subscription signature.
    • New script disable_auto_recharge_for_restricted_workspaces.py to turn off auto‑recharge where not allowed.
  • Misc
    • Minor copy tweaks in VideoBots; PayPal create subscription validates plan key/errors.

Written by Cursor Bugbot for commit 1906f32. This will update automatically on new commits. Configure here.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 28, 2025

📝 Walkthrough

Walkthrough

This pull request introduces a tier-aware pricing system for subscription plans. The changes add a PricingTier type (containing credits and monthly_charge) and integrate tier selection and pricing throughout the billing infrastructure. Key additions include: new tier-aware accessor methods in PricingPlanData (get_active_credits, get_active_monthly_charge, get_pricing_title, get_pricing_caption), a STANDARD plan in the PricingPlan enum with multiple tier definitions, updates to Stripe integration to handle tier-specific product IDs and line items, UI rendering functions in billing.py that propagate selected_tier through plan cards and dialogs, and amount/charged_amount fields on the Subscription model. The changes also update webhooks to extract and store tier-related amounts from Stripe subscription metadata and quantity, refactor plan validation in PayPal router for clearer error messages, and include database migrations to support new fields and updated plan choice lists.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • devxpy

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.73% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title describes adding a new subscription plan called 'Standard', which is a key part of the changeset, but the actual changes encompass much broader scope including tier-aware pricing, billing UI overhauls, webhook updates, and workspace policy changes. Consider a more comprehensive title that better reflects the scope, such as 'Implement tiered Standard plan with billing UI overhaul and credit topup restrictions' or clarify if this is intentionally scoped to just the plan addition.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required Q/A checklist and legal boilerplate, but the Q/A items are all unchecked, and the description itself is marked as 'wip: add standard plan' suggesting work-in-progress status.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch subscription-plan-standard

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

❤️ Share

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

Copy link
Contributor

@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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6172b8 and 87c6e8e.

📒 Files selected for processing (7)
  • daras_ai_v2/billing.py (3 hunks)
  • payments/auto_recharge.py (1 hunks)
  • payments/migrations/0007_alter_subscription_auto_recharge_enabled.py (1 hunks)
  • payments/models.py (1 hunks)
  • payments/plans.py (1 hunks)
  • scripts/disable_auto_recharge_for_restricted_workspaces.py (1 hunks)
  • workspaces/models.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
payments/auto_recharge.py (1)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
daras_ai_v2/billing.py (2)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
workspaces/models.py (2)
app_users/models.py (2)
  • AppUserTransaction (277-396)
  • TransactionReason (265-274)
payments/plans.py (2)
  • PricingPlan (51-405)
  • from_sub (282-283)
scripts/disable_auto_recharge_for_restricted_workspaces.py (3)
app_users/models.py (2)
  • AppUserTransaction (277-396)
  • TransactionReason (265-274)
payments/models.py (1)
  • Subscription (37-390)
payments/plans.py (1)
  • PricingPlan (51-405)
🪛 Ruff (0.14.1)
payments/migrations/0007_alter_subscription_auto_recharge_enabled.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-18: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (python)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (8)
payments/models.py (1)

48-48: LGTM - Default change aligns with new permission model.

The change to default=False for auto_recharge_enabled aligns with the PR's goal to disable auto-recharge for non-Enterprise users without top-up history. The migration handles existing records appropriately.

payments/migrations/0007_alter_subscription_auto_recharge_enabled.py (1)

1-18: LGTM - Migration correctly alters field default.

The migration properly changes the auto_recharge_enabled field default to False. The static analysis hints about ClassVar are false positives - Django migration attributes (dependencies and operations) should not be type-annotated as they are framework-specific class attributes.

scripts/disable_auto_recharge_for_restricted_workspaces.py (1)

24-91: LGTM - Script correctly implements batch updates for restricted workspaces.

The script logic is sound:

  • Uses an efficient Exists subquery to check for top-up history (lines 29-32)
  • Correctly filters for non-Enterprise subscriptions with auto-recharge enabled and no top-up history (lines 38-49)
  • Batch processing with lazy QuerySet evaluation ensures updated records are excluded from subsequent batches (lines 71-91)

The alignment with workspace.allow_credit_topups() ensures consistency across the codebase.

payments/auto_recharge.py (1)

31-38: LGTM - Auto-recharge prerequisite correctly enforces workspace capability.

The addition of workspace.allow_credit_topups() as a prerequisite for auto-recharge ensures that only Enterprise workspaces or those with prior top-up history can use this feature. The placement as the first condition enables short-circuit evaluation for efficiency.

daras_ai_v2/billing.py (3)

187-187: LGTM - Simplified to fixed 3-column layout.

The change from dynamic column count to a fixed 3-column grid simplifies the rendering logic while maintaining appropriate display for all plans.


524-527: LGTM - Addon section properly gated by workspace capability.

The early return when workspace.allow_credit_topups() is False correctly hides the entire addon purchase section for workspaces without top-up permissions. This aligns with the auto-recharge prerequisite changes in payments/auto_recharge.py.


921-924: LGTM - Auto-recharge section properly gated by workspace capability.

The guard correctly hides the auto-recharge configuration UI for restricted workspaces. This ensures UI consistency with the backend logic in payments/auto_recharge.py that enforces the same capability check.

workspaces/models.py (1)

223-247: LGTM - Workspace capability method correctly implements top-up permissions.

The allow_credit_topups() method properly defines workspace-level permissions for credit top-ups:

  • Enterprise workspaces always have access ✓
  • Workspaces with prior top-up history (ADDON or AUTO_RECHARGE transactions) retain access ✓
  • All others are restricted ✓

The implementation efficiently checks the Enterprise case first before querying transaction history, and uses local imports to avoid circular dependencies.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 87c6e8e and 35ff6f2.

📒 Files selected for processing (6)
  • daras_ai_v2/billing.py (25 hunks)
  • payments/migrations/0008_add_plan_tier_key.py (1 hunks)
  • payments/models.py (3 hunks)
  • payments/plans.py (9 hunks)
  • payments/webhooks.py (3 hunks)
  • recipes/VideoBots.py (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-09T06:59:25.756Z
Learnt from: nikochiko
PR: GooeyAI/gooey-server#757
File: daras_ai_v2/safety_checker.py:68-71
Timestamp: 2025-08-09T06:59:25.756Z
Learning: In the capture_openai_content_policy_violation context manager in daras_ai_v2/safety_checker.py, only openai.BadRequestError exceptions with specific error codes ("content_policy_violation" or "content_filter") should be converted to UserError. Other OpenAI exceptions like APIStatusError should not be converted to UserError and should be allowed to propagate unchanged.

Applied to files:

  • recipes/VideoBots.py
🧬 Code graph analysis (5)
payments/webhooks.py (1)
payments/plans.py (2)
  • PricingPlan (86-540)
  • get_by_stripe_product (377-419)
recipes/VideoBots.py (1)
daras_ai_v2/exceptions.py (1)
  • UserError (58-65)
payments/models.py (1)
payments/plans.py (4)
  • PricingPlan (86-540)
  • from_sub (366-367)
  • get_active_credits (67-71)
  • get_active_monthly_charge (73-77)
payments/plans.py (1)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/billing.py (5)
payments/plans.py (11)
  • get_active_monthly_charge (73-77)
  • get_active_credits (67-71)
  • label (29-30)
  • PricingPlan (86-540)
  • get_default_tier_key (79-83)
  • get_tier (61-65)
  • get_pricing_title (49-53)
  • get_pricing_caption (55-59)
  • from_sub (366-367)
  • get_stripe_line_item (439-460)
  • supports_stripe (433-434)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
app_users/models.py (1)
  • PaymentProvider (85-87)
payments/models.py (3)
  • is_paid (145-146)
  • cancel (157-184)
  • Subscription (37-399)
workspaces/models.py (2)
  • cancel (754-757)
  • allow_credit_topups (223-247)
🪛 Ruff (0.14.2)
payments/webhooks.py

173-175: Create your own exception

(TRY002)


173-175: Avoid specifying long messages outside the exception class

(TRY003)

payments/plans.py

441-441: Avoid specifying long messages outside the exception class

(TRY003)

payments/migrations/0008_add_plan_tier_key.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-39: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

daras_ai_v2/billing.py

510-510: Unused function argument: session

(ARG001)


511-511: Unused function argument: user

(ARG001)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (19)
payments/models.py (3)

39-44: LGTM!

The plan_tier_key field is properly defined with nullable constraints to maintain backward compatibility for existing subscriptions without tiers.


103-110: LGTM!

The tier-aware pricing logic is correctly implemented:

  • Plan instance is created once and reused efficiently
  • Tier key is properly passed to retrieve tier-specific credits and charges
  • Falls back gracefully to default values when tier_key is None

54-54: Default change properly addressed through migration and targeted scripts.

Verification confirms the implementation is sound. The default change from True to False is applied via migration 0007_alter_subscription_auto_recharge_enabled.py, which only affects new subscription records. Existing subscriptions are preserved as-is, with selective updates handled by disable_auto_recharge_for_restricted_workspaces.py (only non-Enterprise plans without topup history are modified). Code dependencies are defensive—auto_recharge.py safely checks the flag before use—and users can toggle the setting via the billing UI. No code updates or breaking changes identified.

payments/migrations/0008_add_plan_tier_key.py (1)

1-39: LGTM!

The migration correctly adds the plan_tier_key field and updates plan choices to include the new STANDARD plan. The structure follows Django migration conventions.

Note: The static analysis hints about ClassVar annotations are false positives - Django migrations are expected to define dependencies and operations as class attributes in this exact format.

payments/webhooks.py (2)

168-189: LGTM with tier extraction logic correctly implemented.

The code properly:

  • Retrieves the product with expanded price information
  • Handles the new tuple return from get_by_stripe_product
  • Prefers tier_key from product match over metadata
  • Falls back gracefully to metadata when product doesn't specify a tier

The error handling is appropriate for missing plan mappings.


267-278: LGTM!

The plan_tier_key parameter is properly added with a default value of None for backward compatibility, and correctly assigned to the subscription instance.

payments/plans.py (6)

23-31: LGTM!

The PricingTier NamedTuple is well-designed with appropriate fields and a clean label property for display purposes.


47-83: LGTM!

The tier-aware methods are correctly implemented with proper fallback behavior:

  • get_tier() safely handles None and missing keys
  • get_active_credits() and get_active_monthly_charge() use the walrus operator elegantly
  • get_default_tier_key() provides a sensible default (first tier)

170-204: LGTM!

The STARTER plan updates improve clarity by:

  • Renaming to "Free" to better communicate the pricing
  • Updating descriptions to reflect current features
  • Adding a clear pricing caption about starting credits

These are appropriate marketing/content improvements.


377-419: LGTM!

The get_by_stripe_product method correctly:

  • Returns a tuple (plan, tier_key) for consistency
  • Handles legacy products with backward compatibility
  • Matches tiered plans by name and price
  • Falls back gracefully when price information is unavailable
  • Returns None when no match is found

The tier key extraction logic is sound and aligns with product creation in get_stripe_product_id.


439-460: LGTM!

The method correctly:

  • Accepts an optional tier_key parameter
  • Uses get_active_credits() and get_active_monthly_charge() for tier-aware pricing
  • Propagates the tier_key to get_stripe_product_id() for product resolution

472-500: LGTM!

The tier-aware product ID resolution:

  • Builds appropriate product names including tier labels
  • Matches existing products by both name and price
  • Creates new products when needed with tier-specific pricing
  • Maintains consistency with get_by_stripe_product matching logic
daras_ai_v2/billing.py (7)

83-87: LGTM!

The current plan display correctly retrieves tier-specific pricing using get_active_monthly_charge() and get_active_credits().


159-165: LGTM!

The plan filtering correctly:

  • Excludes deprecated plans
  • Restricts STANDARD plan to personal workspaces only (business logic requirement)

This ensures team workspaces are guided toward Business/Enterprise plans.


212-273: LGTM with tier selection UI properly implemented.

The tier selection logic:

  • Only displays tier dropdown for Stripe subscriptions (line 235)
  • Uses current subscription's tier as default when available
  • Falls back to plan's default tier for new subscriptions
  • Updates pricing display based on selected tier

Note: PayPal subscriptions don't support tier selection, which is consistent with PayPal's simpler subscription model.


301-320: LGTM!

The tier change detection correctly:

  • Identifies when a different tier is selected within the STANDARD plan
  • Delegates to _render_tier_change_button for upgrade/downgrade UI
  • Shows "Your Plan" button when the selected tier matches current tier

346-369: LGTM!

The STANDARD plan button logic:

  • Correctly handles both paid and free plan states
  • Always uses "Upgrade" label for consistent UX
  • Properly propagates selected_tier_key through the flow

733-738: LGTM!

The addon section is correctly gated by workspace.allow_credit_topups(), which aligns with the PR objective to disable addons for new non-enterprise users.


1144-1146: LGTM!

The auto-recharge section is consistently gated using the same workspace.allow_credit_topups() check as the addon section.

if is_enterprise:
gui.write(
f"As a premium customer, please contact us to setup a managed number"
"As a premium customer, please contact us to setup a managed number"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix grammatical error: "setup" → "set up".

When used as a verb phrase, "set up" should be two words. Line 1980 correctly uses "set up" in a similar context. This change maintains consistency across the file.

Apply this diff to fix the grammar:

-                            "As a premium customer, please contact us to setup a managed number"
+                            "As a premium customer, please contact us to set up a managed 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
"As a premium customer, please contact us to setup a managed number"
"As a premium customer, please contact us to set up a managed number"
🤖 Prompt for AI Agents
In recipes/VideoBots.py around line 1939, the string uses the incorrect
noun-form "setup" instead of the verb phrase "set up"; change "As a premium
customer, please contact us to setup a managed number" to "As a premium
customer, please contact us to set up a managed number" so it matches correct
grammar and the usage on line 1980.

@nikochiko nikochiko force-pushed the subscription-plan-standard branch 2 times, most recently from d1024df to ac20427 Compare October 31, 2025 06:00
@nikochiko nikochiko marked this pull request as ready for review October 31, 2025 06:05
Copy link
Contributor

@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: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a820403 and ac20427.

📒 Files selected for processing (5)
  • daras_ai_v2/billing.py (18 hunks)
  • daras_ai_v2/settings.py (1 hunks)
  • payments/plans.py (11 hunks)
  • payments/webhooks.py (5 hunks)
  • routers/paypal.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
routers/paypal.py (1)
payments/plans.py (3)
  • PricingPlan (88-542)
  • get_by_key (429-433)
  • supports_paypal (438-439)
daras_ai_v2/billing.py (6)
payments/plans.py (11)
  • get_active_monthly_charge (76-80)
  • get_active_credits (70-74)
  • get_pricing_title (50-55)
  • label (29-30)
  • PricingPlan (88-542)
  • from_sub (373-376)
  • get_default_tier_key (82-85)
  • get_pricing_caption (57-62)
  • get_tier (64-68)
  • get_stripe_line_item (441-462)
  • supports_stripe (435-436)
app_users/models.py (2)
  • PaymentProvider (85-87)
  • cached_workspaces (242-249)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
routers/account.py (2)
  • account_route (99-111)
  • payment_processing_route (38-84)
payments/models.py (1)
  • Subscription (37-399)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
payments/webhooks.py (1)
payments/plans.py (3)
  • PricingPlan (88-542)
  • get_by_key (429-433)
  • get_by_stripe_product (386-420)
payments/plans.py (1)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
🪛 Ruff (0.14.2)
payments/plans.py

85-85: Avoid specifying long messages outside the exception class

(TRY003)


403-403: Loop control variable tier_key not used within loop body

Rename unused tier_key to _tier_key

(B007)


433-433: Avoid specifying long messages outside the exception class

(TRY003)


443-443: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (10)
routers/paypal.py (1)

123-129: LGTM! Improved error handling for plan validation.

The refactored validation logic provides better separation of concerns with distinct error messages for invalid plan keys versus PayPal support issues, making debugging easier for API consumers.

payments/plans.py (2)

23-86: Well-designed tier-aware pricing structure.

The PricingTier class and tier-aware accessor methods (get_tier, get_active_credits, get_active_monthly_charge) provide a clean abstraction for handling multi-tier plans while maintaining backward compatibility with single-tier plans through sensible defaults.


396-420: Remove unused tier_key unpacking; current return signature is sufficient.

The tier_key variable on line 403 is genuinely unused—only tier is referenced in the loop body. This is not a false positive. Both callers (webhooks.py and migrate_existing_subscriptions.py) obtain tier context elsewhere if needed (webhooks.py retrieves it from stripe_sub.metadata) and don't require tier information from get_by_stripe_product.

Simplify line 403 to for tier in plan.tiers.values(): to eliminate the dead code.

Likely an incorrect or invalid review comment.

daras_ai_v2/settings.py (1)

392-392: LGTM! Constant addition for tier metadata.

The new constant follows the existing pattern for Stripe metadata fields and enables tier-aware subscription handling.

payments/webhooks.py (1)

187-195: LGTM! Safe tier_key extraction and propagation.

The tier_key extraction uses .get() with appropriate None handling, and the value is correctly passed through to set_workspace_subscription.

daras_ai_v2/billing.py (5)

81-142: LGTM! Tier-aware current plan rendering.

The tier_key extraction from the subscription and tier-aware pricing display using get_active_monthly_charge and get_active_credits correctly handles both tiered and non-tiered plans.


154-219: Well-structured tier-aware plan rendering.

The filtering of STANDARD plan for non-personal workspaces (lines 157-158) and the separation of grid vs. full-width plans provides good UX differentiation. The tier context is properly propagated through the rendering pipeline.


626-631: LGTM! Appropriate gating of credit topups.

The allow_credit_topups() check correctly hides the addon section for workspaces that shouldn't have access to credit purchases, aligning with the PR objective to disable addons for new non-enterprise users.


768-870: Tier-aware subscription creation logic looks solid.

The tier_key is correctly propagated through Stripe metadata and line items, and the pricing calculations use get_active_monthly_charge and get_active_credits to handle tier-specific values.


1033-1039: LGTM! Consistent gating of auto-recharge feature.

The allow_credit_topups() check correctly hides the auto-recharge section, consistent with the addon gating and the PR objective to disable auto-recharge for new non-enterprise users.

Comment on lines +170 to +184
try:
plan = PricingPlan.get_by_key(
stripe_sub.metadata[settings.STRIPE_USER_SUBSCRIPTION_METADATA_FIELD]
)
except KeyError:
product = stripe.Product.retrieve(
stripe_sub.plan.product, expand=["default_price"]
)
plan = PricingPlan.get_by_stripe_product(product)
assert plan is not None, f"Plan for product {product.id} not found"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the assertion handles all Stripe subscription scenarios.

The fallback logic asserts that a plan is found when metadata is missing, but this could fail if Stripe contains legacy subscriptions or products that don't match current plan definitions. Consider logging a warning and handling the None case more gracefully.


🏁 Script executed:

#!/bin/bash
# Check if there are other places where missing plan lookups are handled
rg -n -B3 -A3 'get_by_stripe_product|get_by_paypal_plan_id' --type=py

Length of output: 2856



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the webhook handler function and surrounding error handling
rg -n -B10 "def.*webhook" payments/webhooks.py | head -100

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there's try-catch wrapping around the get_by_stripe_product call
rg -n -B10 -A5 "stripe_sub.plan.product" payments/webhooks.py

Length of output: 1046


🏁 Script executed:

#!/bin/bash
# Search for any custom exception handling or logging patterns in webhooks
rg -n "except|logger|logging" payments/webhooks.py | head -40

Length of output: 444



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where handle_subscription_updated is called and any wrapping error handlers
rg -n "handle_subscription_updated" --type=py

Length of output: 727


🏁 Script executed:

#!/bin/bash
# Check if there's a Flask/Django error handler or middleware
rg -n "error_handler|errorhandler|middleware|@app|@route" payments/webhooks.py | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look at the start of the file to understand the context (imports, decorators, flask/django setup)
head -50 payments/webhooks.py

Length of output: 1822



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the Stripe webhook endpoint for error handling
rg -n -B5 -A10 "handle_subscription_updated" routers/stripe.py

Length of output: 582


🏁 Script executed:

#!/bin/bash
# Check the PayPal webhook endpoint for error handling
rg -n -B5 -A10 "handle_subscription_updated" routers/paypal.py

Length of output: 705


🏁 Script executed:

#!/bin/bash
# Check if there's a catch-all error handler in the Django app
fd -e py -x rg -l "exception_handler|handle_exception|catch_all|@app" {} \; routers/ | head -20

Length of output: 337



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full function context from stripe router
rg -n -B20 "handle_subscription_updated" routers/stripe.py | head -80

Length of output: 932


🏁 Script executed:

#!/bin/bash
# Get full function context from paypal router
rg -n -B20 "handle_subscription_updated" routers/paypal.py | head -80

Length of output: 1136


🏁 Script executed:

#!/bin/bash
# Check for exception handlers in main app config
fd . routers/ --name '*.py' -x grep -l "exception_handler\|@app\|def.*webhook" {} \;

Length of output: 232



---

🏁 Script executed:

```shell
#!/bin/bash
# Get the full Stripe webhook function definition
rg -n -B40 "handle_subscription_updated" routers/stripe.py | grep -A40 "def webhook"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if Stripe webhook has any try-catch wrapping the match statement
rg -n "def webhook" routers/stripe.py -A50

Length of output: 1999


🏁 Script executed:

#!/bin/bash
# Look for any global exception handlers or middleware in FastAPI setup
rg -n "exception_handler|ExceptionMiddleware|@app.exception_handler" --type=py

Length of output: 498



---

🏁 Script executed:

```shell
#!/bin/bash
# Check the global exception handler implementation
rg -n -A10 "async def server_error_exception_handler" server.py

Length of output: 460


🏁 Script executed:

#!/bin/bash
# Verify that AssertionError will be caught by the Exception handler
rg -n -B5 -A15 "def server_error_exception_handler" server.py

Length of output: 890


Add graceful error handling for missing plan lookups in Stripe webhook handler.

The assertion at line 179 will crash the webhook on legacy subscriptions or products without matching plan definitions. Unlike the migration script which gracefully logs and skips missing plans (lines 115-117 in scripts/migrate_existing_subscriptions.py), the webhook crashes with a 5xx error that Stripe will retry indefinitely.

Replace the assertion with logging and early return:

plan = PricingPlan.get_by_stripe_product(product)
if plan is None:
    logger.warning(f"Plan not found for Stripe product {product.id}")
    return  # Acknowledge webhook to prevent retries

Also apply the same fix to PayPal webhook handlers at lines 33-34 and 62-63, which have identical assertion patterns without logging.

🤖 Prompt for AI Agents
In payments/webhooks.py around lines 170 to 179 the code asserts that a
PricingPlan exists for a Stripe product which will raise an exception and crash
the webhook for legacy/missing plans; replace the assertion with a null-check
that logs a warning and returns early to acknowledge the webhook (e.g., if plan
is None: logger.warning(f"Plan not found for Stripe product {product.id}");
return). Also apply the same change to the PayPal webhook handlers at lines
~33-34 and ~62-63: remove the assertions, add a warning log indicating the
missing plan with the product/subscription id, and return early so the webhook
responds 200 and Stripe/PayPal will not retry.

Copy link
Contributor

@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 (1)
daras_ai_v2/billing.py (1)

384-390: Add defensive checks for tier lookups to prevent AttributeError.

Lines 388-390 directly access .monthly_charge after get_tier(), which returns PricingTier | None. If either selected_tier_key or current_tier_key is invalid (e.g., from stale data or migration issues), this will raise an AttributeError.

Based on the past review comment, this was supposed to be addressed but the defensive handling is still missing.

Apply this diff to add defensive handling:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
+        and (current_tier := plan.get_tier(current_tier_key))
+        and (selected_tier := plan.get_tier(selected_tier_key))
         and (
-            plan.get_tier(selected_tier_key).monthly_charge
-            > plan.get_tier(current_tier_key).monthly_charge
+            selected_tier.monthly_charge > current_tier.monthly_charge
         )
     ):
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ac20427 and e89dd5f.

📒 Files selected for processing (1)
  • daras_ai_v2/billing.py (17 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
daras_ai_v2/billing.py (6)
payments/plans.py (10)
  • get_active_monthly_charge (76-80)
  • get_active_credits (70-74)
  • get_pricing_title (50-55)
  • PricingPlan (88-542)
  • from_sub (373-376)
  • get_default_tier_key (82-85)
  • get_pricing_caption (57-62)
  • get_tier (64-68)
  • get_stripe_line_item (441-462)
  • supports_stripe (435-436)
app_users/models.py (1)
  • PaymentProvider (85-87)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
routers/account.py (2)
  • account_route (99-111)
  • payment_processing_route (38-84)
payments/models.py (1)
  • Subscription (37-399)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
🔇 Additional comments (12)
daras_ai_v2/billing.py (12)

81-142: LGTM: Tier-aware pricing display is correctly implemented.

The function properly retrieves the tier key from the subscription and consistently uses tier-aware accessors (get_active_monthly_charge, get_active_credits, get_pricing_title) to display pricing information.


168-168: Verify the default payment provider change.

The default payment provider has been changed to PaymentProvider.STRIPE for workspaces without an existing subscription. Ensure this aligns with your product requirements, especially if PayPal was previously the default or if this impacts user experience in specific regions.


243-275: LGTM: Tier selector logic is well-structured.

The function correctly:

  • Displays tier selector only when plan has tiers and Stripe is the payment provider
  • Preserves current tier selection for existing subscriptions
  • Falls back to plan defaults appropriately
  • Returns the selected tier key for downstream use

278-338: LGTM: Action button logic correctly handles tier changes.

The function properly distinguishes between plan changes and tier changes within the same plan, routing to appropriate handlers with the selected tier key.


422-487: LGTM: Upgrade flow correctly handles tier-aware pricing and STANDARD plan specifics.

The function properly:

  • Uses safe tier-aware accessors (get_active_monthly_charge, get_active_credits) that handle None gracefully
  • Distinguishes STANDARD plan from other plans for workspace creation flow
  • Threads tier context through upgrade and fallback paths

489-523: LGTM: Create subscription flow handles plan-specific behavior correctly.

The function appropriately:

  • Differentiates UI labels for STANDARD vs other plans
  • Routes personal workspace users away from workspace creation for STANDARD plan
  • Passes tier context to Stripe (PayPal doesn't support tiers)

525-529: LGTM: Price formatting helper is simple and correct.


532-600: LGTM: Subscription change flow properly integrates tier context.

The function correctly:

  • Validates that plan or tier has actually changed before proceeding
  • Includes tier information in Stripe metadata using the configured metadata field
  • Passes tier context through to line item generation
  • Appropriately omits tier handling for PayPal (which doesn't support tiers)

614-636: LGTM: Addon section correctly gated by workspace capability.

The early return at lines 618-619 appropriately hides the entire addon purchase section for workspaces that don't have credit topup privileges (new non-Enterprise users without topup history).


756-801: LGTM: Stripe subscription button correctly handles tier-aware pricing.

The function properly retrieves tier-specific pricing and passes the tier context through all subscription creation paths.


803-858: LGTM: Stripe subscription creation consistently includes tier context.

The function properly:

  • Conditionally adds tier to metadata when provided
  • Uses configured metadata field name for consistency
  • Passes tier context to line item generation
  • Includes metadata in both direct subscription creation and checkout flows

1021-1103: LGTM: Auto-recharge section correctly gated by workspace capability.

The early return at lines 1026-1027 appropriately hides the auto-recharge configuration section for workspaces without topup privileges, consistent with the addon section gating.

Comment on lines 197 to 287
def _render_plan_details(plan: PricingPlan):
with gui.div(className="flex-grow-1 d-flex flex-column"):
with gui.div():
with gui.tag("h2", className="mb-1"):
gui.html(plan.title)
gui.caption(
plan.description,
style={
"minHeight": "calc(var(--bs-body-line-height) * 2em)",
"display": "block",
},
"""Render plan details and return selected tier key if plan has tiers"""
with gui.div():
gui.write(plan.long_description, unsafe_allow_html=True)
with gui.div(className="mt-3"):
gui.write(plan.footer, unsafe_allow_html=True)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix misleading docstring.

The docstring claims the function returns a selected tier key, but the function doesn't return anything. The tier selection logic was moved to _render_plan_pricing.

Apply this diff to correct the docstring:

-def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+def _render_plan_details(plan: PricingPlan):
+    """Render plan description and footer"""
📝 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
def _render_plan_details(plan: PricingPlan):
with gui.div(className="flex-grow-1 d-flex flex-column"):
with gui.div():
with gui.tag("h2", className="mb-1"):
gui.html(plan.title)
gui.caption(
plan.description,
style={
"minHeight": "calc(var(--bs-body-line-height) * 2em)",
"display": "block",
},
"""Render plan details and return selected tier key if plan has tiers"""
with gui.div():
gui.write(plan.long_description, unsafe_allow_html=True)
with gui.div(className="mt-3"):
gui.write(plan.footer, unsafe_allow_html=True)
def _render_plan_details(plan: PricingPlan):
"""Render plan description and footer"""
with gui.div():
gui.write(plan.long_description, unsafe_allow_html=True)
with gui.div(className="mt-3"):
gui.write(plan.footer, unsafe_allow_html=True)
🤖 Prompt for AI Agents
In daras_ai_v2/billing.py around lines 222 to 228, the function
_render_plan_details has a misleading docstring that says it "return[s] selected
tier key if plan has tiers" even though it returns nothing (tier selection was
moved to _render_plan_pricing); update the docstring to accurately describe
behavior (e.g., "Render plan details" or "Render plan details; this does not
return a tier key — tier selection is handled in _render_plan_pricing") so the
docstring matches the function's actual behavior.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
daras_ai_v2/billing.py (2)

222-228: Fix misleading docstring.

The docstring claims the function returns a selected tier key, but the function doesn't return anything. The tier selection logic was moved to _render_plan_pricing.

Apply this diff to correct the docstring:

 def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+    """Render plan description and footer"""

384-390: Add defensive check for tier lookup in comparison logic.

Lines 388-390 directly access .monthly_charge after get_tier(), which returns PricingTier | None. If the subscription contains an invalid plan_tier_key (e.g., from database migration or stale data), this will raise an AttributeError.

Apply this diff to add defensive handling:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
+        and (current_tier := plan.get_tier(current_tier_key))
+        and (selected_tier := plan.get_tier(selected_tier_key))
         and (
-            plan.get_tier(selected_tier_key).monthly_charge
-            > plan.get_tier(current_tier_key).monthly_charge
+            selected_tier.monthly_charge > current_tier.monthly_charge
         )
     ):
🧹 Nitpick comments (1)
daras_ai_v2/billing.py (1)

433-433: Consider clarifying comment about workspace creation popup logic.

The comments state "Standard plan is only for personal workspaces, skip workspace creation popup," but the condition workspace.is_personal and plan != PricingPlan.STANDARD shows the workspace creation popup for non-Standard plans. The comment could more clearly explain that the popup is shown for personal workspaces upgrading to non-Standard plans (Business, Enterprise).

Consider this clearer wording:

-    # Standard plan is only for personal workspaces, skip workspace creation popup
+    # For personal workspaces upgrading to non-Standard plans, show workspace creation popup
     if workspace.is_personal and plan != PricingPlan.STANDARD:

Also applies to: 495-495

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e89dd5f and d25dfaf.

📒 Files selected for processing (1)
  • daras_ai_v2/billing.py (17 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
daras_ai_v2/billing.py (6)
payments/plans.py (10)
  • get_active_monthly_charge (76-80)
  • get_active_credits (70-74)
  • get_pricing_title (50-55)
  • PricingPlan (88-542)
  • from_sub (373-376)
  • get_default_tier_key (82-85)
  • get_pricing_caption (57-62)
  • get_tier (64-68)
  • get_stripe_line_item (441-462)
  • supports_stripe (435-436)
app_users/models.py (1)
  • PaymentProvider (85-87)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
routers/account.py (2)
  • account_route (99-111)
  • payment_processing_route (38-84)
payments/models.py (1)
  • Subscription (37-399)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (3.10.12, 1.8.3)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (9)
daras_ai_v2/billing.py (9)

83-87: LGTM!

The tier-aware pricing retrieval is correctly implemented. The get_active_monthly_charge and get_active_credits methods properly handle None tier keys and fall back to default values.


230-240: LGTM!

Good refactoring to extract the plan heading into a separate function. The consistent minimum height ensures proper grid alignment across plan cards.


243-275: LGTM!

The tier selection logic is well-structured:

  • Properly defaults to the subscription's current tier when viewing the active plan
  • Falls back to the plan's default tier for new plan selections
  • Correctly restricts tier selection to Stripe payment provider only

610-612: LGTM!

The guard properly restricts addon purchases to Enterprise workspaces or those with existing topup history, aligning with the PR objective to disable addons for new non-enterprise users.


749-793: LGTM!

The Stripe subscription button correctly propagates tier information through the payment flow and displays tier-specific pricing in the confirmation modal.


796-851: LGTM!

The Stripe subscription creation properly:

  • Includes tier information in metadata for webhook processing
  • Uses tier-aware line items from get_stripe_line_item(tier_key)
  • Propagates metadata through both direct subscription and checkout flows

1018-1020: LGTM!

The guard consistently restricts auto-recharge configuration to Enterprise workspaces or those with existing topup history, matching the addon section's behavior and fulfilling the PR objectives.


525-593: LGTM!

The subscription change logic properly:

  • Checks if both plan and tier are unchanged to prevent no-op updates
  • Includes tier information in Stripe metadata
  • Uses tier-aware line items
  • Correctly handles the Stripe-only tier support (PayPal branch unchanged as expected)

518-522: All callers of fmt_price are properly updated.

The verification found two call sites (lines 407 and 455), both passing integer arguments (current_monthly_charge and new_monthly_charge) that match the new signature fmt_price(monthly_charge: int) -> str. No incompatible calls were found.

@nikochiko nikochiko force-pushed the subscription-plan-standard branch from a31142d to a6a26f1 Compare November 2, 2025 07:45
Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
daras_ai_v2/billing.py (2)

282-288: Fix misleading docstring.

The docstring still incorrectly states the function returns a selected tier key, but the function returns None. Tier selection was moved to _render_plan_pricing.

Apply this diff to correct the docstring:

 def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+    """Render plan description and footer"""

444-451: Add defensive check for tier lookup in comparison logic.

Lines 448-449 directly access .monthly_charge after get_tier(), which returns PricingTier | None. If the subscription contains an invalid current_tier_key or selected_tier_key, this will raise an AttributeError.

Apply this diff to add defensive handling:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
+        and (current_tier := plan.get_tier(current_tier_key))
+        and (selected_tier := plan.get_tier(selected_tier_key))
         and (
-            plan.get_tier(selected_tier_key).monthly_charge
-            > plan.get_tier(current_tier_key).monthly_charge
+            selected_tier.monthly_charge > current_tier.monthly_charge
         )
     ):
🧹 Nitpick comments (1)
payments/plans.py (1)

382-416: Consider minor loop optimization.

The loop variable tier_key at line 399 is not used in the loop body. Since only the tier values are needed, consider using .values() instead of .items() for clarity.

Apply this diff if you'd like to simplify:

-            for tier_key, tier in plan.tiers.items():
+            for tier in plan.tiers.values():
                 expected_name = f"{plan.title} - {tier.label}"

Otherwise, the tier-matching logic correctly handles both tiered and non-tiered plans.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a31142d and a6a26f1.

📒 Files selected for processing (4)
  • app_users/migrations/0029_alter_appusertransaction_plan.py (1 hunks)
  • daras_ai_v2/billing.py (17 hunks)
  • payments/migrations/0009_alter_subscription_plan.py (1 hunks)
  • payments/plans.py (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
app_users/migrations/0029_alter_appusertransaction_plan.py (2)
payments/migrations/0009_alter_subscription_plan.py (1)
  • Migration (6-29)
app_users/migrations/0026_alter_appusertransaction_plan.py (1)
  • Migration (6-18)
payments/plans.py (1)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/billing.py (5)
payments/plans.py (10)
  • get_active_monthly_charge (76-80)
  • get_active_credits (70-74)
  • get_pricing_title (50-55)
  • PricingPlan (88-538)
  • from_sub (369-372)
  • get_default_tier_key (82-85)
  • get_pricing_caption (57-62)
  • get_tier (64-68)
  • get_stripe_line_item (437-458)
  • supports_stripe (431-432)
app_users/models.py (1)
  • PaymentProvider (85-87)
daras_ai_v2/grid_layout_widget.py (1)
  • grid_layout (25-38)
payments/models.py (1)
  • Subscription (37-399)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
payments/migrations/0009_alter_subscription_plan.py (3)
app_users/migrations/0029_alter_appusertransaction_plan.py (1)
  • Migration (6-33)
payments/migrations/0005_alter_subscription_plan.py (1)
  • Migration (6-18)
payments/migrations/0006_alter_subscription_plan.py (1)
  • Migration (6-18)
🪛 Ruff (0.14.2)
app_users/migrations/0029_alter_appusertransaction_plan.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-33: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

payments/plans.py

85-85: Avoid specifying long messages outside the exception class

(TRY003)


399-399: Loop control variable tier_key not used within loop body

Rename unused tier_key to _tier_key

(B007)


429-429: Avoid specifying long messages outside the exception class

(TRY003)


439-439: Avoid specifying long messages outside the exception class

(TRY003)

payments/migrations/0009_alter_subscription_plan.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-29: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: test (3.10.12, 1.8.3)
  • GitHub Check: Analyze (python)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (13)
app_users/migrations/0029_alter_appusertransaction_plan.py (1)

1-33: LGTM! Migration correctly adds Standard plan.

The migration properly updates the AppUserTransaction.plan field choices to include the new Standard plan (8) and renames "Pay as you go" to "Free" (3), aligning with the updated PricingPlan enum.

payments/migrations/0009_alter_subscription_plan.py (1)

1-29: LGTM! Migration correctly updates Subscription plan choices.

The migration properly adds the Standard plan (8) to the Subscription.plan field choices and depends on the correct prior migration (0008_add_plan_tier_key), maintaining proper migration sequencing for the tier-aware pricing feature.

daras_ai_v2/billing.py (5)

82-143: LGTM! Tier-aware current plan rendering.

The function correctly retrieves the tier_key from the subscription and uses tier-aware pricing methods to display the appropriate monthly charge and credits. The tier-aware getters handle None values safely.


152-192: LGTM! Correct STANDARD plan filtering for workspace types.

The filtering logic correctly hides the STANDARD plan from non-personal workspaces (except when already subscribed to it), aligning with the business requirement that STANDARD is designed for personal workspaces. The new compact/full-width rendering structure with tier-aware pricing is well-implemented.


667-688: LGTM! Proper guard for addon section.

The early return correctly prevents non-eligible workspaces from accessing the credit top-up functionality, aligning with the PR objective to disable auto-recharge/addons for new non-enterprise users. The check using workspace.allow_credit_topups() ensures only Enterprise plans or workspaces with prior top-up history can access this section.


1074-1157: LGTM! Proper guard for auto-recharge section.

The early return at lines 1079-1080 correctly prevents non-eligible workspaces from accessing auto-recharge functionality, consistent with the addon section guard and aligned with the PR objectives.


809-911: LGTM! Comprehensive tier_key propagation in Stripe subscription flow.

The tier_key is correctly threaded through the Stripe subscription creation flow:

  • Metadata properly includes tier information via STRIPE_USER_SUBSCRIPTION_TIER_METADATA_FIELD
  • Line items use tier-aware pricing via get_stripe_line_item(tier_key)
  • Tier-specific credits and monthly charges are displayed in the UI
payments/plans.py (6)

23-31: LGTM! Clean PricingTier type definition.

The PricingTier NamedTuple provides a clear structure for tier pricing with a convenient label property for consistent formatting.


47-86: LGTM! Robust tier-aware pricing methods.

The tier-aware methods correctly handle None tier keys and missing tiers by falling back to default values. The get_tier() method properly returns None when tiers don't exist or aren't found, and downstream methods handle this safely.


208-267: LGTM! Well-structured STANDARD plan with comprehensive tiers.

The STANDARD plan provides 6 pricing tiers ranging from $25 to $400/month, with credits scaling appropriately (2,000 to 44,000). The plan structure, content, and tier definitions align well with the "power users" target audience.


352-356: LGTM! Comparison logic correctly uses tier-aware pricing.

The comparison correctly uses get_active_monthly_charge() for ordering plans, which defaults to base tier pricing when no tier_key is provided. The ENTERPRISE exception is appropriately maintained.


437-458: LGTM! Stripe line item generation correctly uses tier-aware pricing.

The method properly retrieves tier-specific credits and monthly charges, and correctly passes tier_key to get_stripe_product_id() for tiered plans.


470-498: LGTM! Tier-aware Stripe product ID generation.

The method correctly builds tier-specific product names (e.g., "Standard - 2,000 Credits for $25/month") and searches/creates Stripe products with the appropriate pricing. The logic properly handles both tiered and non-tiered plans.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
daras_ai_v2/billing.py (2)

282-288: Misleading docstring already flagged.

The docstring claims this function returns a selected tier key, but it returns nothing. This issue was previously identified.


444-451: Defensive check for tier lookup already flagged.

Lines 448-450 access .monthly_charge directly after get_tier(), which can return None. This AttributeError risk was previously identified.

🧹 Nitpick comments (3)
payments/plans.py (3)

82-85: Consider custom exception for better error handling.

Static analysis suggests avoiding long messages directly in exception constructors. While the current implementation is functional, you could define a custom exception class if this pattern appears frequently.

Example refactor (if desired):

class NoTiersAvailableError(ValueError):
    def __init__(self, plan_key: str):
        super().__init__(f"No tiers available for plan {plan_key}")

def get_default_tier_key(self) -> str:
    if self.tiers:
        return next(iter(self.tiers.keys()))
    raise NoTiersAvailableError(self.key)

382-416: Consider using _ for unused loop variable.

Static analysis notes that tier_key at line 399 is not used in the loop body. While this doesn't affect functionality, you could use _ to signal the unused variable.

Apply this diff if desired:

             if plan.tiers:
                 # Check each tier by matching product name
-                for tier_key, tier in plan.tiers.items():
+                for _, tier in plan.tiers.items():
                     expected_name = f"{plan.title} - {tier.label}"
                     expected_price = tier.monthly_charge * 100  # convert to cents

Otherwise, the tier-aware product matching logic is well-structured and handles both tiered and non-tiered plans correctly.


425-429: Consider custom exception for consistency.

Similar to line 85, static analysis suggests avoiding long messages in exception constructors. However, for this key lookup failure, the current implementation is clear and functional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6a26f1 and d267bf1.

📒 Files selected for processing (2)
  • daras_ai_v2/billing.py (18 hunks)
  • payments/plans.py (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
payments/plans.py (1)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/billing.py (4)
payments/plans.py (10)
  • get_active_monthly_charge (76-80)
  • get_active_credits (70-74)
  • get_pricing_title (50-55)
  • PricingPlan (88-538)
  • from_sub (369-372)
  • get_default_tier_key (82-85)
  • get_pricing_caption (57-62)
  • get_tier (64-68)
  • get_stripe_line_item (437-458)
  • supports_stripe (431-432)
daras_ai_v2/user_date_widgets.py (1)
  • render_local_date_attrs (30-38)
payments/models.py (1)
  • Subscription (37-399)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
🪛 Ruff (0.14.2)
payments/plans.py

85-85: Avoid specifying long messages outside the exception class

(TRY003)


399-399: Loop control variable tier_key not used within loop body

Rename unused tier_key to _tier_key

(B007)


429-429: Avoid specifying long messages outside the exception class

(TRY003)


439-439: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (3.10.12, 1.8.3)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (21)
daras_ai_v2/billing.py (13)

82-143: LGTM! Tier-aware rendering in current plan display.

The function correctly retrieves the tier key from the subscription and uses tier-aware pricing methods throughout. The next invoice date rendering and pricing display logic are well-structured.


152-192: LGTM! Plan filtering and rendering logic.

The filtering correctly restricts STANDARD to personal workspaces (unless already subscribed), and the grid/full-width separation aligns with the new plan structure.


303-335: LGTM! Tier selection UI implementation.

The tier selection logic correctly renders a selectbox for tiered plans when using Stripe, uses the current subscription's tier as default, and returns the selected tier key for downstream usage.


338-398: LGTM! Action button orchestration with tier support.

The logic correctly handles tier changes within the same plan and delegates to appropriate subscription creation or change flows with tier context.


458-476: LGTM! Downgrade flow with tier-aware pricing.

The downgrade confirmation dialog correctly displays tier-aware monthly charges using get_active_monthly_charge and passes tier_key to the subscription change function.


478-543: LGTM! Upgrade flow with tier-aware pricing.

The upgrade confirmation dialog correctly retrieves and displays tier-aware monthly charges and credits. The tier_key is properly propagated through the subscription change flow. Error handling for payment failures was previously addressed.


545-576: LGTM! Subscription creation with tier support.

The function correctly handles STANDARD as personal-only, passes tier_key through Stripe flows, and documents PayPal's lack of tier support.


578-582: LGTM! Simplified price formatting.

The refactored signature accepting a numeric monthly_charge parameter supports tier-aware pricing nicely.


585-653: LGTM! Subscription change flow with tier support.

The function correctly propagates tier_key through Stripe metadata and line items. The early return when both plan and tier are unchanged is a good optimization.


667-688: LGTM! Addon section gated by workspace allowance.

The early return based on workspace.allow_credit_topups() correctly implements the feature restriction for new non-enterprise users as per PR objectives.


809-854: LGTM! Stripe subscription button with tier support.

The function correctly accepts tier_key, retrieves tier-aware pricing, and propagates the tier context through the subscription creation flow.


856-911: LGTM! Stripe subscription creation with tier support.

The function properly accepts tier_key, includes it in Stripe metadata using the configured metadata field, and passes it through line item generation for both direct subscription and checkout flows.


1074-1157: LGTM! Auto-recharge section gated by workspace allowance.

The early return based on workspace.allow_credit_topups() correctly restricts auto-recharge access as per the PR objectives.

payments/plans.py (8)

23-31: LGTM! Clean PricingTier definition.

The NamedTuple structure is straightforward, and the label property provides a clear user-facing representation.


50-80: LGTM! Tier-aware pricing accessors.

The accessor methods correctly handle tier lookups with appropriate fallbacks. The walrus operator usage in get_active_credits and get_active_monthly_charge is clean and idiomatic.


208-267: LGTM! Comprehensive STANDARD plan definition.

The plan is well-structured with six tiers offering a reasonable pricing progression. The description and footer accurately reflect the capabilities, and the tier naming convention is consistent.


172-206: LGTM! Improved STARTER plan messaging.

The updated title "Free" and expanded feature list provide clearer value communication. The pricing_title and pricing_caption overrides work well for the free tier.


269-347: LGTM! BUSINESS and ENTERPRISE plan updates.

The hierarchy is clear with BUSINESS referencing "Everything in Standard +", and ENTERPRISE's full_width flag correctly supports the UI rendering requirements.


349-362: LGTM! Plan-level comparison operators.

The __gt__ operator correctly uses get_active_monthly_charge() without tier_key, which provides plan-level comparison. Tier-specific comparisons are handled separately in the billing logic where needed.


437-458: LGTM! Tier-aware Stripe line item generation.

The function correctly retrieves tier-specific pricing and passes tier_key through to product ID generation. The ValueError for unsupported plans is appropriate, though static analysis flags the long message as a minor style issue.


470-498: LGTM! Tier-aware Stripe product ID resolution.

The function correctly builds tier-specific product names that match the format expected by get_by_stripe_product. The search and create logic properly handles both existing and new products.

Copy link
Contributor

@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: 0

♻️ Duplicate comments (2)
daras_ai_v2/billing.py (2)

444-450: Add defensive check for tier lookup in comparison logic.

Lines 448-449 directly access .monthly_charge after get_tier(), which returns PricingTier | None. If the subscription contains an invalid selected_tier_key or current_tier_key (e.g., from database migration or stale data), this will raise an AttributeError.

Apply this diff to add defensive handling:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
+        and (current_tier := plan.get_tier(current_tier_key))
+        and (selected_tier := plan.get_tier(selected_tier_key))
         and (
-            plan.get_tier(selected_tier_key).monthly_charge
-            > plan.get_tier(current_tier_key).monthly_charge
+            selected_tier.monthly_charge > current_tier.monthly_charge
         )
     ):

282-288: Fix the misleading docstring.

The docstring at line 283 claims the function "return selected tier key if plan has tiers," but the function doesn't return anything. The tier selection logic was moved to _render_plan_pricing.

Apply this diff to correct the docstring:

 def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+    """Render plan description and footer"""
     with gui.div():
🧹 Nitpick comments (2)
daras_ai_v2/billing.py (2)

303-336: Remove duplicate assignment.

Line 333 unnecessarily assigns current_plan again, which was already assigned on line 306. This is redundant and reduces code clarity.

Apply this diff to remove the duplicate:

     with pricing_div:
         with gui.tag("h3", className="my-0 d-inline me-2"):
             gui.html(plan.get_pricing_title(selected_tier_key))
         with gui.tag("p", className="text-muted my-0"):
             gui.html(plan.get_pricing_caption(selected_tier_key))
-    current_plan = PricingPlan.from_sub(workspace.subscription)
 
     return selected_tier_key

478-543: LGTM on upgrade flow, consider adding defensive check.

The upgrade logic correctly handles tier-aware pricing and propagates tier_key through the confirmation dialog and subscription creation. The tier-based pricing display at lines 505-507 is accurate.

Note: Line 485 directly accesses workspace.subscription.plan_tier_key without a None check. While the calling context ensures subscription exists, adding an assertion or defensive check would make this explicit.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d267bf1 and 8e54268.

📒 Files selected for processing (1)
  • daras_ai_v2/billing.py (18 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (12)
daras_ai_v2/billing.py (12)

1-1: LGTM on the import.

The partial import from functools is correctly used at lines 173-179 for the grid_layout function.


82-143: LGTM on tier-aware current plan rendering.

The tier extraction from the subscription and tier-aware pricing display using get_active_monthly_charge(tier_key) and get_active_credits(tier_key) are correctly implemented.


152-192: LGTM on plan filtering and rendering.

The logic at lines 158-159 correctly restricts the STANDARD plan to personal workspaces while allowing existing STANDARD subscribers to see their current plan. The refactored rendering using _render_plan_compact and _render_plan_full_width helpers improves maintainability.


195-280: LGTM on plan rendering helpers.

The _render_plan_full_width and _render_plan_compact functions correctly delegate tier selection to _render_plan_pricing and propagate selected_tier_key through to the action buttons. The responsive layout handling is well-structured.


338-399: LGTM on action button logic.

The tier comparison at line 354 correctly handles None cases through short-circuit evaluation, and the selected_tier_key is properly propagated through to subscription change and creation flows.


461-476: LGTM on downgrade flow.

The downgrade logic correctly uses tier-aware pricing getters and passes tier_key through to change_subscription. The confirmation dialog appropriately displays the pricing comparison.


545-576: LGTM on subscription creation flow.

The function correctly guards personal workflows for the STANDARD plan (lines 554-555) and appropriately propagates selected_tier_key through Stripe flows while documenting that PayPal doesn't support tiers.


578-583: LGTM on price formatting.

The updated signature accepting monthly_charge: int correctly supports tier-aware pricing throughout the billing flows.


585-654: LGTM on subscription change logic.

The tier-aware subscription changes are well-implemented:

  • Defensive tier extraction at lines 592-594
  • Early exit when plan and tier are unchanged (line 597)
  • Proper metadata construction with tier info (lines 615-622)
  • Correct tier propagation to Stripe line items (line 628)

667-673: LGTM on addon section gating.

The early return based on workspace.allow_credit_topups() correctly hides the credit purchase section for users without top-up permissions, aligning with the PR objectives to restrict addons for non-enterprise users.


809-911: LGTM on tier-aware Stripe integration.

The Stripe subscription flows correctly integrate tier support:

  • Tier-aware pricing display in confirmation dialogs (lines 822-823, 844, 848)
  • Proper metadata construction with tier info (lines 882-884)
  • Consistent tier propagation through both immediate and checkout flows
  • Correct usage of plan.get_stripe_line_item(tier_key) at line 884

1074-1081: LGTM on auto-recharge section gating.

The early return guard based on workspace.allow_credit_topups() correctly hides auto-recharge settings for users without top-up permissions, maintaining consistency with the addon section gating.

@cursor
Copy link

cursor bot commented Nov 26, 2025

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on December 27.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

@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: 5

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e54268 and a8918af.

📒 Files selected for processing (7)
  • daras_ai_v2/billing.py (19 hunks)
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1 hunks)
  • payments/models.py (5 hunks)
  • payments/plans.py (10 hunks)
  • payments/utils.py (1 hunks)
  • payments/webhooks.py (4 hunks)
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • payments/webhooks.py
  • payments/utils.py
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py
  • payments/models.py
  • payments/plans.py
  • daras_ai_v2/billing.py
🧬 Code graph analysis (3)
payments/webhooks.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • get_by_stripe_product (351-385)
payments/models.py (1)
payments/plans.py (4)
  • PricingPlan (81-506)
  • stripe_get_addon_product (522-531)
  • PricingTier (23-29)
  • from_sub (338-341)
payments/plans.py (1)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
🪛 Ruff (0.14.5)
payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


26-26: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

usage_costs/migrations/0038_alter_modelpricing_model_name.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-298: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

payments/models.py

46-46: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

payments/plans.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


398-398: Avoid specifying long messages outside the exception class

(TRY003)


408-408: Avoid specifying long messages outside the exception class

(TRY003)

daras_ai_v2/billing.py

305-305: Local variable current_plan is assigned to but never used

Remove assignment to unused variable current_plan

(F841)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (3.10.12, 1.8.3)
  • GitHub Check: Analyze (python)
🔇 Additional comments (15)
payments/utils.py (1)

20-20: LGTM! Precision increase supports tier-based pricing.

The increase from 4 to 12 decimal places for unit_amount_decimal enables more accurate per-credit pricing calculations, especially for tiered plans where credits and monthly charges vary. This aligns well with the tier-aware pricing introduced in payments/plans.py.

payments/plans.py (5)

23-29: LGTM! Clean tier data structure.

The PricingTier NamedTuple provides a clear, immutable representation of tier-specific pricing with a convenient label property.


49-73: LGTM! Tier-aware accessor methods are well-designed.

The get_active_credits() and get_active_monthly_charge() methods provide a consistent way to retrieve pricing information whether using tiered or default pricing. The fallback logic is clear and correct.


165-199: LGTM! STARTER plan correctly represents free tier.

The updated STARTER plan clearly represents the free tier with appropriate pricing and feature descriptions. The pricing_title and pricing_caption effectively communicate the free offering.


228-235: LGTM! STANDARD plan tiers are well-structured.

The tiered pricing for the STANDARD plan provides clear progression from 2,000 to 44,000 credits with corresponding pricing. The tier structure enables flexible scaling for power users.


351-385: LGTM! Tier-aware product matching is correctly implemented.

The get_by_stripe_product() method correctly handles both tiered and non-tiered plans by matching product names and prices. The logic properly falls back to legacy product name lookups and handles missing default_price gracefully.

daras_ai_v2/billing.py (4)

82-87: LGTM! Tier-aware pricing correctly integrated.

The current plan rendering now uses get_tier() and tier-aware pricing accessors (get_active_monthly_charge, get_active_credits) to display accurate pricing information.


308-329: LGTM! Tier selection UI is well-implemented.

The tier selection logic provides a clean selectbox for Stripe users when tiers are available, defaulting to the current charged_amount. The fallback to None for non-tiered plans is appropriate.


667-669: LGTM! Addon section properly gated.

The workspace.allow_credit_topups() check correctly hides the addon section for workspaces that aren't allowed to purchase topups.


1073-1075: LGTM! Auto-recharge section properly gated.

The workspace.allow_credit_topups() check correctly hides the auto-recharge section for workspaces that aren't allowed to use this feature.

payments/models.py (2)

103-111: LGTM! Tier-aware full_clean correctly implemented.

The full_clean method now derives amount and charged_amount from the plan when auto-recharge is enabled, properly supporting tier-aware pricing. The fallback logic ensures backward compatibility when these fields aren't set.


133-138: LGTM! get_tier method correctly constructs PricingTier.

The get_tier() method properly reconstructs a PricingTier from the stored amount and charged_amount fields, converting charged_amount from cents to dollars. The None return when either field is missing is appropriate.

payments/webhooks.py (2)

171-180: LGTM! Tier-aware plan lookup with proper fallback.

The try/except logic correctly attempts to lookup the plan from metadata first, then falls back to product matching. The assertion with a descriptive message helps identify missing plan configurations.


280-284: LGTM! Tier-aware subscription data correctly persisted.

The set_workspace_subscription function now populates amount and charged_amount with sensible fallbacks (plan defaults when not provided), and calls full_clean() to ensure auto-recharge parameters are properly initialized.

usage_costs/migrations/0038_alter_modelpricing_model_name.py (1)

26-26: Fix Unicode apostrophe in help_text.

Line 26 contains a Unicode right single quotation mark (') in "Learn More" instead of a standard ASCII apostrophe ('). This could cause encoding issues in some environments.

Apply this diff:

-                help_text="The name of the model. Only used for Display purposes.<a href='https://stripe.com/docs/currencies'>Learn More</a>",
+                help_text="The name of the model. Only used for Display purposes.<a href='https://stripe.com/docs/currencies'>Learn More</a>",

Likely an incorrect or invalid review comment.

name="charged_amount",
field=models.PositiveIntegerField(
default=0,
help_text="The charged dollar amount in the currency’s smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Unicode apostrophe in help_text.

Line 26 contains a Unicode right single quotation mark (') in the link instead of a standard ASCII apostrophe ('). This could cause encoding issues.

Apply this diff:

-                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
+                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",

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

🧰 Tools
🪛 Ruff (0.14.5)

26-26: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

🤖 Prompt for AI Agents
In
payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
around line 26, the help_text contains a Unicode right single quotation mark in
the "<a href='https://stripe.com/docs/currencies'>Learn More</a>" part; replace
that Unicode apostrophe with a standard ASCII single quote (') so the href uses
plain ASCII quotes, then save the file with UTF-8 encoding (no other changes).

Comment on lines +40 to +50
amount = models.IntegerField(
help_text="The amount (Gooey credits) added/deducted in this transaction.<br>"
"Positive for credits added, negative for credits deducted.",
default=0,
)
charged_amount = models.PositiveIntegerField(
help_text="The charged dollar amount in the currency’s smallest unit.<br>"
"E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br>"
"<a href='https://stripe.com/docs/currencies'>Learn More</a>",
default=0,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Unicode apostrophe in help_text.

Line 48 contains a Unicode right single quotation mark (') in the link instead of a standard ASCII apostrophe ('). This could cause encoding issues.

Apply this diff:

     charged_amount = models.PositiveIntegerField(
         help_text="The charged dollar amount in the currency's smallest unit.<br>"
         "E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br>"
-        "<a href='https://stripe.com/docs/currencies'>Learn More</a>",
+        "<a href='https://stripe.com/docs/currencies'>Learn More</a>",
         default=0,
     )

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

🧰 Tools
🪛 Ruff (0.14.5)

46-46: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

🤖 Prompt for AI Agents
In payments/models.py around lines 40 to 50, the help_text string for
charged_amount contains a Unicode right single quotation mark in “currency’s”;
replace that character with a standard ASCII apostrophe so the text reads
"currency's" (ensure the string literal is updated accordingly and saved in
UTF-8 without changing other text).

@devxpy devxpy force-pushed the subscription-plan-standard branch from a8918af to 510c49c Compare November 26, 2025 10:09
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (4)
payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1)

26-26: Unicode apostrophe in help_text (static analysis flag).

Ruff flagged an ambiguous Unicode character in the help_text. The ' characters in href='...' may be Unicode right single quotation marks instead of ASCII single quotes. While this likely works fine, it could cause encoding issues in some contexts.

If you want to fix this, ensure the quotes are standard ASCII:

-                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
+                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
daras_ai_v2/billing.py (2)

305-305: Remove unused variable current_plan.

The current_plan variable is assigned but never used in _render_plan_pricing. This was flagged by static analysis.

Apply this diff:

 def _render_plan_pricing(
     plan: PricingPlan, payment_provider: PaymentProvider | None, workspace: "Workspace"
 ) -> PricingTier | None:
-    current_plan = PricingPlan.from_sub(workspace.subscription)
     pricing_div = gui.div()

441-446: Add None check for selected_tier before accessing .monthly_charge.

Line 445 accesses selected_tier.monthly_charge without verifying selected_tier is not None. While current_tier is checked on line 444, selected_tier can still be None if passed from a call site like line 385. This will raise AttributeError.

Apply this diff:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
         and current_tier
+        and selected_tier
         and selected_tier.monthly_charge > current_tier.monthly_charge
     ):
payments/webhooks.py (1)

171-180: Plan lookup fallback still uses assertion that could crash webhook.

Lines 179-180 assert that plan is not None when falling back to product lookup. This will crash the webhook handler on legacy subscriptions or products without matching plan definitions, causing Stripe to retry indefinitely. The past review suggested graceful handling with logging and early return.

Apply this diff for graceful handling:

         try:
             plan = PricingPlan.get_by_key(
                 stripe_sub.metadata[settings.STRIPE_USER_SUBSCRIPTION_METADATA_FIELD]
             )
         except KeyError:
             product = stripe.Product.retrieve(
                 stripe_sub.plan.product, expand=["default_price"]
             )
             plan = PricingPlan.get_by_stripe_product(product)
-            assert plan is not None, f"Plan for product {product.id} not found"
+            if plan is None:
+                logger.warning(f"Plan not found for Stripe product {product.id}")
+                return  # Acknowledge webhook to prevent retries
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8918af and 510c49c.

📒 Files selected for processing (7)
  • daras_ai_v2/billing.py (19 hunks)
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1 hunks)
  • payments/models.py (5 hunks)
  • payments/plans.py (10 hunks)
  • payments/utils.py (1 hunks)
  • payments/webhooks.py (4 hunks)
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • payments/utils.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py
  • payments/webhooks.py
  • payments/plans.py
  • payments/models.py
  • daras_ai_v2/billing.py
🧬 Code graph analysis (3)
payments/plans.py (2)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/paypal.py (2)
  • Product (237-242)
  • create (82-92)
payments/models.py (1)
payments/plans.py (4)
  • PricingPlan (81-506)
  • stripe_get_addon_product (522-531)
  • PricingTier (23-29)
  • from_sub (338-341)
daras_ai_v2/billing.py (2)
payments/plans.py (10)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • get_active_monthly_charge (69-73)
  • get_active_credits (63-67)
  • get_pricing_title (49-54)
  • label (28-29)
  • from_sub (338-341)
  • get_pricing_caption (56-61)
  • get_stripe_line_item (406-427)
  • supports_stripe (400-401)
payments/models.py (2)
  • get_tier (133-138)
  • Subscription (37-403)
🪛 Ruff (0.14.5)
payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


26-26: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

usage_costs/migrations/0038_alter_modelpricing_model_name.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-298: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

payments/plans.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


398-398: Avoid specifying long messages outside the exception class

(TRY003)


408-408: Avoid specifying long messages outside the exception class

(TRY003)

payments/models.py

46-46: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

daras_ai_v2/billing.py

305-305: Local variable current_plan is assigned to but never used

Remove assignment to unused variable current_plan

(F841)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyze (python)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (22)
usage_costs/migrations/0038_alter_modelpricing_model_name.py (1)

8-10: Ignore static analysis hint (false positive).

The Ruff warning about ClassVar annotations on dependencies and operations is a false positive. This is auto-generated Django migration code following Django's standard conventions. These attributes should not be modified with type annotations as they follow Django's expected migration structure.

Also applies to: 12-298

daras_ai_v2/billing.py (6)

1-1: Import moved to top of file.

The functools.partial import placement looks good for the new usage in render_all_plans.


82-141: Good tier-aware pricing integration in render_current_plan.

The tier retrieval and tier-aware pricing methods (get_active_monthly_charge, get_active_credits, get_pricing_title) are properly used, with graceful fallbacks when tier is None.


151-191: Plan rendering refactored for tier support and layout.

The STANDARD plan filtering for non-personal workspaces and the grid/full-width plan separation are well-structured. The partial usage for grid layout is clean.


664-669: Good gating of add-on and auto-recharge sections.

The workspace.allow_credit_topups() check properly hides these sections for workspaces that shouldn't have access to credit top-ups, aligning with the PR objective to disable auto-recharge/addons for non-enterprise users.

Also applies to: 1069-1075


851-906: Tier-aware Stripe subscription creation looks correct.

The tier parameter is properly threaded through to plan.get_stripe_line_item(tier) for line item generation.


582-650: Tier-aware subscription change looks correct.

The early exit check (lines 593-598) properly compares both plan and tier. The Stripe modification correctly uses tier-aware line items. The metadata stores only the plan key, which is appropriate since tier details are embedded in the line item pricing.

payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1)

12-49: Migration operations look correct.

The new amount and charged_amount fields are properly added with sensible defaults. The auto_recharge_enabled default change to False aligns with the PR objective to disable auto-recharge for new users. The plan choices include the new STANDARD plan.

payments/webhooks.py (2)

188-198: Amount and charged_amount calculation looks correct.

The calculation properly uses Decimal for precision when computing charged_amount from Stripe's amount_decimal. The values are correctly passed to set_workspace_subscription.


260-293: set_workspace_subscription correctly handles tier data.

The new amount and charged_amount parameters with fallbacks to plan defaults (lines 279-280) are well-designed. The full_clean() call without explicit parameters aligns with the model's updated signature.

payments/models.py (5)

40-50: New subscription fields for tier data look correct.

The amount and charged_amount fields are properly defined with descriptive help_text and sensible defaults. These fields support the tier-aware pricing system.


45-48: Unicode apostrophe in help_text.

Same Unicode character issue as in the migration file - the ' in the href may be a Unicode right single quotation mark.


103-111: Simplified full_clean with tier-aware defaults.

The refactored full_clean correctly derives amount and charged_amount from subscription fields or falls back to plan defaults. This aligns with the tier-aware pricing system.


133-138: get_tier() method correctly derives tier from subscription data.

The method properly guards against missing data and converts charged_amount (in cents) to monthly_charge (in dollars) when constructing the PricingTier.


61-61: Default auto_recharge_enabled=False aligns with PR objectives.

This change disables auto-recharge by default for new subscriptions, matching the PR goal to disable auto-recharge/addons for new non-enterprise users.

payments/plans.py (7)

23-30: Clean PricingTier implementation.

The PricingTier namedtuple with credits, monthly_charge, and a formatted label property is well-designed for tier-aware pricing.


49-78: Tier-aware accessor methods are well-designed.

The accessor methods (get_pricing_title, get_pricing_caption, get_active_credits, get_active_monthly_charge) properly handle both tier-specific and default pricing scenarios. The get_default_tier method appropriately raises an error for plans without tiers.


201-236: New STANDARD plan with tiered pricing looks correct.

The plan has 6 well-structured tiers ranging from 2,000 credits/$25 to 44,000 credits/$400. The long description accurately reflects the tier range ("Up to 44,000 Cr / month").


350-385: Comprehensive Stripe product lookup with tier support.

The get_by_stripe_product method correctly handles:

  1. Legacy products via REVERSE_STRIPE_PRODUCT_NAMES
  2. Tiered plans by matching product name and price to tier labels
  3. Non-tiered plans by matching product name and default price

Returning None for unmatched products allows callers to handle gracefully.


406-427: Tier-aware Stripe line item generation is correct.

The method properly uses tier-aware pricing methods and passes the tier through to get_stripe_product_id for product lookup/creation.


439-466: Tier-aware Stripe product ID resolution is well-implemented.

The method:

  1. Builds a tier-specific product name (e.g., "Standard - 2,000 Credits for $25/month")
  2. Matches existing products by both name and price
  3. Creates new products with correct tier-specific pricing if not found

This design allows multiple Stripe products per plan, one for each tier.


321-325: Plan comparison logic is correct.

Using get_active_monthly_charge() without a tier argument compares plans by their base monthly charge. This is appropriate since tier selection affects pricing within a plan, not the plan ordering.

Comment on lines 245 to 311
("protogen_2_2", "Protogen V2.2 (darkstorm2150)"),
("epicdream", "epiCDream [Deprecated] (epinikion)"),
("flux_1_dev", "FLUX.1 [dev]"),
("dream_shaper", "DreamShaper (Lykon)"),
("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
("dall_e", "DALL·E 2 (OpenAI)"),
("dall_e_3", "DALL·E 3 (OpenAI)"),
("gpt_image_1", "GPT Image 1 (OpenAI)"),
("nano_banana", "Nano Banana (Google)"),
("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
("openjourney", "Open Journey [Deprecated] (PromptHero)"),
("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
(
"rodent_diffusion_1_5",
"Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
),
("deepfloyd_if", "DeepFloyd IF [Deprecated] (stability.ai)"),
("flux_pro_kontext", "FLUX.1 Pro Kontext (fal.ai)"),
("dream_shaper", "DreamShaper (Lykon)"),
("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
("dall_e", "Dall-E (OpenAI)"),
("gpt_image_1", "GPT Image 1 (OpenAI)"),
("nano_banana", "Nano Banana (Google)"),
("instruct_pix2pix", "✨ InstructPix2Pix (Tim Brooks)"),
("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
("openjourney", "Open Journey [Deprecated] (PromptHero)"),
("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
(
"rodent_diffusion_1_5",
"Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
),
("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
("runway_ml", "Stable Diffusion v1.5 (RunwayML)"),
("dall_e", "Dall-E (OpenAI)"),
("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove duplicate model choices.

The choices list contains numerous duplicate keys starting around line 245. For example:

  • dream_shaper appears at both lines 248 and 267
  • sd_2 appears three times (lines 250, 269, 284)
  • dall_e appears three times with inconsistent display names (line 252: "DALL·E 2 (OpenAI)" vs lines 271, 286: "Dall-E (OpenAI)")
  • jack_qiao appears three times (lines 260, 279, 287)
  • Many others: dreamlike_2, sd_1_5, gpt_image_1, nano_banana, openjourney_2, openjourney, analog_diffusion, protogen_5_3, rodent_diffusion_1_5

Django will only use the first occurrence of each key, rendering the duplicates ineffective and creating maintenance confusion.

Apply this diff to remove the duplicate entries:

                 ("epicdream", "epiCDream [Deprecated] (epinikion)"),
                 ("flux_1_dev", "FLUX.1 [dev]"),
-                ("dream_shaper", "DreamShaper (Lykon)"),
-                ("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
-                ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
-                ("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
-                ("dall_e", "DALL·E 2 (OpenAI)"),
                 ("dall_e_3", "DALL·E 3 (OpenAI)"),
-                ("gpt_image_1", "GPT Image 1 (OpenAI)"),
-                ("nano_banana", "Nano Banana (Google)"),
-                ("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
-                ("openjourney", "Open Journey [Deprecated] (PromptHero)"),
-                ("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
-                ("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
-                ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
-                (
-                    "rodent_diffusion_1_5",
-                    "Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
-                ),
                 ("deepfloyd_if", "DeepFloyd IF [Deprecated] (stability.ai)"),
                 ("flux_pro_kontext", "FLUX.1 Pro Kontext (fal.ai)"),
                 ("dream_shaper", "DreamShaper (Lykon)"),
                 ("dreamlike_2", "Dreamlike Photoreal 2.0 (dreamlike.art)"),
                 ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
                 ("sd_1_5", "Stable Diffusion v1.5 (RunwayML)"),
-                ("dall_e", "Dall-E (OpenAI)"),
-                ("gpt_image_1", "GPT Image 1 (OpenAI)"),
-                ("nano_banana", "Nano Banana (Google)"),
+                ("dall_e", "DALL·E 2 (OpenAI)"),
+                ("gpt_image_1", "GPT Image 1 (OpenAI)"),
+                ("nano_banana", "Nano Banana (Google)"),
                 ("instruct_pix2pix", "✨ InstructPix2Pix (Tim Brooks)"),
                 ("openjourney_2", "Open Journey v2 beta [Deprecated] (PromptHero)"),
                 ("openjourney", "Open Journey [Deprecated] (PromptHero)"),
                 ("analog_diffusion", "Analog Diffusion [Deprecated] (wavymulder)"),
                 ("protogen_5_3", "Protogen v5.3 [Deprecated] (darkstorm2150)"),
                 ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
                 (
                     "rodent_diffusion_1_5",
                     "Rodent Diffusion 1.5 [Deprecated] (NerdyRodent)",
                 ),
-                ("sd_2", "Stable Diffusion v2.1 (stability.ai)"),
                 ("runway_ml", "Stable Diffusion v1.5 (RunwayML)"),
-                ("dall_e", "Dall-E (OpenAI)"),
-                ("jack_qiao", "Stable Diffusion v1.4 [Deprecated] (Jack Qiao)"),
                 ("wav2lip", "LipSync (wav2lip)"),
                 ("sadtalker", "LipSync (sadtalker)"),
                 ("twilio_voice", "Twilio Voice"),

Copy link
Contributor

@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: 0

♻️ Duplicate comments (3)
payments/webhooks.py (1)

170-185: Fallback Stripe plan lookup can still crash webhook on unknown products.

In handle_subscription_updated, the fallback path:

product = stripe.Product.retrieve(stripe_sub.plan.product, expand=["default_price"])
plan = PricingPlan.get_by_stripe_product(product)
assert plan is not None, f"Plan for product {product.id} not found"

will raise and 5xx the webhook if Stripe contains a legacy/misconfigured product that doesn’t map to any PricingPlan, causing Stripe to retry indefinitely. Prefer logging and returning early instead of asserting:

-    plan = PricingPlan.get_by_stripe_product(product)
-    assert plan is not None, f"Plan for product {product.id} not found"
+    plan = PricingPlan.get_by_stripe_product(product)
+    if plan is None:
+        logger.warning(f"Plan not found for Stripe product {product.id}")
+        return  # Acknowledge webhook; avoid retry loop
daras_ai_v2/billing.py (2)

333-472: Tier-aware upgrade/downgrade flows are consistent; main concern is broad Stripe error fallback.

Positives:

  • render_change_subscription_button and _render_upgrade_subscription_button correctly compare PricingPlan + PricingTier via get_active_monthly_charge and pass selected_tier through to change_subscription.
  • fmt_price and the downgrade/upgrade dialogs now show the current plan’s monthly charge vs the new plan/tier, which matches the earlier feedback.
  • change_subscription uses new_plan.get_stripe_line_item(new_tier) and writes metadata={settings.STRIPE_USER_SUBSCRIPTION_METADATA_FIELD: new_plan.key}, aligning webhook plan resolution with StripeWebhookHandler.handle_subscription_updated.

However, the upgrade error handling still cancels and recreates the subscription on any stripe.InvalidRequestError:

except (stripe.CardError, stripe.InvalidRequestError) as e:
    if isinstance(e, stripe.InvalidRequestError):
        sentry_sdk.capture_exception(e)
        logger.warning(e)
    # only handle error if it's related to mandates
    workspace.subscription.cancel()
    stripe_subscription_create(workspace, plan, tier=selected_tier)

This can aggressively cancel for configuration/temporary API errors that aren’t mandate-related. Consider the earlier refinement: handle stripe.CardError as the “payment failed, recreate” path, and narrow stripe.InvalidRequestError handling to known mandate/payment-method cases (e.g., by checking error codes/message), re-raising others instead of canceling the subscription.

Also applies to: 474-539, 574-599, 611-627


281-287: Fix misleading docstring for _render_plan_details (does not return a tier key).

The docstring still says:

"""Render plan details and return selected tier key if plan has tiers"""

but _render_plan_details only writes long_description and footer and returns None. Update this to reflect actual behavior, e.g.:

-def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+def _render_plan_details(plan: PricingPlan):
+    """Render plan description and footer"""
🧹 Nitpick comments (3)
payments/webhooks.py (1)

192-202: Be explicit about None vs 0 when defaulting amount/charged_amount.

amount and charged_amount are derived from Stripe in handle_subscription_updated and defaulted in set_workspace_subscription as:

new_sub.amount = amount or plan.credits
new_sub.charged_amount = charged_amount or (plan.monthly_charge * 100)

Using or makes it impossible to intentionally persist 0 for these fields in the future (it will silently fall back to plan defaults). If you ever introduce free/zero-amount tiers that still need explicit (amount=0, charged_amount=0), consider switching to explicit None checks:

- new_sub.amount = amount or plan.credits
- new_sub.charged_amount = charged_amount or (plan.monthly_charge * 100)
+ new_sub.amount = plan.credits if amount is None else amount
+ new_sub.charged_amount = (
+     plan.monthly_charge * 100 if charged_amount is None else charged_amount
+ )

Also applies to: 282-288

daras_ai_v2/billing.py (2)

151-192: STANDARD plan filtering logic: works now, but remove is brittle if STANDARD is ever deprecated.

render_all_plans builds all_plans = [plan for plan in PricingPlan if not plan.deprecated] and then:

if not workspace.is_personal and current_plan != PricingPlan.STANDARD:
    all_plans.remove(PricingPlan.STANDARD)

This correctly hides STANDARD for non-personal workspaces unless they’re already on STANDARD (matching the “personal-only” intent), but remove will raise ValueError if STANDARD is ever marked deprecated=True or removed from the enum. To future-proof this, consider guarding:

- if not workspace.is_personal and current_plan != PricingPlan.STANDARD:
-    all_plans.remove(PricingPlan.STANDARD)
+ if (
+     not workspace.is_personal
+     and current_plan != PricingPlan.STANDARD
+     and PricingPlan.STANDARD in all_plans
+ ):
+     all_plans.remove(PricingPlan.STANDARD)

194-330: Tier selection UI wiring looks good; consider defending against missing charged_amount on legacy subs.

The new _render_plan_full_width / _render_plan_compact + _render_plan_pricing flow cleanly propagates a PricingTier:

  • Stripe + plan.tiers → selectbox keyed by tier.monthly_charge
  • Default value from round(workspace.subscription.charged_amount / 100)
  • Returned selected_tier used for all downstream pricing text and actions.

If subscription.charged_amount is guaranteed non-null (e.g., via migrations/defaults), this is fine. If older rows may still have charged_amount as NULL, round(workspace.subscription.charged_amount / 100) will raise. A small defensive tweak would make this robust to partially-migrated data:

- value=(
-     workspace.subscription
-     and round(workspace.subscription.charged_amount / 100)
- ),
+ value=(
+     workspace.subscription
+     and workspace.subscription.charged_amount
+     and round(workspace.subscription.charged_amount / 100)
+ ),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 510c49c and c41ba51.

📒 Files selected for processing (2)
  • daras_ai_v2/billing.py (19 hunks)
  • payments/webhooks.py (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • payments/webhooks.py
  • daras_ai_v2/billing.py
🧬 Code graph analysis (2)
payments/webhooks.py (2)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • get_by_stripe_product (351-385)
payments/models.py (1)
  • full_clean (103-111)
daras_ai_v2/billing.py (3)
payments/plans.py (8)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • get_active_monthly_charge (69-73)
  • get_active_credits (63-67)
  • get_pricing_title (49-54)
  • label (28-29)
  • from_sub (338-341)
  • get_pricing_caption (56-61)
payments/models.py (2)
  • get_tier (133-138)
  • Subscription (37-403)
workspaces/models.py (1)
  • allow_credit_topups (223-247)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (4)
payments/webhooks.py (1)

95-140: Stripe invoice handler: metadata-based plan tagging looks consistent.

Using invoice.subscription_details.metadata[settings.STRIPE_USER_SUBSCRIPTION_METADATA_FIELD] to look up the PricingPlan and tagging add_balance_for_payment via kwargs["plan"] aligns with how change_subscription and stripe_subscription_create write subscription metadata, and Sentry capture on KeyError gives observability without breaking the webhook. The amount/charged_amount extraction from invoice.lines.data[0] also matches the new Subscription fields’ semantics.

daras_ai_v2/billing.py (3)

82-142: Tier-aware current plan rendering is consistent with new Subscription fields.

render_current_plan now derives tier = workspace.subscription.get_tier() and uses plan.get_active_monthly_charge(tier) / plan.get_active_credits(tier) along with plan.get_pricing_title(tier), which matches the PricingTier semantics in payments.plans and Subscription.get_tier. The localized “Next invoice on …” badge via render_local_date_attrs is also a clean UX improvement.


541-571: Stripe subscription creation/change now propagates tier correctly and matches webhook expectations.

  • render_stripe_subscription_button and _render_create_subscription_button pass a PricingTier | None down as tier.
  • stripe_subscription_create builds line_items = [plan.get_stripe_line_item(tier)] and sets metadata = {settings.STRIPE_USER_SUBSCRIPTION_METADATA_FIELD: plan.key}, also mirroring that metadata into subscription_data.
  • change_subscription similarly uses new_plan.get_stripe_line_item(new_tier) and updates subscription metadata with the plan key.

This lines up with:

  • PricingPlan.get_stripe_line_item(tier) semantics for tiered pricing.
  • StripeWebhookHandler.handle_subscription_updated, which first reads the plan key from subscription metadata and then uses amount/charged_amount to rebuild the tier.

The Standard-plan-specific workspace popup gating also looks coherent: Standard skips the “create workspace” popup for personal workspaces, while other paid plans still encourage workspace creation.

Also applies to: 805-848, 850-905, 581-599, 615-627


663-685: Addon & auto-recharge gating via allow_credit_topups() matches PR intent.

Both render_addon_section and render_auto_recharge_section now early-return when not workspace.allow_credit_topups(), so:

  • New non-enterprise workspaces without prior ADDON/AUTO_RECHARGE history won’t see purchase UI or auto-recharge controls.
  • Enterprise (and existing topup-using) workspaces still see these sections.

This is a clean, model-level gate that aligns with the “disable auto recharge / addons for new non-enterprise users” objective.

Also applies to: 1068-1075

@nikochiko
Copy link
Member Author

@devxpy i added a couple of fixes. one to render the plan change correctly when downloading. other to not 500-error in a webhook call (only log it) so stripe doesnt keep retrying.

this looks good for merge to me. will have to sync this with the pricing info page (webflow) for deployment

@devxpy
Copy link
Member

devxpy commented Nov 28, 2025

LGTM

Copy link
Contributor

@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 (1)
payments/webhooks.py (1)

35-37: Replace plan assertions with graceful handling to avoid webhook 5xx and retries.

assert on plan lookup in both PayPal (PricingPlan.get_by_paypal_plan_id) and Stripe (fallback via PricingPlan.get_by_stripe_product) will still raise and turn these webhooks into 5xx responses whenever a plan cannot be resolved (legacy products, deleted plans, misconfig, etc.). That causes Stripe/PayPal to keep retrying and can spam logs without a way to fix from the dashboard.

Instead of asserting, log and return early so the webhook is acknowledged:

-        plan = PricingPlan.get_by_paypal_plan_id(pp_sub.plan_id)
-        assert plan, f"Plan {pp_sub.plan_id} not found"
+        plan = PricingPlan.get_by_paypal_plan_id(pp_sub.plan_id)
+        if plan is None:
+            logger.warning(
+                "PayPal subscription plan not found; skipping webhook",
+                plan_id=pp_sub.plan_id,
+                subscription_id=pp_sub.id,
+            )
+            return
-        product = stripe.Product.retrieve(
-            stripe_sub.plan.product, expand=["default_price"]
-        )
-        plan = PricingPlan.get_by_stripe_product(product)
-        assert plan is not None, f"Plan for product {product.id} not found"
+        product = stripe.Product.retrieve(
+            stripe_sub.plan.product, expand=["default_price"]
+        )
+        plan = PricingPlan.get_by_stripe_product(product)
+        if plan is None:
+            logger.warning(
+                "Stripe subscription plan not found from product; skipping webhook",
+                product_id=product.id,
+                subscription_id=stripe_sub.id,
+            )
+            # optionally: sentry_sdk.capture_message(...)
+            return

This keeps your DB consistent with known plans while preventing endless retries on subscriptions you can’t map anymore.

#!/bin/bash
# Quick check: see all call sites of these handlers to confirm
# that a simple early return will still result in a 2xx webhook response.
rg -n "handle_subscription_updated|handle_sale_completed" --type=py -C3

Also applies to: 64-66, 175-185

🧹 Nitpick comments (1)
payments/webhooks.py (1)

100-107: Stripe invoice plan resolution change looks correct; consider adding contextual logging.

Catching KeyError from PricingPlan.get_by_key(...) and reporting it to Sentry avoids 500s when metadata is missing or invalid, which aligns with the stated goal. You might also want a logger.warning including invoice.id and the offending key so local logs are diagnosable even without Sentry, but behavior-wise this is fine.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c41ba51 and 1906f32.

📒 Files selected for processing (2)
  • app_users/tasks.py (0 hunks)
  • payments/webhooks.py (5 hunks)
💤 Files with no reviewable changes (1)
  • app_users/tasks.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • payments/webhooks.py
🧬 Code graph analysis (1)
payments/webhooks.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • get_by_stripe_product (351-385)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (1)
payments/webhooks.py (1)

270-271: Based on my thorough verification of the codebase, I can now provide my rewritten review comment:


The validation concern is incorrect; however, semantic clarity around amount/charged_amount defaults could be improved.

set_workspace_subscription defaults amount and charged_amount to 0, which actually passes validation without issues:

  • amount is IntegerField(default=0) — allows any integer, including 0
  • charged_amount is PositiveIntegerField(default=0) — allows 0 and positive integers
  • The full_clean() validation only runs if auto_recharge_enabled=True, and if so, ensure_default_auto_recharge_params() returns early when amounts ≤ 0 (no error)

However, the semantics warrant clarification:

  • PayPal flows (activation, updates, cancellation) never pass real amounts — always use 0
  • Stripe flows pass explicit amount and charged_amount except during cancellation
  • All cancellation flows intentionally use 0 as a sentinel for "no active paid subscription"

The current code works correctly because PayPal uses plan-based pricing (no custom amounts) and Stripe provides explicit values. But the intent — that 0 represents "not applicable" rather than a genuine zero charge — could be documented more clearly in the function signature or via comments.

No validation will fail; this is a clarity and maintainability concern, not a bug.

Comment on lines +192 to +194
amount = int(stripe_sub.quantity)
charged_amount = round(Decimal(stripe_sub.plan.amount_decimal) * amount)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Stripe Python API plan.amount_decimal smallest currency unit cents documentation

💡 Result:

Short answer

  • amount (or unit_amount) is an integer in the currency's smallest unit (e.g., cents for USD). [1][2]
  • amount_decimal (or unit_amount_decimal / plan.amount_decimal) is the same value represented as a decimal string (can express fractional cents up to ~12 decimal places) and will be returned as a string in client libraries (including Python). Use amount for whole-smallest-unit integers and amount_decimal when you need the precise decimal representation. [1][3]

Sources

  • Stripe API — example plan/price showing amount and amount_decimal. [1]
  • Stripe API — amounts are specified in the smallest currency unit (e.g., cents). [2]
  • Plan object docs / client-library descriptions noting amount_decimal is a decimal string (≤12 decimal places). [3]

🏁 Script executed:

#!/bin/bash
# Search for charged_amount usage and definition in the codebase
rg -n "charged_amount" --type py -B 3 -A 3

Repository: GooeyAI/gooey-server

Length of output: 14138


🏁 Script executed:

#!/bin/bash
# Search for handle_invoice_paid function mentioned in the review
rg -n "handle_invoice_paid" --type py -B 2 -A 10

Repository: GooeyAI/gooey-server

Length of output: 3779


🏁 Script executed:

#!/bin/bash
# Examine the payments/webhooks.py file around lines 192-194
wc -l payments/webhooks.py

Repository: GooeyAI/gooey-server

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Read the specific section of payments/webhooks.py around lines 192-194
sed -n '185,205p' payments/webhooks.py

Repository: GooeyAI/gooey-server

Length of output: 713


🏁 Script executed:

#!/bin/bash
# Check the type annotation for charged_amount in the set_workspace_subscription function
sed -n '265,290p' payments/webhooks.py

Repository: GooeyAI/gooey-server

Length of output: 771


Normalize charged_amount to an int in smallest currency units.

round(Decimal(stripe_sub.plan.amount_decimal) * amount) returns a Decimal, but the set_workspace_subscription function expects charged_amount: int (line 271), and the Subscription model defines it as a PositiveIntegerField. Per Stripe's API, plan.amount_decimal is a decimal string representing the smallest currency unit (cents for USD). To keep types and units consistent with other payment paths like handle_invoice_paid (which passes Stripe's integer amounts directly), cast explicitly:

-        amount = int(stripe_sub.quantity)
-        charged_amount = round(Decimal(stripe_sub.plan.amount_decimal) * amount)
+        amount = int(stripe_sub.quantity)
+        # amount_decimal is in the smallest currency unit; keep charged_amount in cents.
+        unit_amount = Decimal(stripe_sub.plan.amount_decimal)
+        charged_amount = int(unit_amount * amount)
🤖 Prompt for AI Agents
In payments/webhooks.py around lines 192-194, charged_amount is currently a
Decimal produced by round(Decimal(stripe_sub.plan.amount_decimal) * amount) but
downstream expects an int in the smallest currency unit; replace the Decimal
result with an explicit integer (e.g. apply proper rounding then cast to int) so
charged_amount is an int representing smallest currency units before passing to
set_workspace_subscription/Subscription.

@nikochiko nikochiko force-pushed the subscription-plan-standard branch from 1906f32 to 6ab6504 Compare December 4, 2025 09:36
Copy link
Contributor

@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

Caution

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

⚠️ Outside diff range comments (3)
recipes/VideoBots.py (1)

22-63: Remove duplicate UserError import

UserError is already imported at line 22; the second import at line 63 is redundant and flagged by Ruff F811. Please drop the line-63 import and keep just one.

-from daras_ai_v2.exceptions import UserError
payments/models.py (1)

40-80: Align Subscription defaults with migration and fix apostrophe in help_text

Two issues here:

  1. auto_recharge_enabled default mismatch (model vs migration)

    • Subscription.auto_recharge_enabled is defined with default=True in this model, while migration 0007_subscription_amount_subscription_charged_amount_and_more sets the DB field default to False.
    • This divergence means new Subscription() instances created in Python will default to True even though the DB default is False. That undermines the “auto‑recharge off by default” intent for new non‑enterprise users.
    • Recommend updating the model field to default=False to match the migration and intended behavior.
  2. Unicode apostrophe in charged_amount help_text

    • The help_text uses currency’s with a RIGHT SINGLE QUOTATION MARK, which Ruff flags (RUF001) and can be a nuisance in diffs.
    • Replace it with a standard ASCII apostrophe.
-    auto_recharge_enabled = models.BooleanField(default=True)
+    auto_recharge_enabled = models.BooleanField(default=False)
-    charged_amount = models.PositiveIntegerField(
-        help_text="The charged dollar amount in the currency’s smallest unit.<br>"
+    charged_amount = models.PositiveIntegerField(
+        help_text="The charged dollar amount in the currency's smallest unit.<br>"
         "E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br>"
         "<a href='https://stripe.com/docs/currencies'>Learn More</a>",
         default=0,
     )
daras_ai_v2/billing.py (1)

663-680: Addon section is not gated for non-enterprise users.

The render_addon_section call at line 57 lacks protection for non-enterprise workspaces. According to PR objectives, addon sections should only be shown when workspace.allow_credit_topups() returns true. However, this method does not exist in the codebase, and no guard is currently in place. Either implement the allow_credit_topups() method on the Workspace model and gate this call, or verify if a different gating mechanism was intended.

♻️ Duplicate comments (7)
recipes/VideoBots.py (1)

1948-1955: Fix “setup” → “set up” in WhatsApp enterprise copy

The message still uses the verb as “setup”; it should be “set up”, and this was already flagged earlier.

-                        gui.write(
-                            "As a premium customer, please contact us to setup a managed number"
-                        )
+                        gui.write(
+                            "As a premium customer, please contact us to set up a managed number"
+                        )
usage_costs/migrations/0038_alter_modelpricing_model_name.py (1)

245-287: Deduplicate repeated model_name choices and normalize labels

This choices list still has duplicate keys with conflicting labels (e.g., dream_shaper, sd_2, sd_1_5, dall_e, gpt_image_1, nano_banana, jack_qiao, rodent_diffusion_1_5 appear multiple times, with dall_e using both “DALL·E 2 (OpenAI)” and “Dall-E (OpenAI)”). Django will only honor the first entry per key, so the later ones are effectively dead and add confusion.

Consider pruning duplicates and keeping a single canonical label per key (matching whatever you chose in 0037), then regenerating this migration if needed.

payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1)

21-27: Normalize apostrophe in charged_amount help_text (match model)

The help_text here still uses a Unicode right single quote in currency’s, which Ruff flags and which can cause minor encoding headaches. It also diverges from the ASCII apostrophe we’d like in payments/models.py.

-                help_text="The charged dollar amount in the currency’s smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
+                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
payments/webhooks.py (2)

175-185: Avoid assert for missing Stripe plan; log and return instead

In the fallback path, PricingPlan.get_by_stripe_product(product) can still return None for legacy or unknown Stripe products; assert plan is not None will then raise, crash the webhook, and cause Stripe to keep retrying. This was already noted earlier.

Replace the assertion with a warning + early return so the webhook still 2xx’s:

-        except KeyError:
-            product = stripe.Product.retrieve(
-                stripe_sub.plan.product, expand=["default_price"]
-            )
-            plan = PricingPlan.get_by_stripe_product(product)
-            assert plan is not None, f"Plan for product {product.id} not found"
+        except KeyError:
+            product = stripe.Product.retrieve(
+                stripe_sub.plan.product, expand=["default_price"]
+            )
+            plan = PricingPlan.get_by_stripe_product(product)
+            if plan is None:
+                logger.warning(
+                    f"Stripe subscription {stripe_sub.id} has unmapped product {product.id}; ignoring update"
+                )
+                return

192-202: Normalize charged_amount to an int in smallest currency units

round(Decimal(stripe_sub.plan.amount_decimal) * amount) yields a Decimal, but charged_amount is treated as an int everywhere else (PositiveIntegerField on Subscription, add_balance_for_payment signature, and budget calculations). To keep types and units consistent, cast explicitly after rounding.

-        amount = int(stripe_sub.quantity)
-        charged_amount = round(Decimal(stripe_sub.plan.amount_decimal) * amount)
+        amount = int(stripe_sub.quantity)
+        unit_amount = Decimal(stripe_sub.plan.amount_decimal)  # smallest currency unit
+        charged_amount = int(unit_amount * amount)
In Stripe's Python API, is `plan.amount_decimal` documented as a decimal string in the currency's smallest unit, and is it safe to treat `unit_amount_decimal * quantity` as an integer number of cents when casting to `int` after appropriate rounding?
daras_ai_v2/billing.py (2)

281-287: Fix misleading docstring.

The docstring claims the function returns a selected tier key, but the function doesn't return anything. Tier selection was moved to _render_plan_pricing.

Apply this diff to correct the docstring:

 def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+    """Render plan description and footer"""

440-445: Missing None check for selected_tier.

Line 444 accesses selected_tier.monthly_charge without verifying selected_tier is not None. While the condition checks plan.tiers and current_tier, it doesn't check selected_tier. If the payment provider is not Stripe or tier selection fails, selected_tier will be None and cause an AttributeError.

Apply this diff to add the None check:

     if plan > current_plan or (
         plan == current_plan
         and plan.tiers
         and current_tier
+        and selected_tier
         and (selected_tier.monthly_charge > current_tier.monthly_charge)
     ):
🧹 Nitpick comments (1)
payments/plans.py (1)

394-398: Consider defining a custom exception or moving message to exception class.

The static analysis tool flags TRY003: using long messages directly in raise statements is discouraged. While not critical, defining a custom exception class or using a shorter message would align with best practices.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1906f32 and 6ab6504.

📒 Files selected for processing (11)
  • app_users/migrations/0029_alter_appusertransaction_plan.py (1 hunks)
  • app_users/tasks.py (0 hunks)
  • daras_ai_v2/billing.py (17 hunks)
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1 hunks)
  • payments/models.py (4 hunks)
  • payments/plans.py (10 hunks)
  • payments/utils.py (1 hunks)
  • payments/webhooks.py (5 hunks)
  • recipes/VideoBots.py (2 hunks)
  • routers/paypal.py (1 hunks)
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py (1 hunks)
💤 Files with no reviewable changes (1)
  • app_users/tasks.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • payments/models.py
  • payments/webhooks.py
  • payments/plans.py
  • app_users/migrations/0029_alter_appusertransaction_plan.py
  • usage_costs/migrations/0038_alter_modelpricing_model_name.py
  • payments/utils.py
  • routers/paypal.py
  • daras_ai_v2/billing.py
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
  • recipes/VideoBots.py
🧬 Code graph analysis (7)
payments/models.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • from_sub (338-341)
payments/plans.py (2)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/paypal.py (2)
  • Product (237-242)
  • create (82-92)
app_users/migrations/0029_alter_appusertransaction_plan.py (1)
app_users/migrations/0026_alter_appusertransaction_plan.py (1)
  • Migration (6-18)
usage_costs/migrations/0038_alter_modelpricing_model_name.py (2)
usage_costs/migrations/0037_alter_modelpricing_model_name.py (1)
  • Migration (6-298)
usage_costs/migrations/0033_alter_modelpricing_model_name_and_more.py (1)
  • Migration (6-297)
routers/paypal.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • supports_paypal (403-404)
daras_ai_v2/billing.py (2)
payments/plans.py (10)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • get_active_monthly_charge (69-73)
  • get_active_credits (63-67)
  • get_pricing_title (49-54)
  • label (28-29)
  • from_sub (338-341)
  • get_pricing_caption (56-61)
  • get_stripe_line_item (406-427)
  • supports_stripe (400-401)
payments/models.py (2)
  • get_tier (133-138)
  • Subscription (37-403)
recipes/VideoBots.py (1)
daras_ai_v2/exceptions.py (1)
  • UserError (58-65)
🪛 Ruff (0.14.7)
payments/models.py

46-46: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

payments/plans.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


398-398: Avoid specifying long messages outside the exception class

(TRY003)


408-408: Avoid specifying long messages outside the exception class

(TRY003)

app_users/migrations/0029_alter_appusertransaction_plan.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-33: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

usage_costs/migrations/0038_alter_modelpricing_model_name.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-298: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


26-26: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

recipes/VideoBots.py

63-63: Redefinition of unused UserError from line 22

Remove definition: UserError

(F811)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test (3.10.12, 1.8.3)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (27)
payments/models.py (1)

133-138: get_tier mapping from cents to dollars is consistent with PricingTier

Using round(self.charged_amount / 100) to map cents → integer dollars for monthly_charge is consistent with how budgets are computed elsewhere (charged_amount in cents, budgets in dollars). Given charged_amount should be a multiple of 100 for subscriptions, this is safe.

app_users/migrations/0029_alter_appusertransaction_plan.py (1)

12-31: Plan choices update for AppUserTransaction looks consistent

The updated plan choices (including Standard as 8 and Free for 3) align with the subscription plan set used elsewhere; migration wiring looks correct.

routers/paypal.py (1)

122-130: Improved PayPal subscription validation looks good

Separating invalid plan keys (KeyError) from “plan does not support PayPal” is clearer for clients and keeps the control flow simple.

payments/webhooks.py (1)

264-288: New amount/charged_amount plumbed into subscriptions correctly

Passing amount and charged_amount into set_workspace_subscription and persisting them on the Subscription before full_clean() is consistent with the new fields and with Subscription.ensure_default_auto_recharge_params expecting credits and cents. The cloning pattern around old_sub remains unchanged. Call sites pass these parameters where relevant—only the Stripe subscription creation handler passes calculated values, while cancellations and downgrades use the default zeros.

payments/plans.py (11)

23-30: LGTM!

The PricingTier NamedTuple is well-designed with clear fields and a helpful label property for display.


46-79: LGTM!

The tier-aware methods correctly handle both tiered and non-tiered plans with proper fallback logic. The get_default_tier() appropriately raises an error for plans without tiers.


165-199: LGTM!

The STARTER plan updates correctly position it as a free tier with clear feature descriptions and appropriate pricing display.


201-236: LGTM!

The STANDARD plan is well-structured with clear tier definitions and appropriate feature descriptions.


238-274: LGTM!

The BUSINESS plan updates correctly reference the new plan hierarchy and feature set.


276-316: LGTM!

The ENTERPRISE plan updates add full-width display support with a well-structured two-column layout.


318-325: LGTM!

The plan comparison logic correctly uses default pricing (no tier specified) for comparing plans at their base level.


338-341: LGTM!

The updated from_sub method correctly handles None subscriptions by returning STARTER as the default plan.


351-385: LGTM!

The tier-aware Stripe product matching logic correctly handles both tiered and non-tiered plans with appropriate name and price validation.


406-427: LGTM!

The tier-aware line item generation correctly derives pricing and product IDs based on the specified tier.


439-466: LGTM!

The tier-aware Stripe product ID generation correctly builds product names and pricing for both tiered and non-tiered plans.

daras_ai_v2/billing.py (12)

82-142: LGTM!

The current plan rendering correctly retrieves and displays tier-aware pricing information from the subscription.


151-192: LGTM!

The plan rendering logic correctly filters and organizes plans based on workspace type and plan attributes.


194-239: LGTM!

The full-width plan rendering correctly captures tier selection and propagates it through the action button rendering for both desktop and mobile layouts.


242-278: LGTM!

The compact plan rendering correctly captures tier selection and propagates it through the action button rendering.


302-330: LGTM!

The tier selection and pricing display logic correctly handles both tiered and non-tiered plans with appropriate defaults from the current subscription.


333-394: LGTM!

The action button logic correctly handles tier context for all subscription states (current plan tier changes, upgrades, downgrades, and new subscriptions).


474-539: LGTM!

The upgrade subscription logic correctly handles tier-aware pricing and propagates tier context through the upgrade flow. The code assumes workspace.subscription exists (validated by callers) and safely handles None tiers via the tier-aware pricing methods.


541-571: LGTM!

The subscription creation logic correctly propagates tier context to Stripe flows and appropriately documents that PayPal doesn't support tiers.


574-578: LGTM!

The refactored fmt_price function is more flexible and reusable by accepting a numeric price directly.


581-649: LGTM!

The subscription change logic correctly handles tier-aware updates, including no-op detection and proper propagation of tier context to Stripe line items.


801-843: LGTM!

The Stripe subscription button correctly displays tier-aware pricing and propagates tier context to the subscription creation flow.


846-901: LGTM!

The Stripe subscription creation correctly propagates tier context through both direct subscription and checkout flows, with appropriate metadata for webhook handling.

Comment on lines 17 to 21
cents_per_month = amount * 100
price_data = {
"currency": "usd",
"unit_amount_decimal": round(cents_per_month / credits, 4),
"unit_amount_decimal": round(cents_per_month / credits, 12),
"recurring": {"interval": "month"},
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Stripe unit_amount_decimal field decimal places maximum specification

💡 Result:

Stripe's unit_amount_decimal accepts a decimal string in cents (or local equivalent) with a maximum of 12 decimal places. [1][2]

Sources:
[1] stripe.dev — PriceCreateParams / unit_amount_decimal.
[2] async-stripe (docs.rs) — unit_amount_decimal description.


🏁 Script executed:

# Check the structure of payments/utils.py to verify reverse topological order guideline
head -50 payments/utils.py

Repository: GooeyAI/gooey-server

Length of output: 1382


🏁 Script executed:

# Get full file to understand the structure and identify main functions
wc -l payments/utils.py

Repository: GooeyAI/gooey-server

Length of output: 84


🌐 Web query:

Stripe unit_amount_decimal JSON float vs decimal string format requirement

💡 Result:

Short answer: Stripe expects unit_amount_decimal as a decimal string in the JSON (not a JSON float). Send it as a string with at most 12 decimal places (and only one of unit_amount or unit_amount_decimal may be set). Cite: Stripe API docs. [1][2]

Sources

  • Prices / Price object (unit_amount_decimal = "decimal string"). [1]
  • Payment Link / Price fields (unit_amount_decimal = nullable decimal string). [2]

🏁 Script executed:

# Check if there's any JSON serialization or Stripe API calls in the codebase
rg -A 5 "unit_amount_decimal" --type py

Repository: GooeyAI/gooey-server

Length of output: 2211


🏁 Script executed:

# Check how the price_data dict is being serialized/sent to Stripe
rg -B 5 -A 5 "make_stripe_recurring_plan" --type py

Repository: GooeyAI/gooey-server

Length of output: 1800


🏁 Script executed:

# Look for JSON encoding or Stripe API calls that might serialize this
rg "json\.dumps|stripe\." --type py -A 3 | head -50

Repository: GooeyAI/gooey-server

Length of output: 2572


unit_amount_decimal must be sent as a decimal string, not a float

Stripe's API requires unit_amount_decimal as a string with up to 12 decimal places. The current code sends a float from round(cents_per_month / credits, 12), which doesn't match the specification. Convert to string explicitly:

"unit_amount_decimal": str(round(cents_per_month / credits, 12)),

Alternatively, use Decimal for cleaner string representation and better precision handling:

from decimal import Decimal
"unit_amount_decimal": str(Decimal(cents_per_month) / Decimal(credits)),
🤖 Prompt for AI Agents
In payments/utils.py around lines 17 to 21, the code currently sets
unit_amount_decimal using a float from round(cents_per_month / credits, 12);
change this to send a string as required by Stripe — either convert the rounded
value to a string (e.g., str(round(..., 12))) or, preferably, import Decimal
from decimal and compute the division with Decimals then convert to string
(e.g., str(Decimal(cents_per_month) / Decimal(credits))) so unit_amount_decimal
is a decimal string with correct precision.

@nikochiko nikochiko force-pushed the subscription-plan-standard branch from 027beaf to c273a77 Compare December 23, 2025 06:41
Copy link
Contributor

@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: 2

♻️ Duplicate comments (6)
recipes/VideoBots.py (1)

1967-1967: Fix grammatical error: "setup" → "set up".

When used as a verb phrase, "set up" should be two words. Line 2008 correctly uses "set up" in a similar context.

🔎 Proposed fix
-                            "As a premium customer, please contact us to setup a managed number"
+                            "As a premium customer, please contact us to set up a managed number"
payments/utils.py (1)

17-21: ** Convert unit_amount_decimal to string as required by Stripe API.**

Stripe's unit_amount_decimal field must be a decimal string, not a float. The current code passes the result of round(), which is a float. This issue was previously flagged and remains unresolved.

Apply the fix from the previous review:

"unit_amount_decimal": str(round(cents_per_month / credits, 12)),

Or use Decimal for better precision handling:

from decimal import Decimal
"unit_amount_decimal": str(Decimal(cents_per_month) / Decimal(credits)),

Based on the previous review discussion and Stripe API documentation.

daras_ai_v2/billing.py (1)

281-286: ** Update misleading docstring.**

The docstring states the function returns a "selected tier key if plan has tiers," but _render_plan_details doesn't return anything. Tier selection was moved to _render_plan_pricing.

Update to accurately reflect the function's behavior:

-def _render_plan_details(plan: PricingPlan):
-    """Render plan details and return selected tier key if plan has tiers"""
+def _render_plan_details(plan: PricingPlan):
+    """Render plan description and footer"""
payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py (1)

24-27: ** Replace Unicode apostrophe in help_text with ASCII quote.**

Line 26 contains a Unicode right single quotation mark (') in the href attribute instead of a standard ASCII apostrophe ('). This can cause encoding issues.

-                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",
+                help_text="The charged dollar amount in the currency's smallest unit.<br>E.g. for 10 USD, this would be of 1000 (that is, 1000 cents).<br><a href='https://stripe.com/docs/currencies'>Learn More</a>",

As flagged by static analysis (Ruff RUF001).

payments/models.py (1)

45-50: ** Replace Unicode apostrophe in help_text with ASCII quote.**

Line 48 contains a Unicode right single quotation mark (') in the href attribute. Replace with standard ASCII apostrophe:

-        "<a href='https://stripe.com/docs/currencies'>Learn More</a>",
+        "<a href='https://stripe.com/docs/currencies'>Learn More</a>",

As flagged by static analysis (Ruff RUF001).

payments/webhooks.py (1)

192-194: ** Cast charged_amount to int for type consistency.**

round(Decimal(...) * amount) returns a Decimal, but set_workspace_subscription expects charged_amount: int (line 271), and the Subscription.charged_amount field is a PositiveIntegerField. Cast explicitly to int:

 amount = int(stripe_sub.quantity)
-charged_amount = round(Decimal(stripe_sub.plan.amount_decimal) * amount)
+charged_amount = int(round(Decimal(stripe_sub.plan.amount_decimal) * amount))

Or more directly:

-charged_amount = round(Decimal(stripe_sub.plan.amount_decimal) * amount)
+charged_amount = int(Decimal(stripe_sub.plan.amount_decimal) * amount)

Per the previous review and type signatures.

🧹 Nitpick comments (1)
payments/webhooks.py (1)

175-184: Consider logging when falling back to product-based plan lookup.

The fallback from metadata-based to product-based plan resolution works correctly, but it would be helpful to log when this fallback occurs (before line 180) to track how often legacy subscriptions are encountered.

     except KeyError:
+        logger.info(f"Plan key not found in metadata, falling back to product lookup for subscription {stripe_sub.id}")
         product = stripe.Product.retrieve(
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 027beaf and c273a77.

📒 Files selected for processing (10)
  • app_users/migrations/0029_alter_appusertransaction_plan.py
  • app_users/tasks.py
  • daras_ai_v2/billing.py
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
  • payments/models.py
  • payments/plans.py
  • payments/utils.py
  • payments/webhooks.py
  • recipes/VideoBots.py
  • routers/paypal.py
💤 Files with no reviewable changes (1)
  • app_users/tasks.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,js,ts,tsx,java,cs,cpp,c,go,rb,php}

📄 CodeRabbit inference engine (.cursor/rules/devs-rules.mdc)

Format code in reverse topological order: place the main() function at the top and dependencies below it

Files:

  • routers/paypal.py
  • payments/webhooks.py
  • payments/plans.py
  • payments/models.py
  • daras_ai_v2/billing.py
  • recipes/VideoBots.py
  • payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py
  • payments/utils.py
  • app_users/migrations/0029_alter_appusertransaction_plan.py
🧬 Code graph analysis (6)
routers/paypal.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • supports_paypal (403-404)
payments/webhooks.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • get_by_key (394-398)
  • get_by_stripe_product (351-385)
payments/plans.py (2)
payments/utils.py (1)
  • make_stripe_recurring_plan (4-31)
daras_ai_v2/paypal.py (2)
  • Product (237-242)
  • create (82-92)
payments/models.py (1)
payments/plans.py (3)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • from_sub (338-341)
daras_ai_v2/billing.py (3)
payments/plans.py (9)
  • PricingPlan (81-506)
  • PricingTier (23-29)
  • get_active_monthly_charge (69-73)
  • get_active_credits (63-67)
  • get_pricing_title (49-54)
  • label (28-29)
  • get_pricing_caption (56-61)
  • get_stripe_line_item (406-427)
  • supports_stripe (400-401)
payments/models.py (2)
  • get_tier (133-138)
  • Subscription (37-403)
app_users/models.py (1)
  • PaymentProvider (85-87)
recipes/VideoBots.py (1)
daras_ai_v2/exceptions.py (1)
  • UserError (58-65)
🪛 Ruff (0.14.10)
payments/plans.py

78-78: Avoid specifying long messages outside the exception class

(TRY003)


398-398: Avoid specifying long messages outside the exception class

(TRY003)


408-408: Avoid specifying long messages outside the exception class

(TRY003)

payments/models.py

46-46: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

recipes/VideoBots.py

63-63: Redefinition of unused UserError from line 22

Remove definition: UserError

(F811)

payments/migrations/0007_subscription_amount_subscription_charged_amount_and_more.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-50: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


26-26: String contains ambiguous (RIGHT SINGLE QUOTATION MARK). Did you mean ``` (GRAVE ACCENT)?

(RUF001)

app_users/migrations/0029_alter_appusertransaction_plan.py

8-10: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


12-33: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test (3.10.12, 1.8.3)
🔇 Additional comments (11)
routers/paypal.py (1)

123-129: LGTM! Improved error handling for plan validation.

The separation of key validation and PayPal support checks provides clearer, more specific error messages to users. The try/except pattern properly handles the KeyError from get_by_key.

payments/models.py (2)

103-111: LGTM! Tier-aware default computation in full_clean.

The logic correctly computes amount and charged_amount from the plan's tier-aware accessors when auto-recharge is enabled, providing sensible defaults when these fields are not yet populated.


133-138: LGTM! Clean tier reconstruction from stored fields.

The get_tier() method properly reconstructs a PricingTier from the stored amount (credits) and charged_amount (converting cents to dollars), returning None when either field is missing.

app_users/migrations/0029_alter_appusertransaction_plan.py (1)

1-33: LGTM! Migration correctly updates plan choices.

The migration properly updates the AppUserTransaction.plan field choices to align with the new tiered pricing system, including the new Standard plan (value 8).

Note: The Ruff ClassVar warnings are false positives for Django migration files and can be safely ignored.

payments/webhooks.py (1)

100-107: LGTM! Proper error handling for plan key lookup.

The try/except block correctly handles KeyError from get_by_key when metadata is missing or invalid, capturing the exception with Sentry for monitoring while allowing the webhook to continue processing.

payments/plans.py (6)

23-29: LGTM! Clean PricingTier definition.

The PricingTier NamedTuple provides a simple, immutable structure for tier data with a useful label property for display.


49-78: LGTM! Well-designed tier-aware accessor methods.

The tier-aware methods provide a clean API for accessing pricing data, falling back to default values when no tier is specified. The get_default_tier() method appropriately raises an error for non-tiered plans.


201-236: LGTM! Comprehensive STANDARD plan definition with tiered pricing.

The STANDARD plan is well-structured with 6 pricing tiers ranging from $25 to $400/month, clear feature descriptions, and appropriate positioning between Free and Business plans.


406-427: LGTM! Tier-aware line item creation.

The method correctly uses tier-specific or default credits and monthly_charge values, maintaining backward compatibility for non-tiered plans.


439-466: LGTM! Tier-aware Stripe product ID resolution.

The method properly constructs tier-specific product names (e.g., "Standard - 2,000 Credits for $25/month") and creates new products when needed, ensuring each tier has its own Stripe product.


351-385: Edge case in tier matching when product.default_price is missing deserves clarification.

The tier matching logic uses both product name and price for matching, which is comprehensive. However, when product.default_price is None, the condition product_price is None or product_price == expected_price allows a name-only match.

While products created by get_stripe_product_id() always include default_price_data, and callers like payments/webhooks.py explicitly expand it during retrieval, this path could be triggered if:

  • External Stripe products are added without a default price
  • The API expansion fails silently

If name-only matching is intentional as a fallback, add a comment clarifying this. Otherwise, consider asserting that product.default_price exists, or return None if pricing cannot be verified.

The early return on first tier match is correct—each tier has a unique label (based on distinct credit amounts), making the product name unique per tier.

Comment on lines +440 to +445
if plan > current_plan or (
plan == current_plan
and plan.tiers
and current_tier
and selected_tier.monthly_charge > current_tier.monthly_charge
):
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the specific lines from billing.py to verify the review comment
if [ -f "daras_ai_v2/billing.py" ]; then
  echo "=== Lines 435-450 to see function signature and context ==="
  sed -n '435,450p' daras_ai_v2/billing.py | cat -n
else
  echo "File not found"
fi

Repository: GooeyAI/gooey-server

Length of output: 819


Add None check for selected_tier before accessing its attribute at line 444.

The condition accesses selected_tier.monthly_charge without verifying selected_tier is not None, despite the function signature (line 435) allowing it to be None. When selected_tier is None and the other conditions evaluate to true, this raises AttributeError.

Add the check:

and selected_tier
and selected_tier.monthly_charge > current_tier.monthly_charge
🤖 Prompt for AI Agents
In daras_ai_v2/billing.py around lines 440 to 445, the conditional compares
selected_tier.monthly_charge but the function signature allows selected_tier to
be None; add an explicit None check for selected_tier before accessing its
monthly_charge. Modify the condition to include "and selected_tier" (or
equivalent truthy check) immediately before comparing monthly_charge so
selected_tier is guaranteed non-None when selected_tier.monthly_charge is read.

)
from daras_ai_v2.embedding_model import EmbeddingModels
from daras_ai_v2.enum_selector_widget import enum_selector
from daras_ai_v2.exceptions import UserError
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove duplicate import.

UserError is already imported at line 22. This duplicate import is unnecessary and flagged by static analysis.

🔎 Proposed fix
-from daras_ai_v2.exceptions import UserError
📝 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
from daras_ai_v2.exceptions import UserError
🧰 Tools
🪛 Ruff (0.14.10)

63-63: Redefinition of unused UserError from line 22

Remove definition: UserError

(F811)

🤖 Prompt for AI Agents
In recipes/VideoBots.py around line 63, there's a duplicate import of UserError
(already imported at line 22); remove the redundant "from daras_ai_v2.exceptions
import UserError" at line 63 so the file has only the single import at line 22
and run linters to confirm the duplicate-import warning is resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants