diff --git a/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql b/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql new file mode 100644 index 0000000..79258bf --- /dev/null +++ b/models/intermediate/cross/int_monthly_account_revenue_impact_from_growth.sql @@ -0,0 +1,250 @@ +{{ config(materialized="table", unique_key=["end_date", "id_deal"]) }} +with + int_billable_items_growth_score_by_deal as ( + select * from {{ ref("int_billable_items_growth_score_by_deal") }} + ), + int_kpis__agg_monthly_total_and_retained_revenue as ( + select * from {{ ref("int_kpis__agg_monthly_total_and_retained_revenue") }} + ), + int_kpis__agg_dates_main_kpis as ( + select * from {{ ref("int_kpis__agg_dates_main_kpis") }} + ), + int_kpis__dimension_deals as (select * from {{ ref("int_kpis__dimension_deals") }}), + int_hubspot__deal as (select * from {{ ref("int_hubspot__deal") }}), + int_kpis__lifecycle_daily_deal as ( + select * from {{ ref("int_kpis__lifecycle_daily_deal") }} + ), + billable_items_growth_score_refined as ( + select + big.start_date, + big.end_date, + big.id_deal, + big.current_month_billable_items, + big.current_month_share_billable_items, + case + when d.cancellation_date_utc between big.start_date and big.end_date + then -1 + else big.growth_score + end as growth_score, + big.projection_mean_absolute_error, + big.projection_mean_absolute_percentage_error, + big.are_billable_items_projected, + case + when d.cancellation_date_utc between big.start_date and big.end_date + then true + else false + end as is_growth_score_overridden_due_to_cancellation + from int_billable_items_growth_score_by_deal big + left join int_hubspot__deal d on big.id_deal = d.id_deal + ), + revenue_per_deal_and_month as ( + select + d.date, + d.dimension_value as id_deal, + -- Metrics current month + coalesce(r.total_revenue_in_gbp, 0) as current_month_total_revenue_in_gbp, + coalesce( + r.revenue_retained_post_resolutions_in_gbp, 0 + ) as current_month_revenue_retained_post_resolutions_in_gbp, + -- Metrics rolling 12 months - from 11 months ago to current month, + -- inclusive + sum(coalesce(r.total_revenue_in_gbp, 0)) over ( + partition by d.dimension_value + order by d.date asc + rows between 11 preceding and current row + ) as rolling_12_months_total_revenue_in_gbp, + sum(coalesce(r.revenue_retained_post_resolutions_in_gbp, 0)) over ( + partition by d.dimension_value + order by d.date asc + rows between 11 preceding and current row + ) as rolling_12_months_revenue_retained_post_resolutions_in_gbp + from int_kpis__agg_dates_main_kpis d + left join + int_kpis__agg_monthly_total_and_retained_revenue r + on d.dimension_value = r.dimension_value + and d.date = r.end_date + where + d.dimension = 'by_deal' + and d.dimension_value <> 'UNSET' + and d.is_end_of_month = true + ), + deal_revenue_contribution_per_month as ( + select + rpd.date, + -- Transform to next end of month for future join: revenue contribution + -- needs to be multiplied by the current month growth + ( + date_trunc('month', rpd.date)::date + + interval '2 months' + - interval '1 day' + )::date as next_end_of_month, + rpd.id_deal, + -- Deal contribution to metric vs. global + ( + rpd.rolling_12_months_total_revenue_in_gbp + ) / sum(rpd.rolling_12_months_total_revenue_in_gbp) over ( + partition by rpd.date order by rpd.date + ) as share_total_revenue_rolling_12_months, + (rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp) + / sum(rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp) over ( + partition by rpd.date order by rpd.date + ) as share_revenue_retained_post_resolutions_rolling_12_months + from revenue_per_deal_and_month rpd + ), + growth_and_impact_scores_per_deal_and_month as ( + select + -- Time window + date_trunc('month', drc.next_end_of_month)::date as start_date, + drc.next_end_of_month as end_date, + + -- Deal + drc.id_deal, + + -- Deal contribution to revenue + drc.share_total_revenue_rolling_12_months, + drc.share_revenue_retained_post_resolutions_rolling_12_months, + + -- Growth Score: if Growth is not available, set to 0 + coalesce(big.growth_score, 0) as growth_score, + + -- Impact Score + coalesce(big.growth_score, 0) + * share_total_revenue_rolling_12_months as impact_score_total_revenue, + coalesce(big.growth_score, 0) + * share_revenue_retained_post_resolutions_rolling_12_months + as impact_score_revenue_retained_post_resolutions, + + -- If billable items are not available, set to 0 + coalesce( + big.current_month_billable_items, 0 + ) as current_month_billable_items, + coalesce( + big.current_month_share_billable_items, 0 + ) as share_billable_items_current_month, + + -- Projection metrics + sum(coalesce(big.current_month_billable_items, 0)) over ( + partition by drc.date order by drc.date + ) as global_monthly_billable_items, + big.projection_mean_absolute_error, + big.projection_mean_absolute_percentage_error, + big.are_billable_items_projected, + big.is_growth_score_overridden_due_to_cancellation + + from deal_revenue_contribution_per_month drc + left join + billable_items_growth_score_refined big + on drc.next_end_of_month = big.end_date + and drc.id_deal = big.id_deal + ) +select + -- Time window + gai.start_date, + gai.end_date, + + -- Deal Static Attributes + gai.id_deal, + gai.id_deal || '-' || coalesce(ikdd.main_deal_name, '') as deal, + ikdd.client_type, + ikdd.has_active_pms, + ikdd.active_pms_list, + ikdd.main_billing_country_iso_3_per_deal, + + -- Deal Lifecycle -- + deal_lifecycle.deal_lifecycle_state, + + -- Deal Hubspot Attributes -- + d.deal_hubspot_stage, + d.account_manager, + d.live_date_utc, + d.cancellation_date_utc, + + -- Growth & Impact Scores + gai.growth_score, + gai.impact_score_total_revenue, + gai.impact_score_revenue_retained_post_resolutions, + + -- Categorisation + case + when impact_score_revenue_retained_post_resolutions <= -0.3 / 100 + then 'MAJOR DECLINE' + when + impact_score_revenue_retained_post_resolutions < -0.03 / 100 + and impact_score_revenue_retained_post_resolutions > -0.3 / 100 + then 'DECLINE' + when + impact_score_revenue_retained_post_resolutions >= -0.03 / 100 + and impact_score_revenue_retained_post_resolutions <= 0.03 / 100 + then 'FLAT' + when + impact_score_revenue_retained_post_resolutions > 0.03 / 100 + and impact_score_revenue_retained_post_resolutions < 0.3 / 100 + then 'GAIN' + when impact_score_revenue_retained_post_resolutions >= 0.3 / 100 + then 'MAJOR GAIN' + else 'UNSET' + end as categorisation_impact_score_revenue_retained_post_resolutions, + + -- Rank per Impact Score + row_number() over ( + partition by gai.end_date + order by gai.impact_score_total_revenue desc, gai.id_deal + ) as rank_impact_score_total_revenue, + row_number() over ( + partition by gai.end_date + order by gai.impact_score_revenue_retained_post_resolutions desc, gai.id_deal + ) as rank_impact_score_revenue_retained_post_resolutions, + + -- Total Revenue Metrics + rpd.current_month_total_revenue_in_gbp, + rpd.rolling_12_months_total_revenue_in_gbp, + gai.share_total_revenue_rolling_12_months, + row_number() over ( + partition by gai.end_date + order by gai.share_total_revenue_rolling_12_months desc, gai.id_deal + ) as rank_total_revenue_rolling_12_months, + + -- Revenue Retained Post Resolutions Metrics + rpd.current_month_revenue_retained_post_resolutions_in_gbp, + rpd.rolling_12_months_revenue_retained_post_resolutions_in_gbp, + gai.share_revenue_retained_post_resolutions_rolling_12_months, + row_number() over ( + partition by gai.end_date + order by + gai.share_revenue_retained_post_resolutions_rolling_12_months desc, + gai.id_deal + ) as rank_revenue_retained_post_resolutions_rolling_12_months, + + -- Billable Items Metrics + gai.current_month_billable_items, + gai.share_billable_items_current_month, + row_number() over ( + partition by gai.end_date + order by gai.current_month_billable_items desc, gai.id_deal + ) as rank_billable_items_current_month, + gai.projection_mean_absolute_error, + gai.projection_mean_absolute_percentage_error, + gai.are_billable_items_projected, + gai.is_growth_score_overridden_due_to_cancellation + +from growth_and_impact_scores_per_deal_and_month gai +left join + revenue_per_deal_and_month rpd + -- Keep Revenue attributed to the real month (will be null in the ongoing month) + on gai.id_deal = rpd.id_deal + and gai.end_date = rpd.date +left join int_kpis__dimension_deals ikdd on gai.id_deal = ikdd.id_deal +left join int_hubspot__deal d on gai.id_deal = d.id_deal +left join + int_kpis__lifecycle_daily_deal deal_lifecycle + -- Retrieve Deal Lifecycle State for the end of the month or yesterday if it's the + -- ongoing month + on ( + gai.end_date = deal_lifecycle.date + or ( + current_date - interval '1 day' = deal_lifecycle.date + and date_trunc('month', gai.end_date)::date + = date_trunc('month', deal_lifecycle.date)::date + ) + ) + and gai.id_deal = deal_lifecycle.id_deal diff --git a/models/intermediate/cross/schema.yml b/models/intermediate/cross/schema.yml index 0566335..66b8089 100644 --- a/models/intermediate/cross/schema.yml +++ b/models/intermediate/cross/schema.yml @@ -3504,3 +3504,296 @@ models: - dbt_expectations.expect_column_values_to_be_between: min_value: 1 strictly: false + + - name: int_monthly_account_revenue_impact_from_growth + description: | + This model computes the monthly revenue impact from the growth of + billable items for each deal. The revenue impact is computed as the + product of the growth score and the deal contribution to the total revenue + in the previous 12 months. + + There's 2 impact scores computed depending on the revenue metric, namely: + - impact_score_total_revenue: based on Total Revenue + - impact_score_revenue_retained_post_resolutions: based on Revenue Retained + Post Resolutions + + It is important to note that if we check the ongoing month, the count of + billable items and the corresponding share will be based on the projection, + rather than the actual figure. In this case, the MAE and MAPE of the projected + value are indicated in the model. + + While the growth and impact scores are computed at a monthly basis, their values + will update every day with the latest available projection. + + data_tests: + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - end_date + - id_deal + - dbt_utils.unique_combination_of_columns: + combination_of_columns: + - start_date + - id_deal + + columns: + - name: start_date + data_type: date + description: | + Start date of the period for which the revenue impact is computed. + Corresponds to the first day of the month. + data_tests: + - not_null + + - name: end_date + data_type: date + description: | + End date of the period for which the revenue impact is computed. + Corresponds to the last day of the month. + data_tests: + - not_null + + - name: id_deal + data_type: string + description: | + Unique ID for a deal, or account. + data_tests: + - not_null + + - name: deal + data_type: string + description: | + Concatenation of the deal ID and the deal name. + + - name: client_type + data_type: string + description: | + Type of the client, PLATFORM or API. + + - name: has_active_pms + data_type: boolean + description: | + Flag indicating if the deal has an active PMS or not. + + - name: active_pms_list + data_type: string + description: | + List of active PMS for the deal. It can be null if the deal has no + active PMS. + + - name: main_billing_country_iso_3_per_deal + data_type: string + description: | + Main billing country for the deal. It can be null. + + - name: deal_lifecycle_state + data_type: string + description: | + Lifecycle state of the deal. + + - name: deal_hubspot_stage + data_type: string + description: | + Hubspot stage of the deal. + + - name: account_manager + data_type: string + description: | + Account manager of the deal. It can be null. + + - name: live_date_utc + data_type: date + description: | + Live date of the deal according to HubSpot. It can be null. + + - name: cancellation_date_utc + data_type: date + description: | + Cancellation date of the deal according to HubSpot. It can be null. + + - name: growth_score + data_type: decimal + description: | + Growth score of the billable items, based on the average between: + - The billable items of a given month vs. the average of the previous + 3 months. + - The share a deal has in terms of billable items of a given month + if compared to the rest of the deals vs. the average of the previous 3 + months. + The growth score is capped between -1 and 1. + It can be overridden to -1 in case the deal is cancelled in the same month. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: impact_score_total_revenue + data_type: decimal + description: | + Impact score of the growth score on the total revenue. + It is computed as the product of the growth score and the deal + contribution to the total revenue in the previous 12 months. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: impact_score_revenue_retained_post_resolutions + data_type: decimal + description: | + Impact score of the growth score on the revenue retained post + resolutions. It is computed as the product of the growth score and + the deal contribution to the revenue retained post resolutions in + the previous 12 months. + It cannot be null. + data_tests: + - not_null + - dbt_expectations.expect_column_values_to_be_between: + min_value: -1 + max_value: 1 + strictly: false + + - name: categorisation_impact_score_revenue_retained_post_resolutions + data_type: string + description: | + Categorisation of the impact score on the revenue retained post + resolutions. It cannot be null. + data_tests: + - not_null + - accepted_values: + values: + - MAJOR DECLINE + - DECLINE + - FLAT + - GAIN + - MAJOR GAIN + + - name: rank_impact_score_total_revenue + data_type: integer + description: | + Monthly rank of the deal in terms of impact score on the total revenue. + + - name: rank_impact_score_revenue_retained_post_resolutions + data_type: integer + description: | + Monthly rank of the deal in terms of impact score on the revenue + retained post resolutions. + + - name: current_month_total_revenue_in_gbp + data_type: decimal + description: | + Total revenue in GBP for the current month. + If the month is in progress then this value will be null. + + - name: rolling_12_months_total_revenue_in_gbp + data_type: decimal + description: | + Total revenue in GBP for the previous 12 months. + It can be null. + + - name: share_total_revenue_rolling_12_months + data_type: decimal + description: | + Share of the deal in terms of total revenue in the previous 12 months. + It cannot be null. + data_tests: + - not_null + + - name: rank_total_revenue_rolling_12_months + data_type: integer + description: | + Monthly rank of the deal in terms of total revenue in the previous + 12 months. + + - name: current_month_revenue_retained_post_resolutions_in_gbp + data_type: decimal + description: | + Revenue retained post resolutions in GBP for the current month. + If the month is in progress then this value will be null. + + - name: rolling_12_months_revenue_retained_post_resolutions_in_gbp + data_type: decimal + description: | + Revenue retained post resolutions in GBP for the previous 12 months. + It can be null. + + - name: share_revenue_retained_post_resolutions_rolling_12_months + data_type: decimal + description: | + Share of the deal in terms of revenue retained post resolutions in + the previous 12 months. + It cannot be null. + data_tests: + - not_null + + - name: rank_revenue_retained_post_resolutions_rolling_12_months + data_type: integer + description: | + Monthly rank of the deal in terms of revenue retained post + resolutions in the previous 12 months. + + - name: current_month_billable_items + data_type: integer + description: | + Monthly billable items. If the month is in progress + then this value might be projected. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: share_billable_items_current_month + data_type: decimal + description: | + Share of the billable items for a given deal in the current month. + If the month is in progress then this value might be projected. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: rank_billable_items_current_month + data_type: integer + description: | + Monthly rank of the deal in terms of billable items in the current month. + If the month is in progress then this value might be projected. + + - name: projection_mean_absolute_error + data_type: decimal + description: | + Mean absolute error of the projection of the billable items. + It is null if the month is not in progress or value is projected + but there's no prior data to compare the projection against. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: projection_mean_absolute_percentage_error + data_type: decimal + description: | + Mean absolute percentage error of the projection of the billable items. + It is null if the month is not in progress or value is projected + but there's no prior data to compare the projection against. + data_tests: + - dbt_expectations.expect_column_values_to_be_between: + min_value: 0 + strictly: false + + - name: are_billable_items_projected + data_type: boolean + description: | + Flag indicating if the billable items are projected or not. + If the month is in progress then this value might be projected. + It can be null if there's no projection for that deal. + + - name: is_growth_score_overridden_due_to_cancellation + data_type: boolean + description: | + Flag indicating if the growth score is overridden to -1 due to + cancellation in the same month.