diff --git a/src/lib/commandCenter/searchers/organizations.ts b/src/lib/commandCenter/searchers/organizations.ts index 98302e96f3..bcbef363d2 100644 --- a/src/lib/commandCenter/searchers/organizations.ts +++ b/src/lib/commandCenter/searchers/organizations.ts @@ -4,7 +4,7 @@ import { sdk } from '$lib/stores/sdk'; import type { Searcher } from '../commands'; export const orgSearcher = (async (query: string) => { - const { teams } = await sdk.forConsole.teams.list(); + const { teams } = await sdk.forConsole.billing.listOrganization(); return teams .filter((organization) => organization.name.toLowerCase().includes(query.toLowerCase())) .map((organization) => { diff --git a/src/lib/components/billing/discountsApplied.svelte b/src/lib/components/billing/discountsApplied.svelte new file mode 100644 index 0000000000..028854d277 --- /dev/null +++ b/src/lib/components/billing/discountsApplied.svelte @@ -0,0 +1,25 @@ + + +{#if value > 0} + +
+

+

+
+ {#if value >= 100} +

Credits applied

+ {:else} + -{formatCurrency(value)} + {/if} +
+{/if} diff --git a/src/lib/components/billing/estimatedTotal.svelte b/src/lib/components/billing/estimatedTotal.svelte new file mode 100644 index 0000000000..1af444fd36 --- /dev/null +++ b/src/lib/components/billing/estimatedTotal.svelte @@ -0,0 +1,114 @@ + + +{#if error.length} + + + {error} + + +{:else if estimation} +
+ + {#each estimation.items ?? [] as item} + +

{item.label}

+

{formatCurrency(item.value)}

+
+ {/each} + {#each estimation.discounts ?? [] as item} + + {/each} +
+ +

+ Total due
+

+

+ {formatCurrency(estimation.grossAmount)} +

+
+ +

+ You'll pay {formatCurrency(estimation.amount)} now. Once + your credits run out, you'll be charged + {formatCurrency(estimation.amount)} every 30 days. +

+ + + + {#if budgetEnabled} +
+ +
+ {/if} +
+
+
+{/if} diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte index a6c8db6a7c..4d96f87c4d 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?.seats?.price ?? 0) + : isScaleDowngrade + ? currentOrgPlan.price + : 0; + + $: extraSeatsCost = (collaborators?.length ?? 0) * (selectedPlan?.addons?.seats?.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,64 @@ 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?.seats?.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 your first billing cycle starting on {!currentPlan.trialDays + >{!currentOrgPlan.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. + {formatCurrency(currentOrgPlan.price)} plus usage fees every 30 days.

diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts index 266d217d8a..0643e5922d 100644 --- a/src/lib/components/billing/index.ts +++ b/src/lib/components/billing/index.ts @@ -7,3 +7,5 @@ export { default as PlanComparisonBox } from './planComparisonBox.svelte'; export { default as EmptyCardCloud } from './emptyCardCloud.svelte'; export { default as CreditsApplied } from './creditsApplied.svelte'; export { default as PlanSelection } from './planSelection.svelte'; +export { default as EstimatedTotal } from './estimatedTotal.svelte'; +export { default as SelectPlan } from './selectPlan.svelte'; \ No newline at end of file diff --git a/src/lib/components/billing/planSelection.svelte b/src/lib/components/billing/planSelection.svelte index 0d484a9b40..4a6e1f7caf 100644 --- a/src/lib/components/billing/planSelection.svelte +++ b/src/lib/components/billing/planSelection.svelte @@ -76,31 +76,25 @@ - {#if $organization?.billingPlan === BillingPlan.SCALE} -
  • - - -
    -

    - {tierScale.name} - {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg} - Current plan - {/if} -

    -

    - {tierScale.description} -

    -

    - {formatCurrency(scalePlan?.price ?? 0)} per month + usage -

    -
    -
    -
    -
  • - {/if} +
  • + + +
    +

    + {tierScale.name} + {#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg} + Current plan + {/if} +

    +

    + {tierScale.description} +

    +

    + {formatCurrency(scalePlan?.price ?? 0)} per month + usage +

    +
    +
    +
    +
  • {/if} diff --git a/src/lib/components/billing/selectPlan.svelte b/src/lib/components/billing/selectPlan.svelte new file mode 100644 index 0000000000..4447c17e3f --- /dev/null +++ b/src/lib/components/billing/selectPlan.svelte @@ -0,0 +1,51 @@ + + +{#if billingPlan} +
      + {#each $plansInfo.values() as plan} +
    • + + +
      +

      + {plan.name} + {#if $organization?.billingPlan === plan.$id && !isNewOrg} + Current plan + {/if} +

      +

      + {plan.desc} +

      +

      + {formatCurrency(plan?.price ?? 0)} +

      +
      +
      +
      +
    • + {/each} +
    +{/if} diff --git a/src/lib/components/billing/usageRates.svelte b/src/lib/components/billing/usageRates.svelte index d8f57849dd..87f6b00287 100644 --- a/src/lib/components/billing/usageRates.svelte +++ b/src/lib/components/billing/usageRates.svelte @@ -81,16 +81,16 @@ {usage.resource} - {plan[usage.id] || 'Unlimited'} + {plan.addons.seats.limit || 0} {#if !isFree} - {formatCurrency(plan.addons.member.price)}/{usage?.unit} + {formatCurrency(plan.addons.seats.price)}/{usage?.unit} {/if} {:else} - {@const addon = plan.addons[usage.id]} + {@const addon = plan.usage[usage.id]} {usage.resource} diff --git a/src/lib/sdk/billing.ts b/src/lib/sdk/billing.ts index 95c9f5c77d..0d0824561d 100644 --- a/src/lib/sdk/billing.ts +++ b/src/lib/sdk/billing.ts @@ -1,5 +1,5 @@ import type { Client, Models } from '@appwrite.io/console'; -import type { Organization, OrganizationList } from '../stores/organization'; +import type { OrganizationError, Organization, OrganizationList } from '../stores/organization'; import type { PaymentMethod } from '@stripe/stripe-js'; import type { Tier } from '$lib/stores/billing'; import type { Campaign } from '$lib/stores/campaigns'; @@ -65,6 +65,31 @@ export type InvoiceList = { total: number; }; +export type Estimation = { + amount: number; + grossAmount: number; + credits: number; + discount: number; + items: EstimationItem[]; + discounts: EstimationItem[]; + trialDays: number; + trialEndDate: string | undefined; +} + +export type EstimationItem = { + label: string; + value: number; +} + +export type EstimationDeleteOrganization = { + amount: number; + grossAmount: number; + credits: number; + discount: number; + items: EstimationItem[], + unpaidInvoices: Invoice[]; +} + export type Coupon = { $id: string; code: string; @@ -146,10 +171,12 @@ export type Aggregation = { * Total amount of the invoice. */ amount: number; + additionalMembers: number; + /** * Price for additional members */ - additionalMembers: number; + additionalMemberAmount: number; /** * Total storage usage. */ @@ -174,6 +201,10 @@ export type Aggregation = { * Usage logs for the billing period. */ resources: OrganizationUsage; + /** + * Aggregation billing plan + */ + plan: string; }; export type OrganizationUsage = { @@ -257,13 +288,25 @@ export type AdditionalResource = { multiplier?: number; }; +export type PlanAddon = { + supported: boolean; + currency: string; + invoiceDesc: string; + price: number; + limit: number, + value: number; + type: string; + +} + export type Plan = { $id: string; name: string; + desc: string; price: number; + order: number; bandwidth: number; storage: number; - members: number; webhooks: number; users: number; teams: number; @@ -275,7 +318,7 @@ export type Plan = { realtime: number; logs: number; authPhone: number; - addons: { + usage: { bandwidth: AdditionalResource; executions: AdditionalResource; member: AdditionalResource; @@ -283,6 +326,9 @@ export type Plan = { storage: AdditionalResource; users: AdditionalResource; }; + addons: { + seats: PlanAddon + } trialDays: number; isAvailable: boolean; selfService: boolean; @@ -292,9 +338,10 @@ export type Plan = { backupsEnabled: boolean; backupPolicies: number; emailBranding: boolean; + supportsCredit: boolean; }; -export type PlansInfo = { +export type PlanList = { plans: Plan[]; total: number; }; @@ -329,20 +376,45 @@ export class Billing { ); } + async validateOrganization(organizationId: string, invites: string[]): Promise { + const path = `/organizations/${organizationId}/validate`; + const params = { + organizationId, + invites + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'PATCH', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async createOrganization( organizationId: string, name: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string = undefined - ): Promise { + billingAddressId: string = null, + couponId: string = null, + invites: Array = [], + budget: number = undefined, + taxId: string = null + ): Promise { const path = `/organizations`; const params = { organizationId, name, billingPlan, paymentMethodId, - billingAddressId + billingAddressId, + couponId, + invites, + budget, + taxId }; const uri = new URL(this.client.config.endpoint + path); return await this.client.call( @@ -355,6 +427,28 @@ export class Billing { ); } + async estimationCreateOrganization( + billingPlan: string, + couponId: string = null, + invites: Array = [], + ): Promise { + const path = `/organizations/estimations/create-organization`; + const params = { + billingPlan, + couponId, + invites, + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + async deleteOrganization(organizationId: string): Promise { const path = `/organizations/${organizationId}`; const params = { @@ -371,19 +465,39 @@ export class Billing { ); } - async getPlan(organizationId: string): Promise { + async estimationDeleteOrganization(organizationId: string): Promise { + const path = `/organizations/${organizationId}/estimations/delete-organization`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + } + ); + } + + async getOrganizationPlan(organizationId: string): Promise { const path = `/organizations/${organizationId}/plan`; - const params = { - organizationId - }; const uri = new URL(this.client.config.endpoint + path); 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); + return await this.client.call( + 'get', + uri, + { + 'content-type': 'application/json' + } ); } @@ -399,14 +513,22 @@ export class Billing { organizationId: string, billingPlan: string, paymentMethodId: string, - billingAddressId: string = undefined - ): Promise { + billingAddressId: string = undefined, + couponId: string = null, + invites: Array = [], + budget: number = undefined, + taxId: string = null + ): Promise { const path = `/organizations/${organizationId}/plan`; const params = { organizationId, billingPlan, paymentMethodId, - billingAddressId + billingAddressId, + couponId, + invites, + budget, + taxId }; const uri = new URL(this.client.config.endpoint + path); return await this.client.call( @@ -419,6 +541,43 @@ export class Billing { ); } + async estimationUpdatePlan( + organizationId: string, + billingPlan: string, + couponId: string = null, + invites: Array = [], + ): Promise { + const path = `/organizations/${organizationId}/estimations/update-plan`; + const params = { + billingPlan, + couponId, + invites, + }; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + }, + params + ); + } + + async cancelDowngrade( + organizationId: string + ): Promise { + const path = `/organizations/${organizationId}/plan/cancel`; + const uri = new URL(this.client.config.endpoint + path); + return await this.client.call( + 'patch', + uri, + { + 'content-type': 'application/json' + } + ); + } + async updateBudget( organizationId: string, budget: number, @@ -1112,7 +1271,7 @@ export class Billing { ); } - async getPlansInfo(): Promise { + async getPlansInfo(): Promise { const path = `/console/plans`; const params = {}; const uri = new URL(this.client.config.endpoint + path); diff --git a/src/lib/stores/billing.ts b/src/lib/stores/billing.ts index 9b42358d15..6604469cb2 100644 --- a/src/lib/stores/billing.ts +++ b/src/lib/stores/billing.ts @@ -273,7 +273,7 @@ export async function checkForUsageLimit(org: Organization) { const members = org.total; const plan = get(plansInfo)?.get(org.billingPlan); - const membersOverflow = members > plan.members ? members - (plan.members || members) : 0; + const membersOverflow = (members - 1) > plan.addons.seats.limit ? members - (plan.addons.seats.limit || members) : 0; if (resources.some((r) => r.value >= 100) || membersOverflow > 0) { readOnly.set(true); @@ -476,7 +476,7 @@ export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: n storage: calculateResourceSurplus(usage?.storageTotal, plan.storage, 'GB'), users: calculateResourceSurplus(usage?.usersTotal, plan.users), executions: calculateResourceSurplus(usage?.executionsTotal, plan.executions, 'GB'), - members: calculateResourceSurplus(members, plan.members) + members: calculateResourceSurplus(members - 1, plan.addons.seats.limit || 0) }; } diff --git a/src/lib/stores/organization.ts b/src/lib/stores/organization.ts index 128962b731..dcfd0a5621 100644 --- a/src/lib/stores/organization.ts +++ b/src/lib/stores/organization.ts @@ -4,6 +4,15 @@ import type { Models } from '@appwrite.io/console'; import type { Tier } from './billing'; import type { Plan } from '$lib/sdk/billing'; +export type OrganizationError = { + status: number; + message: string; + teamId: string; + invoiceId: string; + clientSecret: string; + type: string; +}; + export type Organization = Models.Team> & { billingBudget: number; billingPlan: Tier; @@ -21,6 +30,8 @@ export type Organization = Models.Team> & { amount: number; billingTaxId?: string; billingPlanDowngrade?: Tier; + billingAggregationId: string; + billingInvoiceId: string; }; export type OrganizationList = { diff --git a/src/lib/stores/stripe.ts b/src/lib/stores/stripe.ts index 58edfd4316..e62110d209 100644 --- a/src/lib/stores/stripe.ts +++ b/src/lib/stores/stripe.ts @@ -117,7 +117,8 @@ export async function confirmPayment( orgId: string, clientSecret: string, paymentMethodId: string, - route?: string + route?: string, + returnError: boolean = false ) { try { const url = @@ -133,6 +134,9 @@ export async function confirmPayment( } }); if (error) { + if (returnError) { + return error; + } throw error.message; } } catch (e) { diff --git a/src/routes/(console)/account/organizations/+page.ts b/src/routes/(console)/account/organizations/+page.ts index 0b89b1d302..237a3772d7 100644 --- a/src/routes/(console)/account/organizations/+page.ts +++ b/src/routes/(console)/account/organizations/+page.ts @@ -3,19 +3,22 @@ import { sdk } from '$lib/stores/sdk'; import { getLimit, getPage, pageToOffset } from '$lib/helpers/load'; import { CARD_LIMIT } from '$lib/constants'; import type { PageLoad } from './$types'; +import { isCloud } from '$lib/system'; export const load: PageLoad = async ({ url, route }) => { const page = getPage(url); const limit = getLimit(url, route, CARD_LIMIT); const offset = pageToOffset(page, limit); + const queries = [Query.offset(offset), Query.limit(limit), Query.orderDesc('')]; + + const organizations = isCloud + ? await sdk.forConsole.billing.listOrganization(queries) + : sdk.forConsole.teams.list(queries); + return { offset, limit, - organizations: await sdk.forConsole.teams.list([ - Query.offset(offset), - Query.limit(limit), - Query.orderDesc('') - ]) + organizations }; }; diff --git a/src/routes/(console)/apply-credit/+page.svelte b/src/routes/(console)/apply-credit/+page.svelte index 95af6175f5..588bb5697b 100644 --- a/src/routes/(console)/apply-credit/+page.svelte +++ b/src/routes/(console)/apply-credit/+page.svelte @@ -3,11 +3,7 @@ import { base } from '$app/paths'; import { page } from '$app/stores'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; - import { - CreditsApplied, - EstimatedTotalBox, - SelectPaymentMethod - } from '$lib/components/billing'; + import { CreditsApplied, EstimatedTotal, SelectPaymentMethod } from '$lib/components/billing'; import { BillingPlan, Dependencies } from '$lib/constants'; import { Button, Form, FormList, InputSelect, InputTags, InputText } from '$lib/elements/forms'; import { toLocaleDate } from '$lib/helpers/date'; @@ -290,12 +286,12 @@ {:else if selectedOrgId}
    - + couponId={couponData.code}> {#if campaign?.template === 'review' && (campaign?.cta || campaign?.claimed || campaign?.unclaimed)}

    {campaign?.cta}

    @@ -308,7 +304,7 @@

    {/if} -
    +
    {/if} diff --git a/src/routes/(console)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte index 63cc1f2100..b1a8f28c87 100644 --- a/src/routes/(console)/create-organization/+page.svelte +++ b/src/routes/(console)/create-organization/+page.svelte @@ -4,11 +4,13 @@ import { page } from '$app/stores'; import { Submit, trackError, trackEvent } from '$lib/actions/analytics'; import { - EstimatedTotalBox, PlanComparisonBox, - PlanSelection, - SelectPaymentMethod + SelectPaymentMethod, + + SelectPlan + } from '$lib/components/billing'; + import EstimatedTotal from '$lib/components/billing/estimatedTotal.svelte'; import ValidateCreditModal from '$lib/components/billing/validateCreditModal.svelte'; import Default from '$lib/components/roles/default.svelte'; import { BillingPlan, Dependencies } from '$lib/constants'; @@ -21,8 +23,13 @@ import type { Coupon, PaymentList } from '$lib/sdk/billing'; import { tierToPlan } from '$lib/stores/billing'; import { addNotification } from '$lib/stores/notifications'; - import { organizationList, type Organization } from '$lib/stores/organization'; + import { + organizationList, + type OrganizationError, + type Organization + } from '$lib/stores/organization'; import { sdk } from '$lib/stores/sdk'; + import { confirmPayment } from '$lib/stores/stripe'; import { ID } from '@appwrite.io/console'; import { onMount } from 'svelte'; import { writable } from 'svelte/store'; @@ -46,6 +53,7 @@ let billingPlan: BillingPlan = BillingPlan.FREE; let paymentMethodId: string; let collaborators: string[] = []; + let couponId: string | undefined; let couponData: Partial = { code: null, status: null, @@ -58,17 +66,7 @@ onMount(async () => { if ($page.url.searchParams.has('coupon')) { - const coupon = $page.url.searchParams.get('coupon'); - try { - const response = await sdk.forConsole.billing.getCoupon(coupon); - couponData = response; - } catch (e) { - couponData = { - code: null, - status: null, - credits: null - }; - } + couponId = $page.url.searchParams.get('coupon'); } if ($page.url.searchParams.has('name')) { name = $page.url.searchParams.get('name'); @@ -86,6 +84,14 @@ ) { billingPlan = BillingPlan.PRO; } + if ($page.url.searchParams.has('type')) { + const type = $page.url.searchParams.get('type'); + if (type === 'payment_confirmed') { + const organizationId = $page.url.searchParams.get('id'); + const invites = $page.url.searchParams.getAll('invites'); + await validate(organizationId, invites); + } + } }); async function loadPaymentMethods() { @@ -93,9 +99,33 @@ paymentMethodId = methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null; } + function isOrganization(org: Organization | OrganizationError): org is Organization { + return (org as Organization).$id !== undefined; + } + + async function validate(organizationId: string, invites: string[]) { + try { + let org = await sdk.forConsole.billing.validateOrganization(organizationId, invites); + if (isOrganization(org)) { + await preloadData(`${base}/console/organization-${org.$id}`); + await goto(`${base}/console/organization-${org.$id}`); + addNotification({ + type: 'success', + message: `${org.name ?? 'Organization'} has been created` + }); + } + } catch (e) { + addNotification({ + type: 'error', + message: e.message + }); + trackError(e, Submit.OrganizationCreate); + } + } + async function create() { try { - let org: Organization; + let org: Organization | OrganizationError; if (billingPlan === BillingPlan.FREE) { org = await sdk.forConsole.billing.createOrganization( @@ -111,37 +141,29 @@ name, billingPlan, paymentMethodId, - null + null, + couponId, + collaborators, + billingBudget, + taxId ); - //Add budget - if (billingBudget) { - await sdk.forConsole.billing.updateBudget(org.$id, billingBudget, [75]); - } - - //Add coupon - if (couponData?.code) { - await sdk.forConsole.billing.addCredit(org.$id, couponData.code); - trackEvent(Submit.CreditRedeem); - } - - //Add collaborators - if (collaborators?.length) { - collaborators.forEach(async (collaborator) => { - await sdk.forConsole.teams.createMembership( - org.$id, - ['developer'], - collaborator, - undefined, - undefined, - `${$page.url.origin}${base}/invite` - ); - }); - } - - // Add tax ID - if (taxId) { - await sdk.forConsole.billing.updateTaxId(org.$id, taxId); + if (!isOrganization(org) && org.status == 402) { + let clientSecret = org.clientSecret; + let params = new URLSearchParams(); + params.append('type', 'payment_confirmed'); + params.append('id', org.teamId); + for (let index = 0; index < collaborators.length; index++) { + const invite = collaborators[index]; + params.append('invites', invite); + } + await confirmPayment( + '', + clientSecret, + paymentMethodId, + '/console/create-organization?' + params.toString() + ); + await validate(org.teamId, collaborators); } } @@ -151,13 +173,15 @@ members_invited: collaborators?.length }); - await invalidate(Dependencies.ACCOUNT); - await preloadData(`${base}/organization-${org.$id}`); - await goto(`${base}/organization-${org.$id}`); - addNotification({ - type: 'success', - message: `${name ?? 'Organization'} has been created` - }); + if (isOrganization(org)) { + await invalidate(Dependencies.ACCOUNT); + await preloadData(`${base}/organization-${org.$id}`); + await goto(`${base}/organization-${org.$id}`); + addNotification({ + type: 'success', + message: `${org.name ?? 'Organization'} has been created` + }); + } } catch (e) { addNotification({ type: 'error', @@ -193,7 +217,7 @@ For more details on our plans, visit our .

    - + {#if billingPlan !== BillingPlan.FREE} + - {#if !couponData?.code} - - {/if} {/if} {#if billingPlan !== BillingPlan.FREE} - + {couponId} + /> {:else} {/if} diff --git a/src/routes/(console)/organization-[organization]/+layout.ts b/src/routes/(console)/organization-[organization]/+layout.ts index 650990e866..cd1968ea8e 100644 --- a/src/routes/(console)/organization-[organization]/+layout.ts +++ b/src/routes/(console)/organization-[organization]/+layout.ts @@ -27,7 +27,7 @@ export const load: LayoutLoad = async ({ params, depends }) => { const res = await sdk.forConsole.billing.getRoles(params.organization); roles = res.roles; scopes = res.scopes; - currentPlan = await sdk.forConsole.billing.getPlan(params.organization); + currentPlan = await sdk.forConsole.billing.getOrganizationPlan(params.organization); if (scopes.includes('billing.read')) { await failedInvoice.load(params.organization); if (get(failedInvoice)) { diff --git a/src/routes/(console)/organization-[organization]/billing/+page.svelte b/src/routes/(console)/organization-[organization]/billing/+page.svelte index 017a1dec08..cc0fb72d17 100644 --- a/src/routes/(console)/organization-[organization]/billing/+page.svelte +++ b/src/routes/(console)/organization-[organization]/billing/+page.svelte @@ -1,6 +1,6 @@ + +
    + +

    + Your organization is set to change to + {tierToPlan($organization?.billingPlanDowngrade).name} + plan on {toLocaleDate($organization.billingNextInvoiceDate)}. +

    +

    Are you sure you want to cancel the change?

    + + + + +
    +
    diff --git a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte index ea6265f7c1..5ee85510ca 100644 --- a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte +++ b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte @@ -49,8 +49,6 @@ invoiceList = await sdk.forConsole.billing.listInvoices($page.params.organization, [ Query.limit(limit), Query.offset(offset), - Query.notEqual('from', $organization.billingCurrentInvoiceDate), - Query.notEqual('status', 'pending'), Query.orderDesc('$createdAt') ]); } @@ -64,8 +62,7 @@ request(); } - -{#if $organization?.billingPlan === BillingPlan.FREE && invoiceList.total > 0} +{#if $organization?.billingPlan !== BillingPlan.FREE && invoiceList.total > 0} Payment history diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte index ab9e3ae106..3d8a89c6d4 100644 --- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte +++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte @@ -5,28 +5,29 @@ import { toLocaleDate } from '$lib/helpers/date'; import { plansInfo, tierToPlan, upgradeURL } from '$lib/stores/billing'; import { organization } from '$lib/stores/organization'; - import type { CreditList, Invoice, Plan } from '$lib/sdk/billing'; + import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing'; import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers'; import { humanFileSize } from '$lib/helpers/sizeConvertion'; import { BillingPlan } from '$lib/constants'; import { trackEvent } from '$lib/actions/analytics'; import { tooltip } from '$lib/actions/tooltip'; import { type Models } from '@appwrite.io/console'; + import CancelDowngradeModel from './cancelDowngradeModel.svelte'; - export let invoices: Array; export let members: Models.MembershipList; export let currentPlan: Plan; export let creditList: CreditList; + export let currentInvoice: Invoice | undefined = undefined; + export let currentAggregation: Aggregation | undefined = undefined + + let showCancel: boolean = false; - const currentInvoice: Invoice | undefined = invoices.length > 0 ? invoices[0] : undefined; - const extraMembers = members.total > 1 ? members.total - 1 : 0; const availableCredit = creditList.available; const today = new Date(); const isTrial = new Date($organization?.billingStartDate).getTime() - today.getTime() > 0 && $plansInfo.get($organization.billingPlan)?.trialDays; const extraUsage = currentInvoice ? currentInvoice.amount - currentPlan?.price : 0; - const extraAddons = currentInvoice ? currentInvoice.usage?.length : 0; {#if $organization} @@ -39,9 +40,7 @@

    - Billing period: {toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate( - $organization?.billingNextInvoiceDate - )} + Due at: {toLocaleDate($organization?.billingNextInvoiceDate)}

    @@ -59,12 +58,12 @@
    - {#if $organization?.billingPlan !== BillingPlan.FREE && $organization?.billingPlan !== BillingPlan.GITHUB_EDUCATION && extraUsage > 0} + {#if currentPlan.budgeting && extraUsage > 0} Add-ons{extraMembers ? extraAddons + 1 : extraAddons} + >{currentAggregation.additionalMembers > 0 ? currentInvoice.usage.length + 1 : currentInvoice.usage.length}
    @@ -78,7 +77,7 @@