Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: PAYG billing #8743

Merged
merged 4 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 3 additions & 10 deletions frontend/src/component/admin/billing/Billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@ import { BillingHistory } from './BillingHistory/BillingHistory';
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';

export const Billing = () => {
const {
instanceStatus,
isBilling,
refetchInstanceStatus,
refresh,
loading,
} = useInstanceStatus();
const { isBilling, refetchInstanceStatus, refresh, loading } =
useInstanceStatus();
const { invoices } = useInvoices();

useEffect(() => {
Expand All @@ -35,9 +30,7 @@ export const Billing = () => {
show={
<PermissionGuard permissions={ADMIN}>
<>
<BillingDashboard
instanceStatus={instanceStatus!}
/>
<BillingDashboard />
<BillingHistory data={invoices} />
</>
</PermissionGuard>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
import { Grid } from '@mui/material';
import type { IInstanceStatus } from 'interfaces/instance';
import type { VFC } from 'react';
import { BillingInformation } from './BillingInformation/BillingInformation';
import { BillingPlan } from './BillingPlan/BillingPlan';

interface IBillingDashboardProps {
instanceStatus: IInstanceStatus;
}

export const BillingDashboard: VFC<IBillingDashboardProps> = ({
instanceStatus,
}) => {
export const BillingDashboard = () => {
return (
<Grid container spacing={4}>
<BillingInformation instanceStatus={instanceStatus} />
<BillingPlan instanceStatus={instanceStatus} />
<BillingInformation />
<BillingPlan />
</Grid>
);
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { FC } from 'react';
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
import { BillingInformationButton } from './BillingInformationButton/BillingInformationButton';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { type IInstanceStatus, InstanceState } from 'interfaces/instance';
import { InstanceState } from 'interfaces/instance';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';

const StyledInfoBox = styled('aside')(({ theme }) => ({
padding: theme.spacing(4),
Expand All @@ -28,13 +29,22 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
margin: `${theme.spacing(2.5)} 0`,
borderColor: theme.palette.divider,
}));
interface IBillingInformationProps {
instanceStatus: IInstanceStatus;
}

export const BillingInformation: FC<IBillingInformationProps> = ({
instanceStatus,
}) => {
export const BillingInformation = () => {
const { instanceStatus } = useInstanceStatus();
const {
uiConfig: { billing },
} = useUiConfig();
const isPAYG = billing === 'pay-as-you-go';

if (!instanceStatus)
return (
<Grid item xs={12} md={5}>
<StyledInfoBox data-loading sx={{ flex: 1, height: '400px' }} />
</Grid>
);

const plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
const inactive = instanceStatus.state !== InstanceState.ACTIVE;

return (
Expand All @@ -58,7 +68,9 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
</StyledInfoLabel>
<StyledDivider />
<StyledInfoLabel>
<a href='mailto:[email protected]?subject=PRO plan clarifications'>
<a
href={`mailto:[email protected]?subject=${plan} plan clarifications`}
>
Get in touch with us
</a>{' '}
for any clarification
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { type IInstanceStatus, InstancePlan } from 'interfaces/instance';
import { BillingDetailsPro } from './BillingDetailsPro';
import { BillingDetailsPAYG } from './BillingDetailsPAYG';

interface IBillingDetailsProps {
instanceStatus: IInstanceStatus;
isPAYG: boolean;
}

export const BillingDetails = ({
instanceStatus,
isPAYG,
}: IBillingDetailsProps) => {
if (isPAYG) {
return <BillingDetailsPAYG instanceStatus={instanceStatus} />;
}

if (instanceStatus.plan === InstancePlan.PRO) {
return <BillingDetailsPro instanceStatus={instanceStatus} />;
}

return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Link } from 'react-router-dom';
import { Divider, Grid, styled, Typography } from '@mui/material';
import { GridRow } from 'component/common/GridRow/GridRow';
import { GridCol } from 'component/common/GridCol/GridCol';
import { GridColLink } from './GridColLink/GridColLink';
import type { IInstanceStatus } from 'interfaces/instance';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import {
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
BILLING_PAYG_USER_PRICE,
} from './BillingPlan';

const StyledInfoLabel = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
}));

const StyledDivider = styled(Divider)(({ theme }) => ({
margin: `${theme.spacing(3)} 0`,
}));

interface IBillingDetailsPAYGProps {
instanceStatus: IInstanceStatus;
}

export const BillingDetailsPAYG = ({
instanceStatus,
}: IBillingDetailsPAYGProps) => {
const { users, loading } = useUsers();

const eligibleUsers = users.filter((user) => user.email);

const minSeats =
instanceStatus.minSeats ?? BILLING_PAYG_DEFAULT_MINIMUM_SEATS;

const billableUsers = Math.max(eligibleUsers.length, minSeats);
const usersCost = BILLING_PAYG_USER_PRICE * billableUsers;

const totalCost = usersCost;

if (loading) return null;

return (
<>
<Grid container>
<GridRow
sx={(theme) => ({
marginBottom: theme.spacing(1.5),
})}
>
<GridCol vertical>
<Typography>
<strong>Paid members</strong>
<GridColLink>
<Link to='/admin/users'>
{eligibleUsers.length} assigned of{' '}
{minSeats} minimum
</Link>
</GridColLink>
</Typography>
<StyledInfoLabel>
${BILLING_PAYG_USER_PRICE}/month per paid member
</StyledInfoLabel>
</GridCol>
<GridCol>
<Typography
sx={(theme) => ({
fontSize: theme.fontSizes.mainHeader,
})}
>
${usersCost.toFixed(2)}
</Typography>
</GridCol>
</GridRow>
</Grid>
<StyledDivider />
<Grid container>
<GridRow>
<GridCol>
<Typography
sx={(theme) => ({
fontWeight: theme.fontWeight.bold,
fontSize: theme.fontSizes.mainHeader,
})}
>
Total
</Typography>
</GridCol>
<GridCol>
<Typography
sx={(theme) => ({
fontWeight: theme.fontWeight.bold,
fontSize: '2rem',
})}
>
${totalCost.toFixed(2)}
</Typography>
</GridCol>
</GridRow>
</Grid>
</>
);
};
Loading
Loading