Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: invoice cycle changes #1331

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c67fea4
update create
lohanidamodar Aug 28, 2024
708517a
authorize payment
lohanidamodar Aug 28, 2024
eec5c6f
support taxid and budget while creating
lohanidamodar Aug 29, 2024
414bc60
handle validation
lohanidamodar Aug 29, 2024
c97e372
fix check with cloud
lohanidamodar Sep 3, 2024
b568e85
improve
lohanidamodar Sep 3, 2024
4376c93
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Sep 19, 2024
acff39b
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Nov 11, 2024
81d4ad8
remove unused
lohanidamodar Nov 11, 2024
c7a9e26
allow scale selection
lohanidamodar Nov 11, 2024
f29ca9a
update plan upgrade
lohanidamodar Nov 14, 2024
7b12f36
reset delete message
lohanidamodar Nov 14, 2024
9d7e866
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Nov 27, 2024
6c97f04
fix taxid
lohanidamodar Dec 2, 2024
660bddd
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Dec 18, 2024
8673209
update models
lohanidamodar Dec 22, 2024
8fdf7c5
refactor and get current plan from aggregation
lohanidamodar Dec 22, 2024
1ee29b2
update summary
lohanidamodar Dec 22, 2024
10195f4
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Dec 23, 2024
16705d3
fix rename
lohanidamodar Dec 23, 2024
14a0cac
refactor
lohanidamodar Dec 23, 2024
9f517bd
improvements on error
lohanidamodar Dec 23, 2024
723da2c
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Dec 24, 2024
3c1c4ed
credit support from plan
lohanidamodar Dec 24, 2024
b696c6c
option to cancel downgrade
lohanidamodar Dec 24, 2024
fb926d9
cancel downgrade
lohanidamodar Dec 24, 2024
81e5d0d
refactor types
lohanidamodar Dec 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/lib/commandCenter/searchers/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
50 changes: 24 additions & 26 deletions src/lib/components/billing/planSelection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,29 @@
</svelte:fragment>
</LabelCard>
</li>
{#if $organization?.billingPlan === BillingPlan.SCALE}
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierScale.name}
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierScale.description}
</p>
<p>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
{/if}
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
value={BillingPlan.SCALE}
padding={1.5}>
<svelte:fragment slot="custom">
<div class="u-flex u-flex-vertical u-gap-4 u-width-full-line">
<h4 class="body-text-2 u-bold">
{tierScale.name}
{#if $organization?.billingPlan === BillingPlan.SCALE && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{tierScale.description}
</p>
<p>
{formatCurrency(scalePlan?.price ?? 0)} per month + usage
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
</ul>
{/if}
6 changes: 3 additions & 3 deletions src/lib/components/billing/usageRates.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -81,16 +81,16 @@
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
{plan[usage.id] || 'Unlimited'}
{plan.addons.seats.limit || 0}
</TableCellText>
{#if !isFree}
<TableCellText title="rate">
{formatCurrency(plan.addons.member.price)}/{usage?.unit}
{formatCurrency(plan.addons.seats.price)}/{usage?.unit}
</TableCellText>
{/if}
</TableRow>
{:else}
{@const addon = plan.addons[usage.id]}
{@const addon = plan.usage[usage.id]}
<TableRow>
<TableCellText title="resource">{usage.resource}</TableCellText>
<TableCellText title="limit">
Expand Down
105 changes: 90 additions & 15 deletions src/lib/sdk/billing.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -174,6 +174,10 @@ export type Aggregation = {
* Usage logs for the billing period.
*/
resources: OrganizationUsage;
/**
* Aggregation billing plan
*/
plan: string;
};

export type OrganizationUsage = {
Expand Down Expand Up @@ -253,13 +257,23 @@ 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;
price: number;
bandwidth: number;
storage: number;
members: number;
webhooks: number;
users: number;
teams: number;
Expand All @@ -270,14 +284,17 @@ export type Plan = {
executions: number;
realtime: number;
logs: number;
addons: {
usage: {
bandwidth: AdditionalResource;
executions: AdditionalResource;
member: AdditionalResource;
realtime: AdditionalResource;
storage: AdditionalResource;
users: AdditionalResource;
};
addons: {
seats: PlanAddon
}
trialDays: number;
isAvailable: boolean;
selfService: boolean;
Expand All @@ -287,6 +304,7 @@ export type Plan = {
backupsEnabled: boolean;
backupPolicies: number;
emailBranding: boolean;
supportsCredit: boolean;
};

export type PlansInfo = {
Expand Down Expand Up @@ -324,20 +342,48 @@ export class Billing {
);
}

async validateOrganization(
organizationId: string,
invites: string[],
): Promise<Organization> {
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<Organization> {
billingAddressId: string = null,
couponId: string = null,
invites: Array<string> = [],
budget: number = undefined,
taxId: string = null
): Promise<Organization | OrganizationError> {
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(
Expand Down Expand Up @@ -366,19 +412,27 @@ export class Billing {
);
}

async getPlan(organizationId: string): Promise<Plan> {
async getOrganizationPlan(organizationId: string): Promise<Plan> {
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<Plan> {
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'
}
);
}

Expand All @@ -394,14 +448,22 @@ export class Billing {
organizationId: string,
billingPlan: string,
paymentMethodId: string,
billingAddressId: string = undefined
): Promise<Organization> {
billingAddressId: string = undefined,
couponId: string = null,
invites: Array<string> = [],
budget: number = undefined,
taxId: string = null
): Promise<Organization | OrganizationError> {
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(
Expand All @@ -413,6 +475,19 @@ export class Billing {
params
);
}
async cancelDowngrade(
organizationId: string
): Promise<Organization | OrganizationError> {
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,
Expand Down
4 changes: 2 additions & 2 deletions src/lib/stores/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,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);
Expand Down Expand Up @@ -475,7 +475,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)
};
}

Expand Down
11 changes: 11 additions & 0 deletions src/lib/stores/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, unknown>> & {
billingBudget: number;
billingPlan: Tier;
Expand All @@ -21,6 +30,8 @@ export type Organization = Models.Team<Record<string, unknown>> & {
amount: number;
billingTaxId?: string;
billingPlanDowngrade?: Tier;
billingAggregationId: string;
billingInvoiceId: string;
};

export type OrganizationList = {
Expand Down
10 changes: 7 additions & 3 deletions src/lib/stores/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ export async function confirmPayment(
orgId: string,
clientSecret: string,
paymentMethodId: string,
route?: string
route?: string,
returnError: boolean = false
) {
try {
const url =
Expand All @@ -129,10 +130,13 @@ export async function confirmPayment(
clientSecret: clientSecret,
confirmParams: {
return_url: url,
payment_method: paymentMethod.providerMethodId
}
payment_method: paymentMethod.providerMethodId,
},
});
if (error) {
if (returnError) {
return error;
}
throw error.message;
}
} catch (e) {
Expand Down
15 changes: 10 additions & 5 deletions src/routes/(console)/account/organizations/+page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@ 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
};
};
Loading
Loading