Skip to content
Open
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
39 changes: 1 addition & 38 deletions src/api/billing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import type { TenantPaymentDetails } from 'src/types';

import pLimit from 'p-limit';

import { supabaseClient } from 'src/context/GlobalProviders';
import { FUNCTIONS, invokeSupabase, TABLES } from 'src/services/supabase';
import { formatDateForApi } from 'src/utils/billing-utils';
import { FUNCTIONS, invokeSupabase } from 'src/services/supabase';

const OPERATIONS = {
SETUP_INTENT: 'setup-intent',
Expand Down Expand Up @@ -90,41 +88,6 @@ export interface Invoice {
};
}

const invoicesQuery = [
'billed_prefix',
'date_start',
'date_end',
'line_items',
'subtotal',
'invoice_type',
'extra',
].join(', ');

export const getInvoicesBetween = (
billed_prefix: string,
date_start: Date,
date_end: Date
) => {
const formattedStart = formatDateForApi(date_start);
const formattedEnd = formatDateForApi(date_end);

return supabaseClient
.from(TABLES.INVOICES_EXT)
.select(invoicesQuery)
.filter('billed_prefix', 'eq', billed_prefix)
.or(
`invoice_type.eq.manual,and(${[
`date_start.gte.${formattedStart}`,
`date_start.lte.${formattedEnd}`,
`date_end.gte.${formattedStart}`,
`date_end.lte.${formattedEnd}`,
].join(',')})`
)
.order('date_start', { ascending: false })
.throwOnError()
.returns<Invoice[]>();
};

export interface MultiplePaymentMethods {
responses: any[];
errors: any[];
Expand Down
29 changes: 29 additions & 0 deletions src/api/gql/billing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { graphql } from 'src/gql-types';

// Upper bound on invoices fetched per tenant. A tenant accrues ~12 invoices a
// year, so this comfortably covers the rolling six-month window the UI shows
// plus any older manual invoices, which the hook filters down client-side.
export const BILLING_INVOICE_FETCH_LIMIT = 100;

// `lineItems` and `extra` are opaque JSON scalars in the schema, so codegen
// types them as `unknown`; the hook casts them to the InvoiceLineItem[] / extra
// shapes the rest of the billing UI already expects. `billed_prefix` is not on
// the node because the tenant is implied by the `tenant(name:)` parent.
export const TENANT_BILLING_INVOICES_QUERY = graphql(`
query TenantBillingInvoices($tenant: String!, $first: Int) {
tenant(name: $tenant) {
billing {
invoices(first: $first) {
nodes {
dateStart
dateEnd
invoiceType
subtotal
lineItems
extra
}
}
}
}
}
`);
8 changes: 3 additions & 5 deletions src/components/admin/Billing/LoadError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,12 @@ import { Grid } from '@mui/material';
import { FormattedMessage } from 'react-intl';

import AlertBox from 'src/components/shared/AlertBox';
import { useBillingStore } from 'src/stores/Billing';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';

function BillingLoadError() {
const hydrationErrorsExist = useBillingStore(
(state) => state.hydrationErrorsExist
);
const { errorExists } = useBillingInvoices();

if (!hydrationErrorsExist) {
if (!errorExists) {
return null;
}

Expand Down
3 changes: 1 addition & 2 deletions src/components/admin/Billing/PricingTierDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ function PricingTierDetails() {
const [externalPaymentMethod, marketPlaceProvider] =
useTenantUsesExternalPayment(selectedTenant);

const billingStoreHydrated = useBillingStore((state) => state.hydrated);
const paymentMethodExists = useBillingStore(
(state) => state.paymentMethodExists
);
Expand All @@ -38,7 +37,7 @@ function PricingTierDetails() {
return 'admin.billing.message.freeTier';
}, [externalPaymentMethod, marketPlaceProvider, paymentMethodExists]);

if (!billingStoreHydrated || typeof paymentMethodExists !== 'boolean') {
if (typeof paymentMethodExists !== 'boolean') {
return (
<Skeleton>
<Typography>
Expand Down
11 changes: 1 addition & 10 deletions src/components/admin/Billing/TenantOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { useCallback } from 'react';

import TenantSelector from 'src/components/shared/TenantSelector';
import { useBillingStore } from 'src/stores/Billing';

function TenantOptions() {
const resetBillingState = useBillingStore((state) => state.resetState);

const updateStoreState = useCallback(() => {
resetBillingState();
}, [resetBillingState]);

return <TenantSelector updateStoreState={updateStoreState} />;
return <TenantSelector />;
}

export default TenantOptions;
92 changes: 4 additions & 88 deletions src/components/admin/Billing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import type { AdminBillingProps } from 'src/components/admin/Billing/types';

import { useEffect, useMemo } from 'react';
import useConstant from 'use-constant';

import { Divider, Grid, Typography } from '@mui/material';

import { useShallow } from 'zustand/react/shallow';

import { endOfMonth, startOfMonth, subMonths } from 'date-fns';
import { ErrorBoundary } from 'react-error-boundary';
import { useIntl } from 'react-intl';
import { useUnmount } from 'react-use';

import { getInvoicesBetween } from 'src/api/billing';
import { authenticatedRoutes } from 'src/app/routes';
import DateRange from 'src/components/admin/Billing/DateRange';
import BillingLoadError from 'src/components/admin/Billing/LoadError';
Expand All @@ -28,14 +20,10 @@ import AlertBox from 'src/components/shared/AlertBox';
import CardWrapper from 'src/components/shared/CardWrapper';
import BillingHistoryTable from 'src/components/tables/Billing';
import BillingLineItemsTable from 'src/components/tables/BillLineItems';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';
import usePageTitle from 'src/hooks/usePageTitle';
import { logRocketEvent } from 'src/services/shared';
import { CustomEvents } from 'src/services/types';
import {
useBilling_selectedInvoice,
useBillingStore,
} from 'src/stores/Billing';
import { useTenantStore } from 'src/stores/Tenant';
import { invoiceId, TOTAL_CARD_HEIGHT } from 'src/utils/billing-utils';

const routeTitle = authenticatedRoutes.admin.billing.title;
Expand All @@ -52,79 +40,7 @@ function AdminBilling({ showAddPayment }: AdminBillingProps) {

const intl = useIntl();

const selectedTenant = useTenantStore((state) => state.selectedTenant);

// Billing Store
// TODO (billing store)
// The `active` stuff could probably be removed now that other stuff is
// cleaned up - but leaving to make it easier
const [active, setActive] = useBillingStore(
useShallow((state) => [state.active, state.setActive])
);
const [hydrated, setHydrated] = useBillingStore(
useShallow((state) => [state.hydrated, state.setHydrated])
);
const setHydrationErrorsExist = useBillingStore(
(state) => state.setHydrationErrorsExist
);
const setInvoices = useBillingStore((state) => state.setInvoices);
const setNetworkFailed = useBillingStore((state) => state.setNetworkFailed);

const selectedInvoice = useBilling_selectedInvoice();

const resetBillingState = useBillingStore((state) => state.resetState);

const currentMonth = useConstant(() => {
const today = new Date();

return endOfMonth(today);
});

const dateRange = useMemo(() => {
const startMonth = startOfMonth(subMonths(currentMonth, 5));

return { start: startMonth, end: currentMonth };
}, [currentMonth]);

useEffect(() => {
if (selectedTenant) {
void (async () => {
setNetworkFailed(null);
setActive(true);
try {
const response = await getInvoicesBetween(
selectedTenant,
dateRange.start,
dateRange.end
);
if (response.error) {
throw new Error(response.error.message);
}
setNetworkFailed(null);
setHydrationErrorsExist(false);
setInvoices(response.data);
} catch (errorMessage: unknown) {
setNetworkFailed(`${errorMessage}`);
setHydrationErrorsExist(true);
setInvoices([]);
} finally {
setHydrated(true);
setActive(false);
}
})();
}
}, [
dateRange.end,
dateRange.start,
selectedTenant,
setActive,
setHydrated,
setHydrationErrorsExist,
setInvoices,
setNetworkFailed,
]);

useUnmount(() => resetBillingState());
const { isLoading, selectedInvoice } = useBillingInvoices();

return (
<>
Expand Down Expand Up @@ -178,7 +94,7 @@ function AdminBilling({ showAddPayment }: AdminBillingProps) {
<CardWrapper
height={invoiceCardHeight}
message={
active || !hydrated ? (
isLoading ? (
intl.formatMessage({
id: 'admin.billing.label.lineItems.loading',
})
Expand All @@ -199,7 +115,7 @@ function AdminBilling({ showAddPayment }: AdminBillingProps) {
)
}
>
{!active && hydrated ? (
{!isLoading ? (
<BillingLineItemsTable
// The key here makes sure that any stateful fetching logic doesn't get confused.
key={
Expand Down
9 changes: 4 additions & 5 deletions src/components/graphs/TaskHoursByMonthGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from 'src/components/graphs/tooltips';
import useTooltipConfig from 'src/components/graphs/useTooltipConfig';
import { defaultOutlineColor } from 'src/context/Theme';
import { useBillingStore } from 'src/stores/Billing';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';
import { CARD_AREA_HEIGHT, stripTimeFromDate } from 'src/utils/billing-utils';

const chartContainerId = 'task-hours-by-month';
Expand All @@ -39,8 +39,7 @@ function TaskHoursByMonthGraph() {
const intl = useIntl();
const tooltipConfig = useTooltipConfig();

const billingStoreHydrated = useBillingStore((state) => state.hydrated);
const invoices = useBillingStore((state) => state.invoices);
const { invoices, isLoading } = useBillingInvoices();

const resizeListener = useRef<EventListener | null>(null);
const [myChart, setMyChart] = useState<echarts.ECharts | null>(null);
Expand Down Expand Up @@ -85,7 +84,7 @@ function TaskHoursByMonthGraph() {
}, [invoices, intl, today]);

useEffect(() => {
if (billingStoreHydrated && invoices.length > 0) {
if (!isLoading && invoices.length > 0) {
if (!myChart) {
echarts.use([
GridComponent,
Expand Down Expand Up @@ -190,7 +189,7 @@ function TaskHoursByMonthGraph() {
}
}, [
invoices,
billingStoreHydrated,
isLoading,
intl,
months,
myChart,
Expand Down
9 changes: 4 additions & 5 deletions src/components/graphs/UsageByMonthGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { useIntl } from 'react-intl';
import useLegendConfig from 'src/components/graphs/useLegendConfig';
import useTooltipConfig from 'src/components/graphs/useTooltipConfig';
import { eChartsColors } from 'src/context/Theme';
import { useBillingStore } from 'src/stores/Billing';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';
import { CARD_AREA_HEIGHT, stripTimeFromDate } from 'src/utils/billing-utils';

const chartContainerId = 'data-by-month';
Expand All @@ -40,8 +40,7 @@ function UsageByMonthGraph() {
const tooltipConfig = useTooltipConfig();
const legendConfig = useLegendConfig([{ name: 'Data' }, { name: 'Hours' }]);

const billingStoreHydrated = useBillingStore((state) => state.hydrated);
const invoices = useBillingStore((state) => state.invoices);
const { invoices, isLoading } = useBillingInvoices();

const [myChart, setMyChart] = useState<echarts.ECharts | null>(null);

Expand Down Expand Up @@ -96,7 +95,7 @@ function UsageByMonthGraph() {
}, [invoices, intl, today]);

useEffect(() => {
if (billingStoreHydrated && invoices.length > 0) {
if (!isLoading && invoices.length > 0) {
if (!myChart) {
echarts.use([
GridComponent,
Expand Down Expand Up @@ -126,7 +125,7 @@ function UsageByMonthGraph() {
return undefined;
}, [
invoices,
billingStoreHydrated,
isLoading,
intl,
legendConfig,
months,
Expand Down
13 changes: 7 additions & 6 deletions src/components/graphs/states/Wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { FormattedMessage } from 'react-intl';
import EmptyGraphState from 'src/components/graphs/states/Empty';
import GraphLoadingState from 'src/components/graphs/states/Loading';
import { eChartsTooltipSX } from 'src/components/graphs/tooltips';
import { useBillingStore } from 'src/stores/Billing';
import { useBillingInvoices } from 'src/hooks/billing/useBillingInvoices';
import { hasLength } from 'src/utils/misc-utils';

function GraphStateWrapper({ children }: BaseComponentProps) {
const billingStoreActive = useBillingStore((state) => state.active);
const billingStoreHydrated = useBillingStore((state) => state.hydrated);
const networkFailed = useBillingStore((state) => state.networkFailed);
const billingHistory = useBillingStore((state) => state.invoices);
const {
invoices: billingHistory,
isLoading,
networkFailed,
} = useBillingInvoices();

if (networkFailed) {
return (
Expand All @@ -29,7 +30,7 @@ function GraphStateWrapper({ children }: BaseComponentProps) {
);
}

if (!billingStoreActive && billingStoreHydrated) {
if (!isLoading) {
return hasLength(billingHistory) ? (
<Box sx={eChartsTooltipSX}>{children}</Box>
) : (
Expand Down
Loading
Loading