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 50 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 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
c1e87a2
updated pricing.
ItzNotABug Dec 16, 2024
660bddd
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Dec 18, 2024
bcbf5c7
address comments.
ItzNotABug Dec 19, 2024
b6a517a
ran: formatter.
ItzNotABug Dec 19, 2024
ed9f145
Merge branch 'appwrite:poc-invoice-cycle-ref' into poc-invoice-cycle-ref
ItzNotABug Dec 19, 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
f09bbc2
Merge branch 'poc-invoice-cycle-ref' into poc-invoice-cycle-ref
ItzNotABug Dec 29, 2024
81e5d0d
refactor types
lohanidamodar Dec 30, 2024
34f0727
ci: empty commit
ItzNotABug Jan 9, 2025
5d84c3b
fix types
lohanidamodar Jan 12, 2025
3ed6edf
Merge pull request #1565 from ItzNotABug/poc-invoice-cycle-ref
lohanidamodar Jan 13, 2025
ff25151
Merge branch 'poc-invoice-cycle-ref' of github.com:appwrite/console i…
lohanidamodar Jan 13, 2025
31b1467
use new estimation api for update plan and create organiation
lohanidamodar Jan 14, 2025
d401ade
fixes
lohanidamodar Jan 15, 2025
293eac0
refactor plan selection
lohanidamodar Jan 15, 2025
1973a23
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Jan 15, 2025
c567681
fix after sync
lohanidamodar Jan 15, 2025
8c9c413
fix coupon
lohanidamodar Jan 15, 2025
9553df7
fix history not showing
lohanidamodar Jan 15, 2025
dee3b53
fix summary
lohanidamodar Jan 15, 2025
654718d
fix errors
lohanidamodar Jan 19, 2025
67aeaed
fixes to applying coupon
lohanidamodar Jan 20, 2025
d64de90
improve estimatie total
lohanidamodar Jan 20, 2025
e696d0c
check estimation before delete flow
lohanidamodar Jan 20, 2025
2f36ff2
remove unused code
lohanidamodar Jan 20, 2025
09ba618
fix check
lohanidamodar Jan 20, 2025
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
25 changes: 25 additions & 0 deletions src/lib/components/billing/discountsApplied.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script lang="ts">
import { tooltip } from '$lib/actions/tooltip';
import { formatCurrency } from '$lib/helpers/numbers';

export let label: string;
export let value: number;
</script>

{#if value > 0}
<span class="u-flex u-main-space-between">
<div class="u-flex u-cross-center u-gap-4">
<p class="text">
<span class="icon-tag u-color-text-success" aria-hidden="true" />
<span use:tooltip={{ content: label }}>
{label}
</span>
</p>
</div>
{#if value >= 100}
<p class="inline-tag">Credits applied</p>
{:else}
<span class="u-color-text-success">-{formatCurrency(value)}</span>
{/if}
</span>
{/if}
114 changes: 114 additions & 0 deletions src/lib/components/billing/estimatedTotal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts">
import { FormList, InputChoice, InputNumber } from '$lib/elements/forms';
import { formatCurrency } from '$lib/helpers/numbers';
import type { Estimation } from '$lib/sdk/billing';
import { sdk } from '$lib/stores/sdk';
import Alert from '../alert.svelte';
import DiscountsApplied from './discountsApplied.svelte';

export let organizationId: string | undefined = undefined;
export let billingPlan: string;
export let collaborators: string[];
export let couponId: string;
export let fixedCoupon = false;
export let error: string = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the type string it's implicitly assigned by ''


export let billingBudget: number;

let budgetEnabled = false;
var estimation: Estimation;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use let not var


let getEstimate = async (billingPlan, collaborators, couponId) => {
try {
error = '';
estimation = await sdk.forConsole.billing.estimationCreateOrganization(
billingPlan,
couponId == '' ? null : couponId,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are using == instead of === intentionally, right?

collaborators ?? []
);
} catch (e) {
//
error = e.message;
console.log(e);
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the empty comment and the console log

}
};

let getUpdatePlanEstimate = async (organizationId, billingPlan, collaborators, couponId) => {
try {
error = '';
estimation = await sdk.forConsole.billing.estimationUpdatePlan(
organizationId,
billingPlan,
couponId && couponId.length > 0 ? couponId : null,
collaborators ?? []
);
} catch (e) {
error = e.message;
console.log(e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove the console log

}
};
Comment on lines +21 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually use "normal functions" instead of "arrow functions" unless needed. If arrow functions are necessary in this case we can use const instead of let


$: organizationId && organizationId.length > 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need to check the length of organizationId, or are there instances where the id exists but has no length?

? getUpdatePlanEstimate(organizationId, billingPlan, collaborators, couponId)
: getEstimate(billingPlan, collaborators, couponId);
</script>

{#if error.length}
<Alert type="error" dismissible>
<span slot="title">
{error}
</span>
</Alert>
{:else if estimation}
<section
class="card u-flex u-flex-vertical u-gap-8"
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
Comment on lines +63 to +66
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use the Card component 👍

<slot />
{#each estimation.items ?? [] as item}
<span class="u-flex u-main-space-between">
<p class="text">{item.label}</p>
<p class="text">{formatCurrency(item.value)}</p>
</span>
{/each}
{#each estimation.discounts ?? [] as item}
<DiscountsApplied {...item} />
{/each}
<div class="u-sep-block-start" />
<span class="u-flex u-main-space-between">
<p class="text">
Total due<br />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the <br />, let's remove it

</p>
<p class="text">
{formatCurrency(estimation.grossAmount)}
</p>
</span>

<p class="text u-margin-block-start-16">
You'll pay <span class="u-bold">{formatCurrency(estimation.amount)}</span> now. Once
your credits run out, you'll be charged
<span class="u-bold">{formatCurrency(estimation.amount)}</span> every 30 days.
</p>

<FormList class="u-margin-block-start-24">
<InputChoice
type="switchbox"
id="budget"
label="Enable budget cap"
tooltip="If enabled, you will be notified when your spending reaches 75% of the set cap. Update cap alerts in your organization settings."
fullWidth
bind:value={budgetEnabled}>
{#if budgetEnabled}
<div class="u-margin-block-start-16">
<InputNumber
id="budget"
label="Budget cap (USD)"
placeholder="0"
min={0}
bind:value={billingBudget} />
</div>
{/if}
</InputChoice>
</FormList>
</section>
{/if}
77 changes: 56 additions & 21 deletions src/lib/components/billing/estimatedTotalBox.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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<Coupon>;
Expand All @@ -14,21 +18,29 @@
export let isDowngrade = false;

const today = new Date();
const isScaleDowngrade = isDowngrade && billingPlan === BillingPlan.PRO;
const isScaleUpgrade = !isDowngrade && billingPlan === BillingPlan.SCALE;
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm not sure I like this approach...It might work now, but I can already foresee some edge cases when we introduce more tiers or the change.
For example, what happens if someone on the Education plan tries to go to the Scale plan?

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
);
</script>

Expand All @@ -37,41 +49,64 @@
style:--p-card-padding="1.5rem"
style:--p-card-border-radius="var(--border-radius-small)">
<slot />
<span class="u-flex u-main-space-between">
<p class="text">{currentPlan.name} plan</p>
<p class="text">{formatCurrency(currentPlan.price)}</p>
</span>
<span class="u-flex u-main-space-between">
<div class="u-flex u-main-space-between">
<p class="text">{selectedPlan.name} plan</p>
<p class="text">{formatCurrency(selectedPlan.price)}</p>
</div>

<div class="u-flex u-main-space-between">
<p class="text" class:u-bold={isDowngrade}>Additional seats ({collaborators?.length})</p>
<p class="text" class:u-bold={isDowngrade}>
{formatCurrency(extraSeatsCost)}
{formatCurrency(
isScaleDowngrade
? (collaborators?.length ?? 0) * (selectedPlan?.addons?.seats?.price ?? 0)
: extraSeatsCost
)}
</p>
</span>
</div>

{#if isScaleUpgrade}
{@const currentPlanName = currentOrgPlan.name}
<div class="u-flex u-main-space-between">
<div class="text">
<span>Unused {currentPlanName} plan balance</span>
<span
use:tooltip={{
placement: 'bottom',
content: `This discount reflects the unused portion of your ${currentPlanName} plan and add-ons. Future credits for extra seats and features will apply automatically.`
}}
class="icon-info">
</span>
</div>
<p class="text">-{formatCurrency(unUsedBalances)}</p>
</div>
{/if}

{#if couponData?.status === 'active'}
<CreditsApplied bind:couponData {fixedCoupon} />
{/if}
<div class="u-sep-block-start" />
<span class="u-flex u-main-space-between">
<div class="u-flex u-main-space-between">
<p class="text">
Upcoming charge<br /><span class="u-color-text-gray"
>Due on {!currentPlan.trialDays
>Due on {!selectedPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span>
</p>
<p class="text">
{formatCurrency(estimatedTotal)}
</p>
</span>
</div>

<p class="text u-margin-block-start-16">
You'll pay <span class="u-bold">{formatCurrency(estimatedTotal)}</span> now, with your first
billing cycle starting on
<span class="u-bold"
>{!currentPlan.trialDays
>{!currentOrgPlan.trialDays
? toLocaleDate(billingPayDate.toString())
: toLocaleDate(trialEndDate.toString())}</span
>. Once your credits run out, you'll be charged
<span class="u-bold">{formatCurrency(currentPlan.price)}</span> plus usage fees every 30 days.
<span class="u-bold">{formatCurrency(currentOrgPlan.price)}</span> plus usage fees every 30 days.
</p>

<FormList class="u-margin-block-start-24">
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components/billing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
46 changes: 20 additions & 26 deletions src/lib/components/billing/planSelection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,25 @@
</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}
51 changes: 51 additions & 0 deletions src/lib/components/billing/selectPlan.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script lang="ts">
import { BillingPlan } from '$lib/constants';
import { formatCurrency } from '$lib/helpers/numbers';
import { plansInfo } from '$lib/stores/billing';
import { organization } from '$lib/stores/organization';
import { LabelCard } from '..';

export let billingPlan: string;
export let anyOrgFree = false;
export let isNewOrg = false;
let classes: string = '';
export { classes as class };
</script>

{#if billingPlan}
<ul class="u-flex u-flex-vertical u-gap-16 u-margin-block-start-8 {classes}">
{#each $plansInfo.values() as plan}
<li>
<LabelCard
name="plan"
bind:group={billingPlan}
disabled={ (plan.$id === BillingPlan.FREE && anyOrgFree) || !plan.selfService}
value={plan.$id}
tooltipShow={plan.$id === BillingPlan.FREE && anyOrgFree}
tooltipText={plan.$id === BillingPlan.FREE
? 'You are limited to 1 Free organization per account.'
: ''}
padding={1.5}>
<svelte:fragment slot="custom" let:disabled>
<div
class="u-flex u-flex-vertical u-gap-4 u-width-full-line"
class:u-opacity-50={disabled}>
<h4 class="body-text-2 u-bold">
{plan.name}
{#if $organization?.billingPlan === plan.$id && !isNewOrg}
<span class="inline-tag">Current plan</span>
{/if}
</h4>
<p class="u-color-text-offline u-small">
{plan.desc}
</p>
<p>
{formatCurrency(plan?.price ?? 0)}
</p>
</div>
</svelte:fragment>
</LabelCard>
</li>
{/each}
</ul>
{/if}
Loading
Loading