diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index 6c21031874..846a887f2d 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -1,81 +1,47 @@ - - - {#if $organization?.billingPlan === BillingPlan.FREE && !isNewOrg} - - {/if} - - - {tierFree.description} - - - {formatCurrency(freePlan?.price ?? 0)} - - - - - {#if $organization?.billingPlan === BillingPlan.PRO && !isNewOrg} - - {/if} - - - {tierPro.description} - - - {formatCurrency(proPlan?.price ?? 0)} per month + usage - - - - - {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg} - - {/if} - - - {tierScale.description} - - - {formatCurrency(scalePlan?.price ?? 0)} per month + usage - - - {#if $currentPlan && !isBasePlan} + {#each plans as plan} + + + {#if $organization?.billingPlan === plan.$id && !isNewOrg} + + {/if} + + + {plan.desc} + + + {@const isZeroPrice = (plan.price ?? 0) <= 0} + {@const price = formatCurrency(plan.price ?? 0)} + {isZeroPrice ? price : `${price} per month + usage`} + + + {/each} + {#if $currentPlan && !currentPlanInList} ; +export type PlansMap = Map; export type Roles = { scopes: string[]; @@ -492,6 +566,22 @@ export class Billing { }); } + async listPlans(queries: string[] = []): Promise { + const path = `/console/plans`; + const uri = new URL(this.client.config.endpoint + path); + const params = { + queries + }; + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async getPlan(planId: string): Promise { const path = `/console/plans/${planId}`; const uri = new URL(this.client.config.endpoint + path); @@ -835,7 +925,7 @@ export class Billing { ); } - async getAggregation(organizationId: string, aggregationId: string): Promise { + async getAggregation(organizationId: string, aggregationId: string): Promise { const path = `/organizations/${organizationId}/aggregations/${aggregationId}`; const params = { organizationId, diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 42608481f2..141087eaf4 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -163,7 +163,7 @@ export function getServiceLimit(serviceId: PlanServices, tier: Tier = null, plan // plan > addons > seats/others if (serviceId === 'members') { // some don't include `limit`, so we fallback! - return plan?.['addons']['seats']['limit'] ?? 1; + return (plan?.['addons']['seats'] || [])['limit'] ?? 1; } return plan?.[serviceId] ?? 0; @@ -323,7 +323,8 @@ export async function checkForProjectsLimit(org: Organization, projects: number) const plan = await sdk.forConsole.billing.getOrganizationPlan(org.$id); if (!plan) return; if (plan.$id !== BillingPlan.FREE) return; - if (org.projects?.length > 0) return; + if (!org.projects) return; + if (org.projects.length > 0) return; if (plan.projects > 0 && projects > plan.projects) { headerAlert.add({ diff --git a/src/routes/(console)/+layout.ts b/src/routes/(console)/+layout.ts index e6d2fb24ac..da660d67fa 100644 --- a/src/routes/(console)/+layout.ts +++ b/src/routes/(console)/+layout.ts @@ -1,7 +1,7 @@ +import { Dependencies } from '$lib/constants'; import { sdk } from '$lib/stores/sdk'; import { isCloud } from '$lib/system'; import type { LayoutLoad } from './$types'; -import { Dependencies } from '$lib/constants'; import type { Tier } from '$lib/stores/billing'; import type { Plan, PlanList } from '$lib/sdk/billing'; import { Query } from '@appwrite.io/console'; @@ -50,6 +50,7 @@ export const load: LayoutLoad = async ({ depends, parent }) => { plansInfo, roles: [], scopes: [], + projects, preferences, currentOrgId, organizations, diff --git a/src/routes/(console)/create-organization/+page.ts b/src/routes/(console)/create-organization/+page.ts index 690baa2bbe..57d6280fe4 100644 --- a/src/routes/(console)/create-organization/+page.ts +++ b/src/routes/(console)/create-organization/+page.ts @@ -7,10 +7,10 @@ import type { Organization } from '$lib/stores/organization'; export const load: PageLoad = async ({ url, parent, depends }) => { const { organizations } = await parent(); depends(Dependencies.ORGANIZATIONS); - - const [coupon, paymentMethods] = await Promise.all([ + const [coupon, paymentMethods, plans] = await Promise.all([ getCoupon(url), - sdk.forConsole.billing.listPaymentMethods() + sdk.forConsole.billing.listPaymentMethods(), + sdk.forConsole.billing.listPlans() ]); let plan = getPlanFromUrl(url); const hasFreeOrganizations = organizations.teams?.some( @@ -24,6 +24,7 @@ export const load: PageLoad = async ({ url, parent, depends }) => { return { plan, coupon, + plans, hasFreeOrganizations, paymentMethods, name: url.searchParams.get('name') ?? '' diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index 3f27119c9a..11b2e824c2 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -5,7 +5,7 @@ import { toLocaleDate } from '$lib/helpers/date'; import { plansInfo, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { Aggregation, Invoice, Plan } from '$lib/sdk/billing'; + import type { AggregationTeam, Invoice, Plan } from '$lib/sdk/billing'; import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers'; import { BillingPlan } from '$lib/constants'; import { Click, trackEvent } from '$lib/actions/analytics'; @@ -15,6 +15,7 @@ Divider, Icon, Layout, + Table, Tooltip, Typography } from '@appwrite.io/pink-svelte'; @@ -23,8 +24,8 @@ export let currentPlan: Plan; export let currentInvoice: Invoice | undefined = undefined; + export let currentAggregation: AggregationTeam | undefined = undefined; export let availableCredit: number | undefined = undefined; - export let currentAggregation: Aggregation | undefined = undefined; let showCancel: boolean = false; @@ -36,63 +37,129 @@ {#if $organization} - - Payment estimates - A breakdown of your estimated upcoming payment for the current billing period. Totals displayed - exclude accumulated credits and applicable taxes. - -

- Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} -

- - - - - {currentPlan.name} plan - - - {isTrial || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION - ? formatCurrency(0) - : currentPlan - ? formatCurrency(currentPlan?.price) - : ''} - - + {#if currentPlan.usagePerProject} + + + + {currentPlan.name} plan + + + Next payment of ${currentAggregation.amount} + will occur on + {toLocaleDate($organization?.billingNextInvoiceDate)} + + + + + + + + Base plan + + {isTrial || + $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} + + + + + {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0 && Object.keys(currentPlan.addons).includes(r.resourceId) && currentPlan.addons[r.resourceId].price > 0) as excess, i} + {#if i > 0} + + {/if} + + + + {excess.resourceId} + + {formatCurrency(excess.amount)} + + + + + {/each} + {#each currentAggregation.projectBreakdown as projectBreakdown} + + + {formatCurrency(projectBreakdown.amount)} + + + {#each projectBreakdown.resources as resource, i} + {#if i > 0} + + {/if} - {#if currentPlan.budgeting && extraUsage > 0} - 0 - ? currentInvoice.usage.length + 1 - : currentInvoice.usage.length - ).toString()}> - - {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} - - - {#if currentAggregation.additionalMembers} - - - Additional members - - {formatCurrency( - currentAggregation.additionalMemberAmount - )} - - - - {currentAggregation.additionalMembers} - + + + + {resource.resourceId} + + + {formatCurrency(resource.amount)} + - {/if} - {#if currentInvoice?.usage} - {#each currentInvoice.usage as excess, i} - {#if i > 0 || currentAggregation.additionalMembers} + + + + {formatNumberWithCommas(resource.value)} + + {abbreviateNumber(resource.value)} + + + + {/each} + + + {/each} + + + {:else} + + Payment estimates + A breakdown of your estimated upcoming payment for the current billing period. Totals displayed + exclude accumulated credits and applicable taxes. + +

+ Due at: {toLocaleDate($organization?.billingNextInvoiceDate)} +

+ + + + + {currentPlan.name} plan + + + {isTrial || + $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION + ? formatCurrency(0) + : currentPlan + ? formatCurrency(currentPlan?.price) + : ''} + + + + {#if currentPlan.budgeting && extraUsage > 0} + r.amount && r.amount > 0) + .length.toString()}> + + {formatCurrency(extraUsage >= 0 ? extraUsage : 0)} + + + {#each currentAggregation.resources.filter((r) => r.amount && r.amount > 0) as excess, i} + {#if i > 0} {/if} @@ -101,7 +168,7 @@ direction="row" justifyContent="space-between"> - {excess.name} + {excess.resourceId} {formatCurrency(excess.amount)} @@ -119,98 +186,101 @@ {/each} - {/if} -
- - {/if} +
+ + {/if} - {#if currentPlan.supportsCredits && availableCredit > 0} - - - - Credits to be applied + {#if currentPlan.supportsCredits && availableCredit > 0} + + + + Credits to be applied + + + -{formatCurrency( + Math.min(availableCredit, currentInvoice?.amount ?? 0) + )} + - - -{formatCurrency( - Math.min(availableCredit, currentInvoice?.amount ?? 0) - )} - - - {/if} + {/if} - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} - - - - - Current total (USD) - - - - Estimates are updated daily and may differ from your - final invoice. - - - - - - {formatCurrency( - Math.max( - (currentInvoice?.amount ?? 0) - - Math.min(availableCredit, currentInvoice?.amount ?? 0), - 0 - ) - )} - - - {/if} - - - - - {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} -
- - -
- {:else} -
- {#if $organization?.billingPlanDowngrade !== null} - - {:else} + {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION} + + + + + Current total (USD) + + + + Estimates are updated daily and may differ from your + final invoice. + + + + + + {formatCurrency( + Math.max( + (currentInvoice?.amount ?? 0) - + Math.min( + availableCredit, + currentInvoice?.amount ?? 0 + ), + 0 + ) + )} + + + {/if} + + + + + {#if $organization?.billingPlan === BillingPlan.FREE || $organization?.billingPlan === BillingPlan.GITHUB_EDUCATION} +
+ - {/if} - -
- {/if} -
- +
+ {:else} +
+ {#if $organization?.billingPlanDowngrade !== null} + + {:else} + + {/if} + +
+ {/if} +
+ + {/if} {/if} diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte index e0bc8eb0c5..f899405b0b 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.svelte +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.svelte @@ -262,8 +262,8 @@ } } - $: isUpgrade = $plansInfo.get(selectedPlan).order > $currentPlan?.order; - $: isDowngrade = $plansInfo.get(selectedPlan).order < $currentPlan?.order; + $: isUpgrade = $plansInfo.get(selectedPlan)?.order > $currentPlan?.order; + $: isDowngrade = $plansInfo.get(selectedPlan)?.order < $currentPlan?.order; $: isButtonDisabled = $organization?.billingPlan === selectedPlan; diff --git a/src/routes/(console)/organization-[organization]/change-plan/+page.ts b/src/routes/(console)/organization-[organization]/change-plan/+page.ts index ecb712008f..5f1af0a4e9 100644 --- a/src/routes/(console)/organization-[organization]/change-plan/+page.ts +++ b/src/routes/(console)/organization-[organization]/change-plan/+page.ts @@ -1,11 +1,15 @@ import type { PageLoad } from './$types'; import type { Organization } from '$lib/stores/organization'; import { BillingPlan, Dependencies } from '$lib/constants'; +import { sdk } from '$lib/stores/sdk'; export const load: PageLoad = async ({ depends, parent }) => { const { members, currentPlan, organizations } = await parent(); depends(Dependencies.UPGRADE_PLAN); + const [plans] = await Promise.all([ + sdk.forConsole.billing.listPlans() + ]); let plan: BillingPlan; if (currentPlan?.$id === BillingPlan.SCALE) { @@ -22,6 +26,7 @@ export const load: PageLoad = async ({ depends, parent }) => { return { members, plan, + plans, selfService, hasFreeOrgs };