diff --git a/app/javascript/dashboard/constants/billing.js b/app/javascript/dashboard/constants/billing.js index d372330f8..83be5081a 100644 --- a/app/javascript/dashboard/constants/billing.js +++ b/app/javascript/dashboard/constants/billing.js @@ -1,7 +1,4 @@ // Single source of truth for billing currencies on the frontend. -// Adding a currency = one entry in BILLING_CURRENCY_CONFIG, add the code to -// SUPPORTED_BILLING_CURRENCIES, and add its label key under -// BILLING_SETTINGS.CURRENCY.OPTIONS in the locale files. export const DEFAULT_BILLING_CURRENCY = 'usd'; diff --git a/app/javascript/dashboard/store/modules/accounts.js b/app/javascript/dashboard/store/modules/accounts.js index f6e06f91e..114781590 100644 --- a/app/javascript/dashboard/store/modules/accounts.js +++ b/app/javascript/dashboard/store/modules/accounts.js @@ -147,8 +147,7 @@ export const actions = { commit(types.default.SET_ACCOUNT_UI_FLAG, { isSwitchingCurrency: true }); try { await EnterpriseAccountAPI.switchCurrency(currency); - // Refresh the account so custom_attributes.billing_currency and the - // subscription details reflect the new currency. + // Refresh the account so billing_currency and subscription details reflect the switch. await dispatch('get', { silent: true }); } catch (error) { throwErrorMessage(error); diff --git a/enterprise/app/models/enterprise/account.rb b/enterprise/app/models/enterprise/account.rb index c604421c8..0ca194454 100644 --- a/enterprise/app/models/enterprise/account.rb +++ b/enterprise/app/models/enterprise/account.rb @@ -72,8 +72,7 @@ module Enterprise::Account stored = custom_attributes&.dig('billing_currency') return Enterprise::Billing::Currencies.normalize(stored) if Enterprise::Billing::Currencies.supported?(stored) - # Existing Stripe customers stay on USD (webhook backfills the real currency); - # only brand-new accounts infer from locale, so existing pt_BR users aren't charged BRL. + # Existing Stripe customers stay on USD (webhook backfills); only new accounts infer from locale. return Enterprise::Billing::Currencies::DEFAULT if custom_attributes&.dig('stripe_customer_id').present? Enterprise::Billing::Currencies.for_locale(locale) diff --git a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb index 631847d50..7f267b3b4 100644 --- a/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb +++ b/enterprise/app/services/enterprise/billing/handle_stripe_event_service.rb @@ -28,9 +28,7 @@ class Enterprise::Billing::HandleStripeEventService # skipping self hosted plan events return if plan.blank? || account.blank? - # A subscription tagged for a currency switch is being cancelled by - # SwitchCurrencyService, which writes the final state itself — ignore its - # interim webhook events so they don't overwrite the new currency. + # SwitchCurrencyService writes the final state itself — ignore its interim webhook events. return if currency_switch_cancellation? previous_usage = capture_previous_usage @@ -88,9 +86,7 @@ class Enterprise::Billing::HandleStripeEventService def process_subscription_deleted # skipping self hosted plan events return if account.blank? - # A currency switch cancels the old subscription itself and creates the new - # one. Don't re-subscribe the default plan here — it would create a stray - # default-plan sub and block the new currency ("cannot combine currencies"). + # A currency switch handles its own re-subscription; skip to avoid a stray default-plan sub. return if currency_switch_cancellation? previous_monthly_credits = current_plan_credits[:responses] diff --git a/enterprise/app/services/enterprise/billing/switch_currency_service.rb b/enterprise/app/services/enterprise/billing/switch_currency_service.rb index 79d2d0d63..36bdf1525 100644 --- a/enterprise/app/services/enterprise/billing/switch_currency_service.rb +++ b/enterprise/app/services/enterprise/billing/switch_currency_service.rb @@ -3,10 +3,7 @@ class Enterprise::Billing::SwitchCurrencyService class Error < StandardError; end - # Tagged on a subscription right before we cancel it for a currency switch, so - # the customer.subscription.deleted webhook knows NOT to re-subscribe the - # default plan (which would create a stray Hacker sub and block the new - # currency). Read by HandleStripeEventService#process_subscription_deleted. + # Tags a cancelled sub so the deleted-webhook skips re-subscribing the default plan. SWITCH_METADATA_KEY = 'chatwoot_currency_switch'.freeze pattr_initialize [:account!, :currency!] @@ -36,18 +33,13 @@ class Enterprise::Billing::SwitchCurrencyService raise Error, I18n.t('errors.billing.stripe_customer_not_configured') if stripe_customer_id.blank? end - # Free plan ($0): no subscription churn — just record the preference and keep - # the Stripe customer location/currency in sync for future upgrades/top-ups. + # Free plan: no subscription churn — record the preference and sync the Stripe customer. def switch_free_plan sync_stripe_customer_location persist_currency(account.custom_attributes.merge('billing_currency' => target_currency)) end - # Cancel every live subscription and create ONE subscription for the current - # paid plan in the target currency, preserving the seat count and the - # already-paid-through date. Decisions are based on the actual Stripe state - # (not stored attributes) so a messy/duplicated state converges to a single - # correct subscription. + # Replace all live subs with one paid sub in the target currency, preserving seats and paid-through. def switch_paid_plan(subscriptions, paid_subscription) validate_payment_method! @@ -81,16 +73,8 @@ class Enterprise::Billing::SwitchCurrencyService plan || Enterprise::Billing::PlanConfiguration.find_plan_by_product_id(subscription['plan']['product']) end - # Cancel all current subscriptions (tagged so the deleted-webhook leaves them - # alone) and create the new-currency subscription. On failure, restore the - # original paid plan/currency so the account is never left without one. - # - # Stripe constraints (verified in test mode): - # - prorate:false — currencies can't combine on one customer; a proration - # item in the old currency would block the new sub. - # - trial_end = current paid-through date — preserves already-paid time (no - # money lost) and makes switching back and forth before the next cycle free - # (each switch just re-creates the sub trialing until the same date). + # Cancel old subs, create the new-currency sub; revert to the original plan on failure. + # prorate:false (Stripe can't mix currencies); trial_end keeps the already-paid time. def replace_subscriptions(subscriptions, change) cancel_subscriptions(subscriptions) @@ -145,9 +129,7 @@ class Enterprise::Billing::SwitchCurrencyService ) end - # Active and trialing subscriptions. A prior currency switch leaves the new - # sub trialing until the carried-over paid-through date, so trialing must be - # included to switch again from that state. + # Includes trialing — a prior switch leaves the new sub trialing until its paid-through date. def live_subscriptions Stripe::Subscription.list(customer: stripe_customer_id, status: 'all', limit: 100).data .select { |subscription| %w[active trialing past_due].include?(subscription.status) }