diff --git a/playwright.config.ts b/playwright.config.ts
index 3f39d87611..fe964db382 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -12,7 +12,7 @@ const config: PlaywrightTestConfig = {
webServer: {
timeout: 120000,
env: {
- PUBLIC_APPWRITE_ENDPOINT: 'https://console-testing-2.appwrite.org/v1',
+ PUBLIC_APPWRITE_ENDPOINT: 'https://dlbillingic.appwrite.org/v1',
PUBLIC_CONSOLE_MODE: 'cloud',
PUBLIC_STRIPE_KEY:
'pk_test_51LT5nsGYD1ySxNCyd7b304wPD8Y1XKKWR6hqo6cu3GIRwgvcVNzoZv4vKt5DfYXL1gRGw4JOqE19afwkJYJq1g3K004eVfpdWn'
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/alerts/newDevUpgradePro.svelte b/src/lib/components/billing/alerts/newDevUpgradePro.svelte
index c58ad72683..7f8c30878a 100644
--- a/src/lib/components/billing/alerts/newDevUpgradePro.svelte
+++ b/src/lib/components/billing/alerts/newDevUpgradePro.svelte
@@ -2,7 +2,7 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { trackEvent } from '$lib/actions/analytics';
- import { BillingPlan } from '$lib/constants';
+ import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import { Button } from '$lib/elements/forms';
import { organization } from '$lib/stores/organization';
import { activeHeaderAlert } from '$routes/(console)/store';
@@ -29,7 +29,7 @@
secondary
fullWidthMobile
class="u-line-height-1"
- href={`${base}/apply-credit?code=appw50&org=${$organization.$id}`}
+ href={`${base}/apply-credit?code=${NEW_DEV_PRO_UPGRADE_COUPON}&org=${$organization.$id}`}
on:click={() => {
trackEvent('click_credits_redeem', {
from: 'button',
diff --git a/src/lib/components/billing/discountsApplied.svelte b/src/lib/components/billing/discountsApplied.svelte
new file mode 100644
index 0000000000..433d02ecf0
--- /dev/null
+++ b/src/lib/components/billing/discountsApplied.svelte
@@ -0,0 +1,47 @@
+
+
+{#if value > 0}
+
+
+
+
+
+ {label}
+
+
+ {#if !fixedCoupon && label.toLowerCase() === 'credits'}
+
+ (couponData = {
+ code: null,
+ status: null,
+ credits: null
+ })}>
+
+
+ {/if}
+
+ {#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..c0e5c52333
--- /dev/null
+++ b/src/lib/components/billing/estimatedTotal.svelte
@@ -0,0 +1,146 @@
+
+
+{#if estimation}
+
+
+ {#if estimation}
+ {#each estimation.items ?? [] as item}
+ {#if item.value > 0}
+
+ {item.label}
+ {formatCurrency(item.value)}
+
+ {/if}
+ {/each}
+ {#each estimation.discounts ?? [] as item}
+
+ {/each}
+
+
+ Total due
+
+ {formatCurrency(estimation.grossAmount)}
+
+
+
+
+ You'll pay {formatCurrency(estimation.grossAmount)} now.
+ {#if couponData?.code}Once your credits run out,{:else}Then{/if} you'll be charged
+ {formatCurrency(estimation.amount)} every 30 days.
+
+ {/if}
+
+
+
+ {#if budgetEnabled}
+
+
+
+ {/if}
+
+
+
+{/if}
diff --git a/src/lib/components/billing/estimatedTotalBox.svelte b/src/lib/components/billing/estimatedTotalBox.svelte
deleted file mode 100644
index 474ed59f8b..0000000000
--- a/src/lib/components/billing/estimatedTotalBox.svelte
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
- {currentPlan.name} plan
- {formatCurrency(currentPlan.price)}
-
-
- Additional seats ({collaborators?.length})
-
- {formatCurrency(extraSeatsCost)}
-
-
- {#if couponData?.status === 'active'}
-
- {/if}
-
-
-
- Upcoming chargeDue on {!currentPlan.trialDays
- ? toLocaleDate(billingPayDate.toString())
- : toLocaleDate(trialEndDate.toString())}
-
-
- {formatCurrency(estimatedTotal)}
-
-
-
-
- You'll pay {formatCurrency(estimatedTotal)} now, with your first
- billing cycle starting on
- {!currentPlan.trialDays
- ? toLocaleDate(billingPayDate.toString())
- : toLocaleDate(trialEndDate.toString())} . {#if couponData?.status === 'active'}Once your credits run out, you'll be charged
- {formatCurrency(currentPlan.price)} plus usage fees every 30
- days.
- {/if}
-
-
-
-
- {#if budgetEnabled}
-
-
-
- {/if}
-
-
-
diff --git a/src/lib/components/billing/index.ts b/src/lib/components/billing/index.ts
index 266d217d8a..1cc70d3d99 100644
--- a/src/lib/components/billing/index.ts
+++ b/src/lib/components/billing/index.ts
@@ -2,8 +2,9 @@ export { default as PaymentBoxes } from './paymentBoxes.svelte';
export { default as CouponInput } from './couponInput.svelte';
export { default as SelectPaymentMethod } from './selectPaymentMethod.svelte';
export { default as UsageRates } from './usageRates.svelte';
-export { default as EstimatedTotalBox } from './estimatedTotalBox.svelte';
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';
diff --git a/src/lib/components/billing/planExcess.svelte b/src/lib/components/billing/planExcess.svelte
index 352f0b29b7..97f56e9d8a 100644
--- a/src/lib/components/billing/planExcess.svelte
+++ b/src/lib/components/billing/planExcess.svelte
@@ -12,18 +12,16 @@
import { calculateExcess, plansInfo, tierToPlan, type Tier } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { toLocaleDate } from '$lib/helpers/date';
- import { Button } from '$lib/elements/forms';
import { humanFileSize } from '$lib/helpers/sizeConvertion';
import { abbreviateNumber } from '$lib/helpers/numbers';
import { formatNum } from '$lib/helpers/string';
import { onMount } from 'svelte';
- import type { OrganizationUsage } from '$lib/sdk/billing';
+ import type { Aggregation } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import { BillingPlan } from '$lib/constants';
import { tooltip } from '$lib/actions/tooltip';
export let tier: Tier;
- export let members: number;
const plan = $plansInfo?.get(tier);
let excess: {
@@ -33,43 +31,39 @@
executions?: number;
members?: number;
} = null;
- let usage: OrganizationUsage = null;
+ let aggregation: Aggregation = null;
let showExcess = false;
onMount(async () => {
- usage = await sdk.forConsole.billing.listUsage(
+ aggregation = await sdk.forConsole.billing.getAggregation(
$organization.$id,
- $organization.billingCurrentInvoiceDate,
- new Date().toISOString()
+ $organization.billingAggregationId
);
- excess = calculateExcess(usage, plan, members);
+ excess = calculateExcess(aggregation, plan);
showExcess = Object.values(excess).some((value) => value > 0);
});
+
+
+ Your organization will switch to {tierToPlan(BillingPlan.FREE).name} plan on {toLocaleDate(
+ $organization.billingNextInvoiceDate
+ )}.
+
+ {#if !showExcess}
+ You will retain access to your {tierToPlan($organization.billingPlan).name} plan features until
+ your billing period ends. After that, your organization will be limited to Free plan resources,
+ and service disruptions may occur if usage exceeds plan limits.
+ {:else}
+ You will retain access to {tierToPlan($organization.billingPlan).name} plan features until your
+ billing period ends. After that,
+ {#if excess?.members > 0}
+ all team members except the owner will be removed,
+ {/if} and service disruptions may occur if usage exceeds Free plan limits.
+ {/if}
+
{#if showExcess}
-
-
- Your {tierToPlan($organization.billingPlan).name} plan subscription will end on {toLocaleDate(
- $organization.billingNextInvoiceDate
- )}
-
- Following payment of your final invoice, your organization will switch to the {tierToPlan(
- BillingPlan.FREE
- ).name} plan. {#if excess?.members > 0}All team members except the owner will be removed on
- that date.{/if} Service disruptions may occur unless resource usage is reduced.
-
-
-
- Learn more
-
-
-
-
-
+
Resource
Free limit
@@ -83,7 +77,8 @@
{#if excess?.members}
Organization members
- {plan.members} members
+ {plan.addons.seats.limit || 0} members
diff --git a/src/lib/components/billing/selectPlan.svelte b/src/lib/components/billing/selectPlan.svelte
new file mode 100644
index 0000000000..8aae2f0acc
--- /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/constants.ts b/src/lib/constants.ts
index c77f213b58..00ee09df31 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -1,6 +1,7 @@
export const PAGE_LIMIT = 12; // default page limit
export const CARD_LIMIT = 6; // default card limit
export const INTERVAL = 5 * 60000; // default interval to check for feedback
+export const NEW_DEV_PRO_UPGRADE_COUPON = 'appw50';
export enum Dependencies {
FACTORS = 'dependency:factors',
diff --git a/src/lib/layout/wizardExitModal.svelte b/src/lib/layout/wizardExitModal.svelte
index 13f677e998..2b71f4a599 100644
--- a/src/lib/layout/wizardExitModal.svelte
+++ b/src/lib/layout/wizardExitModal.svelte
@@ -14,7 +14,7 @@
{
+ 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(
@@ -361,14 +433,20 @@ export class Billing {
);
}
- async deleteOrganization(organizationId: string): Promise {
- const path = `/organizations/${organizationId}`;
+ async estimationCreateOrganization(
+ billingPlan: string,
+ couponId: string = null,
+ invites: Array = []
+ ): Promise {
+ const path = `/organizations/estimations/create-organization`;
const params = {
- organizationId
+ billingPlan,
+ couponId,
+ invites
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
- 'DELETE',
+ 'patch',
uri,
{
'content-type': 'application/json'
@@ -377,14 +455,14 @@ export class Billing {
);
}
- async getPlan(organizationId: string): Promise {
- const path = `/organizations/${organizationId}/plan`;
+ async deleteOrganization(organizationId: string): Promise {
+ const path = `/organizations/${organizationId}`;
const params = {
organizationId
};
const uri = new URL(this.client.config.endpoint + path);
return await this.client.call(
- 'get',
+ 'DELETE',
uri,
{
'content-type': 'application/json'
@@ -393,6 +471,32 @@ export class Billing {
);
}
+ 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 uri = new URL(this.client.config.endpoint + path);
+ return await this.client.call('get', uri, {
+ 'content-type': 'application/json'
+ });
+ }
+
+ 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'
+ });
+ }
+
async getRoles(organizationId: string): Promise {
const path = `/organizations/${organizationId}/roles`;
const uri = new URL(this.client.config.endpoint + path);
@@ -405,14 +509,45 @@ 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(
+ 'patch',
+ uri,
+ {
+ 'content-type': 'application/json'
+ },
+ params
+ );
+ }
+
+ 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(
@@ -425,6 +560,14 @@ export class Billing {
);
}
+ 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,
@@ -749,7 +892,7 @@ export class Billing {
}
async getCoupon(couponId: string): Promise {
- const path = `/console/coupons/${couponId}`;
+ const path = `/account/coupons/${couponId}`;
const params = {
couponId
};
@@ -1118,7 +1261,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 762d985d57..636853952c 100644
--- a/src/lib/stores/billing.ts
+++ b/src/lib/stores/billing.ts
@@ -1,7 +1,7 @@
import { page } from '$app/stores';
import { derived, get, writable } from 'svelte/store';
import { sdk } from './sdk';
-import { organization, type Organization } from './organization';
+import { organization, type Organization, type OrganizationError } from './organization';
import type {
InvoiceList,
AddressesList,
@@ -9,8 +9,8 @@ import type {
PaymentList,
PlansMap,
PaymentMethodData,
- OrganizationUsage,
- Plan
+ Plan,
+ Aggregation
} from '$lib/sdk/billing';
import { isCloud } from '$lib/system';
import { cachedStore } from '$lib/helpers/cache';
@@ -22,13 +22,12 @@ import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { activeHeaderAlert, orgMissingPaymentMethod } from '$routes/(console)/store';
import MarkedForDeletion from '$lib/components/billing/alerts/markedForDeletion.svelte';
-import { BillingPlan } from '$lib/constants';
+import { BillingPlan, NEW_DEV_PRO_UPGRADE_COUPON } from '$lib/constants';
import PaymentMandate from '$lib/components/billing/alerts/paymentMandate.svelte';
import MissingPaymentMethod from '$lib/components/billing/alerts/missingPaymentMethod.svelte';
import LimitReached from '$lib/components/billing/alerts/limitReached.svelte';
import { trackEvent } from '$lib/actions/analytics';
import newDevUpgradePro from '$lib/components/billing/alerts/newDevUpgradePro.svelte';
-import { last } from '$lib/helpers/array';
import { sizeToBytes, type Size } from '$lib/helpers/sizeConvertion';
import { user } from './user';
import { browser } from '$app/environment';
@@ -283,7 +282,8 @@ 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);
@@ -463,30 +463,36 @@ export async function checkForNewDevUpgradePro(org: Organization) {
if (now - accountCreated < 1000 * 60 * 60 * 24 * 7) return;
const isDismissed = !!localStorage.getItem('newDevUpgradePro');
if (isDismissed) return;
- if (now - accountCreated < 1000 * 60 * 60 * 24 * 37) {
- headerAlert.add({
- id: 'newDevUpgradePro',
- component: newDevUpgradePro,
- show: true,
- importance: 1
- });
+ // check if coupon already applied
+ try {
+ await sdk.forConsole.billing.getCoupon(NEW_DEV_PRO_UPGRADE_COUPON);
+ } catch (e) {
+ return;
}
+ headerAlert.add({
+ id: 'newDevUpgradePro',
+ component: newDevUpgradePro,
+ show: true,
+ importance: 1
+ });
}
export const upgradeURL = derived(
page,
($page) => `${base}/organization-${$page.data?.organization?.$id}/change-plan`
);
+export const billingURL = derived(
+ page,
+ ($page) => `${base}/organization-${$page.data?.organization?.$id}/billing`
+);
export const hideBillingHeaderRoutes = ['/console/create-organization', '/console/account'];
-export function calculateExcess(usage: OrganizationUsage, plan: Plan, members: number) {
- const totBandwidth = usage?.bandwidth?.length > 0 ? last(usage.bandwidth).value : 0;
+export function calculateExcess(addon: Aggregation, plan: Plan) {
return {
- bandwidth: calculateResourceSurplus(totBandwidth, plan.bandwidth),
- 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)
+ bandwidth: calculateResourceSurplus(addon.usageBandwidth, plan.bandwidth),
+ storage: calculateResourceSurplus(addon.usageStorage, plan.storage, 'GB'),
+ executions: calculateResourceSurplus(addon.usageExecutions, plan.executions, 'GB'),
+ members: addon.additionalMembers
};
}
@@ -495,3 +501,7 @@ export function calculateResourceSurplus(total: number, limit: number, limitUnit
const realLimit = (limitUnit ? sizeToBytes(limit, limitUnit) : limit) || Infinity;
return total > realLimit ? total - realLimit : 0;
}
+
+export function isOrganization(org: Organization | OrganizationError): org is Organization {
+ return (org as Organization).$id !== undefined;
+}
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/routes/(console)/account/organizations/+page.ts b/src/routes/(console)/account/organizations/+page.ts
index 0b89b1d302..4ba4be7e4a 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)
+ : await 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)/create-organization/+page.svelte b/src/routes/(console)/create-organization/+page.svelte
index 62ac4e1a9a..56c9a93354 100644
--- a/src/routes/(console)/create-organization/+page.svelte
+++ b/src/routes/(console)/create-organization/+page.svelte
@@ -3,12 +3,8 @@
import { base } from '$app/paths';
import { page } from '$app/stores';
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
- import {
- EstimatedTotalBox,
- PlanComparisonBox,
- PlanSelection,
- SelectPaymentMethod
- } from '$lib/components/billing';
+ import { PlanComparisonBox, 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';
@@ -19,10 +15,15 @@
WizardSecondaryFooter
} from '$lib/layout';
import type { Coupon, PaymentList } from '$lib/sdk/billing';
- import { tierToPlan } from '$lib/stores/billing';
+ import { isOrganization, 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';
@@ -79,6 +80,21 @@
billingPlan = plan as BillingPlan;
}
}
+ if (
+ anyOrgFree ||
+ ($page.url.searchParams.has('type') &&
+ $page.url.searchParams.get('type') === 'createPro')
+ ) {
+ 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() {
@@ -86,9 +102,29 @@
paymentMethodId = methods.paymentMethods.find((method) => !!method?.last4)?.$id ?? null;
}
+ async function validate(organizationId: string, invites: string[]) {
+ try {
+ const 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(
@@ -104,37 +140,29 @@
name,
billingPlan,
paymentMethodId,
- null
+ null,
+ couponData.code ? couponData.code : null,
+ 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);
}
}
@@ -144,13 +172,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',
@@ -186,7 +216,7 @@
For more details on our plans, visit our
pricing page .
-
+
{#if billingPlan !== BillingPlan.FREE}
{#if billingPlan !== BillingPlan.FREE}
-
+
{:else}
{/if}
diff --git a/src/routes/(console)/onboarding/+page.svelte b/src/routes/(console)/onboarding/+page.svelte
index e4c1f1ae7f..b32451e1ac 100644
--- a/src/routes/(console)/onboarding/+page.svelte
+++ b/src/routes/(console)/onboarding/+page.svelte
@@ -12,7 +12,7 @@
import { isCloud } from '$lib/system';
import { ID } from '@appwrite.io/console';
import { onMount } from 'svelte';
- import { tierToPlan, type Tier, plansInfo } from '$lib/stores/billing';
+ import { tierToPlan, type Tier, plansInfo, isOrganization } from '$lib/stores/billing';
import { formatCurrency } from '$lib/helpers/numbers';
import { base } from '$app/paths';
import { checkPricingRefAndRedirect } from '$lib/helpers/pricingRedirect';
@@ -59,11 +59,18 @@
plan: tierToPlan(plan)?.name
});
await invalidate(Dependencies.ACCOUNT);
- await goto(`${base}/organization-${org.$id}`);
- addNotification({
- message: `${orgName} organization successfully created`,
- type: 'success'
- });
+ if (isOrganization(org)) {
+ await goto(`${base}/organization-${org.$id}`);
+ addNotification({
+ message: `${orgName} organization successfully created`,
+ type: 'success'
+ });
+ } else {
+ addNotification({
+ message: `${org.message}`,
+ type: 'error'
+ });
+ }
} catch (error) {
addNotification({
message: error.message,
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..c370fbf314 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 this change and keep your current plan?
+
+
+ (showCancel = false)}>Keep change
+ Cancel change
+
+
+
diff --git a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte
index 3728876597..42a7c31187 100644
--- a/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte
+++ b/src/routes/(console)/organization-[organization]/billing/paymentHistory.svelte
@@ -28,12 +28,10 @@
import { onMount } from 'svelte';
import { trackEvent } from '$lib/actions/analytics';
import { selectedInvoice, showRetryModal } from './store';
- import { organization } from '$lib/stores/organization';
import { base } from '$app/paths';
let showDropdown = [];
let showFailedError = false;
- // let isLoadingInvoices = true;
let offset = 0;
let invoiceList: InvoiceList = {
@@ -46,15 +44,11 @@
onMount(request);
async function request() {
- // isLoadingInvoices = true;
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')
]);
- // isLoadingInvoices = false;
}
function retryPayment(invoice: Invoice) {
diff --git a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
index 08bc1771ea..a0e8dae0e8 100644
--- a/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
+++ b/src/routes/(console)/organization-[organization]/billing/planSummary.svelte
@@ -5,28 +5,27 @@
import { toLocaleDate } from '$lib/helpers/date';
import { plansInfo, 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 './cancelDowngradeModal.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 +38,7 @@
- Billing period: {toLocaleDate($organization?.billingCurrentInvoiceDate)} - {toLocaleDate(
- $organization?.billingNextInvoiceDate
- )}
+ Due at: {toLocaleDate($organization?.billingNextInvoiceDate)}
@@ -58,12 +55,14 @@
- {#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}
@@ -77,7 +76,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
@@ -248,6 +251,7 @@
{/if}
+