Skip to content

Commit

Permalink
merge master
Browse files Browse the repository at this point in the history
  • Loading branch information
anirudhpillai committed Aug 20, 2024
2 parents 97bdeff + 20b6d71 commit 9f1e936
Show file tree
Hide file tree
Showing 25 changed files with 164 additions and 58 deletions.
85 changes: 85 additions & 0 deletions cypress/e2e/billing-limits.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
describe('Billing Limits', () => {
it('Show no limits set and allow user to set one', () => {
cy.intercept('GET', '/api/billing/', { fixture: 'api/billing/billing.json' }).as('getBilling')
cy.visit('/organization/billing')
cy.wait('@getBilling')

cy.intercept('PATCH', '/api/billing/', (req) => {
req.reply({
statusCode: 200,
body: {
...require('../fixtures/api/billing/billing.json'),
custom_limits_usd: { product_analytics: 100 },
},
})
}).as('patchBilling')

cy.get('[data-attr="billing-limit-input-wrapper-product_analytics"]').scrollIntoView()
cy.get('[data-attr="billing-limit-not-set-product_analytics"]').should('be.visible')
cy.contains('Set a billing limit').click()
cy.get('[data-attr="billing-limit-input-product_analytics"]').clear().type('100')
cy.get('[data-attr="save-billing-limit-product_analytics"]').click()
cy.wait('@patchBilling')
cy.get('[data-attr="billing-limit-set-product_analytics"]').should(
'contain',
'You have a $100 billing limit set'
)
})

it('Show existing limit and allow user to change it', () => {
cy.intercept('GET', '/api/billing/', (req) => {
req.reply({
statusCode: 200,
body: {
...require('../fixtures/api/billing/billing.json'),
custom_limits_usd: { product_analytics: 100 },
},
})
}).as('getBilling')
cy.visit('/organization/billing')
cy.wait('@getBilling')

cy.intercept('PATCH', '/api/billing/', (req) => {
req.reply({
statusCode: 200,
body: {
...require('../fixtures/api/billing/billing.json'),
custom_limits_usd: { product_analytics: 200 },
},
})
}).as('patchBilling')

cy.get('[data-attr="billing-limit-input-wrapper-product_analytics"]').scrollIntoView()
cy.get('[data-attr="billing-limit-set-product_analytics"]').should('be.visible')
cy.contains('Edit limit').click()
cy.get('[data-attr="billing-limit-input-product_analytics"]').clear().type('200')
cy.get('[data-attr="save-billing-limit-product_analytics"]').click()
cy.wait('@patchBilling')
cy.get('[data-attr="billing-limit-set-product_analytics"]').should(
'contain',
'You have a $200 billing limit set'
)
})

it('Show existing limit and allow user to remove it', () => {
cy.intercept('GET', '/api/billing/', (req) => {
req.reply({
statusCode: 200,
body: {
...require('../fixtures/api/billing/billing.json'),
custom_limits_usd: { product_analytics: 100 },
},
})
}).as('getBilling')
cy.visit('/organization/billing')
cy.wait('@getBilling')

cy.intercept('PATCH', '/api/billing/', { fixture: 'api/billing/billing.json' }).as('patchBilling')

cy.get('[data-attr="billing-limit-input-wrapper-product_analytics"]').scrollIntoView()
cy.get('[data-attr="billing-limit-set-product_analytics"]').should('be.visible')
cy.contains('Edit limit').click()
cy.get('[data-attr="remove-billing-limit-product_analytics"]').click()
cy.get('[data-attr="billing-limit-not-set-product_analytics"]').should('be.visible')
})
})
2 changes: 1 addition & 1 deletion ee/billing/billing_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ class CustomerInfo(TypedDict):
current_total_amount_usd: Optional[str]
current_total_amount_usd_after_discount: Optional[str]
products: Optional[list[CustomerProduct]]
custom_limits_usd: Optional[dict[str, str | int]]
custom_limits_usd: Optional[dict[str, int]]
usage_summary: Optional[dict[str, dict[str, Optional[int]]]]
free_trial_until: Optional[str]
discount_percent: Optional[int]
Expand Down
Binary file modified frontend/__snapshots__/scenes-app-dashboards--edit--light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified frontend/__snapshots__/scenes-app-dashboards--show--light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 0 additions & 1 deletion frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@ export const FEATURE_FLAGS = {
PERSON_BATCH_EXPORTS: 'person-batch-exports', // owner: @tomasfarias
// owner: #team-replay, only to be enabled for PostHog team testing
EXCEPTION_AUTOCAPTURE: 'exception-autocapture',
WEB_VITALS_AUTOCAPTURE: 'web-vitals-autocapture', // owner: @team-replay
FF_DASHBOARD_TEMPLATES: 'ff-dashboard-templates', // owner: @EDsCODE
ARTIFICIAL_HOG: 'artificial-hog', // owner: @Twixes
CS_DASHBOARDS: 'cs-dashboards', // owner: @pauldambra
Expand Down
30 changes: 20 additions & 10 deletions frontend/src/scenes/billing/BillingLimit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { billingProductLogic } from './billingProductLogic'
export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => {
const limitInputRef = useRef<HTMLInputElement | null>(null)
const { billing, billingLoading } = useValues(billingLogic)
const { isEditingBillingLimit, customLimitUsd, currentAndUpgradePlans } = useValues(
const { isEditingBillingLimit, customLimitUsd, hasCustomLimitSet, currentAndUpgradePlans } = useValues(
billingProductLogic({ product, billingLimitInputRef: limitInputRef })
)
const { setIsEditingBillingLimit, setBillingLimitInput, submitBillingLimitInput } = useActions(
Expand All @@ -26,26 +26,28 @@ export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JS
return null
}

const hasCustomLimit = customLimitUsd === 0 || customLimitUsd
return (
<Form formKey="billingLimitInput" props={{ product: product }} logic={billingProductLogic} enableFormOnSubmit>
<div className="border-t border-border p-8" data-attr={`billing-limit-input-${product.type}`}>
<div className="border-t border-border p-8" data-attr={`billing-limit-input-wrapper-${product.type}`}>
<h4 className="mb-2">Billing limit</h4>
<div className="flex">
{!isEditingBillingLimit ? (
<div className="flex items-center justify-center gap-1">
{hasCustomLimit ? (
{hasCustomLimitSet ? (
<>
{usingInitialBillingLimit ? (
<Tooltip title="Initial limits protect you from accidentally incurring large unexpected charges. Some features may stop working and data may be dropped if your usage exceeds your limit.">
<span className="text-sm">
<span
className="text-sm"
data-attr={`default-billing-limit-${product.type}`}
>
This product has a default initial billing limit of{' '}
<b>${initialBillingLimit}</b>.
</span>
</Tooltip>
) : (
<Tooltip title="Set a billing limit to control your recurring costs. Some features may stop working and data may be dropped if your usage exceeds your limit.">
<span className="text-sm">
<span className="text-sm" data-attr={`billing-limit-set-${product.type}`}>
You have a <b>${customLimitUsd}</b> billing limit set for{' '}
{product?.name?.toLowerCase()}.
</span>
Expand All @@ -62,7 +64,7 @@ export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JS
</>
) : (
<>
<span className="text-sm">
<span className="text-sm" data-attr={`billing-limit-not-set-${product.type}`}>
You do not have a billing limit set for {product?.name?.toLowerCase()}.
</span>
<LemonButton
Expand All @@ -87,6 +89,7 @@ export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JS
fullWidth={false}
status={error ? 'danger' : 'default'}
value={value}
data-attr={`billing-limit-input-${product.type}`}
onChange={onChange}
prefix={<b>$</b>}
disabled={billingLoading}
Expand All @@ -100,7 +103,13 @@ export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JS
)}
</Field>

<LemonButton loading={billingLoading} type="primary" size="small" htmlType="submit">
<LemonButton
loading={billingLoading}
type="primary"
size="small"
htmlType="submit"
data-attr={`save-billing-limit-${product.type}`}
>
Save
</LemonButton>
<LemonButton
Expand All @@ -113,13 +122,14 @@ export const BillingLimit = ({ product }: { product: BillingProductV2Type }): JS
>
Cancel
</LemonButton>
{hasCustomLimit ? (
{hasCustomLimitSet ? (
<LemonButton
status="danger"
size="small"
data-attr={`remove-billing-limit-${product.type}`}
tooltip="Remove billing limit"
onClick={() => {
setBillingLimitInput(undefined)
setBillingLimitInput(null)
submitBillingLimitInput()
}}
>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/scenes/billing/BillingProduct.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
const productRef = useRef<HTMLDivElement | null>(null)
const { billing, redirectPath, isUnlicensedDebug, billingError } = useValues(billingLogic)
const {
customLimitUsd,
hasCustomLimitSet,
showTierBreakdown,
billingGaugeItems,
isPricingModalOpen,
Expand Down Expand Up @@ -178,7 +178,7 @@ export const BillingProduct = ({ product }: { product: BillingProductV2Type }):
<div className="px-8 pb-8 sm:pb-0">
{product.percentage_usage > 1 && (
<LemonBanner className="mt-6" type="error">
You have exceeded the {customLimitUsd ? 'billing limit' : 'free tier limit'} for this
You have exceeded the {hasCustomLimitSet ? 'billing limit' : 'free tier limit'} for this
product.
</LemonBanner>
)}
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/scenes/billing/InitialBillingLimitNotice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,10 @@ import { billingLogic } from './billingLogic'
import { billingProductLogic } from './billingProductLogic'

const InitialBillingLimitNoticeContents = ({ product }: { product: BillingProductV2Type }): JSX.Element | null => {
const { currentAndUpgradePlans, customLimitUsd } = useValues(billingProductLogic({ product }))
const { currentAndUpgradePlans, hasCustomLimitSet, customLimitUsd } = useValues(billingProductLogic({ product }))
const initialBillingLimit = currentAndUpgradePlans?.currentPlan?.initial_billing_limit
const isUsingInitialBillingLimit =
currentAndUpgradePlans?.currentPlan?.initial_billing_limit == customLimitUsd &&
customLimitUsd &&
customLimitUsd > 0
hasCustomLimitSet && currentAndUpgradePlans?.currentPlan?.initial_billing_limit == customLimitUsd

return isUsingInitialBillingLimit ? (
<LemonBanner
Expand Down
7 changes: 2 additions & 5 deletions frontend/src/scenes/billing/billingLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ export const billingLogic = kea<billingLogicType>([
return parseBillingResponse(response)
},

updateBillingLimits: async (limits: { [key: string]: string | number | null }) => {
updateBillingLimits: async (limits: { [key: string]: number | null }) => {
const response = await api.update('api/billing', { custom_limits_usd: limits })

lemonToast.success('Billing limits updated')
Expand Down Expand Up @@ -313,10 +313,7 @@ export const billingLogic = kea<billingLogicType>([
const billingLimit =
billing?.custom_limits_usd?.[product.type] ||
(product.usage_key ? billing?.custom_limits_usd?.[product.usage_key] || 0 : 0)
projectedTotal += Math.min(
parseFloat(product.projected_amount_usd || '0'),
typeof billingLimit === 'number' ? billingLimit : parseFloat(billingLimit)
)
projectedTotal += Math.min(parseFloat(product.projected_amount_usd || '0'), billingLimit)
}
return projectedTotal
},
Expand Down
34 changes: 14 additions & 20 deletions frontend/src/scenes/billing/billingProductLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
}),
actions({
setIsEditingBillingLimit: (isEditingBillingLimit: boolean) => ({ isEditingBillingLimit }),
setBillingLimitInput: (billingLimitInput: number | undefined) => ({ billingLimitInput }),
setBillingLimitInput: (billingLimitInput: number | null) => ({ billingLimitInput }),
billingLoaded: true,
setShowTierBreakdown: (showTierBreakdown: boolean) => ({ showTierBreakdown }),
toggleIsPricingModalOpen: true,
Expand Down Expand Up @@ -160,9 +160,10 @@ export const billingProductLogic = kea<billingProductLogicType>([
if (customLimit === 0 || customLimit) {
return customLimit
}
return product.usage_key ? billing?.custom_limits_usd?.[product.usage_key] : null
return product.usage_key ? billing?.custom_limits_usd?.[product.usage_key] ?? null : null
},
],
hasCustomLimitSet: [(s) => [s.customLimitUsd], (customLimitUsd) => !!customLimitUsd && customLimitUsd >= 0],
currentAndUpgradePlans: [
(_s, p) => [p.product],
(product) => {
Expand Down Expand Up @@ -208,11 +209,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
productAndAddonTiers,
billing?.discount_percent
)
: convertAmountToUsage(
typeof customLimitUsd === 'number' ? `${customLimitUsd}` : customLimitUsd || '',
productAndAddonTiers,
billing?.discount_percent
)
: convertAmountToUsage(`${customLimitUsd}`, productAndAddonTiers, billing?.discount_percent)
: 0
},
],
Expand Down Expand Up @@ -264,17 +261,14 @@ export const billingProductLogic = kea<billingProductLogicType>([
actions.billingLoaded()
},
billingLoaded: () => {
function calculateDefaultBillingLimit(product: BillingProductV2Type | BillingProductV2AddonType): number {
const projectedAmount = parseInt(product.projected_amount_usd || '0')
return product.tiers && projectedAmount ? projectedAmount * 1.5 : DEFAULT_BILLING_LIMIT
}

actions.setIsEditingBillingLimit(false)
actions.setBillingLimitInput(
values.customLimitUsd === 0 || values.customLimitUsd
? parseInt(
typeof values.customLimitUsd === 'number'
? `${values.customLimitUsd}`
: values.customLimitUsd || ''
)
: props.product.tiers && parseInt(props.product.projected_amount_usd || '0')
? parseInt(props.product.projected_amount_usd || '0') * 1.5
: DEFAULT_BILLING_LIMIT
values.hasCustomLimitSet ? values.customLimitUsd : calculateDefaultBillingLimit(props.product)
)
},
reportSurveyShown: ({ surveyID }) => {
Expand Down Expand Up @@ -334,7 +328,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
forms(({ actions, props, values }) => ({
billingLimitInput: {
errors: ({ input }) => ({
input: input === undefined || Number.isInteger(input) ? undefined : 'Please enter a whole number',
input: input === null || Number.isInteger(input) ? undefined : 'Please enter a whole number',
}),
submit: async ({ input }) => {
const addonTiers =
Expand Down Expand Up @@ -362,7 +356,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
children: 'I understand',
onClick: () =>
actions.updateBillingLimits({
[props.product.type]: typeof input === 'number' ? `${input}` : null,
[props.product.type]: input,
}),
},
secondaryButton: {
Expand All @@ -381,7 +375,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
children: 'I understand',
onClick: () =>
actions.updateBillingLimits({
[props.product.type]: typeof input === 'number' ? `${input}` : null,
[props.product.type]: input,
}),
},
secondaryButton: {
Expand All @@ -391,7 +385,7 @@ export const billingProductLogic = kea<billingProductLogicType>([
return
}
actions.updateBillingLimits({
[props.product.type]: typeof input === 'number' ? `${input}` : null,
[props.product.type]: input,
})
},
options: {
Expand Down
Loading

0 comments on commit 9f1e936

Please sign in to comment.