Skip to content

Commit

Permalink
feature: Fetch billing historicals and display them on the billing page
Browse files Browse the repository at this point in the history
  • Loading branch information
jshearer committed Aug 28, 2023
1 parent 1454b32 commit 011f22a
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 21 deletions.
112 changes: 92 additions & 20 deletions src/api/billing.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { PostgrestSingleResponse } from '@supabase/postgrest-js';
import { format } from 'date-fns';
import {
PostgrestMaybeSingleResponse,
PostgrestSingleResponse,
} from '@supabase/postgrest-js';
import { format, isBefore, parse, startOfMonth } from 'date-fns';
import {
FUNCTIONS,
invokeSupabase,
RPCS,
supabaseClient,
TABLES,
} from 'services/supabase';

const OPERATIONS = {
Expand Down Expand Up @@ -51,42 +55,110 @@ interface InvoiceLineItem {
subtotal: number;
}

export interface BillingRecord {
billed_prefix: string;
billed_month: string; // Timestamp
export interface BillingReport {
processed_data_gb: number | null;
recurring_fee: number;
task_usage_hours: number | null;
line_items: InvoiceLineItem[];
subtotal: number;
}

export interface BillingHistoricals {
billed_prefix: string;
billed_month: string; // Timestamp
report: BillingReport;
}

export interface BillingRecord extends BillingReport {
billed_prefix: string;
billed_month: string; // Timestamp
}

export const getBillingRecord = (
billed_prefix: string,
month: string | Date
) => {
): PromiseLike<PostgrestMaybeSingleResponse<BillingRecord>> => {
const fmt = "yyyy-MM-dd' 00:00:00+00'";
const formattedMonth: string =
typeof month === 'string'
? month
: format(month, "yyyy-MM-dd' 00:00:00+00'");

return supabaseClient
.rpc<BillingRecord>(RPCS.BILLING_REPORT, {
billed_prefix,
billed_month: formattedMonth,
})
.throwOnError()
.single();
typeof month === 'string' ? month : format(month, fmt);

// If we're asking for a previous month, look up billing_historicals
if (
isBefore(
startOfMonth(parse(formattedMonth, fmt, new Date())),
startOfMonth(new Date())
)
) {
const req = supabaseClient
.from<BillingHistoricals>(TABLES.BILLING_HISTORICALS)
.select('billed_prefix, billed_month, report')
.filter('billed_prefix', 'eq', billed_prefix)
.filter('billed_month', 'eq', formattedMonth);

const url = (req as any).url;

const prom: PromiseLike<PostgrestMaybeSingleResponse<BillingRecord>> =
req.then((response) => {
if (response.body && response.body.length > 0) {
if (response.body.length > 1) {
throw new Error(
`Found multiple billing historical records where at most one was expected. prefix: ${billed_prefix}, month: ${formattedMonth}`
);
}
const modified_body: BillingRecord = {
billed_month: response.body[0].billed_month,
billed_prefix: response.body[0].billed_prefix,
...response.body[0].report,
};
return {
...response,
body: modified_body,
data: modified_body,
url,
};
} else {
return {
...response,
body: null,
data: null,
url,
};
}
});

// Hack alert: `useSelectNew` actually expects to be passed a
// PostgrestFilterBuilder which has a protected `url: URL` field
// which `useSelectNew` uses as the key for SWR purposes. If we `.map()`
// the promise-like object returned by the Supabase SDK, we lose that
// hidden URL field in the process, breaking SWR.
// So... for the moment, this hacks it back into existence.
(prom as any).url = url;
return prom;
} else {
return supabaseClient
.rpc<BillingRecord>(RPCS.BILLING_REPORT, {
billed_prefix,
billed_month: formattedMonth,
})
.throwOnError()
.single();
}
};

function isSingleResponse<T>(
arg: PostgrestMaybeSingleResponse<T>
): arg is PostgrestSingleResponse<T> {
return arg.body !== null;
}

export const getBillingHistory = async (
tenant: string,
dateRange: string[]
): Promise<PostgrestSingleResponse<BillingRecord>[]> => {
const promises: PromiseLike<PostgrestSingleResponse<BillingRecord>>[] =
const promises: PromiseLike<PostgrestMaybeSingleResponse<BillingRecord>>[] =
dateRange.map((date) => getBillingRecord(tenant, date));

const res = await Promise.all(promises);
const resolved = await Promise.all(promises);

return res;
return resolved.filter(isSingleResponse);
};
3 changes: 2 additions & 1 deletion src/services/supabase.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { PostgrestError, PostgrestFilterBuilder } from '@supabase/postgrest-js';
import { User, createClient } from '@supabase/supabase-js';
import { createClient, User } from '@supabase/supabase-js';
import { ToPostgrestFilterBuilder } from 'hooks/supabase-swr';
import { forEach, isEmpty } from 'lodash';
import LogRocket from 'logrocket';
Expand Down Expand Up @@ -36,6 +36,7 @@ export const DEFAULT_FILTER = '__unknown__';

export enum TABLES {
APPLIED_DIRECTIVES = 'applied_directives',
BILLING_HISTORICALS = 'billing_historicals',
CATALOG_STATS = 'catalog_stats',
COMBINED_GRANTS_EXT = 'combined_grants_ext',
CONNECTOR_TAGS = 'connector_tags',
Expand Down

0 comments on commit 011f22a

Please sign in to comment.