Add "free_allowance_units" to service usage APIs

This represents the number of chargeable_units that were actually
free due to the free allowance - they won't be included in "cost".

Although the existing calculations in Admin [^1][^2] will still be
correct with a change in SMS rates - it's cost that's the problem
- it makes sense to have all the knowledge about calculating usage
consistently in these two APIs.

Note that the Integer casting is covered by the API-level tests in
test_rest.

[^1]: 474d7dfda8/app/main/views/dashboard.py (L490)
[^2]: c63660d56d/app/main/views/dashboard.py (L350)
This commit is contained in:
Ben Thorner
2022-04-21 12:55:30 +01:00
parent cd84928a1e
commit 555868c442
4 changed files with 35 additions and 3 deletions

View File

@@ -24,6 +24,7 @@ def serialize_ft_billing_remove_emails(rows):
"rate": float(row.rate),
"postage": row.postage,
"cost": float(row.cost),
"free_allowance_used": row.free_allowance_used,
}
for row in rows
if row.notification_type != 'email'
@@ -42,6 +43,7 @@ def serialize_ft_billing_yearly_totals(rows):
# TEMPORARY: while we migrate to "cost" in the Admin app
"letter_total": float(row.billable_units * row.rate) if row.notification_type == 'letter' else 0,
"cost": float(row.cost),
"free_allowance_used": row.free_allowance_used,
}
for row in rows
]

View File

@@ -224,6 +224,7 @@ def fetch_billing_totals_for_year(service_id, year):
func.sum(query.c.billable_units).label("billable_units"),
func.sum(query.c.chargeable_units).label("chargeable_units"),
func.sum(query.c.cost).label("cost"),
func.sum(query.c.free_allowance_used).label("free_allowance_used"),
).group_by(
query.c.rate,
query.c.notification_type
@@ -282,6 +283,7 @@ def fetch_monthly_billing_for_year(service_id, year):
func.sum(query.c.billable_units).label("billable_units"),
func.sum(query.c.chargeable_units).label("chargeable_units"),
func.sum(query.c.cost).label("cost"),
func.sum(query.c.free_allowance_used).label("free_allowance_used"),
).group_by(
query.c.rate,
query.c.notification_type,
@@ -314,6 +316,7 @@ def query_service_email_usage_for_year(service_id, year):
FactBilling.rate,
FactBilling.notification_type,
literal(0).label("cost"),
literal(0).label("free_allowance_used"),
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
@@ -338,6 +341,7 @@ def query_service_letter_usage_for_year(service_id, year):
FactBilling.rate,
FactBilling.notification_type,
(FactBilling.notifications_sent * FactBilling.rate).label("cost"),
literal(0).label("free_allowance_used"),
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
@@ -380,14 +384,16 @@ def query_service_sms_usage_for_year(service_id, year):
chargeable_units = FactBilling.billable_units * FactBilling.rate_multiplier
# Subquery for the number of chargeable units in all rows preceding this one,
# which might be none if this is the first row (hence the "coalesce").
# which might be none if this is the first row (hence the "coalesce"). For
# some reason the end result is a decimal despite all the input columns being
# integer - this seems to be a Sqlalchemy quirk (works in raw SQL).
cumulative_chargeable_units = func.coalesce(
func.sum(chargeable_units).over(
# order is "ASC" by default
order_by=[FactBilling.bst_date],
# first row to previous row
rows=(None, -1)
),
).cast(Integer),
0
)
@@ -402,6 +408,8 @@ def query_service_sms_usage_for_year(service_id, year):
# for, after taking any remaining free allowance into account.
charged_units = func.greatest(chargeable_units - cumulative_free_remainder, 0)
free_allowance_used = func.least(cumulative_free_remainder, chargeable_units)
return db.session.query(
FactBilling.bst_date,
FactBilling.postage, # should always be "none"
@@ -411,7 +419,8 @@ def query_service_sms_usage_for_year(service_id, year):
chargeable_units.label("chargeable_units"),
FactBilling.rate,
FactBilling.notification_type,
(charged_units * FactBilling.rate).label("cost")
(charged_units * FactBilling.rate).label("cost"),
free_allowance_used.label("free_allowance_used"),
).join(
AnnualBilling,
AnnualBilling.service_id == service_id

View File

@@ -177,6 +177,7 @@ def test_get_yearly_usage_by_monthly_from_ft_billing(admin_request, notify_db_se
assert letter_row["rate"] == 0.33
assert letter_row["postage"] == "second"
assert letter_row["cost"] == 9.9
assert letter_row["free_allowance_used"] == 0
assert sms_row["month"] == "April"
assert sms_row["notification_type"] == "sms"
@@ -187,6 +188,7 @@ def test_get_yearly_usage_by_monthly_from_ft_billing(admin_request, notify_db_se
assert sms_row["postage"] == "none"
# free allowance is 4, so (30 - 4) * 0.162
assert sms_row["cost"] == 4.212
assert sms_row["free_allowance_used"] == 4
def set_up_yearly_data():
@@ -260,6 +262,7 @@ def test_get_yearly_billing_usage_summary_from_ft_billing(admin_request, notify_
assert json_response[0]['rate'] == 0
assert json_response[0]['letter_total'] == 0
assert json_response[0]['cost'] == 0
assert json_response[0]['free_allowance_used'] == 0
assert json_response[1]['notification_type'] == 'letter'
assert json_response[1]['billing_units'] == 275
@@ -268,6 +271,7 @@ def test_get_yearly_billing_usage_summary_from_ft_billing(admin_request, notify_
assert json_response[1]['rate'] == 0.33
assert json_response[1]['letter_total'] == 90.75
assert json_response[1]['cost'] == 90.75
assert json_response[1]['free_allowance_used'] == 0
assert json_response[2]['notification_type'] == 'sms'
assert json_response[2]['billing_units'] == 825
@@ -276,3 +280,4 @@ def test_get_yearly_billing_usage_summary_from_ft_billing(admin_request, notify_
assert json_response[2]['rate'] == 0.0162
assert json_response[2]['letter_total'] == 0
assert json_response[2]['cost'] == 13.3002
assert json_response[2]['free_allowance_used'] == 4

View File

@@ -438,6 +438,7 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
assert results[0].chargeable_units == 0
assert results[0].rate == Decimal('0')
assert results[0].cost == Decimal('0')
assert results[0].free_allowance_used == 0
assert str(results[1].month) == "2016-04-01"
assert results[1].notification_type == 'letter'
@@ -446,6 +447,7 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
assert results[1].chargeable_units == 30
assert results[1].rate == Decimal('0.30')
assert results[1].cost == Decimal('9')
assert results[1].free_allowance_used == 0
assert str(results[1].month) == "2016-04-01"
assert results[2].notification_type == 'letter'
@@ -454,6 +456,7 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
assert results[2].chargeable_units == 30
assert results[2].rate == Decimal('0.33')
assert results[2].cost == Decimal('9.9')
assert results[2].free_allowance_used == 0
assert str(results[3].month) == "2016-04-01"
assert results[3].notification_type == 'sms'
@@ -463,6 +466,7 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
assert results[3].rate == Decimal('0.162')
# free allowance is 10, so (30 - 10) * 0.162
assert results[3].cost == Decimal('3.24')
assert results[3].free_allowance_used == 10
assert str(results[4].month) == "2016-05-01"
assert str(results[47].month) == "2017-03-01"
@@ -483,6 +487,7 @@ def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
assert results[0].chargeable_units == 1
assert results[0].rate == Decimal('0.33')
assert results[0].cost == Decimal('0.33')
assert results[0].free_allowance_used == 0
assert str(results[1].month) == "2018-05-01"
assert results[1].notification_type == 'letter'
@@ -491,6 +496,7 @@ def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
assert results[1].chargeable_units == 2
assert results[1].rate == Decimal('0.36')
assert results[1].cost == Decimal('0.72')
assert results[1].free_allowance_used == 0
assert str(results[2].month) == "2018-05-01"
assert results[2].notification_type == 'sms'
@@ -500,6 +506,7 @@ def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
assert results[2].rate == Decimal('0.015')
# 1 free units on the 17th
assert results[2].cost == Decimal('0.045')
assert results[2].free_allowance_used == 1
assert str(results[3].month) == "2018-05-01"
assert results[3].notification_type == 'sms'
@@ -509,6 +516,7 @@ def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
assert results[3].rate == Decimal('0.162')
# 5 free units on the 16th
assert results[3].cost == Decimal('0')
assert results[3].free_allowance_used == 5
@freeze_time('2018-08-01 13:30:00')
@@ -543,6 +551,7 @@ def test_fetch_billing_totals_for_year(notify_db_session):
assert results[0].chargeable_units == 0
assert results[0].rate == Decimal('0')
assert results[0].cost == Decimal('0')
assert results[0].free_allowance_used == 0
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 365
@@ -550,6 +559,7 @@ def test_fetch_billing_totals_for_year(notify_db_session):
assert results[1].chargeable_units == 365
assert results[1].rate == Decimal('0.3')
assert results[1].cost == Decimal('109.5')
assert results[1].free_allowance_used == 0
assert results[2].notification_type == 'letter'
assert results[2].notifications_sent == 365
@@ -557,6 +567,7 @@ def test_fetch_billing_totals_for_year(notify_db_session):
assert results[2].chargeable_units == 365
assert results[2].rate == Decimal('0.33')
assert results[2].cost == Decimal('120.45')
assert results[2].free_allowance_used == 0
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 365
@@ -564,6 +575,7 @@ def test_fetch_billing_totals_for_year(notify_db_session):
assert results[3].chargeable_units == 365
assert results[3].rate == Decimal('0.162')
assert results[3].cost == Decimal('0')
assert results[3].free_allowance_used == 365
def test_fetch_billing_totals_for_year_uses_current_annual_billing(notify_db_session):
@@ -593,6 +605,7 @@ def test_fetch_billing_totals_for_year_variable_rates(notify_db_session):
assert results[0].chargeable_units == 1
assert results[0].rate == Decimal('0.33')
assert results[0].cost == Decimal('0.33')
assert results[0].free_allowance_used == 0
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 2
@@ -600,6 +613,7 @@ def test_fetch_billing_totals_for_year_variable_rates(notify_db_session):
assert results[1].chargeable_units == 2
assert results[1].rate == Decimal('0.36')
assert results[1].cost == Decimal('0.72')
assert results[1].free_allowance_used == 0
assert results[2].notification_type == 'sms'
assert results[2].notifications_sent == 1
@@ -608,6 +622,7 @@ def test_fetch_billing_totals_for_year_variable_rates(notify_db_session):
assert results[2].rate == Decimal('0.015')
# 1 free unit on the 17th
assert results[2].cost == Decimal('0.045')
assert results[2].free_allowance_used == 1
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 2
@@ -616,6 +631,7 @@ def test_fetch_billing_totals_for_year_variable_rates(notify_db_session):
assert results[3].rate == Decimal('0.162')
# 5 free units on the 16th
assert results[3].cost == Decimal('0')
assert results[3].free_allowance_used == 5
def test_delete_billing_data(notify_db_session):