-
Notifications
You must be signed in to change notification settings - Fork 150
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
base: main
Are you sure you want to change the base?
Changes from all commits
c67fea4
708517a
eec5c6f
414bc60
c97e372
b568e85
4376c93
acff39b
81d4ad8
c7a9e26
f29ca9a
7b12f36
9d7e866
6c97f04
c1e87a2
660bddd
bcbf5c7
b6a517a
ed9f145
8673209
8fdf7c5
1ee29b2
10195f4
16705d3
14a0cac
9f517bd
723da2c
3c1c4ed
b696c6c
fb926d9
f09bbc2
81e5d0d
34f0727
5d84c3b
3ed6edf
ff25151
31b1467
d401ade
293eac0
1973a23
c567681
8c9c413
9553df7
dee3b53
654718d
67aeaed
d64de90
e696d0c
2f36ff2
09ba618
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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} |
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 = ''; | ||
|
||
export let billingBudget: number; | ||
|
||
let budgetEnabled = false; | ||
var estimation: Estimation; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use |
||
|
||
let getEstimate = async (billingPlan, collaborators, couponId) => { | ||
try { | ||
error = ''; | ||
estimation = await sdk.forConsole.billing.estimationCreateOrganization( | ||
billingPlan, | ||
couponId == '' ? null : couponId, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are using |
||
collaborators ?? [] | ||
); | ||
} catch (e) { | ||
// | ||
error = e.message; | ||
console.log(e); | ||
Comment on lines
+30
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's remove the console log |
||
} | ||
}; | ||
Comment on lines
+21
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
||
$: organizationId && organizationId.length > 0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to check the length of |
||
? 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use the |
||
<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 /> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need the |
||
</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} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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>; | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
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> | ||
|
||
|
@@ -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"> | ||
|
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} |
There was a problem hiding this comment.
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''