Skip to content

Conversation

@nikochiko
Copy link
Member

  • 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.

@coderabbitai
Copy link

coderabbitai bot commented Oct 28, 2025

📝 Walkthrough

Walkthrough

The changes add tiered pricing across billing and payments: a new PricingTier type and tier-aware PricingPlan methods; Subscription gains a nullable plan_tier_key and auto_recharge_enabled default flips to False. Billing UI, Stripe/PayPal flows, and webhooks propagate tier_key through product/line-item IDs, metadata, and upgrade/downgrade dialogs. Auto-recharge is gated by Workspace.allow_credit_topups(), a new helper that checks Enterprise subscription or prior top-ups. Several migrations add/alter subscription and transaction plan fields. A script disables auto-recharge for restricted workspaces in batches.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to review closely:

  • payments/plans.py: PricingTier, tier maps, tier-aware getters, comparison logic, Stripe/PayPal product/line-item naming and matching.
  • daras_ai_v2/billing.py: propagation of selected_tier_key/tier_key through many render and action helpers and updated function signatures.
  • payments/models.py and migrations (0007, 0008, 0009): new plan_tier_key field, auto_recharge_enabled default change, and migration ordering/consistency.
  • payments/webhooks.py: reading/writing tier metadata, set_workspace_subscription signature and creation/update of Subscription.plan_tier_key.
  • workspaces/models.py and payments/auto_recharge.py: allow_credit_topups implementation and its use to gate auto-recharge and UI paths.
  • scripts/disable_auto_recharge_for_restricted_workspaces.py: correctness and performance of the Exists subquery, batching, and update_in_batches logic.
  • routers/paypal.py and other request handlers: altered validation/error flows and plan resolution (KeyError handling).

Suggested reviewers

  • devxpy

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The PR description follows the template structure by including the Q/A checklist and legal boilerplate sections, but critical quality concerns limit its effectiveness. All four Q/A checklist items are left unchecked, which indicates the author has not validated that testing was performed on mobile, workflows were tested, code review was conducted, or import time was verified. The description consists primarily of two bullet points and the template content itself, with minimal substantive explanation of the extensive changes across nine modified files and multiple migration files. Most significantly, the PR is explicitly marked as "wip: add standard plan" in the description, indicating incomplete work that should not be merged. Before merging, the PR requires: (1) the author must complete the Q/A checklist by checking all applicable items to confirm testing and review were performed, (2) the "wip" designation should be removed or clarified, indicating whether the work is actually complete or requires additional iterations, and (3) a more detailed description should be added explaining the tier-aware pricing system implementation, auto-recharge controls for non-enterprise users, and database schema changes across the Subscription and related models. These items are essential for reviewers and for maintainability of the codebase.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Add new subscription plan: Standard" refers to a real component of the changeset—the new STANDARD plan tier definition is clearly present in payments/plans.py. However, the title captures only one aspect of the work and misses the primary architectural addition: a comprehensive tier-aware pricing system with multi-tier plan support (evident across billing.py, plans.py, models.py, and related files). Additionally, the PR description lists two objectives, with the first being "disable auto recharge / addons for new non-enterprise users" and the Standard plan marked as "wip" (work-in-progress), suggesting the tier system and auto-recharge disabling are at least equally significant to the plan addition itself. The title therefore reflects only a partial aspect of the changeset rather than its main point.
✨ 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

@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

@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

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 from d073dba to d1024df Compare October 31, 2025 05:58
@nikochiko nikochiko force-pushed the subscription-plan-standard branch 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

@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 +396 to +402
if plan > current_plan or (
plan == current_plan
and plan.tiers
and (
plan.get_tier(selected_tier_key).monthly_charge
> plan.get_tier(current_tier_key).monthly_charge
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add defensive check for tier lookup in comparison logic.

Lines 400-401 directly access .monthly_charge after get_tier(), which returns PricingTier | None. If the subscription contains an invalid 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
         )
     ):
📝 Committable suggestion

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

Suggested change
if plan > current_plan or (
plan == current_plan
and plan.tiers
and (
plan.get_tier(selected_tier_key).monthly_charge
> plan.get_tier(current_tier_key).monthly_charge
)
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 (
selected_tier.monthly_charge > current_tier.monthly_charge
)
🤖 Prompt for AI Agents
In daras_ai_v2/billing.py around lines 396 to 402, the code calls
plan.get_tier(...).monthly_charge directly even though get_tier can return None;
fetch both tiers into local variables (selected_tier =
plan.get_tier(selected_tier_key), current_tier =
plan.get_tier(current_tier_key)), check that they are not None before accessing
.monthly_charge, and if either is None handle defensively (e.g., log a warning
about the missing tier and treat its monthly_charge as 0 or skip the tier-based
comparison) so the comparison never raises an AttributeError.

Comment on lines +170 to +179
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

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

@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 +228
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

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

@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

@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

@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.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants