mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
Feat: plan summary using new estimation endpoint
This commit is contained in:
@@ -82,6 +82,25 @@ export type EstimationItem = {
|
||||
value: number;
|
||||
};
|
||||
|
||||
export type EstimationOrganization = {
|
||||
amount: number;
|
||||
grossAmount: number;
|
||||
credits: number;
|
||||
discount: number;
|
||||
items: EstimationItem[];
|
||||
discounts: EstimationItem[];
|
||||
usage: {
|
||||
name: string;
|
||||
value: number /* service over the limit*/;
|
||||
amount: number /* price of service over the limit*/;
|
||||
rate: number;
|
||||
desc: string;
|
||||
}[];
|
||||
trialDays: number;
|
||||
trialEndDate: string | undefined;
|
||||
error: string | undefined;
|
||||
};
|
||||
|
||||
export type EstimationDeleteOrganization = {
|
||||
amount: number;
|
||||
grossAmount: number;
|
||||
@@ -485,6 +504,14 @@ export class Billing {
|
||||
});
|
||||
}
|
||||
|
||||
async estimationGetOrganization(organizationId: string): Promise<EstimationOrganization> {
|
||||
const path = `/organizations/${organizationId}/estimations`;
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
return await this.client.call('get', uri, {
|
||||
'content-type': 'application/json'
|
||||
});
|
||||
}
|
||||
|
||||
async getOrganizationPlan(organizationId: string): Promise<Plan> {
|
||||
const path = `/organizations/${organizationId}/plan`;
|
||||
const uri = new URL(this.client.config.endpoint + path);
|
||||
|
||||
@@ -118,10 +118,8 @@
|
||||
<Heading tag="h2" size="5">Billing</Heading>
|
||||
</div>
|
||||
<PlanSummary
|
||||
creditList={data?.creditList}
|
||||
currentPlan={data?.aggregationBillingPlan}
|
||||
currentAggregation={data?.billingAggregation}
|
||||
currentInvoice={data?.billingInvoice} />
|
||||
estimation={data?.estimationOrganization}
|
||||
currentPlan={data?.aggregationBillingPlan} />
|
||||
<PaymentHistory />
|
||||
<PaymentMethods />
|
||||
<BillingAddress billingAddress={data?.billingAddress} />
|
||||
|
||||
@@ -39,32 +39,29 @@ export const load: PageLoad = async ({ parent, depends }) => {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
let billingInvoice = null;
|
||||
try {
|
||||
billingInvoice = await sdk.forConsole.billing.getInvoice(
|
||||
organization.$id,
|
||||
(organization as Organization)?.billingInvoiceId
|
||||
);
|
||||
} catch (e) {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
const [paymentMethods, addressList, billingAddress, creditList, aggregationBillingPlan] =
|
||||
await Promise.all([
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
billingAddressPromise,
|
||||
sdk.forConsole.billing.listCredits(organization.$id),
|
||||
sdk.forConsole.billing.getPlan(billingAggregation?.plan ?? organization.billingPlan)
|
||||
]);
|
||||
const [
|
||||
estimationOrganization,
|
||||
paymentMethods,
|
||||
addressList,
|
||||
billingAddress,
|
||||
creditList,
|
||||
aggregationBillingPlan
|
||||
] = await Promise.all([
|
||||
sdk.forConsole.billing.estimationGetOrganization(organization.$id),
|
||||
sdk.forConsole.billing.listPaymentMethods(),
|
||||
sdk.forConsole.billing.listAddresses(),
|
||||
billingAddressPromise,
|
||||
sdk.forConsole.billing.listCredits(organization.$id),
|
||||
sdk.forConsole.billing.getPlan(billingAggregation?.plan ?? organization.billingPlan)
|
||||
]);
|
||||
|
||||
return {
|
||||
estimationOrganization,
|
||||
paymentMethods,
|
||||
addressList,
|
||||
billingAddress,
|
||||
aggregationBillingPlan,
|
||||
creditList,
|
||||
billingAggregation,
|
||||
billingInvoice
|
||||
billingAggregation
|
||||
};
|
||||
};
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { Card, CardGrid, Collapsible, CollapsibleItem, Heading } from '$lib/components';
|
||||
import { Button } from '$lib/elements/forms';
|
||||
import { toLocaleDate } from '$lib/helpers/date';
|
||||
import { plansInfo, upgradeURL } from '$lib/stores/billing';
|
||||
import { upgradeURL } from '$lib/stores/billing';
|
||||
import { organization } from '$lib/stores/organization';
|
||||
import type { Aggregation, CreditList, Invoice, Plan } from '$lib/sdk/billing';
|
||||
import type { EstimationOrganization, Plan } from '$lib/sdk/billing';
|
||||
import { abbreviateNumber, formatCurrency, formatNumberWithCommas } from '$lib/helpers/numbers';
|
||||
import { humanFileSize } from '$lib/helpers/sizeConvertion';
|
||||
import { BillingPlan } from '$lib/constants';
|
||||
@@ -13,19 +13,14 @@
|
||||
import { tooltip } from '$lib/actions/tooltip';
|
||||
import CancelDowngradeModel from './cancelDowngradeModal.svelte';
|
||||
|
||||
export let estimation: EstimationOrganization;
|
||||
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 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 extraUsage = estimation.usage.length
|
||||
? estimation.usage.reduce((total, usage) => total + usage.amount, 0)
|
||||
: 0;
|
||||
</script>
|
||||
|
||||
{#if $organization}
|
||||
@@ -43,26 +38,19 @@
|
||||
<Card isTile style="--p-card-padding: 1.5rem;" class="card-only-on-desktop">
|
||||
<Collapsible>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
<div class="u-flex">
|
||||
<span class="body-text-2">{currentPlan.name} plan</span>
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
{isTrial ||
|
||||
$organization?.billingPlan === BillingPlan.GITHUB_EDUCATION
|
||||
? formatCurrency(0)
|
||||
: currentPlan
|
||||
? formatCurrency(currentPlan?.price)
|
||||
: ''}
|
||||
{#each estimation.items as item}
|
||||
<div class="u-flex">
|
||||
<span class="body-text-2">{item.label}</span>
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
{formatCurrency(item.value)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if currentPlan.budgeting && extraUsage > 0}
|
||||
{/each}
|
||||
{#if estimation.usage.length}
|
||||
<CollapsibleItem gap={8}>
|
||||
<svelte:fragment slot="beforetitle">
|
||||
<span class="body-text-2"><b>Add-ons</b></span><span
|
||||
class="inline-tag"
|
||||
>{currentAggregation.additionalMembers > 0
|
||||
? currentInvoice.usage.length + 1
|
||||
: currentInvoice.usage.length}</span>
|
||||
class="inline-tag">{estimation.usage.length}</span>
|
||||
<div class="icon">
|
||||
<span class="icon-cheveron-down" aria-hidden="true"></span>
|
||||
</div>
|
||||
@@ -70,106 +58,84 @@
|
||||
<svelte:fragment slot="end">
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
<b>
|
||||
{formatCurrency(extraUsage >= 0 ? extraUsage : 0)}
|
||||
{formatCurrency(extraUsage)}
|
||||
</b>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<ul>
|
||||
{#if currentAggregation.additionalMembers}
|
||||
{#each estimation.usage as excess}
|
||||
<li class="u-flex-vertical u-gap-4 u-padding-block-8">
|
||||
<div class="u-flex u-gap-4">
|
||||
<h5 class="body-text-2 u-stretch">
|
||||
Additional members
|
||||
</h5>
|
||||
<div>
|
||||
{formatCurrency(
|
||||
currentAggregation.additionalMemberAmount
|
||||
)}
|
||||
{#if ['storage', 'bandwidth'].includes(excess.name)}
|
||||
{@const excessValue = humanFileSize(excess.value)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5 class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="u-flex u-gap-4">
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline">
|
||||
{currentAggregation.additionalMembers}
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(
|
||||
excess.value ?? 0
|
||||
) + 'bytes'}>
|
||||
{excessValue.value ?? 0}{excessValue.unit}
|
||||
</h5>
|
||||
</div>
|
||||
{:else if excess.name === 'member'}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5 class="body-text-2 u-stretch u-capitalize">
|
||||
Additional members
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={excess.value.toString()}>
|
||||
{excess.value}
|
||||
</h5>
|
||||
{:else}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5 class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(excess.value)}>
|
||||
{abbreviateNumber(excess.value)}
|
||||
</h5>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{#if currentInvoice?.usage}
|
||||
{#each currentInvoice.usage as excess, i}
|
||||
<li
|
||||
class="u-flex-vertical u-gap-4 {currentAggregation.additionalMembers >
|
||||
0
|
||||
? 'u-padding-block-8'
|
||||
: 'u-padding-block-start-8'}"
|
||||
class:u-sep-block-start={i > 0 ||
|
||||
currentAggregation.additionalMembers > 0}>
|
||||
{#if ['storage', 'bandwidth'].includes(excess.name)}
|
||||
{@const excessValue = humanFileSize(
|
||||
excess.value
|
||||
)}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(
|
||||
excess.value ?? 0
|
||||
) + 'bytes'}>
|
||||
{excessValue.value ?? 0}{excessValue.unit}
|
||||
</h5>
|
||||
{:else if excess.name !== 'member'}
|
||||
<div class="u-flex u-main-space-between">
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-capitalize">
|
||||
{excess.name}
|
||||
</h5>
|
||||
{formatCurrency(excess.amount)}
|
||||
</div>
|
||||
<h5
|
||||
class="body-text-2 u-stretch u-color-text-offline"
|
||||
title={formatNumberWithCommas(
|
||||
excess.value
|
||||
)}>
|
||||
{abbreviateNumber(excess.value)}
|
||||
</h5>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</CollapsibleItem>
|
||||
{/if}
|
||||
|
||||
{#if currentPlan.supportsCredits && availableCredit > 0}
|
||||
{#if estimation.discounts.length}
|
||||
<CollapsibleItem noContent gap={4}>
|
||||
<span class="body-text-2 u-flex u-cross-center u-gap-2"
|
||||
><svg
|
||||
width="16"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.166 7.93434C14.4784 8.24676 14.4784 8.75329 14.166 9.06571L8.56603 14.6657C8.25361 14.9781 7.74708 14.9781 7.43466 14.6657L1.83466 9.06571C1.67842 8.90947 1.60032 8.70469 1.60034 8.49992V4.50002C1.60034 3.17454 2.67486 2.10002 4.00034 2.10002H8.00056C8.20523 2.10008 8.40987 2.17818 8.56603 2.33434L14.166 7.93434ZM4.00034 5.30002C4.44217 5.30002 4.80034 4.94185 4.80034 4.50002C4.80034 4.05819 4.44217 3.70002 4.00034 3.70002C3.55851 3.70002 3.20034 4.05819 3.20034 4.50002C3.20034 4.94185 3.55851 5.30002 4.00034 5.30002Z"
|
||||
fill="#00BC5D" />
|
||||
</svg>
|
||||
Credits to be applied</span>
|
||||
{#each estimation.discounts as discount}
|
||||
<span class="body-text-2 u-flex u-cross-center u-gap-2"
|
||||
><svg
|
||||
width="16"
|
||||
height="17"
|
||||
viewBox="0 0 16 17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.166 7.93434C14.4784 8.24676 14.4784 8.75329 14.166 9.06571L8.56603 14.6657C8.25361 14.9781 7.74708 14.9781 7.43466 14.6657L1.83466 9.06571C1.67842 8.90947 1.60032 8.70469 1.60034 8.49992V4.50002C1.60034 3.17454 2.67486 2.10002 4.00034 2.10002H8.00056C8.20523 2.10008 8.40987 2.17818 8.56603 2.33434L14.166 7.93434ZM4.00034 5.30002C4.44217 5.30002 4.80034 4.94185 4.80034 4.50002C4.80034 4.05819 4.44217 3.70002 4.00034 3.70002C3.55851 3.70002 3.20034 4.05819 3.20034 4.50002C3.20034 4.94185 3.55851 5.30002 4.00034 5.30002Z"
|
||||
fill="#00BC5D" />
|
||||
</svg>
|
||||
{discount.label}</span>
|
||||
|
||||
<div
|
||||
class="body-text-2 u-margin-inline-start-auto"
|
||||
style="color: var(--web-green-500, #10B981)">
|
||||
-{formatCurrency(
|
||||
Math.min(availableCredit, currentInvoice?.amount ?? 0)
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
class="body-text-2 u-margin-inline-start-auto"
|
||||
style="color: var(--web-green-500, #10B981)">
|
||||
-{formatCurrency(discount.value)}
|
||||
</div>
|
||||
{/each}
|
||||
</CollapsibleItem>
|
||||
{/if}
|
||||
|
||||
@@ -190,16 +156,7 @@
|
||||
}}></span>
|
||||
</span>
|
||||
<div class="body-text-2 u-margin-inline-start-auto">
|
||||
{formatCurrency(
|
||||
Math.max(
|
||||
(currentInvoice?.amount ?? 0) -
|
||||
Math.min(
|
||||
availableCredit,
|
||||
currentInvoice?.amount ?? 0
|
||||
),
|
||||
0
|
||||
)
|
||||
)}
|
||||
{formatCurrency(estimation.grossAmount)}
|
||||
</div>
|
||||
</CollapsibleItem>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user