diff --git a/app/javascript/dashboard/api/enterprise/specs/account.spec.js b/app/javascript/dashboard/api/enterprise/specs/account.spec.js index 9c65b0b67..47d2eb26d 100644 --- a/app/javascript/dashboard/api/enterprise/specs/account.spec.js +++ b/app/javascript/dashboard/api/enterprise/specs/account.spec.js @@ -11,6 +11,8 @@ describe('#enterpriseAccountAPI', () => { expect(accountAPI).toHaveProperty('delete'); expect(accountAPI).toHaveProperty('checkout'); expect(accountAPI).toHaveProperty('toggleDeletion'); + expect(accountAPI).toHaveProperty('createTopupCheckout'); + expect(accountAPI).toHaveProperty('getLimits'); }); describe('API calls', () => { @@ -59,5 +61,29 @@ describe('#enterpriseAccountAPI', () => { { action_type: 'undelete' } ); }); + + it('#createTopupCheckout with credits', () => { + accountAPI.createTopupCheckout(1000); + expect(axiosMock.post).toHaveBeenCalledWith( + '/enterprise/api/v1/topup_checkout', + { credits: 1000 } + ); + }); + + it('#createTopupCheckout with different credit amounts', () => { + const creditAmounts = [1000, 2500, 6000, 12000]; + creditAmounts.forEach(credits => { + accountAPI.createTopupCheckout(credits); + expect(axiosMock.post).toHaveBeenCalledWith( + '/enterprise/api/v1/topup_checkout', + { credits } + ); + }); + }); + + it('#getLimits', () => { + accountAPI.getLimits(); + expect(axiosMock.get).toHaveBeenCalledWith('/enterprise/api/v1/limits'); + }); }); }); diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 64be235a2..4e51ef37a 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -422,7 +422,15 @@ "PURCHASE": "Purchase Credits", "LOADING": "Loading options...", "FETCH_ERROR": "Failed to load credit options. Please try again.", - "PURCHASE_ERROR": "Failed to process purchase. Please try again." + "PURCHASE_ERROR": "Failed to process purchase. Please try again.", + "PURCHASE_SUCCESS": "Successfully added {credits} credits to your account", + "CONFIRM": { + "TITLE": "Confirm Purchase", + "DESCRIPTION": "You are about to purchase {credits} credits for {amount}.", + "INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.", + "GO_BACK": "Go Back", + "CONFIRM_PURCHASE": "Confirm Purchase" + } } }, "SECURITY_SETTINGS": { diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue index 4f0981967..bcfa46193 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/Index.vue @@ -132,6 +132,11 @@ const openPurchaseCreditsModal = () => { purchaseCreditsModalRef.value?.open(); }; +const handleTopupSuccess = () => { + // Refresh limits to show updated credit balance + fetchLimits(); +}; + onMounted(handleBillingPageLogic); @@ -254,7 +259,10 @@ onMounted(handleBillingPageLogic); - + diff --git a/app/javascript/dashboard/routes/dashboard/settings/billing/components/PurchaseCreditsModal.vue b/app/javascript/dashboard/routes/dashboard/settings/billing/components/PurchaseCreditsModal.vue index ebbab0aa8..33f299b9b 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/billing/components/PurchaseCreditsModal.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/billing/components/PurchaseCreditsModal.vue @@ -7,7 +7,7 @@ import Button from 'dashboard/components-next/button/Button.vue'; import CreditPackageCard from './CreditPackageCard.vue'; import EnterpriseAccountAPI from 'dashboard/api/enterprise/account'; -const emit = defineEmits(['close']); +const emit = defineEmits(['close', 'success']); const { t } = useI18n(); @@ -19,19 +19,78 @@ const TOPUP_OPTIONS = [ ]; const POPULAR_CREDITS_AMOUNT = 6000; +const STEP_SELECT = 'select'; +const STEP_CONFIRM = 'confirm'; const dialogRef = ref(null); const selectedCredits = ref(null); const isLoading = ref(false); +const currentStep = ref(STEP_SELECT); const selectedOption = computed(() => { return TOPUP_OPTIONS.find(o => o.credits === selectedCredits.value); }); +const formattedAmount = computed(() => { + if (!selectedOption.value) return ''; + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: selectedOption.value.currency.toUpperCase(), + }).format(selectedOption.value.amount); +}); + +const formattedCredits = computed(() => { + if (!selectedOption.value) return ''; + return selectedOption.value.credits.toLocaleString(); +}); + +const dialogTitle = computed(() => { + return currentStep.value === STEP_SELECT + ? t('BILLING_SETTINGS.TOPUP.MODAL_TITLE') + : t('BILLING_SETTINGS.TOPUP.CONFIRM.TITLE'); +}); + +const dialogDescription = computed(() => { + return currentStep.value === STEP_SELECT + ? t('BILLING_SETTINGS.TOPUP.MODAL_DESCRIPTION') + : ''; +}); + +const dialogWidth = computed(() => { + return currentStep.value === STEP_SELECT ? 'xl' : 'md'; +}); + const handlePackageSelect = credits => { selectedCredits.value = credits; }; +const open = () => { + const popularOption = TOPUP_OPTIONS.find( + o => o.credits === POPULAR_CREDITS_AMOUNT + ); + selectedCredits.value = popularOption?.credits || TOPUP_OPTIONS[0]?.credits; + currentStep.value = STEP_SELECT; + isLoading.value = false; + dialogRef.value?.open(); +}; + +const close = () => { + dialogRef.value?.close(); +}; + +const handleClose = () => { + emit('close'); +}; + +const goToConfirmStep = () => { + if (!selectedOption.value) return; + currentStep.value = STEP_CONFIRM; +}; + +const goBackToSelectStep = () => { + currentStep.value = STEP_SELECT; +}; + const handlePurchase = async () => { if (!selectedOption.value) return; @@ -40,9 +99,14 @@ const handlePurchase = async () => { const response = await EnterpriseAccountAPI.createTopupCheckout( selectedOption.value.credits ); - if (response.data.redirect_url) { - window.location.href = response.data.redirect_url; - } + + close(); + emit('success', response.data); + useAlert( + t('BILLING_SETTINGS.TOPUP.PURCHASE_SUCCESS', { + credits: response.data.credits, + }) + ); } catch (error) { const errorMessage = error.response?.data?.error || t('BILLING_SETTINGS.TOPUP.PURCHASE_ERROR'); @@ -52,61 +116,71 @@ const handlePurchase = async () => { } }; -const handleClose = () => { - emit('close'); -}; - -const open = () => { - // Pre-select the most popular option - const popularOption = TOPUP_OPTIONS.find( - o => o.credits === POPULAR_CREDITS_AMOUNT - ); - selectedCredits.value = popularOption?.credits || TOPUP_OPTIONS[0]?.credits; - dialogRef.value?.open(); -}; - -const close = () => { - dialogRef.value?.close(); -}; - defineExpose({ open, close });