Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# dbt_stripe v1.9.0

[PR #153](https://github.com/fivetran/dbt_stripe/pull/153) includes the following updates:

## Schema/Data Change
**1 total change • 1 possible breaking change**

| Data Model(s) | Change type | Old | New | Notes |
| ------------- | ----------- | --- | --- | ----- |
| `stripe__subscription_item_mrr_report` (possible breaking change) | Changed model | Date spine starts at `current_period_start`, every month uses the current quantity, and the model runs with only `stripe__using_subscriptions`. | Date spine anchors to each item's `created_at`; historical months use the invoiced quantity (from `invoice_line_item`), the current month uses the live quantity. Now also requires `stripe__using_invoices`. | See the [DECISIONLOG](https://github.com/fivetran/dbt_stripe/blob/main/DECISIONLOG.md#mrr-history-anchored-to-each-subscription-items-creation-date). |

# dbt_stripe v1.8.0

[PR #152](https://github.com/fivetran/dbt_stripe/pull/152) includes the following updates:
Expand Down
22 changes: 22 additions & 0 deletions DECISIONLOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## MRR History Anchored to Each Subscription Item's Creation Date

The `stripe__subscription_item_mrr_report` model anchors each item's MRR timeline to that item's `created_at`, reporting MRR from when the item was added through the current month.

Because `subscription_item` retains only the current state of each item, we reconstruct historical quantity from `invoice_line_item`, which records the quantity actually billed for each period. For each month, we use the quantity from the most recent invoice line whose period overlaps that month (proration adjustment lines excluded); the current month uses the live `subscription_item` quantity. This lets the report surface real `expansion` and `contraction` between months rather than back-projecting today's quantity across all history.

Because this model depends on `invoice_line_item` for historical quantity, it requires `stripe__using_invoices` (enabled by default) and does not build when that variable is disabled. Stripe generates an invoice each billing period, so gaps are not expected, but when a month has no invoice line we carry the most recent prior invoiced quantity forward rather than snapping to today's quantity (which would invent phantom changes in the gap). A month falls back to the current `subscription_item` quantity only when no earlier invoice exists (months before the first invoice). Prices are not back-filled — historical months apply the item's current price.

## MRR Timeline Uses an End-of-Month Snapshot

The `stripe__subscription_item_mrr_report` model keeps a month for an item only when the subscription is still active at month-end. We enforce this by truncating `current_period_end` to its month in the timeline's upper bound, so a month is kept only while its first day is before the start of the `current_period_end` month.

The effect is that a subscription paid through the middle of a month is not on the books at that month's end and is considered to have churned, so MRR is not recognized for that month. For example, a monthly subscription whose last paid period runs May 15 to June 15 recognizes MRR for May (a full month) but considers June as a churned month ($0 MRR), because it has lapsed by June 30.

## Stripe Subscription and Item Start-Date Fields

We anchor to `subscription_item.created_at` because MRR is reported at the item grain. The relevant fields are not interchangeable:

- **`subscription.start_date_at`** — Stripe's authoritative subscription start, backdate-aware, but at the subscription grain.
- **`subscription.created_at`** — when the subscription object was created; usually equals `start_date`, but diverges on backdating or imports.
- **`subscription_item.created_at`** — when the item was added. Stripe allows items to be added or removed mid-cycle (add-ons, seat changes, plan upgrades) and prorates by default, so this can fall later than the subscription's start. Anchoring per item attributes MRR only to the months the item existed.

## MRR Date Spine Capped at the Current Month

The `stripe__subscription_item_mrr_report` model caps its date spine at the end of the current month, regardless of how far into the future a subscription's `current_period_end` extends.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ By default, this package materializes the following final tables:
| [stripe__ending_balance_reconciliation_itemized_4](https://fivetran.github.io/dbt_stripe/#!/model/model.stripe.stripe__ending_balance_reconciliation_itemized_4) | Matches bank payouts with unsettled Stripe transactions.<br><br>**Example Analytics Questions:**<br><ul><li>Which transactions remain unsettled as of the last payout?</li><li>Do all automatic payouts reconcile fully with balance changes?</li></ul> |
| [stripe__payout_itemized_3](https://fivetran.github.io/dbt_stripe/#!/model/model.stripe.stripe__payout_itemized_3) | Details expected and actual payout amounts and statuses.<br><br>**Example Analytics Questions:**<br><ul><li>When should I expect my next payout, and for how much?</li><li>Are there any delayed or failed payouts that need follow-up?</li></ul> |
| [stripe__line_item_enhanced](https://fivetran.github.io/dbt_stripe/#!/model/model.stripe.stripe__line_item_enhanced) | Provides unified reporting across billing platforms on product, customer, and revenue metrics. See the [Fivetran Billing Model Streamlit App](https://fivetran-billing-model.streamlit.app/) for more details.<br><br>**Example Analytics Questions:**<br><ul><li>What are the top revenue-generating products or SKUs?</li><li>What is the average revenue per user (ARPU) by subscription plan?</li></ul> |
| [stripe__subscription_item_mrr_report](https://fivetran.github.io/dbt_stripe/#!/model/model.stripe.stripe__subscription_item_mrr_report) | Shows both contracted and billed MRR (monthly recurring revenue) with discounts applied at the subscription item level. Tracks MRR changes over time, classifying each month as new, expansion, contraction, churned, reactivation, or unchanged.<br><br>**Example Analytics Questions:**<br><ul><li>What percentage of subscription customers are churning each month as compared to new?</li><li>How much subscription revenue was lost last year due to discounts?</li></ul> |
| [stripe__subscription_item_mrr_report](https://fivetran.github.io/dbt_stripe/#!/model/model.stripe.stripe__subscription_item_mrr_report) | Shows both contracted and billed MRR (monthly recurring revenue) with discounts applied at the subscription item level. Tracks MRR changes over time, classifying each month as new, expansion, contraction, churned, reactivation, or unchanged. Relies on `invoice_line_item` for historical quantity, so it requires `stripe__using_invoices` (enabled by default) and does not build when that variable is disabled.<br><br>**Example Analytics Questions:**<br><ul><li>What percentage of subscription customers are churning each month as compared to new?</li><li>How much subscription revenue was lost last year due to discounts?</li></ul> |

¹ Each Quickstart transformation job run materializes these models if all components of this data model are enabled. This count includes all staging, intermediate, and final models materialized as `view`, `table`, or `incremental`.

Expand Down Expand Up @@ -80,7 +80,7 @@ Include the following stripe package version in your `packages.yml` file:
```yaml
packages:
- package: fivetran/stripe
version: [">=1.8.0", "<1.9.0"]
version: [">=1.9.0", "<1.10.0"]
```
> All required sources and staging models are now bundled into this transformation package. Do not include `fivetran/stripe_source` in your `packages.yml` since this package has been deprecated.

Expand Down
2 changes: 1 addition & 1 deletion analyses/stripe__arr_snapshot_analysis.sql

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the analyses/README.md we mention subscriptions must be enabled. We need to also add that invoices need to be enabled as well.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ config(enabled=var('stripe__using_subscriptions', True)) }}
{{ config(enabled=var('stripe__using_subscriptions', True) and var('stripe__using_invoices', True)) }}

with mrr_by_item as (
select *
Expand Down
2 changes: 1 addition & 1 deletion analyses/stripe__customer_mrr_analysis.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ config(enabled=var('stripe__using_subscriptions', True)) }}
{{ config(enabled=var('stripe__using_subscriptions', True) and var('stripe__using_invoices', True)) }}

with item_mrr as (
select *
Expand Down
2 changes: 1 addition & 1 deletion dbt_project.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
config-version: 2
name: 'stripe'

version: '1.8.0'
version: '1.9.0'
require-dbt-version: [">=1.3.0", "<3.0.0"]

models:
Expand Down
2 changes: 1 addition & 1 deletion docs/catalog.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/manifest.json

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions integration_tests/ci/test_scenarios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ test_scenarios:
stripe__standardized_billing_model_enabled: true
include_incremental: false

- name: "subscriptions without invoices"
vars:
stripe__using_subscriptions: true
stripe__using_invoices: false
include_incremental: false

- name: "MDLS"
vars:
fivetran_using_source_casing: true
Expand Down
2 changes: 1 addition & 1 deletion integration_tests/dbt_project.yml
Comment thread
fivetran-joemarkiewicz marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ config-version: 2

name: 'stripe_integration_tests'

version: '1.8.0'
version: '1.9.0'

profile: 'integration_tests'

Expand Down
5 changes: 4 additions & 1 deletion models/stripe.yml
Original file line number Diff line number Diff line change
Expand Up @@ -967,7 +967,10 @@ models:
description: "{{ doc('source_relation') }}"

- name: stripe__subscription_item_mrr_report
description: Each record represents a subscription item for a given month with monthly recurring revenue (MRR) metrics and movement classification. Tracks MRR changes over time, classifying each month as new, expansion, contraction, churned, reactivation, or unchanged.
description: >
Each record represents a subscription item for a given month with monthly recurring revenue (MRR) metrics and movement classification. Tracks MRR changes over time, classifying each month as new, expansion, contraction, churned, reactivation, or unchanged.
Reconstructs historical MRR amount changes from invoice line items and anchors each item's timeline to its creation date. Requires `stripe__using_invoices`: true to build.
See the [DECISIONLOG](https://github.com/fivetran/dbt_stripe/blob/main/DECISIONLOG.md) for full details.
columns:
- name: source_relation
description: "{{ doc('source_relation') }}"
Expand Down
140 changes: 119 additions & 21 deletions models/stripe__subscription_item_mrr_report.sql
Comment thread
fivetran-joemarkiewicz marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{{ config(enabled=var('stripe__using_subscriptions', True)) }}
{{ config(enabled=var('stripe__using_subscriptions', True) and var('stripe__using_invoices', True)) }}

{% if execute and flags.WHICH in ('run', 'build') %}

Expand All @@ -8,7 +8,7 @@
cast(
{{ dbt.date_trunc(
'month',
"coalesce(subscription_item.current_period_start, subscription.current_period_start)"
"coalesce(subscription_item.created_at, subscription.created_at)"
) }} as date
)
),
Expand Down Expand Up @@ -65,6 +65,16 @@ price_plan as (

),

-- Actual billed quantity per period; provides quantity history subscription_item lacks. Prorations excluded.
invoice_line_item as (

select *
from {{ ref('stg_stripe__invoice_line_item') }}
where subscription_item_id is not null
and not coalesce(proration, false)

),

{% if var('stripe__using_coupons', True) and var('stripe__using_subscription_discounts', True) %}
subscription_discount as (

Expand Down Expand Up @@ -95,6 +105,40 @@ date_dimensions as (

),

-- Anchor each billed quantity to its period_start month, keeping the latest line per month.
-- Months the period spans beyond period_start are filled later by the carry-forward logic.
invoiced_quantity_by_month as (

select
invoice_line_item.source_relation,
invoice_line_item.subscription_item_id,
date_dimensions.subscription_month,
invoice_line_item.quantity,
row_number() over (
partition by
invoice_line_item.source_relation,
invoice_line_item.subscription_item_id,
date_dimensions.subscription_month
order by invoice_line_item.period_start desc
) as rn
from invoice_line_item
inner join date_dimensions
on date_dimensions.subscription_month = cast({{ dbt.date_trunc('month', 'invoice_line_item.period_start') }} as date)

),

invoiced_quantity_deduped as (

select
source_relation,
subscription_item_id,
subscription_month,
quantity
from invoiced_quantity_by_month
where rn = 1

),

base as (

select
Expand All @@ -103,9 +147,10 @@ base as (
subscription_item.subscription_id,
subscription.customer_id,
subscription.status as subscription_status,
coalesce(subscription_item.created_at, subscription.created_at) as item_created_at,
coalesce(subscription_item.current_period_start, subscription.current_period_start) as current_period_start,
coalesce(subscription_item.current_period_end, subscription.current_period_end) as current_period_end,
subscription_item.quantity,
subscription_item.quantity as current_quantity,
price_plan.product_id,
price_plan.price_plan_id,
price_plan.recurring_interval,
Expand All @@ -129,8 +174,10 @@ normalized as (
subscription_id,
customer_id,
subscription_status,
item_created_at,
current_period_start,
current_period_end,
current_quantity,
product_id,
price_plan_id,
recurring_interval,
Expand Down Expand Up @@ -175,7 +222,7 @@ subscription_item_periods as (
price_plan_id,
subscription_status,
currency,
min(cast({{ dbt.date_trunc('month', 'current_period_start') }} as date)) as first_active_month,
min(cast({{ dbt.date_trunc('month', 'item_created_at') }} as date)) as first_active_month,
cast({{ dbt.dateadd('month', 3, 'max(cast(' ~ dbt.date_trunc('month', 'current_period_end') ~ ' as date))') }} as date) as last_month_to_track
from normalized
{{ dbt_utils.group_by(8) }}
Expand Down Expand Up @@ -203,28 +250,79 @@ all_item_months as (

),

-- attach the invoiced quantity to each month, then carry the most recent invoiced
-- quantity forward across any months missing an invoice (gap months)
item_month_invoiced as (

select
all_item_months.*,
invoiced_quantity_deduped.quantity as invoiced_quantity
from all_item_months
left join invoiced_quantity_deduped
on all_item_months.source_relation = invoiced_quantity_deduped.source_relation
and all_item_months.subscription_item_id = invoiced_quantity_deduped.subscription_item_id
and all_item_months.subscription_month = invoiced_quantity_deduped.subscription_month

),

-- increment a group id each time an invoiced quantity appears; gap months inherit the prior group
item_month_grouped as (

select
*,
sum(case when invoiced_quantity is not null then 1 else 0 end) over (
partition by source_relation, subscription_item_id
order by subscription_month
rows between unbounded preceding and current row
) as invoiced_group
from item_month_invoiced

),

-- within each group the single invoiced value carries forward to the gap months
item_month_carried as (

select
*,
max(invoiced_quantity) over (
partition by source_relation, subscription_item_id, invoiced_group
) as carried_quantity
from item_month_grouped

),

-- Join back to normalized to determine if subscription was active in each month
item_months as (

select
all_item_months.source_relation,
all_item_months.subscription_item_id,
all_item_months.subscription_id,
all_item_months.customer_id,
all_item_months.product_id,
all_item_months.price_plan_id,
all_item_months.subscription_status,
all_item_months.currency,
all_item_months.subscription_year,
all_item_months.subscription_month,
coalesce(normalized.mrr, 0) as mrr
from all_item_months
item_month_carried.source_relation,
item_month_carried.subscription_item_id,
item_month_carried.subscription_id,
item_month_carried.customer_id,
item_month_carried.product_id,
item_month_carried.price_plan_id,
item_month_carried.subscription_status,
item_month_carried.currency,
item_month_carried.subscription_year,
item_month_carried.subscription_month,
-- set once to stay DRY; reused in the mrr calculation below
{% set effective_quantity %}
case
when item_month_carried.subscription_month = cast({{ last_month }} as date)
then coalesce(normalized.current_quantity, 1)
else coalesce(item_month_carried.carried_quantity, normalized.current_quantity, 1)
end
{% endset %}
-- current month uses live quantity, history uses the carried-forward invoiced quantity
coalesce(normalized.mrr, 0)
* {{ dbt_utils.safe_divide(effective_quantity, "coalesce(normalized.current_quantity, 1)") }} as mrr
Comment thread
fivetran-joemarkiewicz marked this conversation as resolved.
from item_month_carried
left join normalized
on all_item_months.source_relation = normalized.source_relation
and all_item_months.subscription_item_id = normalized.subscription_item_id
and all_item_months.price_plan_id = normalized.price_plan_id
and all_item_months.subscription_month >= cast({{ dbt.date_trunc('month', 'normalized.current_period_start') }} as date)
and all_item_months.subscription_month < cast({{ dbt.date_trunc('month', 'normalized.current_period_end') }} as date)
on item_month_carried.source_relation = normalized.source_relation
and item_month_carried.subscription_item_id = normalized.subscription_item_id
and item_month_carried.price_plan_id = normalized.price_plan_id
and item_month_carried.subscription_month >= cast({{ dbt.date_trunc('month', 'normalized.item_created_at') }} as date)
and item_month_carried.subscription_month < cast({{ dbt.date_trunc('month', 'normalized.current_period_end') }} as date)

),

Expand Down