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 chargeDue 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
pricing page .
-
+
{#if billingPlan !== BillingPlan.FREE}
+
- {#if !couponData?.code}
- (showCreditModal = true)}>
- Add credits
-
- {/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?
+
+ (showCancel = false)}>No
+ Yes
+
+
+
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 @@
{/if}
- {#if $organization?.billingPlan !== BillingPlan.FREE && availableCredit > 0}
+ {#if currentPlan.supportsCredit && availableCredit > 0}
-
- trackEvent('click_organization_plan_update', {
- from: 'button',
- source: 'billing_tab'
- })}>
- Change plan
-
+ {#if $organization?.billingPlanDowngrade !== null}
+ (showCancel = true)}>Cancel change
+ {:else}
+
+ trackEvent('click_organization_plan_update', {
+ from: 'button',
+ source: 'billing_tab'
+ })}>
+ Change plan
+
+ {/if}
View estimated usage
@@ -250,6 +251,7 @@
{/if}
+