From cc0d9bb19e82a42705eeaba6db0242afa5298ad4 Mon Sep 17 00:00:00 2001 From: Philip Chase Date: Wed, 14 Jan 2026 10:31:07 -0500 Subject: [PATCH 1/2] Update revenue_status_and_projections.qmd Split free Contractual work from free Support work. Add 'Figure 5: Pro Bono costs in last FY as portion of revenue by service type'. Add paragraph to explain the role of the new Figure 5 in annual rate review. --- report/revenue_status_and_projections.qmd | 66 ++++++++++++++++++++--- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/report/revenue_status_and_projections.qmd b/report/revenue_status_and_projections.qmd index 3da3715b..0b703128 100644 --- a/report/revenue_status_and_projections.qmd +++ b/report/revenue_status_and_projections.qmd @@ -42,6 +42,7 @@ library(rcc.billing) library(RMariaDB) library(DBI) library(tidyverse) +library(tidyr) library(lubridate) library(dotenv) library(fs) @@ -168,7 +169,7 @@ invoice_line_items_with_fy_and_month_paid <- invoice_status |> #| echo: false #| warning: false -revenue_by_service_type <- +revenue_by_fy_and_service_type <- invoice_line_items_with_fy_and_month_paid |> # Mutate service_type_code here to get 99s on contractual work left_join(contract_support |> select(record_id, contractual, service_type_code), @@ -185,7 +186,10 @@ revenue_by_service_type <- )) |> group_by(FY, service_type) |> summarise(revenue = sum(amount_due, na.rm = T)) |> - ungroup() |> + ungroup() + +revenue_by_service_type <- + revenue_by_fy_and_service_type |> pivot_wider( id_cols = "FY", names_from = "service_type", @@ -302,18 +306,23 @@ CTS-IT does not charge for all services. It offers a free consultation of up to Table @fig-probono-summary-table shows the value of these services by fiscal year since CTS-IT started carefully accounting for charged and non-charged services. Figure @fig-probono-faceted shows the monthly detail underlying the annual figures. Support billing started 21 months after the annual project billing. ```{r} -#| label: probono-support-prep +#| label: probono-contractual-and-support-prep #| echo: false #| warning: false # Support billing - Pro Bono -probono_support <- tbl(rcc_billing_conn, "invoice_line_item") |> +probono_support <- + tbl(rcc_billing_conn, "invoice_line_item") |> filter(service_type_code == 2, price_of_service == 0) |> - select(id, qty_provided, fiscal_year, month_invoiced, created) |> + select(id, service_identifier, qty_provided, fiscal_year, month_invoiced, created) |> collect() |> + left_join(contract_support, by = c("service_identifier" = "record_id")) |> + mutate(contractual = if_else(is.na(contractual), FALSE, TRUE)) |> + mutate(service_type_code = if_else(contractual, 99, service_type_code)) |> mutate(invoice_date = lubridate::floor_date(created - lubridate::dmonths(1), unit = "month")) |> rename(month_invoiced_name = month_invoiced) |> - mutate(month_invoiced = match(month_invoiced_name, month.name)) + mutate(month_invoiced = match(month_invoiced_name, month.name)) |> + select(-c("study_name", "study_complete")) normal_service_rate <- tbl(rcc_billing_conn, "invoice_line_item") |> filter(service_type_code == 2, price_of_service > 0) |> @@ -331,8 +340,8 @@ normal_service_rate <- tbl(rcc_billing_conn, "invoice_line_item") |> probono_support_with_rate <- probono_support |> left_join(normal_service_rate, by = c("fiscal_year", "month_invoiced_name" = "month_invoiced")) |> mutate(value = qty_provided * price_of_service) |> - select(fiscal_year, month_invoiced, invoice_date, value) |> - mutate(revenue_source = "Support") + mutate(revenue_source = if_else(contractual, "Contractual", "Support")) |> + select(fiscal_year, month_invoiced, invoice_date, value, revenue_source) ``` ```{r} @@ -522,6 +531,47 @@ probono_combined |> theme(axis.text.x = element_text(angle = 30)) ``` +CTS-IT's annual rate review requires that we understand the proportion of our revenue that is given away as Pro Bono services so these costs can be accounted for in our rate calculations for the upcoming year. @fig-probono-last-fy-detail shows the breakdown of Pro Bono costs by service type for the last fiscal year. + +```{r} +#| label: fig-probono-last-fy-detail +#| fig-cap: "Pro Bono costs in last FY as portion of revenue by service type" +#| echo: false +#| warning: false + +probono_detail_last_fy <- +revenue_by_fy_and_service_type |> filter(`FY` == "2024-2025") |> + left_join( +probono_combined |> + group_by(fiscal_year, revenue_source) |> + summarise(`Value of Pro Bono Services` = sum(value, na.rm = TRUE), .groups = "drop") |> + filter(fiscal_year == "2024-2025") |> +rename( + FY = "fiscal_year", + service_type = "revenue_source" +) |> +mutate(service_type = if_else(service_type == "Annual Project Billing", "Annual REDCap Project Maintenance", service_type)) |> + ungroup(), +by = c("FY", "service_type") +) |> +mutate(`Probono as portion of revenue for this service type` = `Value of Pro Bono Services` / revenue) |> + rename( + `Revenue` = revenue, + `Service type` = service_type + ) |> + select(FY, `Service type`, `Revenue`, `Value of Pro Bono Services`, `Probono as portion of revenue for this service type`) + + +probono_detail_last_fy |> + gt::gt() |> + gt::fmt_currency(columns = c("Revenue", "Value of Pro Bono Services"), decimals = 0) |> + gt::fmt_percent(columns = "Probono as portion of revenue for this service type", decimals = 0) |> + gt::tab_header( + title = "Pro Bono Value and Portion of Revenue by Service Type for FY25" + ) + +``` + ## Aging Report The unpaid invoices breakdown as shown in @fig-aging-report. For invoices more than 4 months old, the payment rate is `r scales::percent(average_portion_paid)`. From 4f4beb36577082241aa7505ae367b4d2b87da276 Mon Sep 17 00:00:00 2001 From: Philip Chase Date: Wed, 14 Jan 2026 10:57:43 -0500 Subject: [PATCH 2/2] Update revenue_status_and_projections.qmd Address Copilot comments. --- report/revenue_status_and_projections.qmd | 47 ++++++++++++----------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/report/revenue_status_and_projections.qmd b/report/revenue_status_and_projections.qmd index 0b703128..67a65ac8 100644 --- a/report/revenue_status_and_projections.qmd +++ b/report/revenue_status_and_projections.qmd @@ -42,7 +42,6 @@ library(rcc.billing) library(RMariaDB) library(DBI) library(tidyverse) -library(tidyr) library(lubridate) library(dotenv) library(fs) @@ -317,7 +316,7 @@ probono_support <- select(id, service_identifier, qty_provided, fiscal_year, month_invoiced, created) |> collect() |> left_join(contract_support, by = c("service_identifier" = "record_id")) |> - mutate(contractual = if_else(is.na(contractual), FALSE, TRUE)) |> + mutate(contractual = coalesce(contractual, FALSE)) |> mutate(service_type_code = if_else(contractual, 99, service_type_code)) |> mutate(invoice_date = lubridate::floor_date(created - lubridate::dmonths(1), unit = "month")) |> rename(month_invoiced_name = month_invoiced) |> @@ -489,7 +488,7 @@ revenue_by_fy <- revenue_by_service_type |> probono_summary <- revenue_by_fy |> left_join(probono_by_fy, by = c("FY" = "fiscal_year")) |> mutate( - `Pro Bono Services as a portion of revenue` = `Value of Pro Bono Services` / Revenue + `Pro Bono Services as a portion of revenue` = coalesce(`Value of Pro Bono Services`, 0) / if_else(Revenue == 0, NA_real_, Revenue) ) |> select( FY, @@ -539,35 +538,39 @@ CTS-IT's annual rate review requires that we understand the proportion of our re #| echo: false #| warning: false -probono_detail_last_fy <- -revenue_by_fy_and_service_type |> filter(`FY` == "2024-2025") |> +last_fy <- "2024-2025" + +probono_detail_last_fy <- revenue_by_fy_and_service_type |> + filter(`FY` == last_fy) |> left_join( -probono_combined |> - group_by(fiscal_year, revenue_source) |> - summarise(`Value of Pro Bono Services` = sum(value, na.rm = TRUE), .groups = "drop") |> - filter(fiscal_year == "2024-2025") |> -rename( - FY = "fiscal_year", - service_type = "revenue_source" -) |> -mutate(service_type = if_else(service_type == "Annual Project Billing", "Annual REDCap Project Maintenance", service_type)) |> - ungroup(), -by = c("FY", "service_type") -) |> -mutate(`Probono as portion of revenue for this service type` = `Value of Pro Bono Services` / revenue) |> + probono_combined |> + group_by(fiscal_year, revenue_source) |> + summarise(`Value of Pro Bono Services` = sum(value, na.rm = TRUE), .groups = "drop") |> + filter(fiscal_year == last_fy) |> + rename( + FY = "fiscal_year", + service_type = "revenue_source" + ) |> + mutate(service_type = if_else(service_type == "Annual Project Billing", + "Annual REDCap Project Maintenance", + service_type) + ) |> + ungroup(), + by = c("FY", "service_type") + ) |> + mutate(`Pro Bono as portion of revenue for this service type` = `Value of Pro Bono Services` / revenue) |> rename( `Revenue` = revenue, `Service type` = service_type ) |> - select(FY, `Service type`, `Revenue`, `Value of Pro Bono Services`, `Probono as portion of revenue for this service type`) - + select(FY, `Service type`, `Revenue`, `Value of Pro Bono Services`, `Pro Bono as portion of revenue for this service type`) probono_detail_last_fy |> gt::gt() |> gt::fmt_currency(columns = c("Revenue", "Value of Pro Bono Services"), decimals = 0) |> - gt::fmt_percent(columns = "Probono as portion of revenue for this service type", decimals = 0) |> + gt::fmt_percent(columns = "Pro Bono as portion of revenue for this service type", decimals = 0) |> gt::tab_header( - title = "Pro Bono Value and Portion of Revenue by Service Type for FY25" + title = paste0("Pro Bono Value and Portion of Revenue by Service Type for FY", last_fy) ) ```