Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb2045e2e | ||
|
|
c147a82221 |
@@ -27,6 +27,18 @@ class EnterpriseAccountAPI extends ApiClient {
|
|||||||
createTopupCheckout(credits) {
|
createTopupCheckout(credits) {
|
||||||
return axios.post(`${this.url}topup_checkout`, { credits });
|
return axios.post(`${this.url}topup_checkout`, { credits });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBillingDetails() {
|
||||||
|
return axios.get(`${this.url}billing_details`);
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmBillingDetails({ businessName, businessAddress, billingEmail }) {
|
||||||
|
return axios.post(`${this.url}confirm_billing_details`, {
|
||||||
|
business_name: businessName,
|
||||||
|
business_address: businessAddress,
|
||||||
|
billing_email: billingEmail,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new EnterpriseAccountAPI();
|
export default new EnterpriseAccountAPI();
|
||||||
|
|||||||
@@ -483,6 +483,31 @@
|
|||||||
"GO_BACK": "Go Back",
|
"GO_BACK": "Go Back",
|
||||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"CONFIRM_BUSINESS": {
|
||||||
|
"TITLE": "Confirm Business Details",
|
||||||
|
"DESCRIPTION": "Please confirm your business information before accessing the billing portal.",
|
||||||
|
"PREFILLED_BANNER": "We found your existing business details. Review and update if needed, or continue to proceed.",
|
||||||
|
"BUSINESS_NAME": {
|
||||||
|
"LABEL": "Business Name",
|
||||||
|
"PLACEHOLDER": "Enter your business name",
|
||||||
|
"REQUIRED": "Business name is required"
|
||||||
|
},
|
||||||
|
"BUSINESS_ADDRESS": {
|
||||||
|
"LABEL": "Business Address",
|
||||||
|
"PLACEHOLDER": "Enter your business address",
|
||||||
|
"REQUIRED": "Business address is required"
|
||||||
|
},
|
||||||
|
"BILLING_EMAIL": {
|
||||||
|
"LABEL": "Billing Email",
|
||||||
|
"PLACEHOLDER": "Enter your billing email",
|
||||||
|
"REQUIRED": "Billing email is required",
|
||||||
|
"INVALID": "Please enter a valid email address",
|
||||||
|
"HELP_TEXT": "All billing-related emails will be sent to this address"
|
||||||
|
},
|
||||||
|
"CANCEL": "Cancel",
|
||||||
|
"CONTINUE": "Continue to Billing Portal",
|
||||||
|
"ERROR": "Failed to update business details. Please try again."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SECURITY_SETTINGS": {
|
"SECURITY_SETTINGS": {
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, nextTick } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
import { useMapGetter, useStore } from 'dashboard/composables/store.js';
|
||||||
import { useAccount } from 'dashboard/composables/useAccount';
|
import { useAccount } from 'dashboard/composables/useAccount';
|
||||||
import { useCaptain } from 'dashboard/composables/useCaptain';
|
import { useCaptain } from 'dashboard/composables/useCaptain';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import sessionStorage from 'shared/helpers/sessionStorage';
|
import sessionStorage from 'shared/helpers/sessionStorage';
|
||||||
|
import EnterpriseAccountAPI from 'dashboard/api/enterprise/account';
|
||||||
|
|
||||||
import BillingMeter from './components/BillingMeter.vue';
|
import BillingMeter from './components/BillingMeter.vue';
|
||||||
import BillingCard from './components/BillingCard.vue';
|
import BillingCard from './components/BillingCard.vue';
|
||||||
import BillingHeader from './components/BillingHeader.vue';
|
import BillingHeader from './components/BillingHeader.vue';
|
||||||
import DetailItem from './components/DetailItem.vue';
|
import DetailItem from './components/DetailItem.vue';
|
||||||
import PurchaseCreditsModal from './components/PurchaseCreditsModal.vue';
|
import PurchaseCreditsModal from './components/PurchaseCreditsModal.vue';
|
||||||
|
import ConfirmBusinessDetailsModal from './components/ConfirmBusinessDetailsModal.vue';
|
||||||
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
import BaseSettingsHeader from '../components/BaseSettingsHeader.vue';
|
||||||
import SettingsLayout from '../SettingsLayout.vue';
|
import SettingsLayout from '../SettingsLayout.vue';
|
||||||
import ButtonV4 from 'next/button/Button.vue';
|
import ButtonV4 from 'next/button/Button.vue';
|
||||||
@@ -27,6 +31,7 @@ const {
|
|||||||
isFetchingLimits,
|
isFetchingLimits,
|
||||||
} = useCaptain();
|
} = useCaptain();
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
const uiFlags = useMapGetter('accounts/getUIFlags');
|
const uiFlags = useMapGetter('accounts/getUIFlags');
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
|
||||||
@@ -35,11 +40,16 @@ const BILLING_REFRESH_ATTEMPTED = 'billing_refresh_attempted';
|
|||||||
// State for handling refresh attempts and loading
|
// State for handling refresh attempts and loading
|
||||||
const isWaitingForBilling = ref(false);
|
const isWaitingForBilling = ref(false);
|
||||||
const purchaseCreditsModalRef = ref(null);
|
const purchaseCreditsModalRef = ref(null);
|
||||||
|
const confirmBusinessDetailsModalRef = ref(null);
|
||||||
|
|
||||||
const customAttributes = computed(() => {
|
const customAttributes = computed(() => {
|
||||||
return currentAccount.value.custom_attributes || {};
|
return currentAccount.value.custom_attributes || {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const billingDetailsConfirmed = computed(() => {
|
||||||
|
return customAttributes.value.billing_details_confirmed;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computed property for plan name
|
* Computed property for plan name
|
||||||
* @returns {string|undefined}
|
* @returns {string|undefined}
|
||||||
@@ -118,8 +128,30 @@ const handleBillingPageLogic = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onClickBillingPortal = () => {
|
const billingDetails = ref({});
|
||||||
store.dispatch('accounts/checkout');
|
const isFetchingBillingDetails = ref(false);
|
||||||
|
|
||||||
|
const onClickBillingPortal = async () => {
|
||||||
|
if (billingDetailsConfirmed.value) {
|
||||||
|
store.dispatch('accounts/checkout');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isFetchingBillingDetails.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await EnterpriseAccountAPI.getBillingDetails();
|
||||||
|
billingDetails.value = data;
|
||||||
|
await nextTick();
|
||||||
|
confirmBusinessDetailsModalRef.value?.open();
|
||||||
|
} catch {
|
||||||
|
useAlert(t('BILLING_SETTINGS.CONFIRM_BUSINESS.ERROR'));
|
||||||
|
} finally {
|
||||||
|
isFetchingBillingDetails.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBusinessDetailsConfirmed = redirectUrl => {
|
||||||
|
window.location = redirectUrl;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleChatWindow = () => {
|
const onToggleChatWindow = () => {
|
||||||
@@ -263,6 +295,11 @@ onMounted(handleBillingPageLogic);
|
|||||||
ref="purchaseCreditsModalRef"
|
ref="purchaseCreditsModalRef"
|
||||||
@success="handleTopupSuccess"
|
@success="handleTopupSuccess"
|
||||||
/>
|
/>
|
||||||
|
<ConfirmBusinessDetailsModal
|
||||||
|
ref="confirmBusinessDetailsModalRef"
|
||||||
|
:billing-details="billingDetails"
|
||||||
|
@confirmed="onBusinessDetailsConfirmed"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+207
@@ -0,0 +1,207 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, reactive } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { useVuelidate } from '@vuelidate/core';
|
||||||
|
import { required, email } from '@vuelidate/validators';
|
||||||
|
import { useAlert } from 'dashboard/composables';
|
||||||
|
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||||
|
import Button from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import Input from 'dashboard/components-next/input/Input.vue';
|
||||||
|
import TextArea from 'dashboard/components-next/textarea/TextArea.vue';
|
||||||
|
import EnterpriseAccountAPI from 'dashboard/api/enterprise/account';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
billingDetails: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['confirmed']);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const dialogRef = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
const hasPreExistingData = computed(() => {
|
||||||
|
return Boolean(
|
||||||
|
props.billingDetails.business_name ||
|
||||||
|
props.billingDetails.business_address ||
|
||||||
|
props.billingDetails.billing_email
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const formValues = reactive({
|
||||||
|
businessName: '',
|
||||||
|
businessAddress: '',
|
||||||
|
billingEmail: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const validationRules = computed(() => ({
|
||||||
|
businessName: { required },
|
||||||
|
businessAddress: { required },
|
||||||
|
billingEmail: { required, email },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const v$ = useVuelidate(validationRules, formValues);
|
||||||
|
|
||||||
|
const fieldKeyMap = {
|
||||||
|
businessName: 'BUSINESS_NAME',
|
||||||
|
businessAddress: 'BUSINESS_ADDRESS',
|
||||||
|
billingEmail: 'BILLING_EMAIL',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getErrorMessage = field => {
|
||||||
|
const fieldState = v$.value[field];
|
||||||
|
if (!fieldState || !fieldState.$error) return '';
|
||||||
|
|
||||||
|
if (fieldState.required?.$invalid) {
|
||||||
|
return t(
|
||||||
|
`BILLING_SETTINGS.CONFIRM_BUSINESS.${fieldKeyMap[field]}.REQUIRED`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fieldState.email?.$invalid) {
|
||||||
|
return t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.INVALID');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
v$.value.$reset();
|
||||||
|
formValues.businessName = props.billingDetails.business_name || '';
|
||||||
|
formValues.businessAddress = props.billingDetails.business_address || '';
|
||||||
|
formValues.billingEmail = props.billingDetails.billing_email || '';
|
||||||
|
dialogRef.value?.open();
|
||||||
|
};
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
dialogRef.value?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = async () => {
|
||||||
|
v$.value.$touch();
|
||||||
|
if (v$.value.$invalid) return;
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const { data } = await EnterpriseAccountAPI.confirmBillingDetails({
|
||||||
|
businessName: formValues.businessName,
|
||||||
|
businessAddress: formValues.businessAddress,
|
||||||
|
billingEmail: formValues.billingEmail,
|
||||||
|
});
|
||||||
|
emit('confirmed', data.redirect_url);
|
||||||
|
close();
|
||||||
|
} catch {
|
||||||
|
useAlert(t('BILLING_SETTINGS.CONFIRM_BUSINESS.ERROR'));
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({ open, close });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
ref="dialogRef"
|
||||||
|
:title="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.TITLE')"
|
||||||
|
:description="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.DESCRIPTION')"
|
||||||
|
:show-confirm-button="false"
|
||||||
|
:show-cancel-button="false"
|
||||||
|
width="lg"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-if="hasPreExistingData"
|
||||||
|
class="flex items-center gap-3 p-3 rounded-lg bg-n-teal-2 border border-n-teal-6"
|
||||||
|
>
|
||||||
|
<fluent-icon
|
||||||
|
icon="checkmark-circle"
|
||||||
|
size="18"
|
||||||
|
class="text-n-teal-11 shrink-0"
|
||||||
|
/>
|
||||||
|
<p class="text-sm text-n-teal-11">
|
||||||
|
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.PREFILLED_BANNER') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||||
|
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_NAME.LABEL') }}
|
||||||
|
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<span class="text-n-ruby-9">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model="formValues.businessName"
|
||||||
|
:placeholder="
|
||||||
|
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_NAME.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="getErrorMessage('businessName')"
|
||||||
|
:message-type="v$.businessName.$error ? 'error' : 'info'"
|
||||||
|
@blur="v$.businessName.$touch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||||
|
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_ADDRESS.LABEL') }}
|
||||||
|
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<span class="text-n-ruby-9">*</span>
|
||||||
|
</label>
|
||||||
|
<TextArea
|
||||||
|
v-model="formValues.businessAddress"
|
||||||
|
:placeholder="
|
||||||
|
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BUSINESS_ADDRESS.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="getErrorMessage('businessAddress')"
|
||||||
|
:message-type="v$.businessAddress.$error ? 'error' : 'info'"
|
||||||
|
:max-length="500"
|
||||||
|
min-height="5rem"
|
||||||
|
max-height="8rem"
|
||||||
|
@blur="v$.businessAddress.$touch"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="mb-1 text-heading-3 text-n-slate-12">
|
||||||
|
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.LABEL') }}
|
||||||
|
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||||
|
<span class="text-n-ruby-9">*</span>
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
v-model="formValues.billingEmail"
|
||||||
|
type="email"
|
||||||
|
:placeholder="
|
||||||
|
$t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.PLACEHOLDER')
|
||||||
|
"
|
||||||
|
:message="getErrorMessage('billingEmail')"
|
||||||
|
:message-type="v$.billingEmail.$error ? 'error' : 'info'"
|
||||||
|
@blur="v$.billingEmail.$touch"
|
||||||
|
/>
|
||||||
|
<p v-if="!v$.billingEmail.$error" class="mt-1 text-xs text-n-slate-10">
|
||||||
|
{{ $t('BILLING_SETTINGS.CONFIRM_BUSINESS.BILLING_EMAIL.HELP_TEXT') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-between w-full gap-3">
|
||||||
|
<Button
|
||||||
|
variant="faded"
|
||||||
|
color="slate"
|
||||||
|
:label="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.CANCEL')"
|
||||||
|
class="w-full"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="close"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
color="blue"
|
||||||
|
:label="$t('BILLING_SETTINGS.CONFIRM_BUSINESS.CONTINUE')"
|
||||||
|
class="w-full"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
@click="handleConfirm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
@@ -34,4 +34,12 @@ class AccountPolicy < ApplicationPolicy
|
|||||||
def topup_checkout?
|
def topup_checkout?
|
||||||
@account_user.administrator?
|
@account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def billing_details?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_billing_details?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -475,6 +475,8 @@ Rails.application.routes.draw do
|
|||||||
post :checkout
|
post :checkout
|
||||||
post :subscription
|
post :subscription
|
||||||
get :limits
|
get :limits
|
||||||
|
get :billing_details
|
||||||
|
post :confirm_billing_details
|
||||||
post :toggle_deletion
|
post :toggle_deletion
|
||||||
post :topup_checkout
|
post :topup_checkout
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,6 +4,32 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
|||||||
before_action :check_authorization
|
before_action :check_authorization
|
||||||
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
before_action :check_cloud_env, only: [:limits, :toggle_deletion]
|
||||||
|
|
||||||
|
def billing_details
|
||||||
|
return render json: { billing_details_confirmed: false }, status: :ok if stripe_customer_id.blank?
|
||||||
|
|
||||||
|
customer = Stripe::Customer.retrieve(stripe_customer_id)
|
||||||
|
render json: {
|
||||||
|
billing_details_confirmed: @account.custom_attributes['billing_details_confirmed'] || false,
|
||||||
|
business_name: customer.name,
|
||||||
|
billing_email: customer.email,
|
||||||
|
business_address: format_stripe_address(customer.address)
|
||||||
|
}, status: :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
def confirm_billing_details
|
||||||
|
return render_invalid_billing_details if stripe_customer_id.blank?
|
||||||
|
|
||||||
|
Stripe::Customer.update(stripe_customer_id, {
|
||||||
|
name: params[:business_name],
|
||||||
|
email: params[:billing_email],
|
||||||
|
address: { line1: params[:business_address] }
|
||||||
|
})
|
||||||
|
|
||||||
|
@account.update!(custom_attributes: @account.custom_attributes.merge('billing_details_confirmed' => true))
|
||||||
|
|
||||||
|
create_stripe_billing_session(stripe_customer_id)
|
||||||
|
end
|
||||||
|
|
||||||
def subscription
|
def subscription
|
||||||
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
if stripe_customer_id.blank? && @account.custom_attributes['is_creating_customer'].blank?
|
||||||
@account.update(custom_attributes: { is_creating_customer: true })
|
@account.update(custom_attributes: { is_creating_customer: true })
|
||||||
@@ -118,6 +144,12 @@ class Enterprise::Api::V1::AccountsController < Api::BaseController
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def format_stripe_address(address)
|
||||||
|
return '' if address.blank?
|
||||||
|
|
||||||
|
[address.line1, address.line2, address.city, address.state, address.postal_code, address.country].compact_blank.join(', ')
|
||||||
|
end
|
||||||
|
|
||||||
def render_invalid_billing_details
|
def render_invalid_billing_details
|
||||||
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
render_could_not_create_error('Please subscribe to a plan before viewing the billing details')
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user