diff --git a/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.scss b/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.scss index 0ac7a31fd4..f9d7ca4d36 100644 --- a/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.scss +++ b/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.scss @@ -88,18 +88,18 @@ font-size: 12px; } - .totalCost { + .totalCostWarning { width: 141px; } - .totalCost > span:first-child { + .totalCostWarning > span:first-child { color: var(--sys-denim-center-channel-text); font-family: 'Open Sans'; font-size: 14px; font-weight: 700; } - .totalCost > span:last-child { + .totalCostWarning > span:last-child { color: rgba(var(--sys-denim-center-channel-text-rgb), 0.72); font-family: 'Open Sans'; font-size: 12px; diff --git a/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.tsx b/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.tsx index d79d6b66fc..6eadd2de6e 100644 --- a/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.tsx +++ b/webapp/channels/src/components/self_hosted_expansion_modal/expansion_card.tsx @@ -18,7 +18,6 @@ import {findSelfHostedProductBySku} from 'utils/hosted_customer'; import ExternalLink from 'components/external_link'; const MONTHS_IN_YEAR = 12; -const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; const MAX_TRANSACTION_VALUE = 1_000_000 - 1; interface Props { @@ -35,14 +34,14 @@ export default function SelfHostedExpansionCard(props: Props) { const endsAt = moment(parseInt(license.ExpiresAt, 10)).format('MMM. D, YYYY'); const [additionalSeats, setAdditionalSeats] = useState(props.initialSeats); const [overMaxSeats, setOverMaxSeats] = useState(false); - const licenseExpiry = parseInt(license.ExpiresAt, 10); + const licenseExpiry = new Date(parseInt(license.ExpiresAt, 10)); const invalidAdditionalSeats = additionalSeats === 0 || isNaN(additionalSeats); const [products] = useGetSelfHostedProducts(); const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName); const getMonthsUntilExpiry = () => { const now = new Date(); - return Math.ceil((licenseExpiry - now.getTime()) / MILLISECONDS_PER_DAY / 30); + return (licenseExpiry.getMonth() - now.getMonth()) + 12 * (licenseExpiry.getFullYear() - now.getFullYear()); }; const getMonthlyPrice = () => { @@ -209,7 +208,7 @@ export default function SelfHostedExpansionCard(props: Props) {
{'$' + getCostPerUser().toFixed(2)} -
+
- + {'$' + getTotal().toFixed(2)}
diff --git a/webapp/channels/src/components/self_hosted_expansion_modal/index.test.tsx b/webapp/channels/src/components/self_hosted_expansion_modal/index.test.tsx index c44aae70a3..0097108ae8 100644 --- a/webapp/channels/src/components/self_hosted_expansion_modal/index.test.tsx +++ b/webapp/channels/src/components/self_hosted_expansion_modal/index.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; -import {screen, fireEvent} from '@testing-library/react'; +import {screen, fireEvent, waitFor} from '@testing-library/react'; import {GlobalState} from 'types/store'; @@ -11,7 +11,7 @@ import {SelfHostedSignupForm, SelfHostedSignupProgress} from '@mattermost/types/ import {renderWithIntlAndStore} from 'tests/react_testing_utils'; import {TestHelper as TH} from 'utils/test_helper'; -import {SelfHostedProducts, ModalIdentifiers} from 'utils/constants'; +import {SelfHostedProducts, ModalIdentifiers, RecurringIntervals} from 'utils/constants'; import {DeepPartial} from '@mattermost/types/utilities'; @@ -76,6 +76,7 @@ const mockProfessionalProduct = TH.getProductMock({ name: 'Professional', sku: SelfHostedProducts.PROFESSIONAL, price_per_seat: 7.5, + recurring_interval: RecurringIntervals.MONTH }); jest.mock('mattermost-redux/client', () => { @@ -129,6 +130,11 @@ jest.mock('utils/hosted_customer', () => { const productName = SelfHostedProducts.PROFESSIONAL; +// Licensed expiry set as 3 months from the current date (rolls over to new years). +const licenseExpiry = new Date(); +const monthsUntilLicenseExpiry = 3; +licenseExpiry.setMonth(licenseExpiry.getMonth() + monthsUntilLicenseExpiry); + const initialState: DeepPartial = { views: { modals: { @@ -143,11 +149,6 @@ const initialState: DeepPartial = { storage: {}, }, entities: { - admin: { - analytics: { - TOTAL_USERS: existingUsers, - }, - }, teams: { currentTeamId: '', }, @@ -163,6 +164,7 @@ const initialState: DeepPartial = { license: { Sku: productName, Users: '50', + ExpiresAt: licenseExpiry.getTime().toString() }, }, cloud: { @@ -224,6 +226,7 @@ interface PurchaseForm { state: string; zip: string; seats: string; + agree: boolean; } const defaultSuccessForm: PurchaseForm = { @@ -236,6 +239,7 @@ const defaultSuccessForm: PurchaseForm = { state: 'MN', zip: '55423', seats: '10', + agree: true, }; function fillForm(form: PurchaseForm) { @@ -248,6 +252,9 @@ function fillForm(form: PurchaseForm) { selectDropdownValue('selfHostedExpansionStateSelector', form.state); changeByPlaceholder('Zip/Postal Code', form.zip); changeByTestId('seatsInput', form.seats); + if (form.agree) { + fireEvent.click(screen.getByText('I have read and agree', {exact: false})); + } expect(document.getElementsByClassName('SelfHostedExpansionRHSCard__AddSeatsWarning')[0] as HTMLElement).toBeEnabled(); @@ -263,7 +270,7 @@ function fillForm(form: PurchaseForm) { return completeButton; } -describe('SelfHostedExpansionModal', () => { +describe('SelfHostedExpansionModal Open', () => { it('renders the form', () => { renderWithIntlAndStore(
, initialState); @@ -321,7 +328,45 @@ describe('SelfHostedExpansionModal', () => { // }); }); -describe('SelfHostedExpansionModal :: canSubmit', () => { +describe('SelfHostedExpansionModal RHS Card', () => { + it("New seats input should be pre-populated with the difference from the active users and licensed seats", () => { + renderWithIntlAndStore(
, initialState); + + const expectedPrePopulatedSeats = (initialState.entities?.users?.filteredStats?.total_users_count || 1) - parseInt(initialState.entities?.general?.license?.Users || '0', 10); + + const seatsField = screen.getByTestId('seatsInput').querySelector('input'); + expect(seatsField).toBeInTheDocument(); + expect(seatsField?.value).toBe(expectedPrePopulatedSeats.toString()); + }); + + it("Cost per User should be represented as the current subscription price multiplied by the remaining months", () => { + renderWithIntlAndStore(
, initialState); + + const expectedCostPerUser = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat; + + const costPerUser = document.getElementsByClassName('costPerUser')[0]; + expect(costPerUser).toBeInTheDocument(); + expect(costPerUser.innerHTML).toContain('Cost per user
$' + mockProfessionalProduct.price_per_seat.toFixed(2) + ' x ' + monthsUntilLicenseExpiry + ' months'); + + const costAmount = document.getElementsByClassName('costAmount')[0]; + expect(costAmount).toBeInTheDocument(); + expect(costAmount.innerHTML).toContain('$' + expectedCostPerUser) + }); + + it("Total cost User should be represented as the current subscription price multiplied by the remaining months multiplied by the number of users", () => { + renderWithIntlAndStore(
, initialState); + const seatsInputValue = 100; + changeByTestId('seatsInput', seatsInputValue.toString()); + + const expectedTotalCost = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat * seatsInputValue; + + const costAmount = document.getElementsByClassName('totalCostAmount')[0]; + expect(costAmount).toBeInTheDocument(); + expect(costAmount.innerHTML).toContain('$' + expectedTotalCost) + }); +}); + +describe('SelfHostedExpansionModal Submit', () => { function makeHappyPathState(): FormState { return { address: 'string', @@ -366,7 +411,6 @@ describe('SelfHostedExpansionModal :: canSubmit', () => { expect(canSubmit(state, SelfHostedSignupProgress.PAID)).toBe(true); }); - // TODO: Needed? it('if created subscription, can submit', () => { const state = makeInitialState(1); state.submitting = false; diff --git a/webapp/channels/src/components/self_hosted_expansion_modal/index.tsx b/webapp/channels/src/components/self_hosted_expansion_modal/index.tsx index da54d4ce65..845fafcfe2 100644 --- a/webapp/channels/src/components/self_hosted_expansion_modal/index.tsx +++ b/webapp/channels/src/components/self_hosted_expansion_modal/index.tsx @@ -409,6 +409,7 @@ export default function SelfHostedExpansionModal() { />
{ @@ -450,6 +451,7 @@ export default function SelfHostedExpansionModal() { />
{