diff --git a/app/dao/date_util.py b/app/dao/date_util.py new file mode 100644 index 000000000..92932bb4c --- /dev/null +++ b/app/dao/date_util.py @@ -0,0 +1,18 @@ +from datetime import datetime + +import pytz + + +def get_financial_year(year): + return get_april_fools(year), get_april_fools(year + 1) + + +def get_april_fools(year): + """ + This function converts the start of the financial year April 1, 00:00 as BST (British Standard Time) to UTC, + the tzinfo is lastly removed from the datetime becasue the database stores the timestamps without timezone. + :param year: the year to calculate the April 1, 00:00 BST for + :return: the datetime of April 1 for the given year, for example 2016 = 2016-03-31 23:00:00 + """ + return pytz.timezone('Europe/London').localize(datetime(year, 4, 1, 0, 0, 0)).astimezone(pytz.UTC).replace( + tzinfo=None) diff --git a/app/dao/notification_usage_dao.py b/app/dao/notification_usage_dao.py new file mode 100644 index 000000000..fb412d849 --- /dev/null +++ b/app/dao/notification_usage_dao.py @@ -0,0 +1,138 @@ +from datetime import datetime + +from sqlalchemy import Float, Integer +from sqlalchemy import func, case, cast +from sqlalchemy import literal_column + +from app import db +from app.dao.date_util import get_financial_year +from app.models import (NotificationHistory, + Rate, + NOTIFICATION_STATUS_TYPES_BILLABLE, + KEY_TYPE_TEST, + SMS_TYPE, + EMAIL_TYPE) +from app.statsd_decorators import statsd +from app.utils import get_london_month_from_utc_column + + +@statsd(namespace="dao") +def get_yearly_billing_data(service_id, year): + start_date, end_date = get_financial_year(year) + rates = get_rates_for_year(start_date, end_date, SMS_TYPE) + + result = [] + for r, n in zip(rates, rates[1:]): + result.append( + sms_yearly_billing_data_query(r.rate, service_id, r.valid_from, n.valid_from)) + + result.append(sms_yearly_billing_data_query(rates[-1].rate, service_id, rates[-1].valid_from, end_date)) + + result.append(email_yearly_billing_data_query(service_id, start_date, end_date)) + + return sum(result, []) + + +@statsd(namespace="dao") +def get_monthly_billing_data(service_id, year): + start_date, end_date = get_financial_year(year) + rates = get_rates_for_year(start_date, end_date, SMS_TYPE) + + result = [] + for r, n in zip(rates, rates[1:]): + result.extend(sms_billing_data_per_month_query(r.rate, service_id, r.valid_from, n.valid_from)) + result.extend(sms_billing_data_per_month_query(rates[-1].rate, service_id, rates[-1].valid_from, end_date)) + + return [(datetime.strftime(x[0], "%B"), x[1], x[2], x[3], x[4], x[5]) for x in result] + + +def billing_data_filter(notification_type, start_date, end_date, service_id): + return [ + NotificationHistory.notification_type == notification_type, + NotificationHistory.created_at >= start_date, + NotificationHistory.created_at < end_date, + NotificationHistory.service_id == service_id, + NotificationHistory.status.in_(NOTIFICATION_STATUS_TYPES_BILLABLE), + NotificationHistory.key_type != KEY_TYPE_TEST + ] + + +def email_yearly_billing_data_query(service_id, start_date, end_date, rate=0): + result = db.session.query( + func.count(NotificationHistory.id), + func.count(NotificationHistory.id), + rate_multiplier(), + NotificationHistory.notification_type, + NotificationHistory.international, + cast(rate, Integer()) + ).filter( + *billing_data_filter(EMAIL_TYPE, start_date, end_date, service_id) + ).group_by( + NotificationHistory.notification_type, + rate_multiplier(), + NotificationHistory.international + ).first() + if not result: + return [(0, 0, 1, EMAIL_TYPE, False, 0)] + else: + return [result] + + +def sms_yearly_billing_data_query(rate, service_id, start_date, end_date): + result = db.session.query( + cast(func.sum(NotificationHistory.billable_units * rate_multiplier()), Integer()), + func.sum(NotificationHistory.billable_units), + rate_multiplier(), + NotificationHistory.notification_type, + NotificationHistory.international, + cast(rate, Float()) + ).filter( + *billing_data_filter(SMS_TYPE, start_date, end_date, service_id) + ).group_by( + NotificationHistory.notification_type, + NotificationHistory.international, + rate_multiplier() + ).order_by( + rate_multiplier() + ).all() + + if not result: + return [(0, 0, 1, SMS_TYPE, False, rate)] + else: + return result + + +def get_rates_for_year(start_date, end_date, notification_type): + return Rate.query.filter(Rate.valid_from >= start_date, Rate.valid_from < end_date, + Rate.notification_type == notification_type).order_by(Rate.valid_from).all() + + +def sms_billing_data_per_month_query(rate, service_id, start_date, end_date): + month = get_london_month_from_utc_column(NotificationHistory.created_at) + result = db.session.query( + month, + func.sum(NotificationHistory.billable_units), + rate_multiplier(), + NotificationHistory.international, + NotificationHistory.notification_type, + cast(rate, Float()) + ).filter( + *billing_data_filter(SMS_TYPE, start_date, end_date, service_id) + ).group_by( + NotificationHistory.notification_type, + month, + NotificationHistory.rate_multiplier, + NotificationHistory.international + ).order_by( + month, + rate_multiplier() + ).all() + + return result + + +def rate_multiplier(): + return cast(case([ + (NotificationHistory.rate_multiplier == None, literal_column("'1'")), # noqa + (NotificationHistory.rate_multiplier != None, NotificationHistory.rate_multiplier), # noqa + ]), Integer()) diff --git a/app/dao/notifications_dao.py b/app/dao/notifications_dao.py index 377b47e3c..6bb5337f8 100644 --- a/app/dao/notifications_dao.py +++ b/app/dao/notifications_dao.py @@ -1,5 +1,4 @@ import functools -import pytz from datetime import ( datetime, timedelta, @@ -12,6 +11,7 @@ from sqlalchemy.orm import joinedload from app import db, create_uuid from app.dao import days_ago +from app.dao.date_util import get_financial_year from app.models import ( Service, Notification, @@ -243,13 +243,15 @@ def get_notifications_for_job(service_id, job_id, filter_dict=None, page=1, page def get_notification_billable_unit_count_per_month(service_id, year): month = get_london_month_from_utc_column(NotificationHistory.created_at) + start_date, end_date = get_financial_year(year) notifications = db.session.query( month, func.sum(NotificationHistory.billable_units) ).filter( NotificationHistory.billable_units != 0, NotificationHistory.service_id == service_id, - NotificationHistory.created_at.between(*get_financial_year(year)), + NotificationHistory.created_at >= start_date, + NotificationHistory.created_at < end_date ).group_by( month ).order_by( @@ -410,21 +412,6 @@ def dao_timeout_notifications(timeout_period_in_seconds): return updated -def get_financial_year(year): - return get_april_fools(year), get_april_fools(year + 1) - - -def get_april_fools(year): - """ - This function converts the start of the financial year April 1, 00:00 as BST (British Standard Time) to UTC, - the tzinfo is lastly removed from the datetime becasue the database stores the timestamps without timezone. - :param year: the year to calculate the April 1, 00:00 BST for - :return: the datetime of April 1 for the given year, for example 2016 = 2016-03-31 23:00:00 - """ - return pytz.timezone('Europe/London').localize(datetime(year, 4, 1, 0, 0, 0)).astimezone(pytz.UTC).replace( - tzinfo=None) - - def get_total_sent_notifications_in_date_range(start_date, end_date, notification_type): result = db.session.query( func.count(NotificationHistory.id).label('count') diff --git a/app/dao/rates_dao.py b/app/dao/rates_dao.py deleted file mode 100644 index 47ed83a4e..000000000 --- a/app/dao/rates_dao.py +++ /dev/null @@ -1,11 +0,0 @@ -from sqlalchemy import desc - -from app import db -from app.models import Rate - - -def get_rate_for_type_and_date(notification_type, date_sent): - return db.session.query(Rate).filter(Rate.notification_type == notification_type, - Rate.valid_from <= date_sent - ).order_by(Rate.valid_from.desc() - ).limit(1).first() diff --git a/app/dao/services_dao.py b/app/dao/services_dao.py index 928b3188b..fe25469da 100644 --- a/app/dao/services_dao.py +++ b/app/dao/services_dao.py @@ -229,6 +229,7 @@ def _stats_for_service_query(service_id): def dao_fetch_monthly_historical_stats_by_template_for_service(service_id, year): month = get_london_month_from_utc_column(NotificationHistory.created_at) + start_date, end_date = get_financial_year(year) sq = db.session.query( NotificationHistory.template_id, NotificationHistory.status, @@ -236,7 +237,9 @@ def dao_fetch_monthly_historical_stats_by_template_for_service(service_id, year) func.count().label('count') ).filter( NotificationHistory.service_id == service_id, - NotificationHistory.created_at.between(*get_financial_year(year)) + NotificationHistory.created_at >= start_date, + NotificationHistory.created_at < end_date + ).group_by( month, NotificationHistory.template_id, @@ -262,6 +265,7 @@ def dao_fetch_monthly_historical_stats_by_template_for_service(service_id, year) def dao_fetch_monthly_historical_stats_for_service(service_id, year): month = get_london_month_from_utc_column(NotificationHistory.created_at) + start_date, end_date = get_financial_year(year) rows = db.session.query( NotificationHistory.notification_type, NotificationHistory.status, @@ -269,7 +273,8 @@ def dao_fetch_monthly_historical_stats_for_service(service_id, year): func.count(NotificationHistory.id).label('count') ).filter( NotificationHistory.service_id == service_id, - NotificationHistory.created_at.between(*get_financial_year(year)), + NotificationHistory.created_at >= start_date, + NotificationHistory.created_at < end_date ).group_by( NotificationHistory.notification_type, NotificationHistory.status, diff --git a/app/models.py b/app/models.py index 064e62ac5..0ef2de359 100644 --- a/app/models.py +++ b/app/models.py @@ -670,7 +670,7 @@ class Notification(db.Model): international = db.Column(db.Boolean, nullable=False, default=False) phone_prefix = db.Column(db.String, nullable=True) - rate_multiplier = db.Column(db.Float(), nullable=True) + rate_multiplier = db.Column(db.Float(asdecimal=False), nullable=True) @property def personalisation(self): @@ -850,7 +850,7 @@ class NotificationHistory(db.Model, HistoryModel): international = db.Column(db.Boolean, nullable=False, default=False) phone_prefix = db.Column(db.String, nullable=True) - rate_multiplier = db.Column(db.Float(), nullable=True) + rate_multiplier = db.Column(db.Float(asdecimal=False), nullable=True) @classmethod def from_original(cls, notification): @@ -971,5 +971,5 @@ class Rate(db.Model): id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) valid_from = db.Column(db.DateTime, nullable=False) - rate = db.Column(db.Numeric(), nullable=False) + rate = db.Column(db.Float(asdecimal=False), nullable=False) notification_type = db.Column(notification_types, index=True, nullable=False) diff --git a/app/service/rest.py b/app/service/rest.py index 26722373c..25991f30d 100644 --- a/app/service/rest.py +++ b/app/service/rest.py @@ -1,4 +1,5 @@ import itertools +import json from datetime import datetime from flask import ( @@ -8,6 +9,7 @@ from flask import ( ) from sqlalchemy.orm.exc import NoResultFound +from app.dao import notification_usage_dao from app.dao.dao_utils import dao_rollback from app.dao.api_key_dao import ( save_model_api_key, @@ -411,3 +413,38 @@ def get_monthly_template_stats(service_id): )) except ValueError: raise InvalidRequest('Year must be a number', status_code=400) + + +@service_blueprint.route('//yearly-usage') +def get_yearly_billing_usage(service_id): + try: + year = int(request.args.get('year')) + results = notification_usage_dao.get_yearly_billing_data(service_id, year) + json_result = [{"credits": x[0], + "billing_units": x[1], + "rate_multiplier": x[2], + "notification_type": x[3], + "international": x[4], + "rate": x[5] + } for x in results] + return json.dumps(json_result) + + except TypeError: + return jsonify(result='error', message='No valid year provided'), 400 + + +@service_blueprint.route('//monthly-usage') +def get_yearly_monthly_usage(service_id): + try: + year = int(request.args.get('year')) + results = notification_usage_dao.get_monthly_billing_data(service_id, year) + json_results = [{"month": x[0], + "billing_units": x[1], + "rate_multiplier": x[2], + "international": x[3], + "notification_type": x[4], + "rate": x[5] + } for x in results] + return json.dumps(json_results) + except TypeError: + return jsonify(result='error', message='No valid year provided'), 400 diff --git a/tests/app/conftest.py b/tests/app/conftest.py index de69e9240..fc9748870 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -444,7 +444,8 @@ def sample_notification( api_key_id=None, key_type=KEY_TYPE_NORMAL, sent_by=None, - client_reference=None + client_reference=None, + rate_multiplier=1.0 ): if created_at is None: created_at = datetime.utcnow() @@ -481,7 +482,8 @@ def sample_notification( 'key_type': key_type, 'sent_by': sent_by, 'updated_at': created_at if status in NOTIFICATION_STATUS_TYPES_COMPLETED else None, - 'client_reference': client_reference + 'client_reference': client_reference, + 'rate_multiplier': rate_multiplier } if job_row_number is not None: data['job_row_number'] = job_row_number diff --git a/tests/app/dao/test_date_utils.py b/tests/app/dao/test_date_utils.py new file mode 100644 index 000000000..d6be85da2 --- /dev/null +++ b/tests/app/dao/test_date_utils.py @@ -0,0 +1,13 @@ +from app.dao.date_util import get_financial_year, get_april_fools + + +def test_get_financial_year(): + start, end = get_financial_year(2000) + assert str(start) == '2000-03-31 23:00:00' + assert str(end) == '2001-03-31 23:00:00' + + +def test_get_april_fools(): + april_fools = get_april_fools(2016) + assert str(april_fools) == '2016-03-31 23:00:00' + assert april_fools.tzinfo is None diff --git a/tests/app/dao/test_notification_dao.py b/tests/app/dao/test_notification_dao.py index 8f67d2335..b5461e036 100644 --- a/tests/app/dao/test_notification_dao.py +++ b/tests/app/dao/test_notification_dao.py @@ -3,7 +3,6 @@ import uuid from functools import partial import pytest - from freezegun import freeze_time from sqlalchemy.exc import SQLAlchemyError, IntegrityError @@ -18,8 +17,7 @@ from app.models import ( NOTIFICATION_SENT, KEY_TYPE_NORMAL, KEY_TYPE_TEAM, - KEY_TYPE_TEST -) + KEY_TYPE_TEST) from app.dao.notifications_dao import ( dao_create_notification, @@ -40,8 +38,6 @@ from app.dao.notifications_dao import ( update_notification_status_by_reference, dao_delete_notifications_and_history_by_id, dao_timeout_notifications, - get_financial_year, - get_april_fools, is_delivery_slow_for_provider, dao_update_notifications_sent_to_dvla) @@ -1359,18 +1355,6 @@ def test_should_exclude_test_key_notifications_by_default( assert len(all_notifications) == 1 -def test_get_financial_year(): - start, end = get_financial_year(2000) - assert str(start) == '2000-03-31 23:00:00' - assert str(end) == '2001-03-31 23:00:00' - - -def test_get_april_fools(): - april_fools = get_april_fools(2016) - assert str(april_fools) == '2016-03-31 23:00:00' - assert april_fools.tzinfo is None - - @pytest.mark.parametrize('notification_type', ['sms', 'email']) def test_get_total_sent_notifications_in_date_range_returns_only_in_date_range( notify_db, diff --git a/tests/app/dao/test_notification_usage_dao.py b/tests/app/dao/test_notification_usage_dao.py new file mode 100644 index 000000000..0c3ddc952 --- /dev/null +++ b/tests/app/dao/test_notification_usage_dao.py @@ -0,0 +1,172 @@ +import uuid +from datetime import datetime + +from app.dao.notification_usage_dao import (get_rates_for_year, get_yearly_billing_data, + get_monthly_billing_data) +from app.models import Rate +from tests.app.db import create_notification + + +def test_get_rates_for_year(notify_db, notify_db_session): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.50) + set_up_rate(notify_db, datetime(2017, 6, 1), 1.75) + rates = get_rates_for_year(datetime(2016, 4, 1), datetime(2017, 3, 31), 'sms') + assert len(rates) == 1 + assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-04-01 00:00:00" + assert rates[0].rate == 1.50 + rates = get_rates_for_year(datetime(2017, 4, 1), datetime(2018, 3, 31), 'sms') + assert len(rates) == 1 + assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-06-01 00:00:00" + assert rates[0].rate == 1.75 + + +def test_get_yearly_billing_data(notify_db, notify_db_session, sample_template, sample_email_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + set_up_rate(notify_db, datetime(2016, 6, 1), 1.58) + # previous year + create_notification(template=sample_template, created_at=datetime(2016, 3, 31), sent_at=datetime(2016, 3, 31), + status='sending', billable_units=1) + # current year + create_notification(template=sample_template, created_at=datetime(2016, 4, 2), sent_at=datetime(2016, 4, 2), + status='sending', billable_units=1) + create_notification(template=sample_template, created_at=datetime(2016, 5, 18), sent_at=datetime(2016, 5, 18), + status='sending', billable_units=2) + create_notification(template=sample_template, created_at=datetime(2016, 7, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=3, rate_multiplier=2, international=True, phone_prefix="1") + create_notification(template=sample_template, created_at=datetime(2016, 9, 15), sent_at=datetime(2016, 9, 15), + status='sending', billable_units=4) + create_notification(template=sample_template, created_at=datetime(2017, 3, 31), sent_at=datetime(2017, 3, 31), + status='sending', billable_units=5) + create_notification(template=sample_email_template, created_at=datetime(2016, 9, 15), sent_at=datetime(2016, 9, 15), + status='sending', billable_units=0) + create_notification(template=sample_email_template, created_at=datetime(2017, 3, 31), sent_at=datetime(2017, 3, 31), + status='sending', billable_units=0) + # next year + create_notification(template=sample_template, created_at=datetime(2017, 4, 1), sent_at=datetime(2017, 4, 1), + status='sending', billable_units=6) + results = get_yearly_billing_data(sample_template.service_id, 2016) + assert len(results) == 4 + assert results[0] == (3, 3, 1, 'sms', False, 1.4) + assert results[1] == (9, 9, 1, 'sms', False, 1.58) + assert results[2] == (6, 3, 2, 'sms', True, 1.58) + assert results[3] == (2, 2, 1, 'email', False, 0) + + +def test_get_yearly_billing_data_with_one_rate(notify_db, notify_db_session, sample_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + # previous year + create_notification(template=sample_template, created_at=datetime(2016, 3, 31), sent_at=datetime(2016, 3, 31), + status='sending', billable_units=1) + # current year + create_notification(template=sample_template, created_at=datetime(2016, 4, 2), sent_at=datetime(2016, 4, 2), + status='sending', billable_units=1) + create_notification(template=sample_template, created_at=datetime(2016, 5, 18), sent_at=datetime(2016, 5, 18), + status='sending', billable_units=2) + create_notification(template=sample_template, created_at=datetime(2016, 7, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=3) + create_notification(template=sample_template, created_at=datetime(2016, 9, 15), sent_at=datetime(2016, 9, 15), + status='sending', billable_units=4) + create_notification(template=sample_template, created_at=datetime(2017, 3, 31, 22, 59, 59), + sent_at=datetime(2017, 3, 31), status='sending', billable_units=5) + # next year + create_notification(template=sample_template, created_at=datetime(2017, 3, 31, 23, 00, 00), + sent_at=datetime(2017, 3, 31), status='sending', billable_units=6) + create_notification(template=sample_template, created_at=datetime(2017, 4, 1), sent_at=datetime(2017, 4, 1), + status='sending', billable_units=7) + results = get_yearly_billing_data(sample_template.service_id, 2016) + assert len(results) == 2 + assert results[0] == (15, 15, 1, 'sms', False, 1.4) + assert results[1] == (0, 0, 1, 'email', False, 0) + + +def test_get_yearly_billing_data_with_no_sms_notifications(notify_db, notify_db_session, sample_email_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + create_notification(template=sample_email_template, created_at=datetime(2016, 7, 31), sent_at=datetime(2016, 3, 31), + status='sending', billable_units=0) + create_notification(template=sample_email_template, created_at=datetime(2016, 10, 2), sent_at=datetime(2016, 4, 2), + status='sending', billable_units=0) + + results = get_yearly_billing_data(sample_email_template.service_id, 2016) + assert len(results) == 2 + assert results[0] == (0, 0, 1, 'sms', False, 1.4) + assert results[1] == (2, 2, 1, 'email', False, 0) + + +def test_get_monthly_billing_data(notify_db, notify_db_session, sample_template, sample_email_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + # previous year + create_notification(template=sample_template, created_at=datetime(2016, 3, 31), sent_at=datetime(2016, 3, 31), + status='sending', billable_units=1) + # current year + create_notification(template=sample_template, created_at=datetime(2016, 4, 2), sent_at=datetime(2016, 4, 2), + status='sending', billable_units=1) + create_notification(template=sample_template, created_at=datetime(2016, 5, 18), sent_at=datetime(2016, 5, 18), + status='sending', billable_units=2) + create_notification(template=sample_template, created_at=datetime(2016, 7, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=3) + create_notification(template=sample_template, created_at=datetime(2016, 7, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=3, rate_multiplier=2) + create_notification(template=sample_template, created_at=datetime(2016, 7, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=3, rate_multiplier=2) + create_notification(template=sample_template, created_at=datetime(2016, 7, 30), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=4) + + create_notification(template=sample_email_template, created_at=datetime(2016, 8, 22), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=0) + create_notification(template=sample_email_template, created_at=datetime(2016, 8, 30), sent_at=datetime(2016, 7, 22), + status='sending', billable_units=0) + # next year + create_notification(template=sample_template, created_at=datetime(2017, 3, 31, 23, 00, 00), + sent_at=datetime(2017, 3, 31), status='sending', billable_units=6) + results = get_monthly_billing_data(sample_template.service_id, 2016) + assert len(results) == 4 + # (billable_units, rate_multiplier, international, type, rate) + assert results[0] == ('April', 1, 1, False, 'sms', 1.4) + assert results[1] == ('May', 2, 1, False, 'sms', 1.4) + assert results[2] == ('July', 7, 1, False, 'sms', 1.4) + assert results[3] == ('July', 6, 2, False, 'sms', 1.4) + + +def test_get_monthly_billing_data_with_multiple_rates(notify_db, notify_db_session, sample_template, + sample_email_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + set_up_rate(notify_db, datetime(2016, 6, 5), 1.75) + # previous year + create_notification(template=sample_template, created_at=datetime(2016, 3, 31), sent_at=datetime(2016, 3, 31), + status='sending', billable_units=1) + # current year + create_notification(template=sample_template, created_at=datetime(2016, 4, 2), sent_at=datetime(2016, 4, 2), + status='sending', billable_units=1) + create_notification(template=sample_template, created_at=datetime(2016, 5, 18), sent_at=datetime(2016, 5, 18), + status='sending', billable_units=2) + create_notification(template=sample_template, created_at=datetime(2016, 6, 1), sent_at=datetime(2016, 6, 1), + status='sending', billable_units=3) + create_notification(template=sample_template, created_at=datetime(2016, 6, 15), sent_at=datetime(2016, 6, 15), + status='sending', billable_units=4) + create_notification(template=sample_email_template, created_at=datetime(2016, 8, 22), + sent_at=datetime(2016, 7, 22), + status='sending', billable_units=0) + create_notification(template=sample_email_template, created_at=datetime(2016, 8, 30), + sent_at=datetime(2016, 7, 22), + status='sending', billable_units=0) + # next year + create_notification(template=sample_template, created_at=datetime(2017, 3, 31, 23, 00, 00), + sent_at=datetime(2017, 3, 31), status='sending', billable_units=6) + results = get_monthly_billing_data(sample_template.service_id, 2016) + assert len(results) == 4 + assert results[0] == ('April', 1, 1, False, 'sms', 1.4) + assert results[1] == ('May', 2, 1, False, 'sms', 1.4) + assert results[2] == ('June', 3, 1, False, 'sms', 1.4) + assert results[3] == ('June', 4, 1, False, 'sms', 1.75) + + +def test_get_monthly_billing_data_with_no_notifications_for_year(notify_db, notify_db_session, sample_template, + sample_email_template): + set_up_rate(notify_db, datetime(2016, 4, 1), 1.40) + results = get_monthly_billing_data(sample_template.service_id, 2016) + assert len(results) == 0 + + +def set_up_rate(notify_db, start_date, value): + rate = Rate(id=uuid.uuid4(), valid_from=start_date, rate=value, notification_type='sms') + notify_db.session.add(rate) diff --git a/tests/app/dao/test_rates_dao.py b/tests/app/dao/test_rates_dao.py deleted file mode 100644 index 33f68a671..000000000 --- a/tests/app/dao/test_rates_dao.py +++ /dev/null @@ -1,18 +0,0 @@ -from datetime import datetime - -from decimal import Decimal - -from app.dao.rates_dao import get_rate_for_type_and_date - - -def test_get_rate_for_type_and_date(notify_db): - rate = get_rate_for_type_and_date('sms', datetime.utcnow()) - assert rate.rate == Decimal("1.58") - - rate = get_rate_for_type_and_date('sms', datetime(2016, 6, 1)) - assert rate.rate == Decimal("1.65") - - -def test_get_rate_for_type_and_date_early_date(notify_db): - rate = get_rate_for_type_and_date('sms', datetime(2014, 6, 1)) - assert not rate diff --git a/tests/app/db.py b/tests/app/db.py index 38d5eca9d..b5838d693 100644 --- a/tests/app/db.py +++ b/tests/app/db.py @@ -73,7 +73,9 @@ def create_notification( key_type=KEY_TYPE_NORMAL, sent_by=None, client_reference=None, - international=False + rate_multiplier=None, + international=False, + phone_prefix=None ): if created_at is None: created_at = datetime.utcnow() @@ -105,7 +107,9 @@ def create_notification( 'updated_at': updated_at, 'client_reference': client_reference, 'job_row_number': job_row_number, - 'international': international + 'rate_multiplier': rate_multiplier, + 'international': international, + 'phone_prefix': phone_prefix } notification = Notification(**data) dao_create_notification(notification) diff --git a/tests/app/job/test_rest.py b/tests/app/job/test_rest.py index e5a3cc453..e05d52b3a 100644 --- a/tests/app/job/test_rest.py +++ b/tests/app/job/test_rest.py @@ -205,7 +205,6 @@ def test_should_not_create_scheduled_job_more_then_24_hours_hence(notify_api, sa auth_header = create_authorization_header() headers = [('Content-Type', 'application/json'), auth_header] - print(json.dumps(data)) response = client.post( path, data=json.dumps(data), @@ -240,7 +239,6 @@ def test_should_not_create_scheduled_job_in_the_past(notify_api, sample_template auth_header = create_authorization_header() headers = [('Content-Type', 'application/json'), auth_header] - print(json.dumps(data)) response = client.post( path, data=json.dumps(data), diff --git a/tests/app/service/test_rest.py b/tests/app/service/test_rest.py index 2cf666be4..f2150df1a 100644 --- a/tests/app/service/test_rest.py +++ b/tests/app/service/test_rest.py @@ -10,8 +10,9 @@ from freezegun import freeze_time from app.dao.users_dao import save_model_user from app.dao.services_dao import dao_remove_user_from_service -from app.models import User, Organisation, DVLA_ORG_LAND_REGISTRY +from app.models import User, Organisation, DVLA_ORG_LAND_REGISTRY, Rate from tests import create_authorization_header +from tests.app.db import create_template from tests.app.conftest import ( sample_service as create_service, sample_service_permission as create_service_permission, @@ -1509,3 +1510,107 @@ def test_get_template_stats_by_month_returns_error_for_incorrect_year( ) assert response.status_code == expected_status assert json.loads(response.get_data(as_text=True)) == expected_json + + +def test_get_yearly_billing_usage(client, notify_db, notify_db_session): + rate = Rate(id=uuid.uuid4(), valid_from=datetime(2016, 3, 31, 23, 00), rate=1.58, notification_type='sms') + notify_db.session.add(rate) + notification = create_sample_notification(notify_db, notify_db_session, created_at=datetime(2016, 6, 5), + sent_at=datetime(2016, 6, 5), + status='sending') + response = client.get( + '/service/{}/yearly-usage?year=2016'.format(notification.service_id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + + assert json.loads(response.get_data(as_text=True)) == [{'credits': 1, + 'billing_units': 1, + 'rate_multiplier': 1, + 'notification_type': 'sms', + 'international': False, + 'rate': 1.58}, + {'credits': 0, + 'billing_units': 0, + 'rate_multiplier': 1, + 'notification_type': 'email', + 'international': False, + 'rate': 0}] + + +def test_get_yearly_billing_usage_returns_400_if_missing_year(client, sample_service): + response = client.get( + '/service/{}/yearly-usage'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'message': 'No valid year provided', 'result': 'error' + } + + +def test_get_monthly_billing_usage(client, notify_db, notify_db_session, sample_service): + rate = Rate(id=uuid.uuid4(), valid_from=datetime(2016, 3, 31, 23, 00), rate=1.58, notification_type='sms') + notify_db.session.add(rate) + notification = create_sample_notification(notify_db, notify_db_session, created_at=datetime(2016, 6, 5), + sent_at=datetime(2016, 6, 5), + status='sending') + create_sample_notification(notify_db, notify_db_session, created_at=datetime(2016, 6, 5), + sent_at=datetime(2016, 6, 5), + status='sending', rate_multiplier=2) + create_sample_notification(notify_db, notify_db_session, created_at=datetime(2016, 7, 5), + sent_at=datetime(2016, 7, 5), + status='sending') + + template = create_template(sample_service, template_type='email') + create_sample_notification(notify_db, notify_db_session, created_at=datetime(2016, 6, 5), + sent_at=datetime(2016, 6, 5), + status='sending', + template=template) + response = client.get( + '/service/{}/monthly-usage?year=2016'.format(notification.service_id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + actual = json.loads(response.get_data(as_text=True)) + assert len(actual) == 3 + assert actual == [{'month': 'June', + 'international': False, + 'rate_multiplier': 1, + 'notification_type': 'sms', + 'rate': 1.58, + 'billing_units': 1}, + {'month': 'June', + 'international': False, + 'rate_multiplier': 2, + 'notification_type': 'sms', + 'rate': 1.58, + 'billing_units': 1}, + {'month': 'July', + 'international': False, + 'rate_multiplier': 1, + 'notification_type': 'sms', + 'rate': 1.58, + 'billing_units': 1}] + + +def test_get_monthly_billing_usage_returns_400_if_missing_year(client, sample_service): + response = client.get( + '/service/{}/monthly-usage'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 400 + assert json.loads(response.get_data(as_text=True)) == { + 'message': 'No valid year provided', 'result': 'error' + } + + +def test_get_monthly_billing_usage_returns_empty_list_if_no_notifications(client, notify_db, sample_service): + rate = Rate(id=uuid.uuid4(), valid_from=datetime(2016, 3, 31, 23, 00), rate=1.58, notification_type='sms') + notify_db.session.add(rate) + response = client.get( + '/service/{}/monthly-usage?year=2016'.format(sample_service.id), + headers=[create_authorization_header()] + ) + assert response.status_code == 200 + assert json.loads(response.get_data(as_text=True)) == [] diff --git a/tests/app/v2/test_errors.py b/tests/app/v2/test_errors.py index 5f0ed0f81..8345faacc 100644 --- a/tests/app/v2/test_errors.py +++ b/tests/app/v2/test_errors.py @@ -88,7 +88,6 @@ def test_validation_error(app_for_test): response = client.get(url_for('v2_under_test.raising_validation_error')) assert response.status_code == 400 error = json.loads(response.get_data(as_text=True)) - print(error) assert len(error.keys()) == 2 assert error['status_code'] == 400 assert len(error['errors']) == 2 diff --git a/tests/conftest.py b/tests/conftest.py index c7ec7ee3b..b08059725 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -76,8 +76,7 @@ def notify_db_session(notify_db): "job_status", "provider_details_history", "template_process_type", - "dvla_organisation", - "rates"]: + "dvla_organisation"]: notify_db.engine.execute(tbl.delete()) notify_db.session.commit()