User defined views¶
What are user defined views?¶
User defined views (UDVs) are a way to create or modify views (e.g. bframe.prices
or bframe.invoices
) in the bframe schema. While creating a brand new independent view, like bframe.my_random_pricing
, might not be useful combining this with the modification of default views can be impactful. For example, bframe.line_items
is simple a union of two types of line item views.
-- bframe.line_items sql
SELECT *
FROM bframe.fixed_line_items
UNION
SELECT *
FROM bframe.event_line_items
This abstraction is intended so that new types of line items appended. This can be utilized by modifying bframe.line_items
to include an additional union.
-- bframe.line_items sql (modified)
SELECT *
FROM bframe.fixed_line_items
UNION
SELECT *
FROM bframe.event_line_items
UNION
SELECT *
FROM bframe.my_random_pricing
Now when consumers of the view employ bframe.line_items
it will include the additional bframe.my_random_pricing
. Downstream applications like business intelligence and billing will automatically include the new pricing since they already use line items. This is a basic example, but it exhibits how UDVs can enable bframe to be extended and customized.
Why are user defined views important?¶
bframe library intends to be an extensibile set of functions that enable the bframe framework. UDVs are the primary feature that enables this flexibility. If a user understands bframe, then just about anything can be modified to operationalize a unique business model.
A goal of bframe is to handle most business modeling use cases but it is recognized that full coverage is impossible. UDVs exist to give users a path to resolution in the scenario that bframe misses a specific requirement. Without UDVs, users would have to settle for less than a perfect fit.
How to create user defined views¶
Creating a UDV is straight forward. A name and SQL template are the requirements for creation. To demonstrate let’s create a new type of pricing and enable it for Wikipedia.
Wikipedia wants to create a free tier for customers on their standard plan. They believe that folks who try out the product will definitely use more in the future if they are a good fit. The leadership team has decided they can give up roughly $100 in free usage a month without breaking the bank.
Creating the new pricing¶
The first step to creating a new type of pricing is creating a product type. We will be modifying an existing product but the steps would be similar for generating one from scratch. The product type determines how metering and rating is applied. The product type will be called 'TIERED'
.
INSERT INTO src.products (org_id, env_id, branch_id, id, name, ptype, filters, agg_property, event_name, version) values (2, 2, 1, 2, 'Creates (w/ Free tier)', 'TIERED', '{}', 'agg_value', 'create', 1);
Count |
---|
1 |
Now that the product exists we will modify the existing list prices to use it on the Standard pricebook. Additionally for this specific example we also need additional information to understand where a tier begins and ends. The pricing_metadata
field can be utilized for storage in this case.
Since we have two tiers we will need to create an additional list price to hold the free tier.
INSERT INTO src.list_prices (org_id, env_id, branch_id, id, product_uid, pricebook_uid, price, pricing_metadata, version) values (2, 2, 1, 2, 2, 1, '0.00', '{"tier_start": 0, "tier_end": 2000}', 1);
INSERT INTO src.list_prices (org_id, env_id, branch_id, id, product_uid, pricebook_uid, price, pricing_metadata) values (2, 2, 1, 100, 2, 1, '0.05', '{"tier_start": 2000}');
Count |
---|
1 |
Create the tiered pricing views¶
Now that products and prices have been established, instantiating the UDVs is the last step to support the free tier.
Tiered rated events¶
Rating each event with tiered pricing requires knowledge of the prior events. For example, if we want to know if the next event should be free or charged an understanding of the past events is necessary. Due to this, creating a new view called bframe.tiered_rated_events
is a more straightforward solution.
SELECT
rs.*
EXCLUDE (running_agg, tier_start, tier_end)
REPLACE(
rsq.quantity AS quantity,
rsq.quantity * rs.price::DECIMAL AS amount
)
FROM (
SELECT
_BF_ORG_ID as org_id,
_BF_ENV_ID as env_id,
_BF_BRANCH_ID as branch_id,
COALESCE(me.transaction_id, 'EMPTY_RATED_EVENT') as transaction_id,
COALESCE(me.customer_id, ps.customer_id) as customer_id,
COALESCE(me.properties, '{}') as properties,
COALESCE(me.metered_at, ps.started_at) as metered_at,
COALESCE(me.received_at, ps.started_at) as received_at,
ps.list_price_uid,
ps.contract_price_uid,
ps.product_uid,
ps.product_type,
ps.price,
COALESCE(me.quantity, 0) as quantity,
ps.price::DECIMAL * COALESCE(me.quantity, 0) AS amount,
ps.contract_id,
ps.started_at,
ps.ended_at,
ps.effective_at,
ps.ineffective_at,
(CASE
WHEN _BF_RATING_AS_OF_DT >= ps.ended_at
THEN 'FINALIZED'
ELSE 'DRAFT'
END) AS status,
ps.invoice_delivery,
CAST(ps.pricing_metadata->'$.tier_start' AS DECIMAL) as tier_start,
CAST(ps.pricing_metadata->'$.tier_end' AS DECIMAL) as tier_end,
SUM(me.quantity) OVER (
PARTITION BY
me.customer_id,
me.product_uid,
ps.contract_id,
ps.list_price_uid,
ps.contract_price_uid,
ps.started_at,
ps.ended_at
ORDER BY me.metered_at ASC
ROWS BETWEEN
UNBOUNDED PRECEDING
AND CURRENT ROW
) AS running_agg
FROM (
SELECT *
FROM bframe.matched_events
WHERE product_type = 'TIERED'
) AS me
RIGHT JOIN bframe.price_spans AS ps
ON me.product_uid = ps.product_uid
AND me.metered_at >= ps.effective_at
AND me.metered_at < date_trunc('day', ps.ineffective_at)
AND me.customer_id = ps.customer_id
) AS rs,
LATERAL (
SELECT (CASE
WHEN ((rs.running_agg - rs.quantity) < rs.tier_end AND rs.running_agg >= rs.tier_end)
THEN COALESCE(rs.tier_end - (rs.running_agg - rs.quantity), 0)
WHEN ((rs.running_agg - rs.quantity) < rs.tier_start AND rs.running_agg >= rs.tier_start)
THEN COALESCE(rs.running_agg, 0) - tier_start
WHEN (rs.running_agg >= rs.tier_start AND rs.running_agg < rs.tier_end)
THEN rs.quantity
WHEN (rs.running_agg >= rs.tier_start AND rs.tier_end IS NULL)
THEN rs.quantity
ELSE NULL
END) as quantity
) AS rsq
WHERE
rs.running_agg >= rs.tier_start
AND (
rs.running_agg < rs.tier_end
OR ((rs.running_agg - rs.quantity) < rs.tier_end AND rs.running_agg >= rs.tier_end)
OR rs.tier_end IS NULL
)
SELECT
transaction_id,
customer_id,
contract_id,
properties,
metered_at,
received_at,
product_uid,
list_price_uid,
product_type,
price,
quantity,
amount,
started_at,
ended_at,
status,
invoice_delivery
FROM bframe.tiered_rated_events
WHERE customer_id = 'Xenophon'
ORDER BY received_at, list_price_uid
LIMIT 5;
transaction_id |
customer_id |
contract_id |
properties |
metered_at |
received_at |
product_uid |
list_price_uid |
product_type |
price |
quantity |
amount |
started_at |
ended_at |
status |
invoice_delivery |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
1715260620.0Diözesanmuseum_Lapidarium_-_Türsturz.jpg |
Xenophon |
Xenophon_contract |
{“name”:”create”,”agg_value”:336.0,”category”:”Collection_highlights_of_Koninklijke_Bibliotheek”} |
2024-05-09 14:00:00 |
2024-05-09 14:00:00 |
2 |
2 |
TIERED |
0.00 |
336.0 |
0.0 |
2024-05-01 |
2024-06-01 |
DRAFT |
ARREARS |
1715363107.0Bamberger_Dom_Kreuzgang_-_Schlussstein_Wappen.jpg |
Xenophon |
Xenophon_contract |
{“name”:”create”,”agg_value”:776.0,”category”:”UNESCO”} |
2024-05-10 18:00:00 |
2024-05-10 18:00:00 |
2 |
2 |
TIERED |
0.00 |
776.0 |
0.0 |
2024-05-01 |
2024-06-01 |
DRAFT |
ARREARS |
1715435108.0Diözesanmuseum_Bamberg_-_Gunthertuch_4.jpg |
Xenophon |
Xenophon_contract |
{“name”:”create”,”agg_value”:2600.0,”category”:”UNESCO”} |
2024-05-11 14:00:00 |
2024-05-11 14:00:00 |
2 |
2 |
TIERED |
0.00 |
888.0 |
0.0 |
2024-05-01 |
2024-06-01 |
DRAFT |
ARREARS |
1715435108.0Diözesanmuseum_Bamberg_-_Gunthertuch_4.jpg |
Xenophon |
Xenophon_contract |
{“name”:”create”,”agg_value”:2600.0,”category”:”UNESCO”} |
2024-05-11 14:00:00 |
2024-05-11 14:00:00 |
2 |
100 |
TIERED |
0.05 |
1712.0 |
85.6 |
2024-05-01 |
2024-06-01 |
DRAFT |
ARREARS |
Tiered line items¶
Once we have rated events, they need to be aggregated to create line items. This can be accomplished by updating the existing bframe.event_line_items
view or making a new one called bframe.tiered_event_line_items
. We will opt for the latter.
SELECT
_BF_ORG_ID as org_id,
_BF_ENV_ID as env_id,
_BF_BRANCH_ID as branch_id,
CURRENT_TIMESTAMP as created_at,
re.list_price_uid,
re.contract_price_uid,
re.product_uid,
re.contract_id,
re.invoice_delivery,
re.started_at,
re.ended_at,
re.effective_at,
re.ineffective_at,
(CASE
WHEN _BF_RATING_AS_OF_DT >= re.ended_at
THEN 'FINALIZED'
ELSE 'DRAFT'
END) AS status,
SUM(re.quantity) as quantity,
round(SUM(CAST(COALESCE(re.amount, 0) AS decimal)), 2) AS amount
FROM bframe.tiered_rated_events AS re
GROUP BY ALL
SELECT *
FROM bframe.tiered_event_line_items
WHERE contract_id = 'Xenophon_contract'
ORDER BY started_at, list_price_uid
LIMIT 5;
org_id |
env_id |
branch_id |
created_at |
list_price_uid |
contract_price_uid |
product_uid |
contract_id |
invoice_delivery |
started_at |
ended_at |
effective_at |
ineffective_at |
status |
quantity |
amount |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 |
2 |
1 |
2025-01-16 12:21:39.376000-08:00 |
2 |
2 |
Xenophon_contract |
ARREARS |
2024-05-01 |
2024-06-01 |
2024-05-01 |
2024-06-01 |
DRAFT |
2000.0 |
0.0 |
|
2 |
2 |
1 |
2025-01-16 12:21:39.376000-08:00 |
100 |
2 |
Xenophon_contract |
ARREARS |
2024-05-01 |
2024-06-01 |
2024-05-01 |
2024-06-01 |
DRAFT |
1712.0 |
85.6 |
Modify line items¶
To fully integrate the tiered pricing into bframe the UDVs must be included in the top level default views. In this case that means appending bframe.tiered_line_items
along with other line item types within bframe.line_items
.
SELECT *
FROM bframe.fixed_line_items
UNION
SELECT *
FROM bframe.event_line_items
UNION
SELECT *
FROM bframe.tiered_event_line_items
SELECT *
FROM bframe.line_items
WHERE contract_id = 'Xenophon_contract'
ORDER BY started_at, list_price_uid
LIMIT 5;
org_id |
env_id |
branch_id |
created_at |
list_price_uid |
contract_price_uid |
product_uid |
contract_id |
invoice_delivery |
started_at |
ended_at |
effective_at |
ineffective_at |
status |
quantity |
amount |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2 |
2 |
1 |
2025-01-16 12:21:42.400000-08:00 |
1 |
1 |
Xenophon_contract |
ARREARS |
2024-05-01 |
2024-06-01 |
2024-05-01 |
2024-06-01 |
DRAFT |
58.0 |
5.8 |
|
2 |
2 |
1 |
2025-01-16 12:21:42.400000-08:00 |
2 |
2 |
Xenophon_contract |
ARREARS |
2024-05-01 |
2024-06-01 |
2024-05-01 |
2024-06-01 |
DRAFT |
2000.0 |
0.0 |
|
2 |
2 |
1 |
2025-01-16 12:21:42.400000-08:00 |
100 |
2 |
Xenophon_contract |
ARREARS |
2024-05-01 |
2024-06-01 |
2024-05-01 |
2024-06-01 |
DRAFT |
1712.0 |
85.6 |
Exploring the change¶
The free tier is live and the impact can be seen across customers. Let’s see what it looks like for one of our larger customers.
products.id |
products.name |
line_items.quantity |
line_items.amount |
invoices.total |
invoices.invoice_delivery |
invoices.status |
invoices.contract_id |
invoices.started_at |
invoices.ended_at |
---|---|---|---|---|---|---|---|---|---|
1 |
Updates |
1162.0 |
116.2 |
23403.8 |
ARREARS |
DRAFT |
Pikiwikisrael_contract |
2024-05-01 |
2024-06-01 |
2 |
Creates (w/ Free tier) |
2000.0 |
0.0 |
23403.8 |
ARREARS |
DRAFT |
Pikiwikisrael_contract |
2024-05-01 |
2024-06-01 |
2 |
Creates (w/ Free tier) |
465752.0 |
23287.6 |
23403.8 |
ARREARS |
DRAFT |
Pikiwikisrael_contract |
2024-05-01 |
2024-06-01 |
SELECT
prod.id as 'products.id',
prod.name as 'products.name',
li.quantity as 'line_items.quantity',
li.amount as 'line_items.amount',
i.total as 'invoices.total',
i.invoice_delivery as 'invoices.invoice_delivery',
i.status as 'invoices.status',
i.contract_id as 'invoices.contract_id',
i.started_at as 'invoices.started_at',
i.ended_at as 'invoices.ended_at'
FROM bframe.invoices AS i
JOIN bframe.line_items AS li
ON i.contract_id = li.contract_id
AND i.ended_at = li.ended_at
AND i.started_at = li.started_at
AND i.invoice_delivery = li.invoice_delivery
JOIN bframe.products as prod
ON li.product_uid = prod.id
WHERE i.contract_id = 'Pikiwikisrael_contract'
ORDER BY li.started_at, li.list_price_uid;
As expected only $100 of savings were accomplished which is only a small part of their total cost. To ensure Wikipedia isn’t losing out too much we can do a query to see the impact to the bottom line.
list_price_uid |
product_uid |
actual_amt |
no_free_tier_amt |
---|---|---|---|
2 |
2 |
0.0 |
242564.0 |
100 |
2 |
1523350.8 |
1523350.8 |
SELECT
li.list_price_uid,
li.product_uid,
ROUND(SUM(li.amount), 2) AS actual_amt,
ROUND(SUM(li.quantity * 0.05), 2) AS no_free_tier_amt
FROM bframe.line_items AS li
WHERE li.product_uid = 2
GROUP BY 1, 2
ORDER BY li.list_price_uid
The relative difference between a free tier and without one is about 14%. This outcome is much larger than the exective team expected when they first proposed the change. Due to this result, the change isn’t rolled out to production and a free tier is no longer in the strategy for Wikipedia.