Compare commits

...

8 Commits

Author SHA1 Message Date
Ben Thorner
99b2b4642e Fix incorrect chargeable_units column
This was added to migrate away from the vague "billing_units" field,
but without fixing the inconsistent data behind it:

- For emails and letters, "billing_units" was just the number sent.

- For SMS, "billing_units" really was the chargeable_units.

To avoid confusion we need two fields to represent the original mix
of data - this exposes "notifications_sent" in both APIs.
2022-04-26 18:18:36 +01:00
Ben Thorner
2bdaeabbaa Add "charged_units" to usage APIs 2022-04-26 17:56:17 +01:00
Ben Thorner
2999fa6714 Add "free_chargeable_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.

[^1]: 474d7dfda8/app/main/views/dashboard.py (L490)
[^2]: c63660d56d/app/main/views/dashboard.py (L350)
2022-04-26 13:24:17 +01:00
Ben Thorner
ff32000180 Add "cost" field to monthly usage API
This starts to replace the calculation in Admin [^1] and, similar
to the yearly API, also correctly attributes free allowance when
we have a rate change during a month.

[^1]: 474d7dfda8/app/templates/views/usage.html (L98)
2022-04-26 13:24:16 +01:00
Ben Thorner
e276e8a15c Use new functions for monthly usage API
This starts work towards replacing the manual free allowance and
cost calculations currently done in Admin.
2022-04-26 13:24:15 +01:00
Ben Thorner
106da583ea Add costs to each row in yearly usage API
This will replace the manual calculation in Admin [^1] for SMS and
also in API [^2] for letters.

Doing the calculation here also means we correctly attribute free
allowance to the earliest rows in the billing table - Admin doesn't
know when a given rate was applied so can't do this with the data
currently returned from the API.

Since the calculation now depends on annual billing, we need to
change all the tests to make sure a suitable row exists.

Note about "OVER" clause
========================

Using "rows=" ("ROWS BETWEEN") makes more sense than "range=" as
we want the remainder to be incremental within each group in a
"GROUP BY" clause, as well as between groups i.e

  # ROWS BETWEEN (arbitrary numbers to illustrate)
  date=2021-04-03, units=3, cost=3.29
  date=2021-04-03, units=2, cost=4.17
  date=2021-04-04, units=2, cost=5.10

  vs.

  # RANGE BETWEEN
  date=2021-04-03, units=3, cost=4.17
  date=2021-04-03, units=2, cost=4.17
  date=2021-04-04, units=2, cost=5.10

See [^3] for more details and examples.

[^1]: https://github.com/alphagov/notifications-admin/blob/master/app/templates/views/usage.html#L60
[^2]: 072c3b2079/app/billing/billing_schemas.py (L37)
[^3]: https://learnsql.com/blog/difference-between-rows-range-window-functions/
2022-04-26 13:24:14 +01:00
Ben Thorner
0af791e417 Prepare to switch to "chargeable_units" in API
This is so we can migrate from "billing_units" to this new field in
the Admin app, without breaking anything in between.
2022-04-26 13:24:13 +01:00
Ben Thorner
646de16ace Refactor yearly usage API into functions per type
This makes it easier to extend each function with costs and free
allowances - especially for SMS.

In each function I've started using the "chargeable" terminology,
which we should eventually change in the API.

I've chosen to duplicate the "WHERE" clause in each subquery vs.
the top-level query. This will make more sense in later commits
where we start adding free allowance calculations, which need to
be done on a yearly basis - knowledge the subqueries should have.
2022-04-26 13:24:12 +01:00
4 changed files with 296 additions and 89 deletions

View File

@@ -17,9 +17,15 @@ def serialize_ft_billing_remove_emails(rows):
{
"month": (datetime.strftime(row.month, "%B")),
"notification_type": row.notification_type,
# TEMPORARY: while we migrate to "chargeable_units" in the Admin app
"billing_units": row.billable_units,
"chargeable_units": row.chargeable_units,
"rate": float(row.rate),
"postage": row.postage,
"cost": float(row.cost),
"free_chargeable_units": row.free_chargeable_units,
"charged_units": row.charged_units,
"notifications_sent": row.notifications_sent,
}
for row in rows
if row.notification_type != 'email'
@@ -30,9 +36,16 @@ def serialize_ft_billing_yearly_totals(rows):
return [
{
"notification_type": row.notification_type,
# TEMPORARY: while we migrate to "chargeable_units" in the Admin app
"billing_units": row.billable_units,
"chargeable_units": row.chargeable_units,
"rate": float(row.rate),
# 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_chargeable_units": row.free_chargeable_units,
"charged_units": row.charged_units,
"notifications_sent": row.notifications_sent,
}
for row in rows
]

View File

@@ -2,7 +2,7 @@ from datetime import date, datetime, timedelta
from flask import current_app
from notifications_utils.timezones import convert_utc_to_bst
from sqlalchemy import Date, Integer, and_, desc, func
from sqlalchemy import Date, Integer, and_, desc, func, union
from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.sql.expression import case, literal
@@ -197,55 +197,36 @@ def fetch_letter_line_items_for_all_services(start_date, end_date):
def fetch_billing_totals_for_year(service_id, year):
year_start, year_end = get_financial_year_dates(year)
"""
Billing for email: only record the total number of emails.
Billing for letters: The billing units is used to fetch the correct rate for the sheet count of the letter.
Total cost is notifications_sent * rate.
Rate multiplier does not apply to email or letters.
"""
email_and_letters = db.session.query(
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
func.sum(FactBilling.notifications_sent).label("billable_units"),
FactBilling.rate.label('rate'),
FactBilling.notification_type.label('notification_type')
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type.in_([EMAIL_TYPE, LETTER_TYPE])
).group_by(
FactBilling.rate,
FactBilling.notification_type
)
"""
Billing for SMS using the billing_units * rate_multiplier. Billing unit of SMS is the fragment count of a message
"""
sms = db.session.query(
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label("billable_units"),
FactBilling.rate,
FactBilling.notification_type
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type == SMS_TYPE
).group_by(
FactBilling.rate,
FactBilling.notification_type
)
yearly_data = email_and_letters.union_all(sms).order_by(
'notification_type',
'rate'
return db.session.query(
union(*[
db.session.query(
func.sum(query.c.notifications_sent).label("notifications_sent"),
# TEMPORARY: while we switch to "chargeable units"
func.sum(query.c.billable_units).label("billable_units"),
func.sum(query.c.chargeable_units).label("chargeable_units"),
query.c.rate.label("rate"),
query.c.notification_type.label("notification_type"),
func.sum(query.c.cost).label("cost"),
func.sum(query.c.free_chargeable_units).label("free_chargeable_units"),
func.sum(query.c.charged_units).label("charged_units"),
).group_by(
query.c.rate,
query.c.notification_type
)
for query in [
query_service_sms_usage_for_year(service_id, year).subquery(),
query_service_email_usage_for_year(service_id, year).subquery(),
query_service_letter_usage_for_year(service_id, year).subquery(),
]
]).subquery()
).order_by(
"notification_type",
"rate",
).all()
return yearly_data
def fetch_monthly_billing_for_year(service_id, year):
year_start, year_end = get_financial_year_dates(year)
_, year_end = get_financial_year_dates(year)
today = convert_utc_to_bst(datetime.utcnow()).date()
# if year end date is less than today, we are calculating for data in the past and have no need for deltas.
@@ -254,51 +235,141 @@ def fetch_monthly_billing_for_year(service_id, year):
for d in data:
update_fact_billing(data=d, process_day=today)
email_and_letters = db.session.query(
func.date_trunc('month', FactBilling.bst_date).cast(Date).label("month"),
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
func.sum(FactBilling.notifications_sent).label("billable_units"),
FactBilling.rate.label('rate'),
FactBilling.notification_type.label('notification_type'),
FactBilling.postage
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type.in_([EMAIL_TYPE, LETTER_TYPE])
).group_by(
'month',
FactBilling.rate,
FactBilling.notification_type,
FactBilling.postage
)
sms = db.session.query(
func.date_trunc('month', FactBilling.bst_date).cast(Date).label("month"),
func.sum(FactBilling.notifications_sent).label("notifications_sent"),
func.sum(FactBilling.billable_units * FactBilling.rate_multiplier).label("billable_units"),
FactBilling.rate,
FactBilling.notification_type,
FactBilling.postage
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type == SMS_TYPE
).group_by(
'month',
FactBilling.rate,
FactBilling.notification_type,
FactBilling.postage
)
yearly_data = email_and_letters.union_all(sms).order_by(
'month',
'notification_type',
'rate'
return db.session.query(
union(*[
db.session.query(
func.date_trunc('month', query.c.bst_date).cast(Date).label("month"),
func.sum(query.c.notifications_sent).label("notifications_sent"),
# TEMPORARY: while we switch to "chargeable units"
func.sum(query.c.billable_units).label("billable_units"),
func.sum(query.c.chargeable_units).label("chargeable_units"),
query.c.rate.label("rate"),
query.c.postage.label("postage"),
query.c.notification_type.label("notification_type"),
func.sum(query.c.cost).label("cost"),
func.sum(query.c.free_chargeable_units).label("free_chargeable_units"),
func.sum(query.c.charged_units).label("charged_units"),
).group_by(
query.c.rate,
query.c.notification_type,
query.c.postage,
'month',
)
for query in [
query_service_sms_usage_for_year(service_id, year).subquery(),
query_service_email_usage_for_year(service_id, year).subquery(),
query_service_letter_usage_for_year(service_id, year).subquery(),
]
]).subquery()
).order_by(
"month",
"notification_type",
"rate",
).all()
return yearly_data
def query_service_email_usage_for_year(service_id, year):
year_start, year_end = get_financial_year_dates(year)
return db.session.query(
FactBilling.bst_date,
FactBilling.postage, # should always be "none"
FactBilling.notifications_sent,
# TEMPORARY: while we switch to "chargeable units"
FactBilling.notifications_sent.label("billable_units"),
FactBilling.billable_units.label("chargeable_units"),
FactBilling.rate,
FactBilling.notification_type,
FactBilling.notifications_sent.label("charged_units"),
literal(0).label("free_chargeable_units"),
literal(0).label("cost"),
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type == EMAIL_TYPE
)
def query_service_letter_usage_for_year(service_id, year):
year_start, year_end = get_financial_year_dates(year)
return db.session.query(
FactBilling.bst_date,
FactBilling.postage,
FactBilling.notifications_sent,
# TEMPORARY: while we switch to "chargeable units"
FactBilling.notifications_sent.label("billable_units"),
FactBilling.billable_units.label("chargeable_units"),
FactBilling.rate,
FactBilling.notification_type,
FactBilling.notifications_sent.label("charged_units"),
literal(0).label("free_chargeable_units"),
(FactBilling.notifications_sent * FactBilling.rate).label("cost"),
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type == LETTER_TYPE
)
def query_service_sms_usage_for_year(service_id, year):
year_start, year_end = get_financial_year_dates(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").
cumulative_chargeable_units = func.coalesce(
func.sum(chargeable_units).over(
order_by=[
FactBilling.bst_date, # order is "ASC" by default
FactBilling.rate # ensures test stability for rows on the same day
],
rows=(None, -1) # ROWS BETWEEN UNBOUNDED PRECEDING AND 1 ROW PRECEDING
),
literal(0)
)
# Subquery for how much free allowance we have left before the current row,
# so we can work out the cost for this row after taking it into account.
cumulative_free_remainder = func.greatest(
AnnualBilling.free_sms_fragment_limit - cumulative_chargeable_units,
0
)
charged_units = func.greatest(
chargeable_units - cumulative_free_remainder,
literal(0)
).cast(Integer) # for some reason the result is a String!
free_chargeable_units = func.least(
cumulative_free_remainder,
chargeable_units,
).cast(Integer) # for some reason the result is a Decimal
return db.session.query(
FactBilling.bst_date,
FactBilling.postage, # should always be "none"
FactBilling.notifications_sent,
# TEMPORARY: while we switch to "chargeable units"
chargeable_units.label("billable_units"),
chargeable_units.label("chargeable_units"),
FactBilling.rate,
FactBilling.notification_type,
charged_units.label("charged_units"),
free_chargeable_units.label("free_chargeable_units"),
(charged_units * FactBilling.rate).label("cost")
).outerjoin(
AnnualBilling,
AnnualBilling.service_id == service_id
).filter(
FactBilling.service_id == service_id,
FactBilling.bst_date >= year_start,
FactBilling.bst_date <= year_end,
FactBilling.notification_type == SMS_TYPE,
AnnualBilling.financial_year_start == year,
)
def delete_billing_data_for_service_for_day(process_day, service_id):

View File

@@ -147,6 +147,8 @@ def set_up_monthly_data():
billable_unit=1,
rate=0.33,
postage='second')
create_annual_billing(service_id=service.id, free_sms_fragment_limit=4, financial_year_start=2016)
return service
@@ -170,14 +172,25 @@ def test_get_yearly_usage_by_monthly_from_ft_billing(admin_request, notify_db_se
assert letter_row["month"] == "April"
assert letter_row["notification_type"] == "letter"
assert letter_row["billing_units"] == 30
assert letter_row["chargeable_units"] == 30
assert letter_row["rate"] == 0.33
assert letter_row["postage"] == "second"
assert letter_row["cost"] == 9.9
assert letter_row["free_chargeable_units"] == 0
assert letter_row["charged_units"] == 30
assert letter_row["notifications_sent"] == 30
assert sms_row["month"] == "April"
assert sms_row["notification_type"] == "sms"
assert sms_row["billing_units"] == 30
assert sms_row["chargeable_units"] == 30
assert sms_row["rate"] == 0.162
assert sms_row["postage"] == "none"
# free allowance is 4, so (30 - 4) * 0.162
assert sms_row["cost"] == 4.212
assert sms_row["free_chargeable_units"] == 4
assert sms_row["charged_units"] == 26
assert sms_row["notifications_sent"] == 30
def set_up_yearly_data():
@@ -185,6 +198,7 @@ def set_up_yearly_data():
sms_template = create_template(service=service, template_type="sms")
email_template = create_template(service=service, template_type="email")
letter_template = create_template(service=service, template_type="letter")
for month in range(1, 13):
mon = str(month).zfill(2)
for day in range(1, monthrange(2016, month)[1] + 1):
@@ -205,6 +219,8 @@ def set_up_yearly_data():
rate=0.33,
postage='second')
start_date, end_date = get_month_start_and_end_date_in_utc(datetime(2016, int(mon), 1))
create_annual_billing(service_id=service.id, free_sms_fragment_limit=4, financial_year_start=2016)
return service
@@ -240,15 +256,33 @@ def test_get_yearly_billing_usage_summary_from_ft_billing(admin_request, notify_
)
assert len(json_response) == 3
assert json_response[0]['notification_type'] == 'email'
assert json_response[0]['billing_units'] == 275
assert json_response[0]['chargeable_units'] == 275
assert json_response[0]['rate'] == 0
assert json_response[0]['letter_total'] == 0
assert json_response[0]['cost'] == 0
assert json_response[0]['free_chargeable_units'] == 0
assert json_response[0]['charged_units'] == 275
assert json_response[0]['notifications_sent'] == 275
assert json_response[1]['notification_type'] == 'letter'
assert json_response[1]['billing_units'] == 275
assert json_response[1]['chargeable_units'] == 275
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_chargeable_units'] == 0
assert json_response[1]['charged_units'] == 275
assert json_response[1]['notifications_sent'] == 275
assert json_response[2]['notification_type'] == 'sms'
assert json_response[2]['billing_units'] == 825
assert json_response[2]['chargeable_units'] == 825
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_chargeable_units'] == 4
assert json_response[2]['charged_units'] == 821
assert json_response[2]['notifications_sent'] == 825

View File

@@ -417,6 +417,7 @@ def test_get_rate_for_letters_when_page_count_is_zero(notify_db_session):
def test_fetch_monthly_billing_for_year(notify_db_session):
service = set_up_yearly_data()
create_annual_billing(service_id=service.id, free_sms_fragment_limit=10, financial_year_start=2016)
results = fetch_monthly_billing_for_year(service.id, 2016)
assert len(results) == 48
@@ -425,25 +426,42 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
assert results[0].notification_type == 'email'
assert results[0].notifications_sent == 30
assert results[0].billable_units == 30
assert results[0].chargeable_units == 30
assert results[0].rate == Decimal('0')
assert results[0].cost == Decimal('0')
assert results[0].free_chargeable_units == 0
assert results[0].charged_units == 30
assert str(results[1].month) == "2016-04-01"
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 30
assert results[1].billable_units == 30
assert results[1].chargeable_units == 30
assert results[1].rate == Decimal('0.30')
assert results[1].cost == Decimal('9')
assert results[1].free_chargeable_units == 0
assert results[1].charged_units == 30
assert str(results[1].month) == "2016-04-01"
assert results[2].notification_type == 'letter'
assert results[2].notifications_sent == 30
assert results[2].billable_units == 30
assert results[2].chargeable_units == 30
assert results[2].rate == Decimal('0.33')
assert results[2].cost == Decimal('9.9')
assert results[2].free_chargeable_units == 0
assert results[2].charged_units == 30
assert str(results[3].month) == "2016-04-01"
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 30
assert results[3].billable_units == 30
assert results[3].chargeable_units == 30
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_chargeable_units == 10
assert results[3].charged_units == 20
assert str(results[4].month) == "2016-05-01"
assert str(results[47].month) == "2017-03-01"
@@ -451,6 +469,7 @@ def test_fetch_monthly_billing_for_year(notify_db_session):
def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
service = set_up_yearly_data_variable_rates()
create_annual_billing(service_id=service.id, free_sms_fragment_limit=6, financial_year_start=2018)
results = fetch_monthly_billing_for_year(service.id, 2018)
# Test data is only for the month of May
@@ -460,25 +479,43 @@ def test_fetch_monthly_billing_for_year_variable_rates(notify_db_session):
assert results[0].notification_type == 'letter'
assert results[0].notifications_sent == 1
assert results[0].billable_units == 1
assert results[0].chargeable_units == 1
assert results[0].rate == Decimal('0.33')
assert results[0].cost == Decimal('0.33')
assert results[0].free_chargeable_units == 0
assert results[0].charged_units == 1
assert str(results[1].month) == "2018-05-01"
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 1
assert results[1].billable_units == 1
assert results[1].chargeable_units == 2
assert results[1].rate == Decimal('0.36')
assert results[1].cost == Decimal('0.36')
assert results[1].free_chargeable_units == 0
assert results[1].charged_units == 1
assert str(results[2].month) == "2018-05-01"
assert results[2].notification_type == 'sms'
assert results[2].notifications_sent == 1
assert results[2].billable_units == 4
assert results[2].chargeable_units == 4
assert results[2].rate == Decimal('0.015')
# 4 free units sent on the 16th, 0 on the 17th
assert results[2].cost == Decimal('0')
assert results[2].free_chargeable_units == 4
assert results[2].charged_units == 0
assert str(results[3].month) == "2018-05-01"
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 2
assert results[3].billable_units == 5
assert results[3].chargeable_units == 5
assert results[3].rate == Decimal('0.162')
# 1 free unit on the 16th, 1 on the 17th (+ 3 paid)
assert results[3].cost == Decimal('0.486')
assert results[3].free_chargeable_units == 2
assert results[3].charged_units == 3
@freeze_time('2018-08-01 13:30:00')
@@ -487,6 +524,7 @@ def test_fetch_monthly_billing_for_year_adds_data_for_today(notify_db_session):
template = create_template(service=service, template_type="sms")
create_rate(start_date=datetime.utcnow() - timedelta(days=1), value=0.158, notification_type='sms')
create_annual_billing(service_id=service.id, free_sms_fragment_limit=1000, financial_year_start=2018)
for i in range(1, 32):
create_ft_billing(bst_date='2018-07-{}'.format(i), template=template)
@@ -502,54 +540,105 @@ def test_fetch_monthly_billing_for_year_adds_data_for_today(notify_db_session):
def test_fetch_billing_totals_for_year(notify_db_session):
service = set_up_yearly_data()
create_annual_billing(service_id=service.id, free_sms_fragment_limit=1000, financial_year_start=2016)
results = fetch_billing_totals_for_year(service_id=service.id, year=2016)
assert len(results) == 4
assert results[0].notification_type == 'email'
assert results[0].notifications_sent == 365
assert results[0].billable_units == 365
assert results[0].chargeable_units == 365
assert results[0].rate == Decimal('0')
assert results[0].cost == Decimal('0')
assert results[0].free_chargeable_units == 0
assert results[0].charged_units == 365
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 365
assert results[1].billable_units == 365
assert results[1].chargeable_units == 365
assert results[1].rate == Decimal('0.3')
assert results[1].cost == Decimal('109.5')
assert results[1].free_chargeable_units == 0
assert results[1].charged_units == 365
assert results[2].notification_type == 'letter'
assert results[2].notifications_sent == 365
assert results[2].billable_units == 365
assert results[2].chargeable_units == 365
assert results[2].rate == Decimal('0.33')
assert results[2].cost == Decimal('120.45')
assert results[2].free_chargeable_units == 0
assert results[2].charged_units == 365
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 365
assert results[3].billable_units == 365
assert results[3].chargeable_units == 365
assert results[3].rate == Decimal('0.162')
assert results[3].cost == Decimal('0')
assert results[3].free_chargeable_units == 365
assert results[3].charged_units == 0
def test_fetch_billing_totals_for_year_uses_current_annual_billing(notify_db_session):
service = set_up_yearly_data()
create_annual_billing(service_id=service.id, free_sms_fragment_limit=400, financial_year_start=2015)
create_annual_billing(service_id=service.id, free_sms_fragment_limit=0, financial_year_start=2016)
result = next(
result for result in
fetch_billing_totals_for_year(service_id=service.id, year=2016)
if result.notification_type == 'sms'
)
assert result.chargeable_units == 365
assert result.cost > 0
def test_fetch_billing_totals_for_year_variable_rates(notify_db_session):
service = set_up_yearly_data_variable_rates()
create_annual_billing(service_id=service.id, free_sms_fragment_limit=6, financial_year_start=2018)
results = fetch_billing_totals_for_year(service_id=service.id, year=2018)
assert len(results) == 4
assert results[0].notification_type == 'letter'
assert results[0].notifications_sent == 1
assert results[0].billable_units == 1
assert results[0].chargeable_units == 1
assert results[0].rate == Decimal('0.33')
assert results[0].cost == Decimal('0.33')
assert results[0].free_chargeable_units == 0
assert results[0].charged_units == 1
assert results[1].notification_type == 'letter'
assert results[1].notifications_sent == 1
assert results[1].billable_units == 1
assert results[1].chargeable_units == 2
assert results[1].rate == Decimal('0.36')
assert results[1].cost == Decimal('0.36')
assert results[1].free_chargeable_units == 0
assert results[1].charged_units == 1
assert results[2].notification_type == 'sms'
assert results[2].notifications_sent == 1
assert results[2].billable_units == 4
assert results[2].chargeable_units == 4
assert results[2].rate == Decimal('0.015')
# 4 units sent on the 16th, 0 on the 17th
assert results[2].cost == Decimal('0')
assert results[2].free_chargeable_units == 4
assert results[2].charged_units == 0
assert results[3].notification_type == 'sms'
assert results[3].notifications_sent == 2
assert results[3].billable_units == 5
assert results[3].chargeable_units == 5
assert results[3].rate == Decimal('0.162')
# 1 free unit on the 16th, 1 on the 17th (+ 3 paid)
assert results[3].cost == Decimal('0.486') # (5 - 2) * 0.162
assert results[3].free_chargeable_units == 2
assert results[3].charged_units == 3
def test_delete_billing_data(notify_db_session):