Merge branch 'master' into celery_logging

This commit is contained in:
Leo Hemsted
2017-09-11 11:32:49 +01:00
committed by GitHub
41 changed files with 831 additions and 462 deletions
+24 -19
View File
@@ -8,7 +8,7 @@ from sqlalchemy.exc import SQLAlchemyError
from app.aws import s3
from app import notify_celery
from app.performance_platform import total_sent_notifications
from app.performance_platform import total_sent_notifications, processing_time
from app import performance_platform_client
from app.dao.date_util import get_month_start_and_end_date_in_utc
from app.dao.inbound_sms_dao import delete_inbound_sms_created_more_than_a_week_ago
@@ -176,27 +176,32 @@ def timeout_notifications():
@statsd(namespace="tasks")
def send_daily_performance_platform_stats():
if performance_platform_client.active:
count_dict = total_sent_notifications.get_total_sent_notifications_yesterday()
email_sent_count = count_dict.get('email').get('count')
sms_sent_count = count_dict.get('sms').get('count')
start_date = count_dict.get('start_date')
send_total_sent_notifications_to_performance_platform()
processing_time.send_processing_time_to_performance_platform()
current_app.logger.info(
"Attempting to update performance platform for date {} with email count {} and sms count {}"
.format(start_date, email_sent_count, sms_sent_count)
)
total_sent_notifications.send_total_notifications_sent_for_day_stats(
start_date,
'sms',
sms_sent_count
)
def send_total_sent_notifications_to_performance_platform():
count_dict = total_sent_notifications.get_total_sent_notifications_yesterday()
email_sent_count = count_dict.get('email').get('count')
sms_sent_count = count_dict.get('sms').get('count')
start_date = count_dict.get('start_date')
total_sent_notifications.send_total_notifications_sent_for_day_stats(
start_date,
'email',
email_sent_count
)
current_app.logger.info(
"Attempting to update performance platform for date {} with email count {} and sms count {}"
.format(start_date, email_sent_count, sms_sent_count)
)
total_sent_notifications.send_total_notifications_sent_for_day_stats(
start_date,
'sms',
sms_sent_count
)
total_sent_notifications.send_total_notifications_sent_for_day_stats(
start_date,
'email',
email_sent_count
)
@notify_celery.task(name='switch-current-sms-provider-on-slow-delivery')
+5 -3
View File
@@ -86,9 +86,11 @@ def process_job(job_id):
process_row(row_number, recipient, personalisation, template, job, service)
if template.template_type == LETTER_TYPE:
build_dvla_file.apply_async([str(job.id)], queue=QueueNames.JOBS)
# temporary logging
current_app.logger.info("send job {} to build-dvla-file in the process-job queue".format(job_id))
if service.research_mode:
update_job_to_sent_to_dvla.apply_async([str(job.id)], queue=QueueNames.RESEARCH_MODE)
else:
build_dvla_file.apply_async([str(job.id)], queue=QueueNames.JOBS)
current_app.logger.info("send job {} to build-dvla-file in the {} queue".format(job_id, QueueNames.JOBS))
else:
job.job_status = JOB_STATUS_FINISHED
+60 -29
View File
@@ -1,7 +1,7 @@
import uuid
from datetime import datetime
from datetime import datetime, timedelta
from decimal import Decimal
from flask_script import Command, Manager, Option
from flask_script import Command, Option
from app import db
from app.dao.monthly_billing_dao import (
@@ -16,6 +16,8 @@ from app.dao.services_dao import (
)
from app.dao.provider_rates_dao import create_provider_rates
from app.dao.users_dao import (delete_model_user, delete_user_verify_codes)
from app.utils import get_midnight_for_day_before, get_london_midnight_in_utc
from app.performance_platform.processing_time import send_processing_time_for_start_and_end
class CreateProviderRateCommand(Command):
@@ -49,7 +51,7 @@ class PurgeFunctionalTestDataCommand(Command):
Option('-u', '-user-email-prefix', dest='user_email_prefix', help="Functional test user email prefix."),
)
def run(self, service_name_prefix=None, user_email_prefix=None):
def run(self, user_email_prefix=None):
if user_email_prefix:
users = User.query.filter(User.email_address.like("{}%".format(user_email_prefix))).all()
for usr in users:
@@ -167,33 +169,62 @@ class CustomDbScript(Command):
class PopulateMonthlyBilling(Command):
option_list = (
Option('-y', '-year', dest="year", help="Use for integer value for year, e.g. 2017"),
option_list = (
Option('-y', '-year', dest="year", help="Use for integer value for year, e.g. 2017"),
)
def run(self, year):
service_ids = get_service_ids_that_need_billing_populated(
start_date=datetime(2016, 5, 1), end_date=datetime(2017, 8, 16)
)
start, end = 1, 13
if year == '2016':
start = 4
def run(self, year):
service_ids = get_service_ids_that_need_billing_populated(
start_date=datetime(2016, 5, 1), end_date=datetime(2017, 8, 16)
)
start, end = 1, 13
if year == '2016':
start = 4
for service_id in service_ids:
print('Starting to populate data for service {}'.format(str(service_id)))
print('Starting populating monthly billing for {}'.format(year))
for i in range(start, end):
print('Population for {}-{}'.format(i, year))
self.populate(service_id, year, i)
for service_id in service_ids:
print('Starting to populate data for service {}'.format(str(service_id)))
print('Starting populating monthly billing for {}'.format(year))
for i in range(start, end):
print('Population for {}-{}'.format(i, year))
self.populate(service_id, year, i)
def populate(self, service_id, year, month):
create_or_update_monthly_billing(service_id, datetime(int(year), int(month), 1))
sms_res = get_monthly_billing_by_notification_type(
service_id, datetime(int(year), int(month), 1), SMS_TYPE
)
email_res = get_monthly_billing_by_notification_type(
service_id, datetime(int(year), int(month), 1), EMAIL_TYPE
)
print("Finished populating data for {} for service id {}".format(month, str(service_id)))
print('SMS: {}'.format(sms_res.monthly_totals))
print('Email: {}'.format(email_res.monthly_totals))
def populate(self, service_id, year, month):
create_or_update_monthly_billing(service_id, datetime(int(year), int(month), 1))
sms_res = get_monthly_billing_by_notification_type(
service_id, datetime(int(year), int(month), 1), SMS_TYPE
)
email_res = get_monthly_billing_by_notification_type(
service_id, datetime(int(year), int(month), 1), EMAIL_TYPE
)
print("Finished populating data for {} for service id {}".format(month, str(service_id)))
print('SMS: {}'.format(sms_res.monthly_totals))
print('Email: {}'.format(email_res.monthly_totals))
class BackfillProcessingTime(Command):
option_list = (
Option('-s', '--start_date', dest='start_date', help="Date (%Y-%m-%d) start date inclusive"),
Option('-e', '--end_date', dest='end_date', help="Date (%Y-%m-%d) end date inclusive"),
)
def run(self, start_date, end_date):
start_date = datetime.strptime(start_date, '%Y-%m-%d')
end_date = datetime.strptime(end_date, '%Y-%m-%d')
delta = end_date - start_date
print('Sending notification processing-time data for all days between {} and {}'.format(start_date, end_date))
for i in range(delta.days + 1):
# because the tz conversion funcs talk about midnight, and the midnight before last,
# we want to pretend we're running this from the next morning, so add one.
process_date = start_date + timedelta(days=i + 1)
process_start_date = get_midnight_for_day_before(process_date)
process_end_date = get_london_midnight_in_utc(process_date)
print('Sending notification processing-time for {} - {}'.format(
process_start_date.isoformat(),
process_end_date.isoformat()
))
send_processing_time_for_start_and_end(process_start_date, process_end_date)
-2
View File
@@ -1,7 +1,5 @@
from datetime import datetime
from sqlalchemy import func
from app import db
from app.dao.dao_utils import transactional
+1 -82
View File
@@ -1,4 +1,3 @@
from collections import namedtuple
from datetime import datetime, timedelta
from sqlalchemy import Float, Integer
@@ -17,41 +16,7 @@ from app.models import (
EMAIL_TYPE
)
from app.statsd_decorators import statsd
from app.utils import get_london_month_from_utc_column, convert_utc_to_bst
@statsd(namespace="dao")
def get_yearly_billing_data(service_id, year):
start_date, end_date = get_financial_year(year)
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
if not rates:
return []
def get_valid_from(valid_from):
return start_date if valid_from < start_date else valid_from
result = []
for r, n in zip(rates, rates[1:]):
result.append(
sms_yearly_billing_data_query(
r.rate,
service_id,
get_valid_from(r.valid_from),
n.valid_from
)
)
result.append(
sms_yearly_billing_data_query(
rates[-1].rate,
service_id,
get_valid_from(rates[-1].valid_from),
end_date
)
)
result.append(email_yearly_billing_data_query(service_id, start_date, end_date))
return sum(result, [])
from app.utils import get_london_month_from_utc_column
@statsd(namespace="dao")
@@ -112,52 +77,6 @@ def billing_data_filter(notification_type, start_date, end_date, service_id):
]
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_daterange(start_date, end_date, notification_type):
rates = Rate.query.filter(Rate.notification_type == notification_type).order_by(Rate.valid_from).all()
+37 -1
View File
@@ -16,6 +16,8 @@ from notifications_utils.recipients import (
from werkzeug.datastructures import MultiDict
from sqlalchemy import (desc, func, or_, and_, asc)
from sqlalchemy.orm import joinedload
from sqlalchemy.sql.expression import case
from sqlalchemy.sql import functions
from notifications_utils.international_billing_rates import INTERNATIONAL_BILLING_RATES
from app import db, create_uuid
@@ -42,7 +44,6 @@ from app.models import (
from app.dao.dao_utils import transactional
from app.statsd_decorators import statsd
from app.utils import get_london_month_from_utc_column
def dao_get_notification_statistics_for_service_and_day(service_id, day):
@@ -519,3 +520,38 @@ def set_scheduled_notification_to_processed(notification_id):
{'pending': False}
)
db.session.commit()
def dao_get_total_notifications_sent_per_day_for_performance_platform(start_date, end_date):
"""
SELECT
count(notification_history),
coalesce(sum(CASE WHEN sent_at - created_at <= interval '10 seconds' THEN 1 ELSE 0 END), 0)
FROM notification_history
WHERE
created_at > 'START DATE' AND
created_at < 'END DATE' AND
api_key_id IS NOT NULL AND
key_type != 'test' AND
notification_type != 'letter';
"""
under_10_secs = NotificationHistory.sent_at - NotificationHistory.created_at <= timedelta(seconds=10)
sum_column = functions.coalesce(functions.sum(
case(
[
(under_10_secs, 1)
],
else_=0
)
), 0)
return db.session.query(
func.count(NotificationHistory.id).label('messages_total'),
sum_column.label('messages_within_10_secs')
).filter(
NotificationHistory.created_at >= start_date,
NotificationHistory.created_at < end_date,
NotificationHistory.api_key_id.isnot(None),
NotificationHistory.key_type != KEY_TYPE_TEST,
NotificationHistory.notification_type != LETTER_TYPE
).one()
+32
View File
@@ -0,0 +1,32 @@
from app import db
from app.dao.dao_utils import transactional
from app.models import ServiceEmailReplyTo
def create_or_update_email_reply_to(service_id, email_address):
reply_to = dao_get_reply_to_by_service_id(service_id)
if reply_to:
reply_to.email_address = email_address
dao_update_reply_to_email(reply_to)
else:
reply_to = ServiceEmailReplyTo(service_id=service_id, email_address=email_address)
dao_create_reply_to_email_address(reply_to)
@transactional
def dao_create_reply_to_email_address(reply_to_email):
db.session.add(reply_to_email)
def dao_get_reply_to_by_service_id(service_id):
reply_to = db.session.query(
ServiceEmailReplyTo
).filter(
ServiceEmailReplyTo.service_id == service_id
).first()
return reply_to
@transactional
def dao_update_reply_to_email(reply_to):
db.session.add(reply_to)
+4 -1
View File
@@ -32,7 +32,7 @@ from app.schemas import (
from app.celery.tasks import process_job
from app.models import JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING, JOB_STATUS_CANCELLED
from app.models import JOB_STATUS_SCHEDULED, JOB_STATUS_PENDING, JOB_STATUS_CANCELLED, LETTER_TYPE
from app.utils import pagination_links
@@ -190,6 +190,9 @@ def create_job(service_id):
})
template = dao_get_template_by_id(data['template'])
if template.template_type == LETTER_TYPE and service.restricted:
raise InvalidRequest("Create letter job is not allowed for service in trial mode ", 403)
errors = unarchived_template_schema.validate({'archived': template.archived})
if errors:
+31 -2
View File
@@ -245,7 +245,7 @@ class Service(db.Model, Versioned):
if self.inbound_number and self.inbound_number.active:
return self.inbound_number.number
else:
return self.sms_sender or current_app.config['FROM_NUMBER']
return self.sms_sender
class InboundNumber(db.Model):
@@ -278,6 +278,21 @@ class InboundNumber(db.Model):
}
class ServiceSmsSender(db.Model):
__tablename__ = "service_sms_senders"
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
sms_sender = db.Column(db.String(11), nullable=False)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), unique=True, index=True, nullable=False)
service = db.relationship(Service, backref=db.backref("service_sms_senders", uselist=False))
is_default = db.Column(db.Boolean, nullable=False, default=True)
inbound_number_id = db.Column(UUID(as_uuid=True), db.ForeignKey('inbound_numbers.id'),
unique=True, index=True, nullable=True)
inbound_number = db.relationship(InboundNumber, backref=db.backref("inbound_number", uselist=False))
created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
class ServicePermission(db.Model):
__tablename__ = "service_permissions"
@@ -318,7 +333,7 @@ class ServiceWhitelist(db.Model):
try:
if recipient_type == MOBILE_TYPE:
validate_phone_number(recipient)
validate_phone_number(recipient, international=True)
instance.recipient = recipient
elif recipient_type == EMAIL_TYPE:
validate_email_address(recipient)
@@ -1314,3 +1329,17 @@ class MonthlyBilling(db.Model):
def __repr__(self):
return str(self.serialized())
class ServiceEmailReplyTo(db.Model):
__tablename__ = "service_email_reply_to"
id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), unique=False, index=True, nullable=False)
service = db.relationship(Service, backref=db.backref("reply_to_email_addresses", uselist=False))
email_address = db.Column(db.Text, nullable=False, index=False, unique=False)
is_default = db.Column(db.Boolean, nullable=False, default=True)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow)
@@ -0,0 +1,40 @@
from datetime import datetime
from flask import current_app
from app.utils import get_midnight_for_day_before, get_london_midnight_in_utc
from app.dao.notifications_dao import dao_get_total_notifications_sent_per_day_for_performance_platform
from app import performance_platform_client
def send_processing_time_to_performance_platform():
today = datetime.utcnow()
start_date = get_midnight_for_day_before(today)
end_date = get_london_midnight_in_utc(today)
send_processing_time_for_start_and_end(start_date, end_date)
def send_processing_time_for_start_and_end(start_date, end_date):
result = dao_get_total_notifications_sent_per_day_for_performance_platform(start_date, end_date)
current_app.logger.info(
'Sending processing-time to performance platform for date {}. Total: {}, under 10 secs {}'.format(
start_date, result.messages_total, result.messages_within_10_secs
)
)
send_processing_time_data(start_date, 'messages-total', result.messages_total)
send_processing_time_data(start_date, 'messages-within-10-secs', result.messages_within_10_secs)
def send_processing_time_data(date, status, count):
payload = performance_platform_client.format_payload(
dataset='processing-time',
date=date,
group_name='status',
group_value=status,
count=count
)
performance_platform_client.send_stats_to_performance_platform(payload)
+1 -1
View File
@@ -130,7 +130,7 @@ class UserUpdateAttributeSchema(BaseSchema):
@validates('mobile_number')
def validate_mobile_number(self, value):
try:
validate_phone_number(value)
validate_phone_number(value, international=True)
except InvalidPhoneError as error:
raise ValidationError('Invalid phone number: {}'.format(error))
+7 -40
View File
@@ -1,5 +1,4 @@
import itertools
import json
from datetime import datetime
from flask import (
@@ -11,7 +10,7 @@ from flask import (
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.exc import NoResultFound
from app.dao import notification_usage_dao, notifications_dao
from app.dao import notifications_dao
from app.dao.dao_utils import dao_rollback
from app.dao.api_key_dao import (
save_model_api_key,
@@ -46,6 +45,7 @@ from app.dao.service_whitelist_dao import (
dao_add_and_commit_whitelisted_contacts,
dao_remove_service_whitelist
)
from app.dao.service_email_reply_to_dao import create_or_update_email_reply_to
from app.dao.provider_statistics_dao import get_fragment_count
from app.dao.users_dao import get_user_by_id
from app.errors import (
@@ -132,9 +132,13 @@ def create_service():
@service_blueprint.route('/<uuid:service_id>', methods=['POST'])
def update_service(service_id):
req_json = request.get_json()
fetched_service = dao_fetch_service_by_id(service_id)
# Capture the status change here as Marshmallow changes this later
service_going_live = fetched_service.restricted and not request.get_json().get('restricted', True)
service_going_live = fetched_service.restricted and not req_json.get('restricted', True)
if 'reply_to_email_address' in req_json:
create_or_update_email_reply_to(fetched_service.id, req_json['reply_to_email_address'])
current_data = dict(service_schema.dump(fetched_service).data.items())
current_data.update(request.get_json())
@@ -458,43 +462,6 @@ def get_monthly_template_stats(service_id):
raise InvalidRequest('Year must be a number', status_code=400)
@service_blueprint.route('/<uuid:service_id>/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('/<uuid:service_id>/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
@service_blueprint.route('/<uuid:service_id>/inbound-api', methods=['POST'])
def create_service_inbound_api(service_id):
data = request.get_json()
+1 -1
View File
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
import pytz
from flask import url_for
from sqlalchemy import func
from notifications_utils.template import SMSMessageTemplate, PlainTextEmailTemplate, LetterPreviewTemplate
from notifications_utils.template import SMSMessageTemplate, PlainTextEmailTemplate
local_timezone = pytz.timezone("Europe/London")
+3 -2
View File
@@ -150,15 +150,16 @@ def process_letter_notification(*, letter_data, api_key, template):
if api_key.key_type == KEY_TYPE_TEAM:
raise BadRequestError(message='Cannot send letters with a team api key', status_code=403)
if api_key.service.restricted and api_key.key_type != KEY_TYPE_TEST:
raise BadRequestError(message='Cannot send letters when service is in trial mode', status_code=403)
job = create_letter_api_job(template)
notification = create_letter_notification(letter_data, job, api_key)
if api_key.service.research_mode or api_key.key_type == KEY_TYPE_TEST:
# distinguish real API jobs from test jobs by giving the test jobs a different filename
job.original_file_name = LETTER_TEST_API_FILENAME
dao_update_job(job)
update_job_to_sent_to_dvla.apply_async([str(job.id)], queue=QueueNames.RESEARCH_MODE)
else:
build_dvla_file.apply_async([str(job.id)], queue=QueueNames.JOBS)
+1
View File
@@ -17,6 +17,7 @@ manager.add_command('create_provider_rate', commands.CreateProviderRateCommand)
manager.add_command('purge_functional_test_data', commands.PurgeFunctionalTestDataCommand)
manager.add_command('custom_db_script', commands.CustomDbScript)
manager.add_command('populate_monthly_billing', commands.PopulateMonthlyBilling)
manager.add_command('backfill_processing_time', commands.BackfillProcessingTime)
@manager.command
+5 -6
View File
@@ -1,19 +1,18 @@
"""${message}
"""
Revision ID: ${up_revision}
Revises: ${down_revision}
Create Date: ${create_date}
"""
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
def upgrade():
${upgrades if upgrades else "pass"}
@@ -0,0 +1,33 @@
"""empty message
Revision ID: 0117_international_sms_notify
Revises: 0116_another_letter_org
Create Date: 2017-08-29 14:09:41.042061
"""
# revision identifiers, used by Alembic.
revision = '0117_international_sms_notify'
down_revision = '0116_another_letter_org'
from alembic import op
from datetime import datetime
NOTIFY_SERVICE_ID = 'd6aa2c68-a2d9-4437-ab19-3ae8eb202553'
def upgrade():
op.execute("""
INSERT INTO service_permissions VALUES
('{}', 'international_sms', '{}')
""".format(NOTIFY_SERVICE_ID, datetime.utcnow()))
def downgrade():
op.execute("""
DELETE FROM service_permissions
WHERE
service_id = '{}' AND
permission = 'international_sms'
""".format(NOTIFY_SERVICE_ID))
@@ -0,0 +1,39 @@
"""empty message
Revision ID: 0118_service_sms_senders
Revises: 0117_international_sms_notify
Create Date: 2017-09-05 17:29:38.921045
"""
# revision identifiers, used by Alembic.
revision = '0118_service_sms_senders'
down_revision = '0117_international_sms_notify'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
def upgrade():
op.create_table('service_sms_senders',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('sms_sender', sa.String(length=11), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('inbound_number_id', postgresql.UUID(as_uuid=True), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['inbound_number_id'], ['inbound_numbers.id'], ),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_service_sms_senders_inbound_number_id'), 'service_sms_senders', ['inbound_number_id'],
unique=True)
op.create_index(op.f('ix_service_sms_senders_service_id'), 'service_sms_senders', ['service_id'], unique=True)
def downgrade():
op.drop_index(op.f('ix_service_sms_senders_service_id'), table_name='service_sms_senders')
op.drop_index(op.f('ix_service_sms_senders_inbound_number_id'), table_name='service_sms_senders')
op.drop_table('service_sms_senders')
@@ -0,0 +1,34 @@
"""
Revision ID: 0119_add_email_reply_to
Revises: 0118_service_sms_senders
Create Date: 2017-09-07 15:29:49.087143
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
revision = '0119_add_email_reply_to'
down_revision = '0118_service_sms_senders'
def upgrade():
op.create_table('service_email_reply_to',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('email_address', sa.Text(), nullable=False),
sa.Column('is_default', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=False),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(
op.f('ix_service_email_reply_to_service_id'), 'service_email_reply_to', ['service_id'], unique=False
)
def downgrade():
op.drop_index(op.f('ix_service_email_reply_to_service_id'), table_name='service_email_reply_to')
op.drop_table('service_email_reply_to')
+6 -6
View File
@@ -1,4 +1,4 @@
boto3==1.4.6
boto3==1.4.7
celery==3.1.25 # pyup: <4
docopt==0.6.2
# ignore flask-bcrypt - when upgrading, it installs an invalid version of bcrypt that isn't removed when a different
@@ -6,7 +6,7 @@ docopt==0.6.2
# TODO: Upgrade flask-bcrypt in a safe way
Flask-Bcrypt==0.6.2 # pyup: ignore
Flask-Marshmallow==0.8.0
Flask-Migrate==2.1.0
Flask-Migrate==2.1.1
Flask-Script==2.0.5
Flask-SQLAlchemy==2.2
Flask==0.12.2
@@ -16,19 +16,19 @@ jsonschema==2.6.0
marshmallow-sqlalchemy==0.13.1
marshmallow==2.13.6
monotonic==1.3
psycopg2==2.7.3
psycopg2==2.7.3.1
PyJWT==1.5.2
six==1.10.0
SQLAlchemy-Utils==0.32.14
SQLAlchemy==1.1.13
SQLAlchemy==1.1.14
statsd==3.2.1
notifications-python-client==4.3.1
notifications-python-client==4.4.0
# PaaS
awscli>=1.11,<1.12
awscli-cwlogs>=1.4,<1.5
git+https://github.com/alphagov/notifications-utils.git@20.0.3#egg=notifications-utils==20.0.3
git+https://github.com/alphagov/notifications-utils.git@21.0.0#egg=notifications-utils==21.0.0
git+https://github.com/alphagov/boto.git@2.43.0-patch3#egg=boto==2.43.0-patch3
+2 -2
View File
@@ -1,11 +1,11 @@
-r requirements.txt
pycodestyle==2.3.1
pytest==3.2.1
pytest==3.2.2
pytest-mock==1.6.2
pytest-cov==2.5.1
pytest-xdist==1.20.0
coveralls==1.2.0
moto==1.1.1
moto==1.1.2
freezegun==0.3.9
requests-mock==1.3.0
strict-rfc3339==0.7
+1 -1
View File
@@ -26,7 +26,7 @@ function configure_aws_logs {
state_file = /home/vcap/logs/awslogs-state
[/home/vcap/logs/app.log]
file = /home/vcap/logs/app.log*.json
file = /home/vcap/logs/app.log.json
log_group_name = paas-${CW_APP_NAME}-application
log_stream_name = {hostname}
EOF
+6 -7
View File
@@ -1,5 +1,3 @@
import uuid
from datetime import datetime, timedelta
from functools import partial
from unittest.mock import call, patch, PropertyMock
@@ -29,7 +27,8 @@ from app.celery.scheduled_tasks import (
switch_current_sms_provider_on_slow_delivery,
timeout_job_statistics,
timeout_notifications,
populate_monthly_billing)
populate_monthly_billing,
send_total_sent_notifications_to_performance_platform)
from app.clients.performance_platform.performance_platform_client import PerformancePlatformClient
from app.config import QueueNames, TaskNames
from app.dao.jobs_dao import dao_get_job_by_id
@@ -43,7 +42,7 @@ from app.models import (
SMS_TYPE, LETTER_TYPE,
JOB_STATUS_READY_TO_SEND,
MonthlyBilling)
from app.utils import get_london_midnight_in_utc, convert_utc_to_bst
from app.utils import get_london_midnight_in_utc
from tests.app.db import create_notification, create_service, create_template, create_job, create_rate
from tests.app.conftest import (
sample_job as create_sample_job,
@@ -285,7 +284,7 @@ def test_send_daily_performance_stats_calls_does_not_send_if_inactive(client, mo
@freeze_time("2016-01-11 12:30:00")
def test_send_daily_performance_stats_calls_with_correct_totals(
def test_send_total_sent_notifications_to_performance_platform_calls_with_correct_totals(
notify_db,
notify_db_session,
sample_template,
@@ -319,7 +318,7 @@ def test_send_daily_performance_stats_calls_with_correct_totals(
new_callable=PropertyMock
) as mock_active:
mock_active.return_value = True
send_daily_performance_platform_stats()
send_total_sent_notifications_to_performance_platform()
perf_mock.assert_has_calls([
call(get_london_midnight_in_utc(yesterday), 'sms', 2),
@@ -682,7 +681,7 @@ def test_populate_monthly_billing_updates_correct_month_in_bst(sample_template):
def test_run_letter_jobs(client, mocker, sample_letter_template):
jobs = [create_job(template=sample_letter_template, job_status=JOB_STATUS_READY_TO_SEND),
create_job(template=sample_letter_template, job_status=JOB_STATUS_READY_TO_SEND)]
job_ids = [str(job.id) for job in jobs]
job_ids = [str(j.id) for j in jobs]
mocker.patch(
"app.celery.scheduled_tasks.dao_get_letter_job_ids_by_status",
return_value=job_ids
+59 -1
View File
@@ -29,6 +29,7 @@ from app.celery.tasks import (
update_letter_notifications_statuses,
process_updates_from_file,
send_inbound_sms_to_service)
from app.config import QueueNames
from app.dao import jobs_dao, services_dao
from app.models import (
Notification,
@@ -38,7 +39,8 @@ from app.models import (
SMS_TYPE,
EMAIL_TYPE,
LETTER_TYPE,
Job)
Job,
JOB_STATUS_ERROR)
from tests.app import load_example_csv
from tests.conftest import set_config
@@ -608,6 +610,62 @@ def test_should_put_send_email_task_in_research_mode_queue_if_research_mode_serv
)
def test_should_not_build_dvla_file_in_research_mode_for_letter_job(
mocker, sample_service, sample_letter_job, fake_uuid
):
test_encrypted_data = 'some encrypted data'
sample_service.research_mode = True
csv = """address_line_1,address_line_2,address_line_3,address_line_4,postcode,name
A1,A2,A3,A4,A_POST,Alice
"""
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=csv)
mocker.patch('app.celery.tasks.update_job_to_sent_to_dvla.apply_async')
mocker.patch('app.celery.tasks.persist_letter.apply_async')
mocker.patch('app.celery.tasks.create_uuid', return_value=fake_uuid)
mocker.patch('app.celery.tasks.encryption.encrypt', return_value=test_encrypted_data)
mock_dvla_file_task = mocker.patch('app.celery.tasks.build_dvla_file.apply_async')
process_job(sample_letter_job.id)
assert not mock_dvla_file_task.called
@freeze_time("2017-08-29 17:30:00")
def test_should_update_job_to_sent_to_dvla_in_research_mode_for_letter_job(
mocker, sample_service, sample_letter_job, fake_uuid
):
test_encrypted_data = 'some encrypted data'
sample_service.research_mode = True
csv = """address_line_1,address_line_2,address_line_3,address_line_4,postcode,name
A1,A2,A3,A4,A_POST,Alice
"""
mocker.patch('app.celery.tasks.s3.get_job_from_s3', return_value=csv)
mocker.patch('app.celery.tasks.update_job_to_sent_to_dvla.apply_async')
mocker.patch('app.celery.tasks.persist_letter.apply_async')
mocker.patch('app.celery.tasks.create_uuid', return_value=fake_uuid)
mocker.patch('app.celery.tasks.encryption.encrypt', return_value=test_encrypted_data)
mock_dvla_file_task = mocker.patch('app.celery.tasks.build_dvla_file.apply_async')
process_job(sample_letter_job.id)
job = jobs_dao.dao_get_job_by_id(sample_letter_job.id)
persist_letter.apply_async.assert_called_once_with(
(
str(sample_service.id),
fake_uuid,
test_encrypted_data,
datetime.utcnow().strftime(DATETIME_FORMAT)
),
queue=QueueNames.RESEARCH_MODE
)
update_job_to_sent_to_dvla.apply_async.assert_called_once_with(
[str(job.id)], queue=QueueNames.RESEARCH_MODE)
def test_should_send_sms_template_to_and_persist_with_job_id(sample_job, sample_api_key, mocker):
notification = _notification_json(
sample_job.template,
+28 -4
View File
@@ -136,24 +136,30 @@ def sample_service(
restricted=False,
limit=1000,
email_from=None,
permissions=[SMS_TYPE, EMAIL_TYPE]
permissions=[SMS_TYPE, EMAIL_TYPE],
research_mode=None
):
if user is None:
user = create_user()
if email_from is None:
email_from = service_name.lower().replace(' ', '.')
data = {
'name': service_name,
'message_limit': limit,
'restricted': restricted,
'email_from': email_from,
'created_by': user,
'letter_contact_block': 'London,\nSW1A 1AA',
'letter_contact_block': 'London,\nSW1A 1AA'
}
service = Service.query.filter_by(name=service_name).first()
if not service:
service = Service(**data)
dao_create_service(service, user, service_permissions=permissions)
if research_mode:
service.research_mode = research_mode
else:
if user not in service.users:
dao_add_user_to_service(service, user)
@@ -206,6 +212,7 @@ def sample_template(
})
template = Template(**data)
dao_create_template(template)
return template
@@ -271,6 +278,12 @@ def sample_letter_template(sample_service_full_permissions):
return create_template(sample_service_full_permissions, template_type=LETTER_TYPE)
@pytest.fixture
def sample_trial_letter_template(sample_service_full_permissions):
sample_service_full_permissions.restricted = True
return create_template(sample_service_full_permissions, template_type=LETTER_TYPE)
@pytest.fixture(scope='function')
def sample_email_template_with_placeholders(notify_db, notify_db_session):
return sample_email_template(
@@ -631,14 +644,22 @@ def sample_notification_history(
status='created',
created_at=None,
notification_type=None,
key_type=KEY_TYPE_NORMAL
key_type=KEY_TYPE_NORMAL,
sent_at=None,
api_key=None
):
if created_at is None:
created_at = datetime.utcnow()
if sent_at is None:
sent_at = datetime.utcnow()
if notification_type is None:
notification_type = sample_template.template_type
if not api_key:
api_key = create_api_key(sample_template.service, key_type=key_type)
notification_history = NotificationHistory(
id=uuid.uuid4(),
service=sample_template.service,
@@ -647,7 +668,10 @@ def sample_notification_history(
status=status,
created_at=created_at,
notification_type=notification_type,
key_type=key_type
key_type=key_type,
api_key=api_key,
api_key_id=api_key and api_key.id,
sent_at=sent_at
)
notify_db.session.add(notification_history)
notify_db.session.commit()
@@ -0,0 +1,135 @@
from datetime import date, datetime, timedelta
from freezegun import freeze_time
from app.dao.notifications_dao import dao_get_total_notifications_sent_per_day_for_performance_platform
from app.models import KEY_TYPE_NORMAL, KEY_TYPE_TEAM, KEY_TYPE_TEST
from tests.app.db import create_notification
from tests.app.conftest import (
sample_notification_history,
sample_service,
sample_template
)
BEGINNING_OF_DAY = date(2016, 10, 18)
END_OF_DAY = date(2016, 10, 19)
def test_get_total_notifications_filters_on_date_within_date_range(sample_template):
create_notification(sample_template, created_at=datetime(2016, 10, 17, 23, 59, 59))
create_notification(sample_template, created_at=BEGINNING_OF_DAY)
create_notification(sample_template, created_at=datetime(2016, 10, 18, 23, 59, 59))
create_notification(sample_template, created_at=END_OF_DAY)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 2
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_only_counts_api_notifications(sample_template, sample_job, sample_api_key):
create_notification(sample_template, one_off=True)
create_notification(sample_template, one_off=True)
create_notification(sample_template, job=sample_job)
create_notification(sample_template, job=sample_job)
create_notification(sample_template, api_key=sample_api_key)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 1
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_ignores_test_keys(sample_template):
# Creating multiple templates with normal and team keys but only 1 template
# with a test key to test that the count ignores letters
create_notification(sample_template, key_type=KEY_TYPE_NORMAL)
create_notification(sample_template, key_type=KEY_TYPE_NORMAL)
create_notification(sample_template, key_type=KEY_TYPE_TEAM)
create_notification(sample_template, key_type=KEY_TYPE_TEAM)
create_notification(sample_template, key_type=KEY_TYPE_TEST)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 4
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_ignores_letters(
sample_template,
sample_email_template,
sample_letter_template
):
# Creating multiple sms and email templates but only 1 letter template to
# test that the count ignores letters
create_notification(sample_template)
create_notification(sample_template)
create_notification(sample_email_template)
create_notification(sample_email_template)
create_notification(sample_letter_template)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 4
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_counts_messages_within_10_seconds(sample_template):
created_at = datetime.utcnow()
create_notification(sample_template, sent_at=created_at + timedelta(seconds=5))
create_notification(sample_template, sent_at=created_at + timedelta(seconds=10))
create_notification(sample_template, sent_at=created_at + timedelta(seconds=15))
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 3
assert result.messages_within_10_secs == 2
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_counts_messages_that_have_not_sent(sample_template):
create_notification(sample_template, status='created', sent_at=None)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 1
assert result.messages_within_10_secs == 0
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_returns_zero_if_no_data(notify_db_session):
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 0
assert result.messages_within_10_secs == 0
@freeze_time('2016-10-18T10:00')
def test_get_total_notifications_counts_ignores_research_mode(notify_db, notify_db_session):
created_at = datetime.utcnow()
service = sample_service(notify_db, notify_db_session, research_mode=True)
template = sample_template(notify_db, notify_db_session, service=service)
create_notification(template, status='created', sent_at=None)
sample_notification_history(
notify_db,
notify_db_session,
template,
notification_type='email',
sent_at=created_at + timedelta(seconds=5)
)
sample_notification_history(
notify_db,
notify_db_session,
template,
notification_type='sms',
sent_at=created_at + timedelta(seconds=5)
)
result = dao_get_total_notifications_sent_per_day_for_performance_platform(BEGINNING_OF_DAY, END_OF_DAY)
assert result.messages_total == 2
assert result.messages_within_10_secs == 2
-1
View File
@@ -3,7 +3,6 @@ from dateutil.relativedelta import relativedelta
from freezegun import freeze_time
from functools import partial
from app import db
from app.dao.monthly_billing_dao import (
create_or_update_monthly_billing,
get_monthly_billing_entry,
+17 -119
View File
@@ -6,7 +6,6 @@ from freezegun import freeze_time
from app.dao.date_util import get_financial_year
from app.dao.notification_usage_dao import (
get_rates_for_daterange,
get_yearly_billing_data,
get_billing_data_for_month,
get_monthly_billing_data
)
@@ -21,7 +20,7 @@ def test_get_rates_for_daterange(notify_db, notify_db_session):
set_up_rate(notify_db, datetime(2016, 5, 18), 0.016)
set_up_rate(notify_db, datetime(2017, 3, 31, 23), 0.0158)
start_date, end_date = get_financial_year(2017)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates) == 1
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-03-31 23:00:00"
assert rates[0].rate == 0.0158
@@ -32,7 +31,7 @@ def test_get_rates_for_daterange_multiple_result_per_year(notify_db, notify_db_s
set_up_rate(notify_db, datetime(2016, 5, 18), 0.016)
set_up_rate(notify_db, datetime(2017, 4, 1), 0.0158)
start_date, end_date = get_financial_year(2016)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates) == 2
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-04-01 00:00:00"
assert rates[0].rate == 0.015
@@ -45,7 +44,7 @@ def test_get_rates_for_daterange_returns_correct_rates(notify_db, notify_db_sess
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
start_date, end_date = get_financial_year(2017)
rates_2017 = get_rates_for_daterange(start_date, end_date, 'sms')
rates_2017 = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates_2017) == 2
assert datetime.strftime(rates_2017[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-09-01 00:00:00"
assert rates_2017[0].rate == 0.016
@@ -57,7 +56,7 @@ def test_get_rates_for_daterange_in_the_future(notify_db, notify_db_session):
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
start_date, end_date = get_financial_year(2018)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-06-01 00:00:00"
assert rates[0].rate == 0.0175
@@ -66,7 +65,7 @@ def test_get_rates_for_daterange_returns_empty_list_if_year_is_before_earliest_r
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
start_date, end_date = get_financial_year(2015)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert rates == []
@@ -76,7 +75,7 @@ def test_get_rates_for_daterange_early_rate(notify_db, notify_db_session):
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
start_date, end_date = get_financial_year(2016)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates) == 3
@@ -84,7 +83,7 @@ def test_get_rates_for_daterange_edge_case(notify_db, notify_db_session):
set_up_rate(notify_db, datetime(2016, 3, 31, 23, 00), 0.015)
set_up_rate(notify_db, datetime(2017, 3, 31, 23, 00), 0.0175)
start_date, end_date = get_financial_year(2016)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates) == 1
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-03-31 23:00:00"
assert rates[0].rate == 0.015
@@ -97,100 +96,12 @@ def test_get_rates_for_daterange_where_daterange_is_one_month_that_falls_between
set_up_rate(notify_db, datetime(2017, 3, 31), 0.123)
start_date = datetime(2017, 2, 1, 00, 00, 00)
end_date = datetime(2017, 2, 28, 23, 59, 59, 99999)
rates = get_rates_for_daterange(start_date, end_date, 'sms')
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
assert len(rates) == 1
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-01-01 00:00:00"
assert rates[0].rate == 0.175
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), 0.014)
set_up_rate(notify_db, datetime(2016, 6, 1), 0.0158)
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0165)
# 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, 0.014)
assert results[1] == (9, 9, 1, 'sms', False, 0.0158)
assert results[2] == (6, 3, 2, 'sms', True, 0.0158)
assert results[3] == (2, 2, 1, 'email', False, 0)
def test_get_future_yearly_billing_data(notify_db, notify_db_session, sample_template, sample_email_template):
set_up_rate(notify_db, datetime(2017, 4, 1), 0.0158)
create_notification(template=sample_template, created_at=datetime(2017, 3, 30), sent_at=datetime(2017, 3, 30),
status='sending', billable_units=1)
create_notification(template=sample_template, created_at=datetime(2017, 4, 6), sent_at=datetime(2017, 4, 6),
status='sending', billable_units=1)
create_notification(template=sample_template, created_at=datetime(2017, 4, 6), sent_at=datetime(2017, 4, 6),
status='sending', billable_units=1)
results = get_yearly_billing_data(sample_template.service_id, 2018)
assert len(results) == 2
assert results[0] == (0, 0, 1, 'sms', False, 0.0158)
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), 0.014)
# 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, 0.014)
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), 0.014)
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, 0.014)
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), 0.014)
# previous year
@@ -220,10 +131,10 @@ def test_get_monthly_billing_data(notify_db, notify_db_session, sample_template,
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', 0.014)
assert results[1] == ('May', 2, 1, False, 'sms', 0.014)
assert results[2] == ('July', 7, 1, False, 'sms', 0.014)
assert results[3] == ('July', 6, 2, False, 'sms', 0.014)
assert results[0] == ('April', 1, 1, False, SMS_TYPE, 0.014)
assert results[1] == ('May', 2, 1, False, SMS_TYPE, 0.014)
assert results[2] == ('July', 7, 1, False, SMS_TYPE, 0.014)
assert results[3] == ('July', 6, 2, False, SMS_TYPE, 0.014)
def test_get_monthly_billing_data_with_multiple_rates(notify_db, notify_db_session, sample_template,
@@ -254,10 +165,10 @@ def test_get_monthly_billing_data_with_multiple_rates(notify_db, notify_db_sessi
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', 0.014)
assert results[1] == ('May', 2, 1, False, 'sms', 0.014)
assert results[2] == ('June', 3, 1, False, 'sms', 0.014)
assert results[3] == ('June', 4, 1, False, 'sms', 0.0175)
assert results[0] == ('April', 1, 1, False, SMS_TYPE, 0.014)
assert results[1] == ('May', 2, 1, False, SMS_TYPE, 0.014)
assert results[2] == ('June', 3, 1, False, SMS_TYPE, 0.014)
assert results[3] == ('June', 4, 1, False, SMS_TYPE, 0.0175)
def test_get_monthly_billing_data_with_no_notifications_for_daterange(notify_db, notify_db_session, sample_template):
@@ -267,23 +178,10 @@ def test_get_monthly_billing_data_with_no_notifications_for_daterange(notify_db,
def set_up_rate(notify_db, start_date, value):
rate = Rate(id=uuid.uuid4(), valid_from=start_date, rate=value, notification_type='sms')
rate = Rate(id=uuid.uuid4(), valid_from=start_date, rate=value, notification_type=SMS_TYPE)
notify_db.session.add(rate)
def test_get_yearly_billing_data_for_start_date_before_rate_returns_empty(
sample_template
):
create_rate(datetime(2016, 4, 1), 0.014, SMS_TYPE)
results = get_yearly_billing_data(
service_id=sample_template.service_id,
year=2015
)
assert not results
@freeze_time("2016-05-01")
def test_get_billing_data_for_month_where_start_date_before_rate_returns_empty(
sample_template
@@ -0,0 +1,41 @@
from app.dao.service_email_reply_to_dao import (
create_or_update_email_reply_to,
dao_get_reply_to_by_service_id
)
from app.models import ServiceEmailReplyTo
from tests.app.db import create_reply_to_email, create_service
def test_create_or_update_email_reply_to_does_not_create_another_entry(notify_db_session):
service = create_service()
create_reply_to_email(service, 'test@mail.com')
create_or_update_email_reply_to(service.id, 'different@mail.com')
reply_to = dao_get_reply_to_by_service_id(service.id)
assert ServiceEmailReplyTo.query.count() == 1
def test_create_or_update_email_reply_to_updates_existing_entry(notify_db_session):
service = create_service()
create_reply_to_email(service, 'test@mail.com')
create_or_update_email_reply_to(service.id, 'different@mail.com')
reply_to = dao_get_reply_to_by_service_id(service.id)
assert reply_to.service.id == service.id
assert reply_to.email_address == 'different@mail.com'
def test_create_or_update_email_reply_to_creates_new_entry(notify_db_session):
service = create_service()
create_or_update_email_reply_to(service.id, 'test@mail.com')
reply_to = dao_get_reply_to_by_service_id(service.id)
assert ServiceEmailReplyTo.query.count() == 1
assert reply_to.service.id == service.id
assert reply_to.email_address == 'test@mail.com'
+7 -2
View File
@@ -23,19 +23,24 @@ from app.models import User, VerifyCode
from tests.app.db import create_user
def test_create_user(notify_db_session):
@pytest.mark.parametrize('phone_number', [
'+447700900986',
'+1-800-555-5555',
])
def test_create_user(notify_db_session, phone_number):
email = 'notify@digital.cabinet-office.gov.uk'
data = {
'name': 'Test User',
'email_address': email,
'password': 'password',
'mobile_number': '+447700900986'
'mobile_number': phone_number
}
user = User(**data)
save_model_user(user)
assert User.query.count() == 1
assert User.query.first().email_address == email
assert User.query.first().id == user.id
assert User.query.first().mobile_number == phone_number
assert not user.platform_admin
+22 -3
View File
@@ -25,7 +25,9 @@ from app.models import (
SMS_TYPE,
INBOUND_SMS_TYPE,
KEY_TYPE_NORMAL,
ServiceInboundApi)
ServiceInboundApi,
ServiceEmailReplyTo
)
from app.dao.users_dao import save_model_user
from app.dao.notifications_dao import dao_create_notification, dao_created_scheduled_notification
from app.dao.templates_dao import dao_create_template
@@ -123,7 +125,8 @@ def create_notification(
international=False,
phone_prefix=None,
scheduled_for=None,
normalised_to=None
normalised_to=None,
one_off=False,
):
if created_at is None:
created_at = datetime.utcnow()
@@ -132,7 +135,7 @@ def create_notification(
sent_at = sent_at or datetime.utcnow()
updated_at = updated_at or datetime.utcnow()
if job is None and api_key is None:
if not one_off and (job is None and api_key is None):
# we didn't specify in test - lets create it
api_key = ApiKey.query.filter(ApiKey.service == template.service, ApiKey.key_type == key_type).first()
if not api_key:
@@ -326,3 +329,19 @@ def create_monthly_billing_entry(
db.session.commit()
return entry
def create_reply_to_email(
service,
email_address
):
data = {
'service': service,
'email_address': email_address,
}
reply_to = ServiceEmailReplyTo(**data)
db.session.add(reply_to)
db.session.commit()
return reply_to
+23
View File
@@ -186,6 +186,29 @@ def test_create_job_returns_403_if_service_is_not_active(client, fake_uuid, samp
mock_job_dao.assert_not_called()
def test_create_job_returns_403_if_letter_template_type_and_service_in_trial(
client, fake_uuid, sample_service, sample_trial_letter_template, mocker):
data = {
'id': fake_uuid,
'service': str(sample_trial_letter_template.service.id),
'template': str(sample_trial_letter_template.id),
'original_file_name': 'thisisatest.csv',
'notification_count': 1,
'created_by': str(sample_trial_letter_template.created_by.id)
}
mock_job_dao = mocker.patch("app.dao.jobs_dao.dao_create_job")
auth_header = create_authorization_header()
response = client.post('/service/{}/job'.format(sample_service.id),
data=json.dumps(data),
headers=[('Content-Type', 'application/json'), auth_header])
assert response.status_code == 403
resp_json = json.loads(response.get_data(as_text=True))
assert resp_json['result'] == 'error'
assert resp_json['message'] == "Create letter job is not allowed for service in trial mode "
mock_job_dao.assert_not_called()
def test_should_not_create_scheduled_job_more_then_24_hours_hence(notify_api, sample_template, mocker, fake_uuid):
with notify_api.test_request_context():
with notify_api.test_client() as client:
+6 -2
View File
@@ -5,11 +5,15 @@ from flask import json
from app.variables import LETTER_TEST_API_FILENAME
from tests import create_authorization_header
from tests.app.db import create_job
def test_send_letter_jobs(client, mocker):
def test_send_letter_jobs(client, mocker, sample_letter_template):
mock_celery = mocker.patch("app.letters.rest.notify_celery.send_task")
job_ids = {"job_ids": [str(uuid.uuid4()), str(uuid.uuid4()), str(uuid.uuid4())]}
job_1 = create_job(sample_letter_template)
job_2 = create_job(sample_letter_template)
job_3 = create_job(sample_letter_template)
job_ids = {"job_ids": [str(job_1.id), str(job_2.id), str(job_3.id)]}
auth_header = create_authorization_header()
@@ -0,0 +1,47 @@
from datetime import datetime, timedelta
from freezegun import freeze_time
from tests.app.db import create_notification
from app.performance_platform.processing_time import (
send_processing_time_to_performance_platform,
send_processing_time_data
)
@freeze_time('2016-10-18T02:00')
def test_send_processing_time_to_performance_platform_generates_correct_calls(mocker, sample_template):
send_mock = mocker.patch('app.performance_platform.processing_time.send_processing_time_data')
created_at = datetime.utcnow() - timedelta(days=1)
create_notification(sample_template, created_at=created_at, sent_at=created_at + timedelta(seconds=5))
create_notification(sample_template, created_at=created_at, sent_at=created_at + timedelta(seconds=15))
create_notification(sample_template, created_at=datetime.utcnow() - timedelta(days=2))
send_processing_time_to_performance_platform()
send_mock.assert_any_call(datetime(2016, 10, 16, 23, 0), 'messages-total', 2)
send_mock.assert_any_call(datetime(2016, 10, 16, 23, 0), 'messages-within-10-secs', 1)
def test_send_processing_time_to_performance_platform_creates_correct_call_to_perf_platform(mocker):
send_stats = mocker.patch('app.performance_platform.total_sent_notifications.performance_platform_client.send_stats_to_performance_platform') # noqa
send_processing_time_data(
date=datetime(2016, 10, 15, 23, 0, 0),
status='foo',
count=142
)
assert send_stats.call_count == 1
request_args = send_stats.call_args[0][0]
assert request_args['dataType'] == 'processing-time'
assert request_args['service'] == 'govuk-notify'
assert request_args['period'] == 'day'
assert request_args['status'] == 'foo'
assert request_args['_timestamp'] == '2016-10-16T00:00:00'
assert request_args['count'] == 142
expected_base64_id = 'MjAxNi0xMC0xNlQwMDowMDowMGdvdnVrLW5vdGlmeWZvb3Byb2Nlc3NpbmctdGltZWRheQ=='
assert request_args['_id'] == expected_base64_id
+16 -115
View File
@@ -8,12 +8,11 @@ import pytest
from flask import url_for, current_app
from freezegun import freeze_time
from app.dao.monthly_billing_dao import create_or_update_monthly_billing
from app.dao.services_dao import dao_remove_user_from_service
from app.dao.templates_dao import dao_redact_template
from app.dao.users_dao import save_model_user
from app.models import (
User, Organisation, Rate, Service, ServicePermission, Notification,
User, Organisation, Service, ServicePermission, Notification,
DVLA_ORG_LAND_REGISTRY,
KEY_TYPE_NORMAL, KEY_TYPE_TEAM, KEY_TYPE_TEST,
EMAIL_TYPE, SMS_TYPE, LETTER_TYPE, INTERNATIONAL_SMS_TYPE, INBOUND_SMS_TYPE,
@@ -1730,119 +1729,6 @@ def test_get_template_stats_by_month_returns_error_for_incorrect_year(
assert json.loads(response.get_data(as_text=True)) == expected_json
def test_get_yearly_billing_usage(client, notify_db, notify_db_session, sample_service):
rate = Rate(id=uuid.uuid4(), valid_from=datetime(2016, 3, 31, 23, 00), rate=0.0158, notification_type=SMS_TYPE)
notify_db.session.add(rate)
after_rate_created = datetime(2016, 6, 5)
notification = create_sample_notification(
notify_db,
notify_db_session,
created_at=after_rate_created,
sent_at=after_rate_created,
status='sending',
service=sample_service
)
create_or_update_monthly_billing(sample_service.id, after_rate_created)
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_TYPE,
'international': False,
'rate': 0.0158},
{'credits': 0,
'billing_units': 0,
'rate_multiplier': 1,
'notification_type': EMAIL_TYPE,
'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=0.0158, notification_type=SMS_TYPE)
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_TYPE)
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_TYPE,
'rate': 0.0158,
'billing_units': 1},
{'month': 'June',
'international': False,
'rate_multiplier': 2,
'notification_type': SMS_TYPE,
'rate': 0.0158,
'billing_units': 1},
{'month': 'July',
'international': False,
'rate_multiplier': 1,
'notification_type': SMS_TYPE,
'rate': 0.0158,
'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=0.0158, notification_type=SMS_TYPE)
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
results = json.loads(response.get_data(as_text=True))
assert results == []
def test_search_for_notification_by_to_field(client, notify_db, notify_db_session):
create_notification = partial(create_sample_notification, notify_db, notify_db_session)
notification1 = create_notification(to_field='+447700900855', normalised_to='447700900855')
@@ -2237,3 +2123,18 @@ def test_is_service_name_unique_returns_400_when_name_does_not_exist(client):
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp["message"][0]["name"] == ["Can't be empty"]
assert json_resp["message"][1]["email_from"] == ["Can't be empty"]
def test_update_service_reply_to_email_address_upserts_email_reply_to(mocker, admin_request, sample_service):
update_mock = mocker.patch('app.service.rest.create_or_update_email_reply_to')
admin_request.post(
'service.update_service',
service_id=sample_service.id,
_data={
'reply_to_email_address': 'new@mail.com'
},
_expected_status=200
)
assert update_mock.called
+5 -5
View File
@@ -24,15 +24,15 @@ def test_get_whitelist_returns_data(client, sample_service_whitelist):
def test_get_whitelist_separates_emails_and_phones(client, sample_service):
dao_add_and_commit_whitelisted_contacts([
ServiceWhitelist.from_string(sample_service.id, EMAIL_TYPE, 'service@example.com'),
ServiceWhitelist.from_string(sample_service.id, MOBILE_TYPE, '07123456789')
ServiceWhitelist.from_string(sample_service.id, MOBILE_TYPE, '07123456789'),
ServiceWhitelist.from_string(sample_service.id, MOBILE_TYPE, '+1800-555-555'),
])
response = client.get('service/{}/whitelist'.format(sample_service.id), headers=[create_authorization_header()])
assert response.status_code == 200
assert json.loads(response.get_data(as_text=True)) == {
'email_addresses': ['service@example.com'],
'phone_numbers': ['07123456789']
}
json_resp = json.loads(response.get_data(as_text=True))
assert json_resp['email_addresses'] == ['service@example.com']
assert sorted(json_resp['phone_numbers']) == sorted(['+1800-555-555', '07123456789'])
def test_get_whitelist_404s_with_unknown_service_id(client):
+2 -2
View File
@@ -213,7 +213,7 @@ def test_sms_inbound_config():
def test_performance_platform_config():
extract_cloudfoundry_config()
assert os.environ['PERFORMANCE_PLATFORM_ENDPOINTS'] == json.dumps({
assert json.loads(os.environ['PERFORMANCE_PLATFORM_ENDPOINTS']) == {
'foo': 'my_token',
'bar': 'other_token'
})
}
+14
View File
@@ -0,0 +1,14 @@
from datetime import datetime
from app.commands import BackfillProcessingTime
def test_backfill_processing_time_works_for_correct_dates(mocker):
send_mock = mocker.patch('app.commands.send_processing_time_for_start_and_end')
BackfillProcessingTime().run('2017-08-01', '2017-08-03')
assert send_mock.call_count == 3
send_mock.assert_any_call(datetime(2017, 7, 31, 23, 0), datetime(2017, 8, 1, 23, 0))
send_mock.assert_any_call(datetime(2017, 8, 1, 23, 0), datetime(2017, 8, 2, 23, 0))
send_mock.assert_any_call(datetime(2017, 8, 2, 23, 0), datetime(2017, 8, 3, 23, 0))
@@ -17,13 +17,12 @@ from app.variables import LETTER_TEST_API_FILENAME
from app.variables import LETTER_API_FILENAME
from tests import create_authorization_header
from tests.app.db import create_service
from tests.app.db import create_template
from tests.app.db import create_service, create_template
def letter_request(client, data, service_id, key_type=KEY_TYPE_NORMAL, _expected_status=201):
resp = client.post(
url_for('v2_notifications.post_notification', notification_type='letter'),
url_for('v2_notifications.post_notification', notification_type=LETTER_TYPE),
data=json.dumps(data),
headers=[
('Content-Type', 'application/json'),
@@ -232,3 +231,37 @@ def test_post_letter_notification_doesnt_accept_team_key(client, sample_letter_t
assert error_json['status_code'] == 403
assert error_json['errors'] == [{'error': 'BadRequestError', 'message': 'Cannot send letters with a team api key'}]
def test_post_letter_notification_doesnt_send_in_trial(client, sample_trial_letter_template):
data = {
'template_id': str(sample_trial_letter_template.id),
'personalisation': {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'Baz'}
}
error_json = letter_request(
client,
data,
sample_trial_letter_template.service_id,
_expected_status=403
)
assert error_json['status_code'] == 403
assert error_json['errors'] == [
{'error': 'BadRequestError', 'message': 'Cannot send letters when service is in trial mode'}]
def test_post_letter_notification_calls_update_job_sent_to_dvla_when_service_is_in_trial_mode_but_using_test_key(
client, sample_trial_letter_template, mocker):
build_dvla_task = mocker.patch('app.celery.tasks.build_dvla_file.apply_async')
update_job_task = mocker.patch('app.celery.tasks.update_job_to_sent_to_dvla.apply_async')
data = {
"template_id": sample_trial_letter_template.id,
"personalisation": {'address_line_1': 'Foo', 'address_line_2': 'Bar', 'postcode': 'Baz'}
}
letter_request(client, data=data, service_id=sample_trial_letter_template.service_id,
key_type=KEY_TYPE_TEST)
job = Job.query.one()
update_job_task.assert_called_once_with([str(job.id)], queue='research-mode-tasks')
assert not build_dvla_task.called