diff --git a/package.json b/package.json index 6c2351a7d0..fc06e93a5d 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "@types/js-cookie": "3.0.6", "@types/lodash": "4.17.6", "@types/node": "^22.9.0", + "@types/pluralize": "^0.0.33", "@types/prismjs": "^1.26.4", "@types/prop-types": "15.7.12", "@types/qs": "6.9.15", diff --git a/src/pages/AccountSettings/AccountSettings.test.jsx b/src/pages/AccountSettings/AccountSettings.test.jsx index c67e650a45..de90d7e030 100644 --- a/src/pages/AccountSettings/AccountSettings.test.jsx +++ b/src/pages/AccountSettings/AccountSettings.test.jsx @@ -38,6 +38,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, } diff --git a/src/pages/AccountSettings/AccountSettingsSideMenu.test.jsx b/src/pages/AccountSettings/AccountSettingsSideMenu.test.jsx index d47e234867..b1622b36f3 100644 --- a/src/pages/AccountSettings/AccountSettingsSideMenu.test.jsx +++ b/src/pages/AccountSettings/AccountSettingsSideMenu.test.jsx @@ -26,6 +26,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, } diff --git a/src/pages/MembersPage/MembersActivation/Activation/Activation.jsx b/src/pages/MembersPage/MembersActivation/Activation/Activation.jsx index 2fcd8cbd0f..846d92757d 100644 --- a/src/pages/MembersPage/MembersActivation/Activation/Activation.jsx +++ b/src/pages/MembersPage/MembersActivation/Activation/Activation.jsx @@ -1,3 +1,4 @@ +import pluralize from 'pluralize' import { useParams } from 'react-router-dom' import { useAccountDetails } from 'services/account/useAccountDetails' @@ -74,6 +75,9 @@ function Activation() { activated members of{' '} {planQuantity} available seats{' '} + {planData?.plan?.freeSeatCount + ? `(${planData?.plan?.freeSeatCount} free ${pluralize('seat', planData?.plan?.freeSeatCount)} included) ` + : ''} {accountDetails && ( { expect(availableSeats).toBeInTheDocument() }) + it('displays number of plan free seats', async () => { + setup() + + render(, { wrapper: wrapper() }) + + const freeSeats = await screen.findByText(/2 free seats included/) + expect(freeSeats).toBeInTheDocument() + }) + it('displays change plan link', async () => { setup() diff --git a/src/pages/MembersPage/MembersActivation/MembersActivation.test.jsx b/src/pages/MembersPage/MembersActivation/MembersActivation.test.jsx index 564cb42401..6364ef3f4a 100644 --- a/src/pages/MembersPage/MembersActivation/MembersActivation.test.jsx +++ b/src/pages/MembersPage/MembersActivation/MembersActivation.test.jsx @@ -39,6 +39,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 1, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/MembersPage/MembersList/MembersTable/MembersTable.test.tsx b/src/pages/MembersPage/MembersList/MembersTable/MembersTable.test.tsx index d436cadc75..8d3d0c31d6 100644 --- a/src/pages/MembersPage/MembersList/MembersTable/MembersTable.test.tsx +++ b/src/pages/MembersPage/MembersList/MembersTable/MembersTable.test.tsx @@ -126,6 +126,7 @@ const mockPlanData = { trialEndDate: '', trialTotalDays: 0, pretrialUsersCount: 0, + freeSeatCount: 0, } const server = setupServer() diff --git a/src/pages/OwnerPage/HeaderBanners/ExceededUploadsAlert/ExceededUploadsAlert.test.jsx b/src/pages/OwnerPage/HeaderBanners/ExceededUploadsAlert/ExceededUploadsAlert.test.jsx index 1020e7e610..6c55e4277a 100644 --- a/src/pages/OwnerPage/HeaderBanners/ExceededUploadsAlert/ExceededUploadsAlert.test.jsx +++ b/src/pages/OwnerPage/HeaderBanners/ExceededUploadsAlert/ExceededUploadsAlert.test.jsx @@ -36,6 +36,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx index d01b33bc09..5d060c73fa 100644 --- a/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx +++ b/src/pages/OwnerPage/HeaderBanners/HeaderBanners.test.jsx @@ -39,6 +39,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/OwnerPage/HeaderBanners/ReachingUploadLimitAlert/ReachingUploadLimitAlert.test.jsx b/src/pages/OwnerPage/HeaderBanners/ReachingUploadLimitAlert/ReachingUploadLimitAlert.test.jsx index 83a17a8e5d..98f934d8be 100644 --- a/src/pages/OwnerPage/HeaderBanners/ReachingUploadLimitAlert/ReachingUploadLimitAlert.test.jsx +++ b/src/pages/OwnerPage/HeaderBanners/ReachingUploadLimitAlert/ReachingUploadLimitAlert.test.jsx @@ -42,6 +42,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, } diff --git a/src/pages/OwnerPage/Tabs/TrialReminder/TrialReminder.test.tsx b/src/pages/OwnerPage/Tabs/TrialReminder/TrialReminder.test.tsx index 3aa5c66836..715e20d1ea 100644 --- a/src/pages/OwnerPage/Tabs/TrialReminder/TrialReminder.test.tsx +++ b/src/pages/OwnerPage/Tabs/TrialReminder/TrialReminder.test.tsx @@ -38,6 +38,7 @@ const mockResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isSentryPlan: false, diff --git a/src/pages/PlanPage/subRoutes/CancelPlanPage/CancelPlanPage.test.tsx b/src/pages/PlanPage/subRoutes/CancelPlanPage/CancelPlanPage.test.tsx index f19687315d..db68ba6342 100644 --- a/src/pages/PlanPage/subRoutes/CancelPlanPage/CancelPlanPage.test.tsx +++ b/src/pages/PlanPage/subRoutes/CancelPlanPage/CancelPlanPage.test.tsx @@ -19,7 +19,7 @@ vi.mock('./subRoutes/TeamPlanSpecialOffer', () => ({ const teamPlans = [ { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -29,7 +29,7 @@ const teamPlans = [ }, { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -85,6 +85,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/DowngradePlan/DowngradePlan.test.jsx b/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/DowngradePlan/DowngradePlan.test.jsx index 24aeee22b2..0076e9b33c 100644 --- a/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/DowngradePlan/DowngradePlan.test.jsx +++ b/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/DowngradePlan/DowngradePlan.test.jsx @@ -47,6 +47,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 5, + freeSeatCount: 0, hasSeatsLeft: false, }, }, diff --git a/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/TeamPlanSpecialOffer/TeamPlanCard/TeamPlanCard.test.tsx b/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/TeamPlanSpecialOffer/TeamPlanCard/TeamPlanCard.test.tsx index 2cbfcab47b..84bfa09673 100644 --- a/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/TeamPlanSpecialOffer/TeamPlanCard/TeamPlanCard.test.tsx +++ b/src/pages/PlanPage/subRoutes/CancelPlanPage/subRoutes/TeamPlanSpecialOffer/TeamPlanCard/TeamPlanCard.test.tsx @@ -87,7 +87,7 @@ const mockAvailablePlans = [ }, { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -97,7 +97,7 @@ const mockAvailablePlans = [ }, { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx index 1403f1d69b..26322956d0 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails.tsx @@ -12,6 +12,8 @@ interface URLParams { owner: string } +export const MONTHS_PER_YEAR = 12 + function BillingDetails() { const { provider, owner } = useParams() const { data: accountDetails } = useAccountDetails({ diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/CardInformation.jsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/CardInformation.jsx index 278d8c3be3..9d0fe78290 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/CardInformation.jsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/CardInformation.jsx @@ -33,7 +33,7 @@ const cardBrand = { }, } -function CardInformation({ subscriptionDetail, card }) { +function CardInformation({ subscriptionDetail, card, nextBillPrice }) { const typeCard = cardBrand[card?.brand] ?? cardBrand?.fallback let nextBilling = null @@ -61,7 +61,11 @@ function CardInformation({ subscriptionDetail, card }) { {nextBilling && (

Your next billing date is{' '} - {nextBilling}. + + {nextBilling} + {nextBillPrice ? ` for ${nextBillPrice}` : ''} + + .

)} @@ -76,6 +80,7 @@ CardInformation.propTypes = { expMonth: PropTypes.number.isRequired, expYear: PropTypes.number.isRequired, }).isRequired, + nextBillPrice: PropTypes.string, } export default CardInformation diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/PaymentCard.jsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/PaymentCard.jsx index 56699229b8..ef82960af5 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/PaymentCard.jsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/PaymentCard/PaymentCard.jsx @@ -1,8 +1,18 @@ import PropTypes from 'prop-types' -import { useState } from 'react' +import { useMemo, useState } from 'react' import { accountDetailsPropType } from 'services/account/propTypes' -import { formatTimestampToCalendarDate } from 'shared/utils/billing' +import { usePlanData } from 'services/account/usePlanData' +import { + BillingRate, + formatNumberToUSD, + formatTimestampToCalendarDate, +} from 'shared/utils/billing' +import { + calculatePriceProPlan, + calculatePriceSentryPlan, + calculatePriceTeamPlan, +} from 'shared/utils/upgradeForm' import A from 'ui/A' import Button from 'ui/Button' import Icon from 'ui/Icon' @@ -10,18 +20,61 @@ import Icon from 'ui/Icon' import BankInformation from './BankInformation' import CardInformation from './CardInformation' import PaymentMethodForm from './PaymentMethodForm' + +import { MONTHS_PER_YEAR } from '../BillingDetails' function PaymentCard({ accountDetails, provider, owner }) { const [isFormOpen, setIsFormOpen] = useState(false) + const { data: planData } = usePlanData({ + provider, + owner, + }) const subscriptionDetail = accountDetails?.subscriptionDetail const card = subscriptionDetail?.defaultPaymentMethod?.card const usBankAccount = subscriptionDetail?.defaultPaymentMethod?.usBankAccount - let nextBillingDisplayDate = null if (!subscriptionDetail?.cancelAtPeriodEnd) { nextBillingDisplayDate = formatTimestampToCalendarDate( subscriptionDetail?.currentPeriodEnd ) } + const scheduledPhaseQuantity = + accountDetails?.scheduleDetail?.scheduledPhase?.quantity + + const nextBillPrice = useMemo(() => { + const isPerYear = planData?.plan?.billingRate === BillingRate.ANNUALLY + let seats = + scheduledPhaseQuantity ?? + (planData?.plan?.planUserCount ?? 0) - + (planData?.plan?.freeSeatCount ?? 0) + seats = Math.max(seats, 0) + const planBaseUnitPrice = planData?.plan?.baseUnitPrice ?? 0 + const billPrice = planData?.plan?.isProPlan + ? calculatePriceProPlan({ + seats, + baseUnitPrice: planBaseUnitPrice, + }) + : planData?.plan?.isTeamPlan + ? calculatePriceTeamPlan({ + seats, + baseUnitPrice: planBaseUnitPrice, + }) + : calculatePriceSentryPlan({ + seats, + baseUnitPrice: planBaseUnitPrice, + }) + + return formatNumberToUSD( + isPerYear ? billPrice * MONTHS_PER_YEAR : billPrice + ) + }, [ + planData?.plan?.billingRate, + planData?.plan?.baseUnitPrice, + planData?.plan?.planUserCount, + planData?.plan?.freeSeatCount, + planData?.plan?.isProPlan, + planData?.plan?.isTeamPlan, + scheduledPhaseQuantity, + ]) return (
@@ -45,7 +98,11 @@ function PaymentCard({ accountDetails, provider, owner }) { accountDetails={accountDetails} /> ) : card ? ( - + ) : usBankAccount ? ( ({ useUpdatePaymentMethod: vi.fn(), useCreateStripeSetupIntent: vi.fn(), @@ -35,11 +75,20 @@ vi.mock('services/account/useCreateStripeSetupIntent', async () => { } }) +beforeAll(() => { + server.listen() +}) + afterEach(() => { + server.resetHandlers() queryClient.clear() vi.clearAllMocks() }) +afterAll(() => { + server.close() +}) + const subscriptionDetail = { defaultPaymentMethod: { card: { @@ -102,16 +151,42 @@ vi.mock('@stripe/react-stripe-js', () => { }) describe('PaymentCard', () => { - function setup() { - const user = userEvent.setup() + function setup({ + trialStatus = TrialStatuses.NOT_STARTED, + planValue = mockedAccountDetails.plan.value, + isEnterprisePlan = false, + isTeamPlan = true, + }) { + const user = userEvent.setup({}) + + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + trialStatus, + value: planValue, + isEnterprisePlan, + isTeamPlan, + }, + }, + }, + }) + }) + ) return { user } } describe(`when the user doesn't have any accountDetails`, () => { it('renders the set payment method message', () => { + setup({}) render( - + , + { wrapper } ) expect( @@ -124,6 +199,7 @@ describe('PaymentCard', () => { describe(`when the user doesn't have any payment method`, () => { it('renders an error message', () => { + setup({}) render( { describe('when the user clicks on Set card', () => { it(`doesn't render the card anymore`, async () => { - const { user } = setup() + const { user } = setup({}) render( { }) it('renders the form', async () => { - const { user } = setup() + const { user } = setup({}) render( { describe('when the user have a card', () => { it('renders the card', () => { + setup({}) render( { }) it('renders the next billing', () => { + setup({}) render( { expect(screen.getByText(/December 1, 2020/)).toBeInTheDocument() }) + + it('renders the next billing price', async () => { + setup({}) + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$30.00/)).toBeInTheDocument() + }) + }) + + describe('Pro Plan pricing', () => { + it('calculates Pro plan monthly billing correctly', async () => { + // Pro plan: baseUnitPrice 12, 4 paid seats = $48.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.MONTHLY, + baseUnitPrice: 12, + planUserCount: 5, + freeSeatCount: 1, + isProPlan: true, + isTeamPlan: false, + isSentryPlan: false, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$48.00/)).toBeInTheDocument() + }) + }) + + it('calculates Pro plan annual billing correctly', async () => { + // Pro plan: baseUnitPrice 10, 3 paid seats × 12 = $360.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.ANNUALLY, + baseUnitPrice: 10, + planUserCount: 4, + freeSeatCount: 1, + isProPlan: true, + isTeamPlan: false, + isSentryPlan: false, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$360.00/)).toBeInTheDocument() + }) + }) + }) + + describe('Team Plan pricing', () => { + it('calculates Team plan monthly billing correctly', async () => { + // Team plan: baseUnitPrice 6, 8 paid seats = $48.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.MONTHLY, + baseUnitPrice: 6, + planUserCount: 10, + freeSeatCount: 2, + isProPlan: false, + isTeamPlan: true, + isSentryPlan: false, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$48.00/)).toBeInTheDocument() + }) + }) + + it('calculates Team plan annual billing correctly', async () => { + // Team plan: baseUnitPrice 5, 7 paid seats × 12 = $420.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.ANNUALLY, + baseUnitPrice: 5, + planUserCount: 9, + freeSeatCount: 2, + isProPlan: false, + isTeamPlan: true, + isSentryPlan: false, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$420.00/)).toBeInTheDocument() + }) + }) + }) + + describe('Sentry Plan pricing', () => { + it('calculates Sentry plan monthly billing with 5 or fewer seats correctly', async () => { + // Sentry plan: 5 seats = $29.00 (base price) + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.MONTHLY, + baseUnitPrice: 12, + planUserCount: 5, + freeSeatCount: 0, + isProPlan: false, + isTeamPlan: false, + isSentryPlan: true, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$29.00/)).toBeInTheDocument() + }) + }) + + it('calculates Sentry plan monthly billing with more than 5 seats correctly', async () => { + // Sentry plan: 5 seats included + 3 additional seats × $12 = $29 + $36 = $65.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.MONTHLY, + baseUnitPrice: 12, + planUserCount: 8, + freeSeatCount: 0, + isProPlan: false, + isTeamPlan: false, + isSentryPlan: true, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$65.00/)).toBeInTheDocument() + }) + }) + + it('calculates Sentry plan annual billing with 5 or fewer seats correctly', async () => { + // Sentry plan: 5 seats × 12 months = $29 × 12 = $348.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.ANNUALLY, + baseUnitPrice: 10, + planUserCount: 5, + freeSeatCount: 0, + isProPlan: false, + isTeamPlan: false, + isSentryPlan: true, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$348.00/)).toBeInTheDocument() + }) + }) + + it('calculates Sentry plan annual billing with more than 5 seats correctly', async () => { + // Sentry plan: (5 seats included + 2 additional seats × $10) × 12 = ($29 + $20) × 12 = $588.00 + server.use( + graphql.query('GetPlanData', () => { + return HttpResponse.json({ + data: { + owner: { + hasPrivateRepos: true, + plan: { + ...mockPlanData, + billingRate: BillingRate.ANNUALLY, + baseUnitPrice: 10, + planUserCount: 7, + freeSeatCount: 0, + isProPlan: false, + isTeamPlan: false, + isSentryPlan: true, + }, + }, + }, + }) + }) + ) + + render( + , + { wrapper } + ) + + await waitFor(() => { + expect(screen.getByText(/for \$588.00/)).toBeInTheDocument() + }) + }) + }) }) describe('when the user has a US bank account', () => { it('renders the bank account details', () => { + setup({}) const testAccountDetails = { ...accountDetails, subscriptionDetail: { @@ -254,6 +659,7 @@ describe('PaymentCard', () => { describe('when the subscription is set to expire', () => { it(`doesn't render the next billing`, () => { + setup({}) render( { describe('when the user clicks on Edit card', () => { it(`doesn't render the card anymore`, async () => { - const { user } = setup() + const { user } = setup({}) const updatePaymentMethod = vi.fn() mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: updatePaymentMethod, @@ -296,7 +702,7 @@ describe('PaymentCard', () => { }) it('renders the form', async () => { - const { user } = setup() + const { user } = setup({}) const updatePaymentMethod = vi.fn() mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: updatePaymentMethod, @@ -317,7 +723,7 @@ describe('PaymentCard', () => { describe('when submitting', () => { it('calls the service to update the card', async () => { - const { user } = setup() + const { user } = setup({}) const updatePaymentMethod = vi.fn() mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: updatePaymentMethod, @@ -344,7 +750,7 @@ describe('PaymentCard', () => { describe('when the user clicks on cancel', () => { it(`doesn't render the form anymore`, async () => { - const { user } = setup() + const { user } = setup({}) mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: vi.fn(), isLoading: false, @@ -370,7 +776,7 @@ describe('PaymentCard', () => { describe('when there is an error in the form', () => { it('renders the error', async () => { - const { user } = setup() + const { user } = setup({}) const randomError = 'not rich enough' mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: vi.fn(), @@ -399,7 +805,7 @@ describe('PaymentCard', () => { describe('when the form is loading', () => { it('has the error and save button disabled', async () => { - const { user } = setup() + const { user } = setup({}) mocks.useUpdatePaymentMethod.mockReturnValue({ mutate: vi.fn(), isLoading: true, diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.test.tsx index e6c9b0a66a..3885cbec88 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.test.tsx @@ -217,7 +217,7 @@ describe('CurrentOrgPlan', () => { const updatedAlert = await screen.findByText('Plan successfully updated') expect(updatedAlert).toBeInTheDocument() expect( - screen.getByText(/with a monthly subscription for 34 seats/) + screen.getByText(/with a monthly subscription for 34 paid seats/) ).toBeInTheDocument() }) diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx index 5cede8bdd3..95a673f7d1 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentOrgPlan.tsx @@ -91,7 +91,7 @@ function CurrentOrgPlan() { Plan successfully updated The start date is {scheduleStart} with a monthly - subscription for {scheduledPhase.quantity} seats. + subscription for {scheduledPhase.quantity} paid seats. ) : ( diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/CurrentPlanCard.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/CurrentPlanCard.test.tsx index 73ae27b27c..48a64f779c 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/CurrentPlanCard.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/CurrentPlanCard.test.tsx @@ -156,6 +156,7 @@ describe('CurrentPlanCard', () => { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, monthlyUploadLimit: 100, } diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/FreePlanCard.test.jsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/FreePlanCard.test.jsx index bc63496d0b..d3dcb828b2 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/FreePlanCard.test.jsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/FreePlanCard.test.jsx @@ -93,7 +93,7 @@ const allPlans = [ }, { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -103,7 +103,7 @@ const allPlans = [ }, { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -170,6 +170,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/PlanUpgradeTeam/PlanUpgradeTeam.test.jsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/PlanUpgradeTeam/PlanUpgradeTeam.test.jsx index 9e69a0bd21..9045c4a113 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/PlanUpgradeTeam/PlanUpgradeTeam.test.jsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/PlanUpgradeTeam/PlanUpgradeTeam.test.jsx @@ -31,6 +31,7 @@ const mockPlanBasic = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, } @@ -53,6 +54,7 @@ const mockPlanPro = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 4, + freeSeatCount: 0, hasSeatsLeft: true, } @@ -75,6 +77,7 @@ const mockPlanTrialing = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 4, + freeSeatCount: 0, hasSeatsLeft: true, } @@ -155,7 +158,7 @@ const mockAvailablePlans = [ }, { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -165,7 +168,7 @@ const mockAvailablePlans = [ }, { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/ProPlanSubheading/ProPlanSubheading.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/ProPlanSubheading/ProPlanSubheading.test.tsx index aa1d2da344..5d80b11728 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/ProPlanSubheading/ProPlanSubheading.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/FreePlanCard/ProPlanSubheading/ProPlanSubheading.test.tsx @@ -22,6 +22,7 @@ const mockResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: true, diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.test.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.test.tsx index 3fa5894bdd..4b3731513f 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.test.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.test.tsx @@ -40,6 +40,7 @@ const mockProPlan = { baseUnitPrice: 0, benefits: ['Unlimited public repositories', 'Unlimited private repositories'], planUserCount: 5, + freeSeatCount: 0, monthlyUploadLimit: null, trialStatus: TrialStatuses.CANNOT_TRIAL, trialStartDate: '', @@ -62,6 +63,7 @@ const mockTeamPlan = { baseUnitPrice: 123, benefits: ['Team benefits', 'Unlimited private repositories'], planUserCount: 8, + freeSeatCount: 0, monthlyUploadLimit: 2500, trialStatus: TrialStatuses.CANNOT_TRIAL, trialStartDate: '', @@ -229,13 +231,15 @@ describe('PaidPlanCard', () => { expect(benefitsList).toBeInTheDocument() }) - it('renders seats number', async () => { + it('renders seats number without free/paid distinction', async () => { render(, { wrapper, }) const seats = await screen.findByText(/plan has 8 seats/) expect(seats).toBeInTheDocument() + const seatsWithPaid = screen.queryByText(/plan has 8 seats with \d+ paid/) + expect(seatsWithPaid).not.toBeInTheDocument() }) it('renders the plan pricing', async () => { @@ -255,6 +259,45 @@ describe('PaidPlanCard', () => { const actionsBilling = await screen.findByText(/Actions Billing/) expect(actionsBilling).toBeInTheDocument() }) + + it('does not renders the free seats number if there are no free seats', () => { + render(, { + wrapper, + }) + + const planValue = screen.queryByText( + /Current plan \(\d+ free seats included\)/ + ) + expect(planValue).not.toBeInTheDocument() + const seats = screen.queryByText(/plan has 8 seats with \d+ free/) + expect(seats).not.toBeInTheDocument() + }) + }) + + describe('When rendered with free seats', () => { + beforeEach(() => { + setup({ plan: { ...mockTeamPlan, freeSeatCount: 2 } }) + }) + + it('renders the free seats number', async () => { + render(, { + wrapper, + }) + + const planValue = await screen.findByText( + /Current plan \(2 free seats included\)/ + ) + expect(planValue).toBeInTheDocument() + }) + + it('specifies the number of paid seats', async () => { + render(, { + wrapper, + }) + + const seats = await screen.findByText(/plan has 8 seats with 6 paid/) + expect(seats).toBeInTheDocument() + }) }) describe('When rendered with scheduled details', () => { diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.tsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.tsx index c1938be2c5..6b54214049 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.tsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/PaidPlanCard/PaidPlanCard.tsx @@ -1,5 +1,6 @@ import { useSuspenseQuery as useSuspenseQueryV5 } from '@tanstack/react-queryV5' import isNumber from 'lodash/isNumber' +import pluralize from 'pluralize' import { useParams } from 'react-router-dom' import { PlanPageDataQueryOpts } from 'pages/PlanPage/queries/PlanPageDataQueryOpts' @@ -34,6 +35,7 @@ function PaidPlanCard() { const benefits = plan?.benefits const baseUnitPrice = plan?.baseUnitPrice const seats = plan?.planUserCount + const freeSeats = plan?.freeSeatCount ?? 0 const numberOfUploads = ownerData?.numberOfUploads return ( @@ -41,7 +43,12 @@ function PaidPlanCard() {

{marketingName} plan

- Current plan + + Current plan + {freeSeats + ? ` (${freeSeats} free ${pluralize('seat', freeSeats)} included)` + : ''} +
@@ -64,6 +71,9 @@ function PaidPlanCard() { {seats ? (

plan has {seats} seats + {freeSeats && seats - freeSeats > 0 + ? ` with ${seats - freeSeats} paid` + : ''}

) : null}
diff --git a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/shared/ActionsBilling/ActionsBilling.test.jsx b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/shared/ActionsBilling/ActionsBilling.test.jsx index a8f84e89b5..9c62acd828 100644 --- a/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/shared/ActionsBilling/ActionsBilling.test.jsx +++ b/src/pages/PlanPage/subRoutes/CurrentOrgPlan/CurrentPlanCard/shared/ActionsBilling/ActionsBilling.test.jsx @@ -174,6 +174,7 @@ const mockTrialData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: true, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/PlanDetailsControls/PlanDetailsControls.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/PlanDetailsControls/PlanDetailsControls.test.tsx index bc1282ddab..0c1e18b676 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/PlanDetailsControls/PlanDetailsControls.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/PlanDetailsControls/PlanDetailsControls.test.tsx @@ -76,7 +76,7 @@ const sentryPlanYear = { const teamPlanMonth = { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -87,7 +87,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.test.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.test.jsx index 9ff3ae38f3..98278d9769 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.test.jsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/ProPlanDetails/ProPlanDetails.test.jsx @@ -142,6 +142,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.test.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.test.jsx index cc767e0252..cd5b30a046 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.test.jsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/SentryPlanDetails/SentryPlanDetails.test.jsx @@ -139,6 +139,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/TeamPlanDetails/TeamPlanDetails.test.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/TeamPlanDetails/TeamPlanDetails.test.jsx index bb2a9138a1..f5fa41cf87 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/TeamPlanDetails/TeamPlanDetails.test.jsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/TeamPlanDetails/TeamPlanDetails.test.jsx @@ -17,7 +17,7 @@ vi.mock('shared/plan/ScheduledPlanDetails', () => ({ const teamPlanMonth = { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -28,7 +28,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -100,6 +100,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.test.tsx index 060ff04df7..c8792a5d17 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeDetails/UpgradeDetails.test.tsx @@ -82,7 +82,7 @@ const sentryPlanMonth = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -93,7 +93,7 @@ const teamPlanYear = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/Controller.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/Controller.test.tsx index 4eaa554bcb..b4dda1c2d5 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/Controller.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/Controller.test.tsx @@ -84,7 +84,7 @@ const sentryPlanYear = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -95,7 +95,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/BillingOptions/BillingOptions.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/BillingOptions/BillingOptions.test.tsx index e9964e6fe5..8696efe3fb 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/BillingOptions/BillingOptions.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/BillingOptions/BillingOptions.test.tsx @@ -73,6 +73,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.test.tsx index c0cd5a6e34..95817776a2 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.test.tsx @@ -126,9 +126,7 @@ describe('PriceCallout', () => { const mockAccountDetails = { subscriptionDetail: { - latestInvoice: { - periodEnd: periodEnd, - }, + currentPeriodEnd: periodEnd, }, } diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.tsx index d077122bb9..77644c1753 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/PriceCallout/PriceCallout.tsx @@ -2,6 +2,7 @@ import { Fragment } from 'react' import { UseFormSetValue } from 'react-hook-form' import { useParams } from 'react-router-dom' +import { MONTHS_PER_YEAR } from 'pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails' import { useAccountDetails } from 'services/account/useAccountDetails' import { IndividualPlan, @@ -60,12 +61,16 @@ const PriceCallout: React.FC = ({ {formatNumberToUSD(perYearPrice)} - /month billed annually at {formatNumberToUSD(perYearPrice * 12)} + /month billed annually at{' '} + {formatNumberToUSD(perYearPrice * MONTHS_PER_YEAR)}

🎉 You{' '} - save {formatNumberToUSD((perMonthPrice - perYearPrice) * 12)} + save{' '} + {formatNumberToUSD( + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR + )} {' '} with annual billing {nextBillingDate && ( @@ -92,7 +97,10 @@ const PriceCallout: React.FC = ({

You could{' '} - save {formatNumberToUSD((perMonthPrice - perYearPrice) * 12)} + save{' '} + {formatNumberToUSD( + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR + )} {' '} a year with annual billing {nextBillingDate && ( diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/ProPlanController.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/ProPlanController.test.tsx index ff5c68874a..f9d8d0147c 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/ProPlanController.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/ProPlanController/ProPlanController.test.tsx @@ -136,6 +136,7 @@ const mockPlanDataResponseMonthly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: true, @@ -158,6 +159,7 @@ const mockPlanDataResponseYearly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: true, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/BillingOptions/BillingOptions.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/BillingOptions/BillingOptions.test.tsx index 128b712580..54486fb35f 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/BillingOptions/BillingOptions.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/BillingOptions/BillingOptions.test.tsx @@ -73,6 +73,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.test.tsx index de2f5f9cec..0f20b2da65 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.test.tsx @@ -98,9 +98,7 @@ describe('PriceCallout', () => { const mockAccountDetails = { subscriptionDetail: { - latestInvoice: { - periodEnd: periodEnd, - }, + currentPeriodEnd: periodEnd, }, } diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.tsx index deed6a31c0..e01c0fa234 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/PriceCallout/PriceCallout.tsx @@ -2,6 +2,7 @@ import { Fragment } from 'react' import { UseFormSetValue } from 'react-hook-form' import { useParams } from 'react-router-dom' +import { MONTHS_PER_YEAR } from 'pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails' import { useAccountDetails } from 'services/account/useAccountDetails' import { IndividualPlan, @@ -64,14 +65,15 @@ const PriceCallout: React.FC = ({ {formatNumberToUSD(perYearPrice)} - /month billed annually at {formatNumberToUSD(perYearPrice * 12)} + /month billed annually at{' '} + {formatNumberToUSD(perYearPrice * MONTHS_PER_YEAR)}

🎉 You{' '} save{' '} {formatNumberToUSD( - nonBundledCost + (perMonthPrice - perYearPrice) * 12 + nonBundledCost + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR )} {' '} with the Sentry bundle plan @@ -110,7 +112,9 @@ const PriceCallout: React.FC = ({ , save an{' '} additional{' '} - {formatNumberToUSD((perMonthPrice - perYearPrice) * 12)} + {formatNumberToUSD( + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR + )} {' '} a year with annual billing {nextBillingDate && ( diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/SentryPlanController.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/SentryPlanController.test.tsx index c92ec5e5d8..580928996b 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/SentryPlanController.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/SentryPlanController/SentryPlanController.test.tsx @@ -120,6 +120,7 @@ const mockPlanDataResponseMonthly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, @@ -142,6 +143,7 @@ const mockPlanDataResponseYearly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/BillingOptions/BillingOptions.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/BillingOptions/BillingOptions.test.tsx index f92d0e5857..0c7575d698 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/BillingOptions/BillingOptions.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/BillingOptions/BillingOptions.test.tsx @@ -28,7 +28,7 @@ const freePlan = { const teamPlanMonthly = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Team', monthlyUploadLimit: 2500, @@ -39,7 +39,7 @@ const teamPlanMonthly = { const teamPlanYearly = { baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Team', monthlyUploadLimit: 2500, @@ -64,6 +64,7 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isFreePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/ErrorBanner/ErrorBanner.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/ErrorBanner/ErrorBanner.test.tsx index a9aff67bf3..414d7cfb84 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/ErrorBanner/ErrorBanner.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/ErrorBanner/ErrorBanner.test.tsx @@ -30,7 +30,7 @@ const basicPlan = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -42,7 +42,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -140,6 +140,7 @@ describe('ErrorBanner', () => { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, isEnterprisePlan: false, isFreePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.test.tsx index 7f7e90660f..4a59b677c0 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.test.tsx @@ -109,9 +109,7 @@ describe('PriceCallout', () => { const mockAccountDetails = { subscriptionDetail: { - latestInvoice: { - periodEnd: periodEnd, - }, + currentPeriodEnd: periodEnd, }, } diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.tsx index 980d23c870..4bf0c56a7e 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/PriceCallout/PriceCallout.tsx @@ -3,6 +3,7 @@ import { Fragment } from 'react' import { UseFormSetValue } from 'react-hook-form' import { useParams } from 'react-router-dom' +import { MONTHS_PER_YEAR } from 'pages/PlanPage/subRoutes/CurrentOrgPlan/BillingDetails/BillingDetails' import { useAccountDetails } from 'services/account/useAccountDetails' import { IndividualPlan, @@ -61,12 +62,16 @@ const PriceCallout: React.FC = ({ {formatNumberToUSD(perYearPrice)} - /month billed annually at {formatNumberToUSD(perYearPrice * 12)} + /month billed annually at{' '} + {formatNumberToUSD(perYearPrice * MONTHS_PER_YEAR)}

🎉 You{' '} - save {formatNumberToUSD((perMonthPrice - perYearPrice) * 12)} + save{' '} + {formatNumberToUSD( + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR + )} {' '} with annual billing {nextBillingDate && ( @@ -93,7 +98,10 @@ const PriceCallout: React.FC = ({

You could{' '} - save {formatNumberToUSD((perMonthPrice - perYearPrice) * 12)} + save{' '} + {formatNumberToUSD( + (perMonthPrice - perYearPrice) * MONTHS_PER_YEAR + )} {' '} a year with annual billing {nextBillingDate && ( diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/TeamPlanController.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/TeamPlanController.test.tsx index 6df7cf9543..7d5f793a7e 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/TeamPlanController.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/Controllers/TeamPlanController/TeamPlanController.test.tsx @@ -43,7 +43,7 @@ const basicPlan = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -55,7 +55,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -120,6 +120,7 @@ const mockPlanDataResponseMonthly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, @@ -142,6 +143,7 @@ const mockPlanDataResponseYearly = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PersonalOrgWarning.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PersonalOrgWarning.tsx index ef33a19978..8509dc2446 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PersonalOrgWarning.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PersonalOrgWarning.tsx @@ -12,9 +12,6 @@ export function PersonalOrgWarning() { const { data } = useUser() const username = data?.user.username - console.log('owner', owner) - console.log('username', username) - if (owner !== username) { return null } diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.test.tsx index 1c50244cb5..e2c904d81e 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.test.tsx @@ -93,7 +93,7 @@ const sentryPlanYear = { const teamPlanMonth = { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -104,7 +104,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -203,6 +203,7 @@ describe('PlanTypeOptions', () => { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, isEnterprisePlan: false, isFreePlan: false, isProPlan: false, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.tsx index 320ce2ff91..b2b8af96cf 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/PlanTypeOptions/PlanTypeOptions.tsx @@ -110,7 +110,7 @@ const PlanTypeOptions: React.FC = ({ {planOption === TierName.TEAM && ( -

Up to {TEAM_PLAN_MAX_ACTIVE_USERS} users

+

Up to {TEAM_PLAN_MAX_ACTIVE_USERS} paid users

)} diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateBlurb/UpdateBlurb.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateBlurb/UpdateBlurb.test.tsx index 3fc9ed029b..e0b2a9cb66 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateBlurb/UpdateBlurb.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateBlurb/UpdateBlurb.test.tsx @@ -14,6 +14,7 @@ const planChunk = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 2, + freeSeatCount: 0, isEnterprisePlan: false, isFreePlan: false, isSentryPlan: false, @@ -41,7 +42,7 @@ const proPlanYear = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -53,7 +54,7 @@ const teamPlanYear = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -65,7 +66,7 @@ const teamPlanMonth = { const freePlan = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx index f944be88fa..a14ce477c4 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpdateButton/UpdateButton.test.tsx @@ -90,12 +90,13 @@ afterAll(() => { const mockPlanBasic = { value: Plans.USERS_DEVELOPER, baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: 'annually', marketingName: 'Users Team', monthlyUploadLimit: 2500, hasSeatsLeft: true, planUserCount: 1, + freeSeatCount: 0, isFreePlan: true, isTeamPlan: false, } @@ -103,12 +104,13 @@ const mockPlanBasic = { const mockPlanProMonthly = { value: Plans.USERS_PR_INAPPM, baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: 'annually', marketingName: 'Users Team', monthlyUploadLimit: 2500, hasSeatsLeft: true, planUserCount: 4, + freeSeatCount: 0, isFreePlan: false, isTeamPlan: false, } @@ -116,12 +118,13 @@ const mockPlanProMonthly = { const mockPlanTeamMonthly = { value: Plans.USERS_TEAMM, baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: 'annually', marketingName: 'Users Team', monthlyUploadLimit: 2500, hasSeatsLeft: true, planUserCount: 3, + freeSeatCount: 0, isFreePlan: false, isTeamPlan: true, } diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx index e1ad1549de..2c07b9eb1d 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradeForm/UpgradeForm.test.tsx @@ -122,7 +122,7 @@ const sentryPlanYear = { const teamPlanMonth = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -133,7 +133,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 4, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -228,9 +228,11 @@ const mockPlanDataResponse = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 10, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isSentryPlan: false, + isFreePlan: false, } const mockUser = { @@ -460,6 +462,7 @@ describe('UpgradeForm', () => { value: planValue, planUserCount, }, + pretrialPlan: null, }, }, }) @@ -691,7 +694,7 @@ describe('UpgradeForm', () => { }) describe('when updating to a team plan', () => { - it('renders up to 10 seats text', async () => { + it('renders up to 10 paid users text', async () => { const { user } = setup({ planValue: Plans.USERS_DEVELOPER, hasTeamPlans: true, @@ -701,7 +704,7 @@ describe('UpgradeForm', () => { const teamOption = await screen.findByTestId('radio-team') await user.click(teamOption) - const auxiliaryText = await screen.findByText(/Up to 10 users/) + const auxiliaryText = await screen.findByText(/Up to 10 paid users/) expect(auxiliaryText).toBeInTheDocument() }) @@ -1072,7 +1075,7 @@ describe('UpgradeForm', () => { }) describe('when updating to a team plan', () => { - it('renders up to 10 seats text', async () => { + it('renders up to 10 paid users text', async () => { const { user } = setup({ planValue: Plans.USERS_PR_INAPPM, hasTeamPlans: true, @@ -1082,7 +1085,7 @@ describe('UpgradeForm', () => { const teamOption = await screen.findByTestId('radio-team') await user.click(teamOption) - const auxiliaryText = await screen.findByText(/Up to 10 users/) + const auxiliaryText = await screen.findByText(/Up to 10 paid users/) expect(auxiliaryText).toBeInTheDocument() }) @@ -1419,7 +1422,7 @@ describe('UpgradeForm', () => { }) describe('when updating to a team plan', () => { - it('renders up to 10 seats text', async () => { + it('renders up to 10 paid users text', async () => { const { user } = setup({ planValue: Plans.USERS_PR_INAPPY, hasTeamPlans: true, @@ -1430,7 +1433,7 @@ describe('UpgradeForm', () => { const teamOption = await screen.findByTestId('radio-team') await user.click(teamOption) - const auxiliaryText = await screen.findByText(/Up to 10 users/) + const auxiliaryText = await screen.findByText(/Up to 10 paid users/) expect(auxiliaryText).toBeInTheDocument() }) @@ -2035,7 +2038,7 @@ describe('UpgradeForm', () => { expect(ownerTitle).toBeInTheDocument() }) - it('renders up to 10 seats text', async () => { + it('renders up to 10 paid users text', async () => { const { user } = setup({ planValue: Plans.USERS_TEAMY, hasTeamPlans: true, @@ -2046,7 +2049,7 @@ describe('UpgradeForm', () => { const teamOption = await screen.findByTestId('radio-team') await user.click(teamOption) - const auxiliaryText = await screen.findByText(/Up to 10 users/) + const auxiliaryText = await screen.findByText(/Up to 10 paid users/) expect(auxiliaryText).toBeInTheDocument() }) @@ -2151,7 +2154,7 @@ describe('UpgradeForm', () => { expect(update).toBeDisabled() const error = screen.getByText( - /Team plan is only available for 10 seats or fewer./ + /Team plan is only available for 10 paid seats or fewer./ ) expect(error).toBeInTheDocument() }) diff --git a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.test.jsx b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.test.jsx index 4e11de5074..6fc492ed19 100644 --- a/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.test.jsx +++ b/src/pages/PlanPage/subRoutes/UpgradePlanPage/UpgradePlanPage.test.jsx @@ -130,7 +130,7 @@ const sentryPlanYear = { const teamPlanMonth = { baseUnitPrice: 6, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.MONTHLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -141,7 +141,7 @@ const teamPlanMonth = { const teamPlanYear = { baseUnitPrice: 5, - benefits: ['Up to 10 users'], + benefits: ['Up to 10 paid users'], billingRate: BillingRate.ANNUALLY, marketingName: 'Users Team', monthlyUploadLimit: 2500, @@ -163,6 +163,7 @@ const mockPlanData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, } @@ -387,7 +388,7 @@ describe('UpgradePlanPage', () => { wrapper: wrapper('/plan/gh/codecov/upgrade?plan=team'), }) - const userCount = await screen.findByText(/Up to 10 users/) + const userCount = await screen.findByText(/Up to 10 paid users/) expect(userCount).toBeInTheDocument() }) }) diff --git a/src/pages/RepoPage/ActivationAlert/ActivationAlert.test.tsx b/src/pages/RepoPage/ActivationAlert/ActivationAlert.test.tsx index 314cc43eef..39628feaf0 100644 --- a/src/pages/RepoPage/ActivationAlert/ActivationAlert.test.tsx +++ b/src/pages/RepoPage/ActivationAlert/ActivationAlert.test.tsx @@ -62,6 +62,7 @@ const mockTrialData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: false, diff --git a/src/pages/RepoPage/CoverageOnboarding/ActivationBanner/ActivationBanner.test.tsx b/src/pages/RepoPage/CoverageOnboarding/ActivationBanner/ActivationBanner.test.tsx index 8d732521a9..c4b0bdf3ad 100644 --- a/src/pages/RepoPage/CoverageOnboarding/ActivationBanner/ActivationBanner.test.tsx +++ b/src/pages/RepoPage/CoverageOnboarding/ActivationBanner/ActivationBanner.test.tsx @@ -62,6 +62,7 @@ const mockTrialData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isProPlan: false, diff --git a/src/services/account/usePlanData.test.tsx b/src/services/account/usePlanData.test.tsx index e048cb4036..9f858b2ed2 100644 --- a/src/services/account/usePlanData.test.tsx +++ b/src/services/account/usePlanData.test.tsx @@ -22,6 +22,7 @@ const mockTrialData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, isEnterprisePlan: false, isFreePlan: true, @@ -102,6 +103,7 @@ describe('usePlanData', () => { marketingName: 'Users Developer', monthlyUploadLimit: 250, planUserCount: 1, + freeSeatCount: 0, pretrialUsersCount: 0, trialEndDate: '2023-01-10T08:55:25', trialStartDate: '2023-01-01T08:55:25', diff --git a/src/services/account/usePlanData.ts b/src/services/account/usePlanData.ts index 96cb7f72fc..9988624885 100644 --- a/src/services/account/usePlanData.ts +++ b/src/services/account/usePlanData.ts @@ -27,6 +27,7 @@ const PlanSchema = z.object({ trialStartDate: z.string().nullable(), trialTotalDays: z.number().nullable(), planUserCount: z.number().nullable(), + freeSeatCount: z.number().nullable(), hasSeatsLeft: z.boolean(), isEnterprisePlan: z.boolean(), isFreePlan: z.boolean(), @@ -86,6 +87,7 @@ export const query = ` trialStartDate trialTotalDays planUserCount + freeSeatCount hasSeatsLeft isEnterprisePlan isFreePlan diff --git a/src/shared/GlobalTopBanners/ProPlanFeedbackBanner/ProPlanFeedbackBanner.test.tsx b/src/shared/GlobalTopBanners/ProPlanFeedbackBanner/ProPlanFeedbackBanner.test.tsx index f41198278b..551cd98ea2 100644 --- a/src/shared/GlobalTopBanners/ProPlanFeedbackBanner/ProPlanFeedbackBanner.test.tsx +++ b/src/shared/GlobalTopBanners/ProPlanFeedbackBanner/ProPlanFeedbackBanner.test.tsx @@ -32,6 +32,7 @@ const mockTrialData = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, hasSeatsLeft: true, }, pretrialPlan: { diff --git a/src/shared/GlobalTopBanners/TrialBanner/TrialBanner.test.tsx b/src/shared/GlobalTopBanners/TrialBanner/TrialBanner.test.tsx index 7d70dc1dc4..e918ec2a47 100644 --- a/src/shared/GlobalTopBanners/TrialBanner/TrialBanner.test.tsx +++ b/src/shared/GlobalTopBanners/TrialBanner/TrialBanner.test.tsx @@ -30,6 +30,7 @@ const proPlanMonth = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, isEnterprisePlan: false, isFreePlan: false, isProPlan: true, @@ -53,6 +54,7 @@ const trialPlan = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, isEnterprisePlan: false, isFreePlan: false, isProPlan: false, @@ -76,6 +78,7 @@ const basicPlan = { trialTotalDays: 0, pretrialUsersCount: 0, planUserCount: 1, + freeSeatCount: 0, isFreePlan: true, isProPlan: false, isSentryPlan: false, @@ -166,6 +169,7 @@ describe('TrialBanner', () => { trialTotalDays: plan.trialTotalDays, pretrialUsersCount: plan.pretrialUsersCount, planUserCount: plan.planUserCount, + freeSeatCount: plan.freeSeatCount, hasSeatsLeft: true, isEnterprisePlan: plan.isEnterprisePlan, isFreePlan: plan.isFreePlan, diff --git a/src/shared/plan/ScheduledPlanDetails/ScheduledPlanDetails.tsx b/src/shared/plan/ScheduledPlanDetails/ScheduledPlanDetails.tsx index 9054f300c5..2eb542c872 100644 --- a/src/shared/plan/ScheduledPlanDetails/ScheduledPlanDetails.tsx +++ b/src/shared/plan/ScheduledPlanDetails/ScheduledPlanDetails.tsx @@ -26,7 +26,7 @@ function ScheduledPlanDetails({ Start date {scheduleStart}

- {plan} with {quantity} seats + {plan} with {quantity} paid seats

) diff --git a/src/shared/utils/billing.test.ts b/src/shared/utils/billing.test.ts index 368ea1ea30..96490ae0ae 100644 --- a/src/shared/utils/billing.test.ts +++ b/src/shared/utils/billing.test.ts @@ -141,7 +141,7 @@ function getPlans() { baseUnitPrice: 6, monthlyUploadLimit: null, benefits: [ - 'Up to 10 users', + 'Up to 10 paid users', 'Unlimited repositories', '2500 repositories', 'Patch coverage analysis', @@ -157,7 +157,7 @@ function getPlans() { baseUnitPrice: 5, monthlyUploadLimit: null, benefits: [ - 'Up to 10 users', + 'Up to 10 paid users', 'Unlimited repositories', '2500 repositories', 'Patch coverage analysis', @@ -253,11 +253,9 @@ describe('getNextBillingDate', () => { describe('there is a valid timestamp', () => { it('returns formatted timestamp', () => { const value = getNextBillingDate({ + // @ts-expect-error - we're just testing this property we can ignore the other properties subscriptionDetail: { - // @ts-expect-error - we're just testing this property we can ignore the other properties - latestInvoice: { - periodEnd: 1660000000, - }, + currentPeriodEnd: 1660000000, }, }) @@ -385,7 +383,7 @@ describe('findTeamPlans', () => { baseUnitPrice: 6, monthlyUploadLimit: null, benefits: [ - 'Up to 10 users', + 'Up to 10 paid users', 'Unlimited repositories', '2500 repositories', 'Patch coverage analysis', @@ -409,7 +407,7 @@ describe('findTeamPlans', () => { baseUnitPrice: 5, monthlyUploadLimit: null, benefits: [ - 'Up to 10 users', + 'Up to 10 paid users', 'Unlimited repositories', '2500 repositories', 'Patch coverage analysis', diff --git a/src/shared/utils/billing.ts b/src/shared/utils/billing.ts index a49258660f..51170486b8 100644 --- a/src/shared/utils/billing.ts +++ b/src/shared/utils/billing.ts @@ -131,7 +131,7 @@ export const formatNumberToUSD = (value: number) => export function getNextBillingDate( accountDetails?: z.infer | null ) { - const timestamp = accountDetails?.subscriptionDetail?.latestInvoice?.periodEnd + const timestamp = accountDetails?.subscriptionDetail?.currentPeriodEnd return timestamp ? format(fromUnixTime(timestamp), 'MMMM do, yyyy') : null } diff --git a/src/shared/utils/upgradeForm.test.ts b/src/shared/utils/upgradeForm.test.ts index 0e6479efdb..e372c08650 100644 --- a/src/shared/utils/upgradeForm.test.ts +++ b/src/shared/utils/upgradeForm.test.ts @@ -166,6 +166,26 @@ describe('getDefaultValuesUpgradeForm', () => { }) }) + it('returns correct seats when free seats are present', () => { + const data = getDefaultValuesUpgradeForm({ + accountDetails, + selectedPlan: proPlanYear, + plans: [teamPlanMonth], + plan: { + billingRate: BillingRate.MONTHLY, + value: Plans.USERS_TEAMM, + planUserCount: 5, + freeSeatCount: 2, + isTeamPlan: true, + } as Plan, + }) + + expect(data).toStrictEqual({ + newPlan: { value: Plans.USERS_TEAMM }, + seats: 3, + }) + }) + it('returns pro sentry plan if user is sentry upgrade', () => { const data = getDefaultValuesUpgradeForm({ accountDetails, @@ -214,6 +234,33 @@ describe('getDefaultValuesUpgradeForm', () => { seats: 2, }) }) + + describe('quantity calculation edge cases', () => { + it('handles case where freeSeatCount equals planUserCount', () => { + const data = getDefaultValuesUpgradeForm({ + accountDetails, + selectedPlan: proPlanYear, + plans: [proPlanYear], + plan: { + billingRate: BillingRate.MONTHLY, + value: Plans.USERS_PR_INAPPM, + planUserCount: 3, + freeSeatCount: 3, + } as Plan, + }) + + expect(data).toStrictEqual({ + newPlan: { + value: Plans.USERS_PR_INAPPM, + billingRate: BillingRate.MONTHLY, + planUserCount: 3, + freeSeatCount: 3, + }, + // extractSeats() will be passed quantity: 0, but returns min plan seats + seats: 2, + }) + }) + }) }) describe('getSchema', () => { @@ -328,7 +375,7 @@ describe('getSchema', () => { const [issue] = response.error!.issues expect(issue).toEqual( expect.objectContaining({ - message: 'Team plan is only available for 10 seats or fewer.', + message: 'Team plan is only available for 10 paid seats or fewer.', }) ) }) diff --git a/src/shared/utils/upgradeForm.ts b/src/shared/utils/upgradeForm.ts index 6ee9e07a7a..a6fce721e4 100644 --- a/src/shared/utils/upgradeForm.ts +++ b/src/shared/utils/upgradeForm.ts @@ -16,8 +16,9 @@ export const MIN_NB_SEATS_PRO = 2 export const MIN_SENTRY_SEATS = 5 export const SENTRY_PRICE = 29 export const TEAM_PLAN_MAX_ACTIVE_USERS = 10 +export const MONTHS_PER_YEAR = 12 -export const UPGRADE_FORM_TOO_MANY_SEATS_MESSAGE = `Team plan is only available for ${TEAM_PLAN_MAX_ACTIVE_USERS} seats or fewer.` +export const UPGRADE_FORM_TOO_MANY_SEATS_MESSAGE = `Team plan is only available for ${TEAM_PLAN_MAX_ACTIVE_USERS} paid seats or fewer.` export function extractSeats({ quantity, @@ -194,7 +195,9 @@ export const calculateSentryNonBundledCost = ({ baseUnitPrice = 0, }: { baseUnitPrice?: number -}) => MIN_SENTRY_SEATS * baseUnitPrice * 12 - SENTRY_PRICE * 12 +}) => + MIN_SENTRY_SEATS * baseUnitPrice * MONTHS_PER_YEAR - + SENTRY_PRICE * MONTHS_PER_YEAR export const getDefaultValuesUpgradeForm = ({ accountDetails, @@ -235,7 +238,10 @@ export const getDefaultValuesUpgradeForm = ({ } const seats = extractSeats({ - quantity: plan?.planUserCount ?? 0, + // free seats are included in planUserCount but we want to use the paid number + quantity: plan?.planUserCount + ? plan?.planUserCount - (plan?.freeSeatCount ?? 0) + : 0, activatedUserCount, inactiveUserCount, trialStatus, diff --git a/yarn.lock b/yarn.lock index e0d0f4fa2f..e88be4eb66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5706,6 +5706,13 @@ __metadata: languageName: node linkType: hard +"@types/pluralize@npm:^0.0.33": + version: 0.0.33 + resolution: "@types/pluralize@npm:0.0.33" + checksum: 10c0/24899caf85b79dd291a6b6e9b9f3b67b452b18d578d0ac0d531a705bf5ee0361d9386ea1f8532c64de9e22c6e9606c5497787bb5e31bd299c487980436c59785 + languageName: node + linkType: hard + "@types/prismjs@npm:^1.26.4": version: 1.26.4 resolution: "@types/prismjs@npm:1.26.4" @@ -9128,6 +9135,7 @@ __metadata: "@types/js-cookie": "npm:3.0.6" "@types/lodash": "npm:4.17.6" "@types/node": "npm:^22.9.0" + "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:^1.26.4" "@types/prop-types": "npm:15.7.12" "@types/qs": "npm:6.9.15"