Setting the file. One moment.
Skill 99 · Modeling Revenue Metrics
Subchapter 99.4
references/dbt/fct_revenue_item.sql
SQL65 lines3 KB
-- Line-item grain revenue fact, the dbt analogue of PostHog's managed revenue_item view.
-- Recognizes deferred revenue: each recurring line item is spread evenly across the calendar months
-- of its service period [period_start, period_end], so an annual plan feeds 1/12 of its value into
-- each month's MRR rather than the whole amount into a single month. One-time (non-subscription)
-- items resolve to just the month they were charged. Grain is one row per (line item, service month).
-- Converts to the reporting base currency via the currency_rates seed (dbt has no convertCurrency()).
{{ config(materialized='table') }}
with items as (
select * from {{ source('stripe', 'invoice_line_items') }}
),
rates as (
select currency, rate_date, rate_to_base from {{ ref('currency_rates') }}
),
-- month spine covering the full span of all line items; generate_series over dates is
-- Postgres/DuckDB syntax — swap in your warehouse's month generator if it differs.
month_spine as (
select generate_series(
date_trunc('month', min(period_start)),
date_trunc('month', max(coalesce(period_end, period_start))),
interval '1 month'
) as month
from items
),
recognized as (
select
i.id,
i.subscription_id,
i.customer_id,
i.product_id,
i.period_start::date as period_start,
i.period_end::date as period_end,
i.currency,
i.amount as line_amount,
(i.subscription_id is not null) as is_recurring,
s.month::date as month,
-- number of service months, to split the line amount evenly across the period
count(*) over (partition by i.id) as service_months
from items i
join month_spine s
on s.month >= date_trunc('month', i.period_start)
and s.month <= date_trunc('month', case
when i.subscription_id is not null then coalesce(i.period_end, i.period_start)
else i.period_start
end)
)
select
r.id as revenue_item_id,
r.month,
r.subscription_id,
r.customer_id,
r.product_id,
r.period_start,
r.period_end,
r.is_recurring,
r.currency as original_currency,
r.line_amount / r.service_months as original_amount,
-- base-currency amount at the month's rate. A MISSING rate stays NULL on purpose so the
-- not_null test on `amount` fails loudly instead of silently assuming 1:1 parity. Seed the
-- base currency itself at rate_to_base = 1.0 so base-currency rows resolve.
(r.line_amount / r.service_months) * rt.rate_to_base as amount
from recognized r
left join rates rt
on rt.currency = r.currency
and rt.rate_date = r.month