diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index b7ab73af7..b73c87378 100644 --- a/src/lib/components/billing/estimatedTotalBox.svelte +++ b/src/lib/components/billing/estimatedTotalBox.svelte @@ -5,7 +5,11 @@ import type { Coupon } from '$lib/sdk/billing'; import { plansInfo, type Tier } from '$lib/stores/billing'; import { CreditsApplied } from '.'; + import { BillingPlan } from '$lib/constants'; + import { tooltip } from '$lib/actions/tooltip'; + // undefined as we only need this on `change-plan` + export let currentTier: Tier | undefined = undefined; export let billingPlan: Tier; export let collaborators: string[]; export let couponData: Partial; @@ -14,21 +18,29 @@ export let isDowngrade = false; const today = new Date(); + const isScaleDowngrade = isDowngrade && billingPlan === BillingPlan.PRO; + const isScaleUpgrade = !isDowngrade && billingPlan === BillingPlan.SCALE; const billingPayDate = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000); let budgetEnabled = false; - $: currentPlan = $plansInfo.get(billingPlan); - $: extraSeatsCost = 0; // 0 untile trial period later replace (collaborators?.length ?? 0) * (currentPlan?.addons?.member?.price ?? 0); - $: grossCost = currentPlan.price + extraSeatsCost; + $: selectedPlan = $plansInfo.get(billingPlan); + $: currentOrgPlan = $plansInfo.get(currentTier); + $: unUsedBalances = isScaleUpgrade + ? currentOrgPlan.price + + (collaborators?.length ?? 0) * (currentOrgPlan?.addons?.member?.price ?? 0) + : isScaleDowngrade + ? currentOrgPlan.price + : 0; + + $: extraSeatsCost = (collaborators?.length ?? 0) * (selectedPlan?.addons?.member?.price ?? 0); + $: grossCost = isScaleUpgrade + ? selectedPlan.price + extraSeatsCost - unUsedBalances + : selectedPlan.price + extraSeatsCost; $: estimatedTotal = - couponData?.status === 'active' - ? grossCost - couponData.credits >= 0 - ? grossCost - couponData.credits - : 0 - : grossCost; + couponData?.status === 'active' ? Math.max(0, grossCost - couponData.credits) : grossCost; $: trialEndDate = new Date( - billingPayDate.getTime() + currentPlan.trialDays * 24 * 60 * 60 * 1000 + billingPayDate.getTime() + selectedPlan.trialDays * 24 * 60 * 60 * 1000 ); @@ -37,41 +49,78 @@ style:--p-card-padding="1.5rem" style:--p-card-border-radius="var(--border-radius-small)"> - -

{currentPlan.name} plan

-

{formatCurrency(currentPlan.price)}

-
- +
+

{selectedPlan.name} plan

+

{formatCurrency(selectedPlan.price)}

+
+ +

Additional seats ({collaborators?.length})

- {formatCurrency(extraSeatsCost)} + {formatCurrency( + isScaleDowngrade + ? (collaborators?.length ?? 0) * (selectedPlan?.addons?.member?.price ?? 0) + : extraSeatsCost + )}

- +
+ + {#if isScaleUpgrade} + {@const currentPlanName = currentOrgPlan.name} +
+
+ Unused {currentPlanName} plan balance + + +
+

-{formatCurrency(unUsedBalances)}

+
+ {/if} + {#if couponData?.status === 'active'} {/if}
- +

Upcoming charge
Due on {!currentPlan.trialDays + >Due on {!selectedPlan.trialDays ? toLocaleDate(billingPayDate.toString()) : toLocaleDate(trialEndDate.toString())}

{formatCurrency(estimatedTotal)}

- +

- You'll pay {formatCurrency(estimatedTotal)} now, with our first - billing cycle starting on - {!currentPlan.trialDays - ? toLocaleDate(billingPayDate.toString()) - : toLocaleDate(trialEndDate.toString())}. Once your credits run out, you'll be charged - {formatCurrency(currentPlan.price)} plus usage fees every 30 days. + {#if isScaleDowngrade} + You'll continue using the {currentOrgPlan.name} plan until your current cycle ends on + {!selectedPlan.trialDays + ? toLocaleDate(billingPayDate.toString()) + : toLocaleDate(trialEndDate.toString())}. Starting the next cycle, your plan will switch to {selectedPlan.name}, and you'll be + charged + {formatCurrency(grossCost)} + every 30 days. + {:else} + You'll pay + {formatCurrency(estimatedTotal)} + now, with our first billing cycle starting on + {!selectedPlan.trialDays + ? toLocaleDate(billingPayDate.toString()) + : toLocaleDate(trialEndDate.toString())}. Once your credits run out, you'll be charged + {formatCurrency(selectedPlan.price)} plus usage fees every 30 + days. + {/if}

diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index cfcb28bba..4a6e1f7ca 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -77,11 +77,7 @@
  • - +

    diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 85673c615..977755574 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -342,14 +342,11 @@ export class Billing { ); } - async validateOrganization( - organizationId: string, - invites: string[], - ): Promise { + async validateOrganization(organizationId: string, invites: string[]): Promise { const path = `/organizations/${organizationId}/validate`; const params = { organizationId, - invites, + invites }; const uri = new URL(this.client.config.endpoint + path); return await this.client.call( diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 357974646..e62110d20 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -130,8 +130,8 @@ export async function confirmPayment( clientSecret: clientSecret, confirmParams: { return_url: url, - payment_method: paymentMethod.providerMethodId, - }, + payment_method: paymentMethod.providerMethodId + } }); if (error) { if (returnError) { diff --git a/src/routes/(console)/account/organizations/+page.ts b/src/routes/(console)/account/organizations/+page.ts index 803497c6a..237a3772d 100644 --- a/src/routes/(console)/account/organizations/+page.ts +++ b/src/routes/(console)/account/organizations/+page.ts @@ -10,13 +10,11 @@ export const load: PageLoad = async ({ url, route }) => { const limit = getLimit(url, route, CARD_LIMIT); const offset = pageToOffset(page, limit); - const queries = [ - Query.offset(offset), - Query.limit(limit), - Query.orderDesc('') - ]; + const queries = [Query.offset(offset), Query.limit(limit), Query.orderDesc('')]; - const organizations = isCloud ? await sdk.forConsole.billing.listOrganization(queries) : sdk.forConsole.teams.list(queries); + const organizations = isCloud + ? await sdk.forConsole.billing.listOrganization(queries) + : sdk.forConsole.teams.list(queries); return { offset, diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index e1072e247..67fb0e52c 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -23,7 +23,7 @@ InputTextarea, Label } from '$lib/elements/forms'; - import { formatCurrency } from '$lib/helpers/numbers.js'; + import { formatCurrency } from '$lib/helpers/numbers'; import { WizardSecondaryContainer, WizardSecondaryContent, @@ -40,7 +40,7 @@ type Organization } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; - import { confirmPayment } from '$lib/stores/stripe.js'; + import { confirmPayment } from '$lib/stores/stripe'; import { user } from '$lib/stores/user'; import { VARS } from '$lib/system'; import { onMount } from 'svelte'; @@ -212,7 +212,7 @@ async function upgrade() { try { //Add collaborators - var newCollaborators = []; + let newCollaborators = []; if (collaborators?.length) { newCollaborators = collaborators.filter( (collaborator) => @@ -371,11 +371,12 @@ {#if billingPlan !== BillingPlan.FREE && $organization.billingPlan !== billingPlan && $organization.billingPlan !== BillingPlan.CUSTOM} + currentTier={$organization.billingPlan} /> {:else if $organization.billingPlan !== BillingPlan.CUSTOM} {/if} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 6468cab57..4c9b760ca 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -29,7 +29,9 @@ export const load: LayoutLoad = async ({ depends, url, route }) => { if (account) { return { account, - organizations: isCloud ? await sdk.forConsole.billing.listOrganization() : await sdk.forConsole.teams.list() + organizations: isCloud + ? await sdk.forConsole.billing.listOrganization() + : await sdk.forConsole.teams.list() }; }