feat(billing): let accounts switch billing currency with a confirmation note
This commit is contained in:
@@ -32,6 +32,10 @@ class EnterpriseAccountAPI extends ApiClient {
|
||||
getTopupOptions() {
|
||||
return axios.get(`${this.url}topup_options`);
|
||||
}
|
||||
|
||||
switchCurrency(currency) {
|
||||
return axios.post(`${this.url}switch_currency`, { currency });
|
||||
}
|
||||
}
|
||||
|
||||
export default new EnterpriseAccountAPI();
|
||||
|
||||
@@ -447,6 +447,23 @@
|
||||
"DESCRIPTION": "View your previous invoices, edit your billing details, or cancel your subscription.",
|
||||
"BUTTON_TXT": "Go to the billing portal"
|
||||
},
|
||||
"CURRENCY": {
|
||||
"TITLE": "Billing currency",
|
||||
"DESCRIPTION": "Choose the currency used for your subscription and credit purchases.",
|
||||
"OPTIONS": {
|
||||
"USD": "US Dollar (USD)",
|
||||
"BRL": "Brazilian Real (BRL)"
|
||||
},
|
||||
"SUCCESS": "Billing currency updated successfully.",
|
||||
"ERROR": "Failed to update billing currency. Please try again.",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Switch billing currency?",
|
||||
"DESCRIPTION": "You are about to switch your billing currency to {currency}.",
|
||||
"WARNING": "Your current subscription will be cancelled and a new one will be started in the selected currency. Pricing and any remaining credits may differ. This cannot be undone automatically.",
|
||||
"CONFIRM_BUTTON": "Switch currency",
|
||||
"CANCEL_BUTTON": "Cancel"
|
||||
}
|
||||
},
|
||||
"CAPTAIN": {
|
||||
"TITLE": "Captain",
|
||||
"DESCRIPTION": "Manage usage and credits for Captain AI.",
|
||||
|
||||
@@ -1,22 +1,32 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { format } from 'date-fns';
|
||||
import sessionStorage from 'shared/helpers/sessionStorage';
|
||||
import {
|
||||
SUPPORTED_BILLING_CURRENCIES,
|
||||
DEFAULT_BILLING_CURRENCY,
|
||||
getCurrencyConfig,
|
||||
} from 'dashboard/constants/billing';
|
||||
|
||||
import BillingMeter from './components/BillingMeter.vue';
|
||||
import BillingCard from './components/BillingCard.vue';
|
||||
import BillingHeader from './components/BillingHeader.vue';
|
||||
import DetailItem from './components/DetailItem.vue';
|
||||
import PurchaseCreditsModal from './components/PurchaseCreditsModal.vue';
|
||||
import SwitchCurrencyDialog from './components/SwitchCurrencyDialog.vue';
|
||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||
import SettingsLayout from '../SettingsLayout.vue';
|
||||
import TabBar from 'dashboard/components-next/tabbar/TabBar.vue';
|
||||
import ButtonV4 from 'next/button/Button.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const { currentAccount, isOnChatwootCloud } = useAccount();
|
||||
const {
|
||||
captainEnabled,
|
||||
@@ -118,6 +128,55 @@ const handleBillingPageLogic = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const isSwitchingCurrency = computed(() => uiFlags.value.isSwitchingCurrency);
|
||||
|
||||
// Currency switching is rolled out to Brazil (pt_BR) accounts only for now.
|
||||
const showCurrencyToggle = computed(
|
||||
() => currentAccount.value?.locale === 'pt_BR'
|
||||
);
|
||||
|
||||
const currentBillingCurrency = computed(() =>
|
||||
(
|
||||
customAttributes.value.billing_currency || DEFAULT_BILLING_CURRENCY
|
||||
).toLowerCase()
|
||||
);
|
||||
|
||||
const currencyTabs = computed(() =>
|
||||
SUPPORTED_BILLING_CURRENCIES.map(code => ({
|
||||
label: t(getCurrencyConfig(code).i18nLabelKey),
|
||||
value: code,
|
||||
}))
|
||||
);
|
||||
|
||||
const activeCurrencyIndex = computed(() => {
|
||||
const index = SUPPORTED_BILLING_CURRENCIES.indexOf(
|
||||
currentBillingCurrency.value
|
||||
);
|
||||
return index === -1 ? 0 : index;
|
||||
});
|
||||
|
||||
const pendingCurrency = ref(null);
|
||||
const switchCurrencyDialogRef = ref(null);
|
||||
|
||||
const onSelectCurrency = tab => {
|
||||
if (!tab?.value || tab.value === currentBillingCurrency.value) return;
|
||||
if (isSwitchingCurrency.value) return;
|
||||
pendingCurrency.value = tab.value;
|
||||
switchCurrencyDialogRef.value?.open();
|
||||
};
|
||||
|
||||
const onConfirmSwitchCurrency = async () => {
|
||||
try {
|
||||
await store.dispatch('accounts/switchBillingCurrency', {
|
||||
currency: pendingCurrency.value,
|
||||
});
|
||||
switchCurrencyDialogRef.value?.close();
|
||||
useAlert(t('BILLING_SETTINGS.CURRENCY.SUCCESS'));
|
||||
} catch (error) {
|
||||
useAlert(error.message || t('BILLING_SETTINGS.CURRENCY.ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
const onClickBillingPortal = () => {
|
||||
store.dispatch('accounts/checkout');
|
||||
};
|
||||
@@ -190,6 +249,23 @@ onMounted(handleBillingPageLogic);
|
||||
/>
|
||||
</div>
|
||||
</BillingCard>
|
||||
<BillingCard
|
||||
v-if="showCurrencyToggle"
|
||||
:title="$t('BILLING_SETTINGS.CURRENCY.TITLE')"
|
||||
:description="$t('BILLING_SETTINGS.CURRENCY.DESCRIPTION')"
|
||||
>
|
||||
<template #action>
|
||||
<div
|
||||
:class="{ 'pointer-events-none opacity-60': isSwitchingCurrency }"
|
||||
>
|
||||
<TabBar
|
||||
:tabs="currencyTabs"
|
||||
:initial-active-tab="activeCurrencyIndex"
|
||||
@tab-changed="onSelectCurrency"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BillingCard>
|
||||
<BillingCard
|
||||
v-if="captainEnabled"
|
||||
:title="$t('BILLING_SETTINGS.CAPTAIN.TITLE')"
|
||||
@@ -263,6 +339,12 @@ onMounted(handleBillingPageLogic);
|
||||
ref="purchaseCreditsModalRef"
|
||||
@success="handleTopupSuccess"
|
||||
/>
|
||||
<SwitchCurrencyDialog
|
||||
ref="switchCurrencyDialogRef"
|
||||
:target-currency="pendingCurrency"
|
||||
:is-loading="isSwitchingCurrency"
|
||||
@confirm="onConfirmSwitchCurrency"
|
||||
/>
|
||||
</template>
|
||||
</SettingsLayout>
|
||||
</template>
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
import { getCurrencyConfig } from 'dashboard/constants/billing';
|
||||
|
||||
const props = defineProps({
|
||||
targetCurrency: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['confirm']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogRef = ref(null);
|
||||
|
||||
const currencyLabel = computed(() =>
|
||||
t(getCurrencyConfig(props.targetCurrency).i18nLabelKey)
|
||||
);
|
||||
|
||||
const open = () => dialogRef.value?.open();
|
||||
const close = () => dialogRef.value?.close();
|
||||
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
type="alert"
|
||||
:title="$t('BILLING_SETTINGS.CURRENCY.CONFIRM.TITLE')"
|
||||
:description="
|
||||
$t('BILLING_SETTINGS.CURRENCY.CONFIRM.DESCRIPTION', {
|
||||
currency: currencyLabel,
|
||||
})
|
||||
"
|
||||
:confirm-button-label="
|
||||
$t('BILLING_SETTINGS.CURRENCY.CONFIRM.CONFIRM_BUTTON')
|
||||
"
|
||||
:cancel-button-label="$t('BILLING_SETTINGS.CURRENCY.CONFIRM.CANCEL_BUTTON')"
|
||||
:is-loading="isLoading"
|
||||
@confirm="emit('confirm')"
|
||||
>
|
||||
<div class="p-2.5 rounded-lg bg-n-amber-2 border border-n-amber-6">
|
||||
<p class="text-sm text-n-amber-11">
|
||||
{{ $t('BILLING_SETTINGS.CURRENCY.CONFIRM.WARNING') }}
|
||||
</p>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -19,6 +19,7 @@ const state = {
|
||||
isUpdating: false,
|
||||
isCheckoutInProcess: false,
|
||||
isFetchingLimits: false,
|
||||
isSwitchingCurrency: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -142,6 +143,20 @@ export const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
switchBillingCurrency: async ({ commit, dispatch }, { currency }) => {
|
||||
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.
|
||||
await dispatch('get', { silent: true });
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
} finally {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isSwitchingCurrency: false });
|
||||
}
|
||||
},
|
||||
|
||||
limits: async ({ commit }) => {
|
||||
commit(types.default.SET_ACCOUNT_UI_FLAG, { isFetchingLimits: true });
|
||||
try {
|
||||
|
||||
@@ -38,4 +38,8 @@ class AccountPolicy < ApplicationPolicy
|
||||
def topup_options?
|
||||
@account_user.administrator?
|
||||
end
|
||||
|
||||
def switch_currency?
|
||||
@account_user.administrator?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,7 @@ if resource.custom_attributes.present?
|
||||
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
||||
json.subscription_status resource.custom_attributes['subscription_status']
|
||||
json.subscription_ends_on resource.custom_attributes['subscription_ends_on']
|
||||
json.billing_currency resource.custom_attributes['billing_currency'] if resource.custom_attributes['billing_currency'].present?
|
||||
json.website resource.custom_attributes['website'] if resource.custom_attributes['website'].present?
|
||||
json.industry resource.custom_attributes['industry'] if resource.custom_attributes['industry'].present?
|
||||
json.company_size resource.custom_attributes['company_size'] if resource.custom_attributes['company_size'].present?
|
||||
|
||||
@@ -166,6 +166,14 @@ en:
|
||||
plan_not_eligible: Top-ups are only available for paid plans. Please upgrade your plan first.
|
||||
stripe_customer_not_configured: Stripe customer not configured
|
||||
no_payment_method: No payment methods found. Please add a payment method before making a purchase.
|
||||
billing:
|
||||
currency_required: Currency is required
|
||||
unsupported_currency: This currency is not supported
|
||||
same_currency: This account is already billed in the selected currency
|
||||
stripe_customer_not_configured: Stripe customer not configured
|
||||
unknown_plan: Could not determine the current plan
|
||||
currency_not_available_for_plan: The selected currency is not available for your current plan
|
||||
no_payment_method: No payment methods found. Please add a payment method before switching currency.
|
||||
reports:
|
||||
date_range_too_long: Date range cannot exceed 6 months
|
||||
profile:
|
||||
|
||||
@@ -524,6 +524,7 @@ Rails.application.routes.draw do
|
||||
post :toggle_deletion
|
||||
post :topup_checkout
|
||||
get :topup_options
|
||||
post :switch_currency
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
include BillingHelper
|
||||
before_action :fetch_account
|
||||
before_action :check_authorization
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion, :topup_options]
|
||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion, :topup_options, :switch_currency]
|
||||
|
||||
def subscription
|
||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||
@@ -76,6 +76,17 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
||||
render json: { id: @account.id, currency: @account.billing_currency, options: service.available_options }
|
||||
end
|
||||
|
||||
def switch_currency
|
||||
return render json: { error: I18n.t('errors.billing.currency_required') }, status: :unprocessable_entity if params[:currency].blank?
|
||||
|
||||
Enterprise::Billing::SwitchCurrencyService.new(account: @account, currency: params[:currency]).perform
|
||||
|
||||
@account.reload
|
||||
render json: { id: @account.id, limits: @account.limits, custom_attributes: @account.custom_attributes }
|
||||
rescue Enterprise::Billing::SwitchCurrencyService::Error, Stripe::StripeError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_cloud_env
|
||||
|
||||
@@ -12,7 +12,7 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
@event = event
|
||||
|
||||
case @event.type
|
||||
when 'customer.subscription.updated'
|
||||
when 'customer.subscription.created', 'customer.subscription.updated'
|
||||
process_subscription_updated
|
||||
when 'customer.subscription.deleted'
|
||||
process_subscription_deleted
|
||||
@@ -28,6 +28,10 @@ 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.
|
||||
return if currency_switch_cancellation?
|
||||
|
||||
previous_usage = capture_previous_usage
|
||||
update_account_attributes(subscription, plan)
|
||||
@@ -77,9 +81,17 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
inferred_currency || account.billing_currency
|
||||
end
|
||||
|
||||
def currency_switch_cancellation?
|
||||
subscription['metadata'][Enterprise::Billing::SwitchCurrencyService::SWITCH_METADATA_KEY] == 'true'
|
||||
end
|
||||
|
||||
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").
|
||||
return if currency_switch_cancellation?
|
||||
|
||||
previous_monthly_credits = current_plan_credits[:responses]
|
||||
return unless Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
class Enterprise::Billing::SwitchCurrencyService
|
||||
include BillingHelper
|
||||
|
||||
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.
|
||||
SWITCH_METADATA_KEY = 'chatwoot_currency_switch'.freeze
|
||||
|
||||
pattr_initialize [:account!, :currency!]
|
||||
|
||||
def perform
|
||||
validate!
|
||||
|
||||
subscriptions = live_subscriptions
|
||||
paid_subscription = subscriptions.find { |subscription| !default_price?(subscription) }
|
||||
|
||||
if paid_subscription
|
||||
switch_paid_plan(subscriptions, paid_subscription)
|
||||
else
|
||||
switch_free_plan
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def target_currency
|
||||
@target_currency ||= Enterprise::Billing::Currencies.normalize(currency)
|
||||
end
|
||||
|
||||
def validate!
|
||||
raise Error, I18n.t('errors.billing.unsupported_currency') unless Enterprise::Billing::Currencies.supported?(currency)
|
||||
raise Error, I18n.t('errors.billing.same_currency') if target_currency == account.billing_currency
|
||||
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.
|
||||
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.
|
||||
def switch_paid_plan(subscriptions, paid_subscription)
|
||||
validate_payment_method!
|
||||
|
||||
plan = current_plan(paid_subscription)
|
||||
raise Error, I18n.t('errors.billing.unknown_plan') if plan.blank?
|
||||
|
||||
change = {
|
||||
new_price_id: resolve_new_price_id(plan),
|
||||
original_price_id: paid_subscription['plan']['id'],
|
||||
quantity: paid_subscription['quantity'],
|
||||
paid_through: subscriptions.filter_map { |subscription| subscription_period_end(subscription) }.max,
|
||||
key: paid_subscription.id
|
||||
}
|
||||
|
||||
sync_stripe_customer_location
|
||||
new_subscription = replace_subscriptions(subscriptions, change)
|
||||
|
||||
persist_currency(build_custom_attributes(new_subscription, plan))
|
||||
Enterprise::Billing::ReconcilePlanFeaturesService.new(account: account).perform
|
||||
end
|
||||
|
||||
def resolve_new_price_id(plan)
|
||||
target_prices = Enterprise::Billing::PlanConfiguration.price_ids_by_currency(plan)[target_currency]
|
||||
raise Error, I18n.t('errors.billing.currency_not_available_for_plan') if target_prices.blank?
|
||||
|
||||
target_prices.first
|
||||
end
|
||||
|
||||
def current_plan(subscription)
|
||||
plan, = Enterprise::Billing::PlanConfiguration.find_plan_by_price_id(subscription['plan']['id'])
|
||||
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).
|
||||
def replace_subscriptions(subscriptions, change)
|
||||
cancel_subscriptions(subscriptions)
|
||||
|
||||
begin
|
||||
create_currency_subscription(change[:new_price_id], change, 'switch')
|
||||
rescue Stripe::StripeError => e
|
||||
create_currency_subscription(change[:original_price_id], change, 'switch-revert')
|
||||
raise Error, e.message
|
||||
end
|
||||
end
|
||||
|
||||
def cancel_subscriptions(subscriptions)
|
||||
subscriptions.each do |subscription|
|
||||
Stripe::Subscription.update(subscription.id, metadata: { SWITCH_METADATA_KEY => 'true' })
|
||||
Stripe::Subscription.cancel(subscription.id, { prorate: false })
|
||||
end
|
||||
end
|
||||
|
||||
def create_currency_subscription(price_id, change, key_prefix)
|
||||
params = { customer: stripe_customer_id, items: [{ price: price_id, quantity: change[:quantity] }] }
|
||||
params[:trial_end] = change[:paid_through] if change[:paid_through].present? && change[:paid_through] > Time.current.to_i
|
||||
Stripe::Subscription.create(params, { idempotency_key: "#{key_prefix}-#{account.id}-#{change[:key]}" })
|
||||
end
|
||||
|
||||
def build_custom_attributes(subscription, plan)
|
||||
account.custom_attributes.merge(
|
||||
'billing_currency' => target_currency,
|
||||
'stripe_price_id' => subscription['plan']['id'],
|
||||
'stripe_product_id' => subscription['plan']['product'],
|
||||
'plan_name' => plan['name'],
|
||||
'subscribed_quantity' => subscription['quantity'],
|
||||
'subscription_status' => subscription['status'],
|
||||
'subscription_ends_on' => subscription_ends_on(subscription)
|
||||
)
|
||||
end
|
||||
|
||||
def default_price?(subscription)
|
||||
Enterprise::Billing::PlanConfiguration.plan_contains_price_id?(
|
||||
Enterprise::Billing::PlanConfiguration.default_plan, subscription['plan']['id']
|
||||
)
|
||||
end
|
||||
|
||||
def persist_currency(custom_attributes)
|
||||
account.update!(custom_attributes: custom_attributes)
|
||||
end
|
||||
|
||||
def sync_stripe_customer_location
|
||||
Stripe::Customer.update(
|
||||
stripe_customer_id,
|
||||
address: { country: Enterprise::Billing::Currencies.country_for(target_currency) },
|
||||
preferred_locales: [Enterprise::Billing::Currencies.preferred_locale_for(target_currency)]
|
||||
)
|
||||
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.
|
||||
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) }
|
||||
end
|
||||
|
||||
def validate_payment_method!
|
||||
customer = Stripe::Customer.retrieve(stripe_customer_id)
|
||||
return if customer.invoice_settings.default_payment_method.present? || customer.default_source.present?
|
||||
|
||||
payment_methods = Stripe::PaymentMethod.list(customer: stripe_customer_id, limit: 1)
|
||||
raise Error, I18n.t('errors.billing.no_payment_method') if payment_methods.data.empty?
|
||||
|
||||
Stripe::Customer.update(stripe_customer_id, invoice_settings: { default_payment_method: payment_methods.data.first.id })
|
||||
end
|
||||
|
||||
def stripe_customer_id
|
||||
account.custom_attributes['stripe_customer_id']
|
||||
end
|
||||
end
|
||||
@@ -36,6 +36,7 @@ describe Enterprise::Billing::HandleStripeEventService do
|
||||
allow(subscription).to receive(:[]).with('quantity').and_return('10')
|
||||
allow(subscription).to receive(:[]).with('status').and_return('active')
|
||||
allow(subscription).to receive(:[]).with('current_period_end').and_return(1_686_567_520)
|
||||
allow(subscription).to receive(:[]).with('metadata').and_return({})
|
||||
allow(subscription).to receive(:customer).and_return('cus_123')
|
||||
allow(event).to receive(:type).and_return('customer.subscription.updated')
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user