diff --git a/app/billing/billing_schemas.py b/app/billing/billing_schemas.py index d437ff616..1c1f6e8ee 100644 --- a/app/billing/billing_schemas.py +++ b/app/billing/billing_schemas.py @@ -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 ] diff --git a/app/dao/fact_billing_dao.py b/app/dao/fact_billing_dao.py index 131e53b35..0ad1b0334 100644 --- a/app/dao/fact_billing_dao.py +++ b/app/dao/fact_billing_dao.py @@ -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 diff --git a/tests/app/billing/test_rest.py b/tests/app/billing/test_rest.py index 51c40ef3d..8dad33ed0 100644 --- a/tests/app/billing/test_rest.py +++ b/tests/app/billing/test_rest.py @@ -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 diff --git a/tests/app/dao/test_ft_billing_dao.py b/tests/app/dao/test_ft_billing_dao.py index f11632fec..2b515e3e9 100644 --- a/tests/app/dao/test_ft_billing_dao.py +++ b/tests/app/dao/test_ft_billing_dao.py @@ -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):