diff --git a/app/dao/fact_billing_dao.py b/app/dao/fact_billing_dao.py index f31da2d9d..ac2ef4103 100644 --- a/app/dao/fact_billing_dao.py +++ b/app/dao/fact_billing_dao.py @@ -720,52 +720,42 @@ def fetch_email_usage_for_organisation(organisation_id, start_date, end_date): return query.all() -def fetch_sms_billing_for_organisation(organisation_id, start_date, end_date): +def fetch_sms_billing_for_organisation(organisation_id, financial_year): # ASSUMPTION: AnnualBilling has been populated for year. - allowance_left_at_start_date_query = fetch_sms_free_allowance_remainder_until_date(start_date).subquery() + ft_billing_subquery = query_organisation_sms_usage_for_year(organisation_id, financial_year).subquery() - sms_billable_units = func.coalesce(func.sum(FactBilling.billable_units * FactBilling.rate_multiplier), 0) + sms_billable_units = func.sum(func.coalesce(ft_billing_subquery.c.chargeable_units, 0)) # subtract sms_billable_units units accrued since report's start date to get up-to-date # allowance remainder - sms_allowance_left = func.greatest(allowance_left_at_start_date_query.c.sms_remainder - sms_billable_units, 0) + sms_allowance_left = func.greatest(AnnualBilling.free_sms_fragment_limit - sms_billable_units, 0) - # billable units here are for period between start date and end date only, so to see - # how many are chargeable, we need to see how much free allowance was used up in the - # period up until report's start date and then do a subtraction - chargeable_sms = func.greatest(sms_billable_units - allowance_left_at_start_date_query.c.sms_remainder, 0) - sms_cost = chargeable_sms * FactBilling.rate + chargeable_sms = func.sum(ft_billing_subquery.c.charged_units) + sms_cost = func.sum(ft_billing_subquery.c.cost) query = db.session.query( Service.name.label("service_name"), Service.id.label("service_id"), - func.coalesce(allowance_left_at_start_date_query.c.free_sms_fragment_limit, 0).label('free_sms_fragment_limit'), - func.coalesce(FactBilling.rate, 0).label('sms_rate'), + AnnualBilling.free_sms_fragment_limit, func.coalesce(sms_allowance_left, 0).label("sms_remainder"), func.coalesce(sms_billable_units, 0).label('sms_billable_units'), func.coalesce(chargeable_sms, 0).label("chargeable_billable_sms"), func.coalesce(sms_cost, 0).label('sms_cost'), - Service.active.label("active") + Service.active ).select_from( Service ).outerjoin( - allowance_left_at_start_date_query, Service.id == allowance_left_at_start_date_query.c.service_id + AnnualBilling, + and_(Service.id == AnnualBilling.service_id, AnnualBilling.financial_year_start == financial_year) ).outerjoin( - FactBilling, and_( - Service.id == FactBilling.service_id, - FactBilling.bst_date >= start_date, - FactBilling.bst_date < end_date, - FactBilling.notification_type == SMS_TYPE, - ) + ft_billing_subquery, Service.id == ft_billing_subquery.c.service_id ).filter( Service.organisation_id == organisation_id, Service.restricted.is_(False) ).group_by( Service.id, Service.name, - allowance_left_at_start_date_query.c.free_sms_fragment_limit, - allowance_left_at_start_date_query.c.sms_remainder, - FactBilling.rate, + AnnualBilling.free_sms_fragment_limit ).order_by( Service.name ) @@ -856,7 +846,7 @@ def fetch_usage_year_for_organisation(organisation_id, year): 'emails_sent': 0, 'active': service.active } - sms_usages = fetch_sms_billing_for_organisation(organisation_id, year_start, year_end) + sms_usages = fetch_sms_billing_for_organisation(organisation_id, year) letter_usages = fetch_letter_costs_for_organisation(organisation_id, year_start, year_end) email_usages = fetch_email_usage_for_organisation(organisation_id, year_start, year_end) for usage in sms_usages: diff --git a/tests/app/dao/test_fact_billing_dao.py b/tests/app/dao/test_fact_billing_dao.py index 6452a02b2..e90cfa5b2 100644 --- a/tests/app/dao/test_fact_billing_dao.py +++ b/tests/app/dao/test_fact_billing_dao.py @@ -906,6 +906,95 @@ def test_fetch_usage_year_for_organisation_populates_ft_billing_for_today(notify assert FactBilling.query.count() == 1 +@freeze_time('2022-05-01 13:30') +def test_fetch_usage_year_for_organisation_calculates_cost_from_multiple_rates(notify_db_session): + old_rate_date = date(2022, 4, 29) + new_rate_date = date(2022, 5, 1) + current_year = datetime.utcnow().year + + org = create_organisation(name='Organisation 1') + + service_1 = create_service(restricted=False, service_name="Service 1") + dao_add_service_to_organisation(service=service_1, organisation_id=org.id) + sms_template_1 = create_template(service=service_1) + create_ft_billing( + bst_date=old_rate_date, template=sms_template_1, rate=2, + billable_unit=4, notifications_sent=4 + ) + create_ft_billing( + bst_date=new_rate_date, template=sms_template_1, rate=3, + billable_unit=2, notifications_sent=2 + ) + create_annual_billing(service_id=service_1.id, free_sms_fragment_limit=3, financial_year_start=current_year) + + results = fetch_usage_year_for_organisation(organisation_id=org.id, year=current_year) + + assert len(results) == 1 + assert results[str(service_1.id)]['free_sms_limit'] == 3 + assert results[str(service_1.id)]['sms_remainder'] == 0 + assert results[str(service_1.id)]['sms_billable_units'] == 6 + assert results[str(service_1.id)]['chargeable_billable_sms'] == 3 + assert results[str(service_1.id)]['sms_cost'] == 8.0 + + +@freeze_time('2022-05-01 13:30') +def test_fetch_usage_year_for_organisation_when_no_usage(notify_db_session): + current_year = datetime.utcnow().year + + org = create_organisation(name='Organisation 1') + + service_1 = create_service(restricted=False, service_name="Service 1") + dao_add_service_to_organisation(service=service_1, organisation_id=org.id) + create_annual_billing(service_id=service_1.id, free_sms_fragment_limit=3, financial_year_start=current_year) + + results = fetch_usage_year_for_organisation(organisation_id=org.id, year=current_year) + + assert len(results) == 1 + assert results[str(service_1.id)]['free_sms_limit'] == 3 + assert results[str(service_1.id)]['sms_remainder'] == 3 + assert results[str(service_1.id)]['sms_billable_units'] == 0 + assert results[str(service_1.id)]['chargeable_billable_sms'] == 0 + assert results[str(service_1.id)]['sms_cost'] == 0.0 + + +@freeze_time('2022-05-01 13:30') +def test_fetch_usage_year_for_organisation_only_queries_present_year(notify_db_session): + current_year = datetime.utcnow().year + last_year = current_year - 1 + date_two_years_ago = date(2021, 3, 31) + date_in_last_financial_year = date(2022, 3, 31) + date_in_this_year = date.today() + + org = create_organisation(name='Organisation 1') + + service_1 = create_service(restricted=False, service_name="Service 1") + dao_add_service_to_organisation(service=service_1, organisation_id=org.id) + sms_template_1 = create_template(service=service_1) + + create_ft_billing( + bst_date=date_two_years_ago, template=sms_template_1, rate=1, + billable_unit=2, notifications_sent=2 + ) + create_ft_billing( + bst_date=date_in_last_financial_year, template=sms_template_1, rate=1, + billable_unit=4, notifications_sent=4 + ) + create_ft_billing( + bst_date=date_in_this_year, template=sms_template_1, rate=1, + billable_unit=8, notifications_sent=8 + ) + create_annual_billing(service_id=service_1.id, free_sms_fragment_limit=4, financial_year_start=last_year - 1) + create_annual_billing(service_id=service_1.id, free_sms_fragment_limit=0, financial_year_start=last_year) + create_annual_billing(service_id=service_1.id, free_sms_fragment_limit=8, financial_year_start=current_year) + + results = fetch_usage_year_for_organisation(organisation_id=org.id, year=last_year) + + assert len(results) == 1 + assert results[str(service_1.id)]['sms_billable_units'] == 4 + assert results[str(service_1.id)]['chargeable_billable_sms'] == 4 + assert results[str(service_1.id)]['sms_cost'] == 4.0 + + @freeze_time('2020-02-27 13:30') def test_fetch_usage_year_for_organisation_only_returns_data_for_live_services(notify_db_session): org = create_organisation(name='Organisation without live services')