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

Open
wants to merge 82 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
82 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
0cb3512
Fix review comments and errors
lohanidamodar Jan 26, 2025
dda2fd8
update review comments
lohanidamodar Jan 26, 2025
8e994cb
remove old component
lohanidamodar Jan 26, 2025
6e6daca
removed unused change
lohanidamodar Jan 26, 2025
27c5453
more review fixes
lohanidamodar Jan 26, 2025
cc0f655
fix component
lohanidamodar Jan 26, 2025
f3a28cd
remove duplicates
lohanidamodar Jan 26, 2025
acb814b
fix function definiation
lohanidamodar Jan 26, 2025
4fbd814
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Jan 26, 2025
dccfb28
fix new org error
lohanidamodar Jan 26, 2025
609cf5f
fix button state
lohanidamodar Jan 26, 2025
4fb76da
fix alert
lohanidamodar Jan 27, 2025
d9fe601
fix spacing
lohanidamodar Jan 28, 2025
429c7da
bring back credits modal
lohanidamodar Jan 28, 2025
a75af80
update designs
lohanidamodar Jan 28, 2025
c52db33
fix alert
lohanidamodar Jan 28, 2025
47d0b32
review improvements
lohanidamodar Jan 29, 2025
269bf9f
fix formatting
lohanidamodar Jan 29, 2025
079b7b5
fix refactor
lohanidamodar Jan 29, 2025
3c37336
remove unused code
lohanidamodar Jan 29, 2025
65720b2
improve delete organization
lohanidamodar Jan 29, 2025
9be7e17
bind data
lohanidamodar Jan 30, 2025
998e6ab
fix downgrade info
lohanidamodar Jan 30, 2025
d0747b8
fix information on downgrade
lohanidamodar Jan 30, 2025
e53e305
unused code
lohanidamodar Jan 30, 2025
6b69f55
Update src/routes/(console)/create-organization/+page.svelte
lohanidamodar Jan 30, 2025
8abab80
update downgrade info
lohanidamodar Feb 2, 2025
4246d48
fix issue
lohanidamodar Feb 2, 2025
f28fcd6
update cancel model
lohanidamodar Feb 2, 2025
d31b971
Merge remote-tracking branch 'origin/main' into poc-invoice-cycle-ref
lohanidamodar Feb 2, 2025
bcfd428
fix alert
lohanidamodar Feb 3, 2025
9486712
format
lohanidamodar Feb 3, 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 = '';
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved

export let billingBudget: number;

let budgetEnabled = false;
var estimation: Estimation;
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved

let getEstimate = async (billingPlan, collaborators, couponId) => {
try {
error = '';
estimation = await sdk.forConsole.billing.estimationCreateOrganization(
billingPlan,
couponId == '' ? null : couponId,
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
collaborators ?? []
);
} catch (e) {
//
error = e.message;
console.log(e);
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
}
};

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);
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
}
};
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved

$: 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?

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

? 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)">
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
<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 />
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
</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."
ItzNotABug marked this conversation as resolved.
Show resolved Hide resolved
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;
lohanidamodar marked this conversation as resolved.
Show resolved Hide resolved
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