From 011f22ae0d1ccd33fed6ad758ef32e723d7d700f Mon Sep 17 00:00:00 2001 From: Joseph Shearer Date: Mon, 28 Aug 2023 16:46:24 -0400 Subject: [PATCH 1/4] feature: Fetch billing historicals and display them on the billing page --- src/api/billing.ts | 112 ++++++++++++++++++++++++++++++++------- src/services/supabase.ts | 3 +- 2 files changed, 94 insertions(+), 21 deletions(-) diff --git a/src/api/billing.ts b/src/api/billing.ts index 7722d327c..eb894af3d 100644 --- a/src/api/billing.ts +++ b/src/api/billing.ts @@ -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 = { @@ -51,9 +55,7 @@ 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; @@ -61,32 +63,102 @@ export interface BillingRecord { 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> => { + 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(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(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> = + 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(RPCS.BILLING_REPORT, { + billed_prefix, + billed_month: formattedMonth, + }) + .throwOnError() + .single(); + } }; +function isSingleResponse( + arg: PostgrestMaybeSingleResponse +): arg is PostgrestSingleResponse { + return arg.body !== null; +} + export const getBillingHistory = async ( tenant: string, dateRange: string[] ): Promise[]> => { - const promises: PromiseLike>[] = + const promises: PromiseLike>[] = dateRange.map((date) => getBillingRecord(tenant, date)); - const res = await Promise.all(promises); + const resolved = await Promise.all(promises); - return res; + return resolved.filter(isSingleResponse); }; diff --git a/src/services/supabase.ts b/src/services/supabase.ts index 80fb5c9d1..82a2f932a 100644 --- a/src/services/supabase.ts +++ b/src/services/supabase.ts @@ -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'; @@ -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', From 3e0eb4faacb58edfe62500dafec1c8fdc314f2a7 Mon Sep 17 00:00:00 2001 From: Joseph Shearer Date: Tue, 29 Aug 2023 10:41:18 -0400 Subject: [PATCH 2/4] refactor: Better solution requiring no hacks --- src/api/billing.ts | 75 ++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 56 deletions(-) diff --git a/src/api/billing.ts b/src/api/billing.ts index eb894af3d..e7bf6d752 100644 --- a/src/api/billing.ts +++ b/src/api/billing.ts @@ -55,7 +55,9 @@ interface InvoiceLineItem { subtotal: number; } -export interface BillingReport { +export interface BillingRecord { + billed_prefix: string; + billed_month: string; // Timestamp processed_data_gb: number | null; recurring_fee: number; task_usage_hours: number | null; @@ -63,17 +65,6 @@ export interface BillingReport { 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 @@ -89,51 +80,23 @@ export const getBillingRecord = ( startOfMonth(new Date()) ) ) { - const req = supabaseClient - .from(TABLES.BILLING_HISTORICALS) - .select('billed_prefix, billed_month, report') + return supabaseClient + .from(TABLES.BILLING_HISTORICALS) + .select( + [ + 'billed_prefix', + 'billed_month', + 'report->processed_data_gb', + 'report->recurring_fee', + 'report->task_usage_hours', + 'report->line_items', + 'report->subtotal', + ].join(', ') + ) .filter('billed_prefix', 'eq', billed_prefix) - .filter('billed_month', 'eq', formattedMonth); - - const url = (req as any).url; - - const prom: PromiseLike> = - 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; + .filter('billed_month', 'eq', formattedMonth) + .throwOnError() + .maybeSingle(); } else { return supabaseClient .rpc(RPCS.BILLING_REPORT, { From 5f92ee78facaa0aae4b3c2256c866b2e50b1bbe5 Mon Sep 17 00:00:00 2001 From: Joseph Shearer Date: Tue, 29 Aug 2023 13:53:10 -0400 Subject: [PATCH 3/4] Update to refer to `tenant` --- src/api/billing.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api/billing.ts b/src/api/billing.ts index e7bf6d752..e6c28dc68 100644 --- a/src/api/billing.ts +++ b/src/api/billing.ts @@ -56,6 +56,10 @@ interface InvoiceLineItem { } export interface BillingRecord { + // In `billing_historicals` this is called tenant + // and so needs to be included here in order to + // filter by tenant + tenant?: string; billed_prefix: string; billed_month: string; // Timestamp processed_data_gb: number | null; @@ -84,7 +88,7 @@ export const getBillingRecord = ( .from(TABLES.BILLING_HISTORICALS) .select( [ - 'billed_prefix', + 'billed_prefix:tenant', 'billed_month', 'report->processed_data_gb', 'report->recurring_fee', @@ -93,7 +97,7 @@ export const getBillingRecord = ( 'report->subtotal', ].join(', ') ) - .filter('billed_prefix', 'eq', billed_prefix) + .filter('tenant', 'eq', billed_prefix) .filter('billed_month', 'eq', formattedMonth) .throwOnError() .maybeSingle(); From 39a19840d0c05959daf40de17cc4e228efd5d871 Mon Sep 17 00:00:00 2001 From: Travis Jenkins Date: Tue, 29 Aug 2023 15:55:33 -0400 Subject: [PATCH 4/4] Commenting a todo Moving some basic code --- src/api/billing.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/api/billing.ts b/src/api/billing.ts index e6c28dc68..4d3d78988 100644 --- a/src/api/billing.ts +++ b/src/api/billing.ts @@ -69,6 +69,16 @@ export interface BillingRecord { subtotal: number; } +const billingHistoricalQuery = [ + 'billed_prefix:tenant', + 'billed_month', + 'report->processed_data_gb', + 'report->recurring_fee', + 'report->task_usage_hours', + 'report->line_items', + 'report->subtotal', +].join(', '); + export const getBillingRecord = ( billed_prefix: string, month: string | Date @@ -86,17 +96,7 @@ export const getBillingRecord = ( ) { return supabaseClient .from(TABLES.BILLING_HISTORICALS) - .select( - [ - 'billed_prefix:tenant', - 'billed_month', - 'report->processed_data_gb', - 'report->recurring_fee', - 'report->task_usage_hours', - 'report->line_items', - 'report->subtotal', - ].join(', ') - ) + .select(billingHistoricalQuery) .filter('tenant', 'eq', billed_prefix) .filter('billed_month', 'eq', formattedMonth) .throwOnError() @@ -118,6 +118,8 @@ function isSingleResponse( return arg.body !== null; } +// TODO (billing) need to make this so it does not cause multipl calls +// and instead just builds up a single query that matches all months export const getBillingHistory = async ( tenant: string, dateRange: string[]