mirror of
https://github.com/GSA/notifications-api.git
synced 2026-09-09 17:45:41 -04:00
Merge branch 'master' into pyup-update-monotonic-1.2-to-1.3
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -69,7 +69,5 @@ environment.sh
|
|||||||
|
|
||||||
celerybeat-schedule
|
celerybeat-schedule
|
||||||
|
|
||||||
wheelhouse/
|
|
||||||
|
|
||||||
# CloudFoundry
|
# CloudFoundry
|
||||||
.cf
|
.cf
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -80,8 +80,7 @@ generate-version-file: ## Generates the app version file
|
|||||||
|
|
||||||
.PHONY: build
|
.PHONY: build
|
||||||
build: dependencies generate-version-file ## Build project
|
build: dependencies generate-version-file ## Build project
|
||||||
rm -rf wheelhouse
|
. venv/bin/activate && PIP_ACCEL_CACHE=${PIP_ACCEL_CACHE} pip-accel install -r requirements.txt
|
||||||
. venv/bin/activate && PIP_ACCEL_CACHE=${PIP_ACCEL_CACHE} pip-accel wheel --wheel-dir=wheelhouse -r requirements.txt
|
|
||||||
|
|
||||||
.PHONY: cf-build
|
.PHONY: cf-build
|
||||||
cf-build: dependencies generate-version-file ## Build project for PAAS
|
cf-build: dependencies generate-version-file ## Build project for PAAS
|
||||||
@@ -260,7 +259,7 @@ clean-docker-containers: ## Clean up any remaining docker containers
|
|||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -rf node_modules cache target venv .coverage build tests/.cache wheelhouse
|
rm -rf node_modules cache target venv .coverage build tests/.cache
|
||||||
|
|
||||||
.PHONY: cf-login
|
.PHONY: cf-login
|
||||||
cf-login: ## Log in to Cloud Foundry
|
cf-login: ## Log in to Cloud Foundry
|
||||||
|
|||||||
@@ -52,13 +52,18 @@ def restrict_ip_sms():
|
|||||||
ip_list = ip_route.split(',')
|
ip_list = ip_route.split(',')
|
||||||
if len(ip_list) >= 3:
|
if len(ip_list) >= 3:
|
||||||
ip = ip_list[len(ip_list) - 3]
|
ip = ip_list[len(ip_list) - 3]
|
||||||
current_app.logger.info("Inbound sms ip route list {}".format(ip_route))
|
current_app.logger.info("Inbound sms ip route list {}"
|
||||||
|
.format(ip_route))
|
||||||
|
|
||||||
|
# Temporary custom header for route security - to experiment if the header passes through
|
||||||
|
if request.headers.get("X-Custom-forwarder"):
|
||||||
|
current_app.logger.info("X-Custom-forwarder {}".format(request.headers.get("X-Custom-forwarder")))
|
||||||
|
|
||||||
if ip in current_app.config.get('SMS_INBOUND_WHITELIST'):
|
if ip in current_app.config.get('SMS_INBOUND_WHITELIST'):
|
||||||
current_app.logger.info("Inbound sms ip addresses {} passed ".format(ip))
|
current_app.logger.info("Inbound sms ip addresses {} passed ".format(ip))
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
current_app.logger.info("Inbound sms ip addresses {} blocked ".format(ip))
|
current_app.logger.info("Inbound sms ip addresses blocked {}".format(ip))
|
||||||
return
|
return
|
||||||
# raise AuthError('Unknown source IP address from the SMS provider', 403)
|
# raise AuthError('Unknown source IP address from the SMS provider', 403)
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,17 @@ from sqlalchemy.exc import SQLAlchemyError
|
|||||||
from app.aws import s3
|
from app.aws import s3
|
||||||
from app import notify_celery
|
from app import notify_celery
|
||||||
from app import performance_platform_client
|
from app import performance_platform_client
|
||||||
|
from app.dao.date_util import get_month_start_end_date
|
||||||
from app.dao.inbound_sms_dao import delete_inbound_sms_created_more_than_a_week_ago
|
from app.dao.inbound_sms_dao import delete_inbound_sms_created_more_than_a_week_ago
|
||||||
from app.dao.invited_user_dao import delete_invitations_created_more_than_two_days_ago
|
from app.dao.invited_user_dao import delete_invitations_created_more_than_two_days_ago
|
||||||
from app.dao.jobs_dao import dao_set_scheduled_jobs_to_pending, dao_get_jobs_older_than_limited_by
|
from app.dao.jobs_dao import (
|
||||||
|
dao_set_scheduled_jobs_to_pending,
|
||||||
|
dao_get_jobs_older_than_limited_by
|
||||||
|
)
|
||||||
|
from app.dao.monthly_billing_dao import (
|
||||||
|
get_service_ids_that_need_sms_billing_populated,
|
||||||
|
create_or_update_monthly_billing_sms
|
||||||
|
)
|
||||||
from app.dao.notifications_dao import (
|
from app.dao.notifications_dao import (
|
||||||
dao_timeout_notifications,
|
dao_timeout_notifications,
|
||||||
is_delivery_slow_for_provider,
|
is_delivery_slow_for_provider,
|
||||||
@@ -281,3 +289,14 @@ def delete_dvla_response_files_older_than_seven_days():
|
|||||||
except SQLAlchemyError as e:
|
except SQLAlchemyError as e:
|
||||||
current_app.logger.exception("Failed to delete dvla response files")
|
current_app.logger.exception("Failed to delete dvla response files")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@notify_celery.task(name="populate_monthly_billing")
|
||||||
|
@statsd(namespace="tasks")
|
||||||
|
def populate_monthly_billing():
|
||||||
|
# for every service with billable units this month update billing totals for yesterday
|
||||||
|
# this will overwrite the existing amount.
|
||||||
|
yesterday = datetime.utcnow() - timedelta(days=1)
|
||||||
|
start_date, end_date = get_month_start_end_date(yesterday)
|
||||||
|
services = get_service_ids_that_need_sms_billing_populated(start_date=start_date, end_date=end_date)
|
||||||
|
[create_or_update_monthly_billing_sms(service_id=s.service_id, billing_month=yesterday) for s in services]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ from flask.ext.script import Command, Manager, Option
|
|||||||
|
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.models import (PROVIDERS, Service, User, NotificationHistory)
|
from app.dao.monthly_billing_dao import create_or_update_monthly_billing_sms, get_monthly_billing_sms
|
||||||
|
from app.models import (PROVIDERS, User)
|
||||||
from app.dao.services_dao import (
|
from app.dao.services_dao import (
|
||||||
delete_service_and_all_associated_db_objects,
|
delete_service_and_all_associated_db_objects,
|
||||||
dao_fetch_all_services_by_user
|
dao_fetch_all_services_by_user
|
||||||
@@ -146,3 +147,19 @@ class CustomDbScript(Command):
|
|||||||
print('Committed {} updates at {}'.format(len(result), datetime.utcnow()))
|
print('Committed {} updates at {}'.format(len(result), datetime.utcnow()))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
result = db.session.execute(subq_hist).fetchall()
|
result = db.session.execute(subq_hist).fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
class PopulateMonthlyBilling(Command):
|
||||||
|
option_list = (
|
||||||
|
Option('-s', '-service-id', dest='service_id',
|
||||||
|
help="Service id to populate monthly billing for"),
|
||||||
|
Option('-m', '-month', dest="month", help="Use for integer value for month, e.g. 7 for July"),
|
||||||
|
Option('-y', '-year', dest="year", help="Use for integer value for year, e.g. 2017")
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self, service_id, month, year):
|
||||||
|
print('Starting populating monthly billing')
|
||||||
|
create_or_update_monthly_billing_sms(service_id, datetime(int(year), int(month), 1))
|
||||||
|
results = get_monthly_billing_sms(service_id, datetime(int(year), int(month), 1))
|
||||||
|
print("Finished populating data for {} for service id {}".format(month, service_id))
|
||||||
|
print(results.monthly_totals)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ class QueueNames(object):
|
|||||||
PERIODIC = 'periodic-tasks'
|
PERIODIC = 'periodic-tasks'
|
||||||
PRIORITY = 'priority-tasks'
|
PRIORITY = 'priority-tasks'
|
||||||
DATABASE = 'database-tasks'
|
DATABASE = 'database-tasks'
|
||||||
SEND_COMBINED = 'send-tasks'
|
|
||||||
SEND_SMS = 'send-sms-tasks'
|
SEND_SMS = 'send-sms-tasks'
|
||||||
SEND_EMAIL = 'send-email-tasks'
|
SEND_EMAIL = 'send-email-tasks'
|
||||||
RESEARCH_MODE = 'research-mode-tasks'
|
RESEARCH_MODE = 'research-mode-tasks'
|
||||||
@@ -38,7 +37,6 @@ class QueueNames(object):
|
|||||||
QueueNames.PRIORITY,
|
QueueNames.PRIORITY,
|
||||||
QueueNames.PERIODIC,
|
QueueNames.PERIODIC,
|
||||||
QueueNames.DATABASE,
|
QueueNames.DATABASE,
|
||||||
QueueNames.SEND_COMBINED,
|
|
||||||
QueueNames.SEND_SMS,
|
QueueNames.SEND_SMS,
|
||||||
QueueNames.SEND_EMAIL,
|
QueueNames.SEND_EMAIL,
|
||||||
QueueNames.RESEARCH_MODE,
|
QueueNames.RESEARCH_MODE,
|
||||||
@@ -115,7 +113,6 @@ class Config(object):
|
|||||||
PAGE_SIZE = 50
|
PAGE_SIZE = 50
|
||||||
API_PAGE_SIZE = 250
|
API_PAGE_SIZE = 250
|
||||||
SMS_CHAR_COUNT_LIMIT = 495
|
SMS_CHAR_COUNT_LIMIT = 495
|
||||||
BRANDING_PATH = '/images/email-template/crests/'
|
|
||||||
TEST_MESSAGE_FILENAME = 'Test message'
|
TEST_MESSAGE_FILENAME = 'Test message'
|
||||||
ONE_OFF_MESSAGE_FILENAME = 'Report'
|
ONE_OFF_MESSAGE_FILENAME = 'Report'
|
||||||
MAX_VERIFY_CODE_COUNT = 10
|
MAX_VERIFY_CODE_COUNT = 10
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
|
from app.utils import convert_bst_to_utc
|
||||||
|
|
||||||
|
|
||||||
def get_financial_year(year):
|
def get_financial_year(year):
|
||||||
return get_april_fools(year), get_april_fools(year + 1) - timedelta(microseconds=1)
|
return get_april_fools(year), get_april_fools(year + 1) - timedelta(microseconds=1)
|
||||||
@@ -16,3 +18,16 @@ def get_april_fools(year):
|
|||||||
"""
|
"""
|
||||||
return pytz.timezone('Europe/London').localize(datetime(year, 4, 1, 0, 0, 0)).astimezone(pytz.UTC).replace(
|
return pytz.timezone('Europe/London').localize(datetime(year, 4, 1, 0, 0, 0)).astimezone(pytz.UTC).replace(
|
||||||
tzinfo=None)
|
tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_start_end_date(month_year):
|
||||||
|
"""
|
||||||
|
This function return the start and date of the month_year as UTC,
|
||||||
|
:param month_year: the datetime to calculate the start and end date for that month
|
||||||
|
:return: start_date, end_date, month
|
||||||
|
"""
|
||||||
|
import calendar
|
||||||
|
_, num_days = calendar.monthrange(month_year.year, month_year.month)
|
||||||
|
first_day = datetime(month_year.year, month_year.month, 1, 0, 0, 0)
|
||||||
|
last_day = datetime(month_year.year, month_year.month, num_days, 23, 59, 59, 99999)
|
||||||
|
return convert_bst_to_utc(first_day), convert_bst_to_utc(last_day)
|
||||||
|
|||||||
58
app/dao/monthly_billing_dao.py
Normal file
58
app/dao/monthly_billing_dao.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import db
|
||||||
|
from app.dao.dao_utils import transactional
|
||||||
|
from app.dao.date_util import get_month_start_end_date
|
||||||
|
from app.dao.notification_usage_dao import get_billing_data_for_month
|
||||||
|
from app.models import MonthlyBilling, SMS_TYPE, NotificationHistory
|
||||||
|
from app.statsd_decorators import statsd
|
||||||
|
|
||||||
|
|
||||||
|
def get_service_ids_that_need_sms_billing_populated(start_date, end_date):
|
||||||
|
return db.session.query(
|
||||||
|
NotificationHistory.service_id
|
||||||
|
).filter(
|
||||||
|
NotificationHistory.created_at >= start_date,
|
||||||
|
NotificationHistory.created_at <= end_date,
|
||||||
|
NotificationHistory.notification_type == SMS_TYPE,
|
||||||
|
NotificationHistory.billable_units != 0
|
||||||
|
).distinct().all()
|
||||||
|
|
||||||
|
|
||||||
|
@transactional
|
||||||
|
def create_or_update_monthly_billing_sms(service_id, billing_month):
|
||||||
|
start_date, end_date = get_month_start_end_date(billing_month)
|
||||||
|
monthly = get_billing_data_for_month(service_id=service_id, start_date=start_date, end_date=end_date)
|
||||||
|
# update monthly
|
||||||
|
monthly_totals = _monthly_billing_data_to_json(monthly)
|
||||||
|
row = MonthlyBilling.query.filter_by(start_date=start_date,
|
||||||
|
notification_type='sms').first()
|
||||||
|
if row:
|
||||||
|
row.monthly_totals = monthly_totals
|
||||||
|
row.updated_at = datetime.utcnow()
|
||||||
|
else:
|
||||||
|
row = MonthlyBilling(service_id=service_id,
|
||||||
|
notification_type=SMS_TYPE,
|
||||||
|
monthly_totals=monthly_totals,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date)
|
||||||
|
db.session.add(row)
|
||||||
|
|
||||||
|
|
||||||
|
@statsd(namespace="dao")
|
||||||
|
def get_monthly_billing_sms(service_id, billing_month):
|
||||||
|
start_date, end_date = get_month_start_end_date(billing_month)
|
||||||
|
monthly = MonthlyBilling.query.filter_by(service_id=service_id,
|
||||||
|
start_date=start_date,
|
||||||
|
notification_type=SMS_TYPE).first()
|
||||||
|
return monthly
|
||||||
|
|
||||||
|
|
||||||
|
def _monthly_billing_data_to_json(monthly):
|
||||||
|
# total cost must take into account the free allowance.
|
||||||
|
# might be a good idea to capture free allowance in this table
|
||||||
|
return [{"billing_units": x.billing_units,
|
||||||
|
"rate_multiplier": x.rate_multiplier,
|
||||||
|
"international": x.international,
|
||||||
|
"rate": x.rate,
|
||||||
|
"total_cost": (x.billing_units * x.rate_multiplier) * x.rate} for x in monthly]
|
||||||
@@ -6,7 +6,7 @@ from sqlalchemy import func, case, cast
|
|||||||
from sqlalchemy import literal_column
|
from sqlalchemy import literal_column
|
||||||
|
|
||||||
from app import db
|
from app import db
|
||||||
from app.dao.date_util import get_financial_year
|
from app.dao.date_util import get_financial_year, get_month_start_end_date
|
||||||
from app.models import (NotificationHistory,
|
from app.models import (NotificationHistory,
|
||||||
Rate,
|
Rate,
|
||||||
NOTIFICATION_STATUS_TYPES_BILLABLE,
|
NOTIFICATION_STATUS_TYPES_BILLABLE,
|
||||||
@@ -20,7 +20,7 @@ from app.utils import get_london_month_from_utc_column
|
|||||||
@statsd(namespace="dao")
|
@statsd(namespace="dao")
|
||||||
def get_yearly_billing_data(service_id, year):
|
def get_yearly_billing_data(service_id, year):
|
||||||
start_date, end_date = get_financial_year(year)
|
start_date, end_date = get_financial_year(year)
|
||||||
rates = get_rates_for_year(start_date, end_date, SMS_TYPE)
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
||||||
|
|
||||||
def get_valid_from(valid_from):
|
def get_valid_from(valid_from):
|
||||||
return start_date if valid_from < start_date else valid_from
|
return start_date if valid_from < start_date else valid_from
|
||||||
@@ -35,10 +35,24 @@ def get_yearly_billing_data(service_id, year):
|
|||||||
return sum(result, [])
|
return sum(result, [])
|
||||||
|
|
||||||
|
|
||||||
|
@statsd(namespace="dao")
|
||||||
|
def get_billing_data_for_month(service_id, start_date, end_date):
|
||||||
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
||||||
|
result = []
|
||||||
|
# so the start end date in the query are the valid from the rate, not the month - this is going to take some thought
|
||||||
|
for r, n in zip(rates, rates[1:]):
|
||||||
|
result.extend(sms_billing_data_per_month_query(r.rate, service_id, max(r.valid_from, start_date),
|
||||||
|
min(n.valid_from, end_date)))
|
||||||
|
result.extend(
|
||||||
|
sms_billing_data_per_month_query(rates[-1].rate, service_id, max(rates[-1].valid_from, start_date), end_date))
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@statsd(namespace="dao")
|
@statsd(namespace="dao")
|
||||||
def get_monthly_billing_data(service_id, year):
|
def get_monthly_billing_data(service_id, year):
|
||||||
start_date, end_date = get_financial_year(year)
|
start_date, end_date = get_financial_year(year)
|
||||||
rates = get_rates_for_year(start_date, end_date, SMS_TYPE)
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for r, n in zip(rates, rates[1:]):
|
for r, n in zip(rates, rates[1:]):
|
||||||
@@ -103,7 +117,7 @@ def sms_yearly_billing_data_query(rate, service_id, start_date, end_date):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def get_rates_for_year(start_date, end_date, notification_type):
|
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()
|
rates = Rate.query.filter(Rate.notification_type == notification_type).order_by(Rate.valid_from).all()
|
||||||
results = []
|
results = []
|
||||||
for current_rate, current_rate_expiry_date in zip(rates, rates[1:]):
|
for current_rate, current_rate_expiry_date in zip(rates, rates[1:]):
|
||||||
@@ -115,8 +129,10 @@ def get_rates_for_year(start_date, end_date, notification_type):
|
|||||||
results.append(rates[-1])
|
results.append(rates[-1])
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
if start_date >= rates[-1].valid_from:
|
for x in reversed(rates):
|
||||||
results.append(rates[-1])
|
if start_date >= x.valid_from:
|
||||||
|
results.append(x)
|
||||||
|
break
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
@@ -128,12 +144,12 @@ def is_between(date, start_date, end_date):
|
|||||||
def sms_billing_data_per_month_query(rate, service_id, start_date, end_date):
|
def sms_billing_data_per_month_query(rate, service_id, start_date, end_date):
|
||||||
month = get_london_month_from_utc_column(NotificationHistory.created_at)
|
month = get_london_month_from_utc_column(NotificationHistory.created_at)
|
||||||
result = db.session.query(
|
result = db.session.query(
|
||||||
month,
|
month.label('month'),
|
||||||
func.sum(NotificationHistory.billable_units),
|
func.sum(NotificationHistory.billable_units).label('billing_units'),
|
||||||
rate_multiplier(),
|
rate_multiplier().label('rate_multiplier'),
|
||||||
NotificationHistory.international,
|
NotificationHistory.international,
|
||||||
NotificationHistory.notification_type,
|
NotificationHistory.notification_type,
|
||||||
cast(rate, Float())
|
cast(rate, Float()).label('rate')
|
||||||
).filter(
|
).filter(
|
||||||
*billing_data_filter(SMS_TYPE, start_date, end_date, service_id)
|
*billing_data_filter(SMS_TYPE, start_date, end_date, service_id)
|
||||||
).group_by(
|
).group_by(
|
||||||
@@ -193,7 +209,7 @@ def get_total_billable_units_for_sent_sms_notifications_in_date_range(start_date
|
|||||||
|
|
||||||
def discover_rate_bounds_for_billing_query(start_date, end_date):
|
def discover_rate_bounds_for_billing_query(start_date, end_date):
|
||||||
bounds = []
|
bounds = []
|
||||||
rates = get_rates_for_year(start_date, end_date, SMS_TYPE)
|
rates = get_rates_for_daterange(start_date, end_date, SMS_TYPE)
|
||||||
|
|
||||||
def current_valid_from(index):
|
def current_valid_from(index):
|
||||||
return rates[index].valid_from
|
return rates[index].valid_from
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from app.dao.templates_dao import dao_get_template_by_id
|
|||||||
from app.models import SMS_TYPE, KEY_TYPE_TEST, BRANDING_ORG, EMAIL_TYPE, NOTIFICATION_TECHNICAL_FAILURE, \
|
from app.models import SMS_TYPE, KEY_TYPE_TEST, BRANDING_ORG, EMAIL_TYPE, NOTIFICATION_TECHNICAL_FAILURE, \
|
||||||
NOTIFICATION_SENT, NOTIFICATION_SENDING
|
NOTIFICATION_SENT, NOTIFICATION_SENDING
|
||||||
|
|
||||||
from app.celery.statistics_tasks import record_initial_job_statistics, create_initial_notification_statistic_tasks
|
from app.celery.statistics_tasks import create_initial_notification_statistic_tasks
|
||||||
|
|
||||||
|
|
||||||
def send_sms_to_provider(notification):
|
def send_sms_to_provider(notification):
|
||||||
@@ -145,34 +145,20 @@ def provider_to_use(notification_type, notification_id, international=False):
|
|||||||
return clients.get_client_by_name_and_type(active_providers_in_order[0].identifier, notification_type)
|
return clients.get_client_by_name_and_type(active_providers_in_order[0].identifier, notification_type)
|
||||||
|
|
||||||
|
|
||||||
def get_logo_url(base_url, branding_path, logo_file):
|
def get_logo_url(base_url, logo_file):
|
||||||
"""
|
|
||||||
Get the complete URL for a given logo.
|
|
||||||
|
|
||||||
We have to convert the base_url into a static url. Our hosted environments all have their own cloudfront instances,
|
|
||||||
found at the static subdomain (eg https://static.notifications.service.gov.uk).
|
|
||||||
|
|
||||||
If running locally (dev environment), don't try and use cloudfront - just stick to the actual underlying source
|
|
||||||
({URL}/static/{PATH})
|
|
||||||
"""
|
|
||||||
base_url = parse.urlparse(base_url)
|
base_url = parse.urlparse(base_url)
|
||||||
netloc = base_url.netloc
|
netloc = base_url.netloc
|
||||||
|
|
||||||
# covers both preview and staging
|
if base_url.netloc.startswith('localhost'):
|
||||||
if base_url.netloc.startswith('localhost') or 'notify.works' in base_url.netloc:
|
netloc = 'notify.tools'
|
||||||
path = '/static' + branding_path + logo_file
|
elif base_url.netloc.startswith('www'):
|
||||||
else:
|
# strip "www."
|
||||||
if base_url.netloc.startswith('www'):
|
netloc = base_url.netloc[4:]
|
||||||
# strip "www."
|
|
||||||
netloc = base_url.netloc[4:]
|
|
||||||
|
|
||||||
netloc = 'static.' + netloc
|
|
||||||
path = branding_path + logo_file
|
|
||||||
|
|
||||||
logo_url = parse.ParseResult(
|
logo_url = parse.ParseResult(
|
||||||
scheme=base_url.scheme,
|
scheme=base_url.scheme,
|
||||||
netloc=netloc,
|
netloc='static-logos.' + netloc,
|
||||||
path=path,
|
path=logo_file,
|
||||||
params=base_url.params,
|
params=base_url.params,
|
||||||
query=base_url.query,
|
query=base_url.query,
|
||||||
fragment=base_url.fragment
|
fragment=base_url.fragment
|
||||||
@@ -185,7 +171,6 @@ def get_html_email_options(service):
|
|||||||
if service.organisation:
|
if service.organisation:
|
||||||
logo_url = get_logo_url(
|
logo_url = get_logo_url(
|
||||||
current_app.config['ADMIN_BASE_URL'],
|
current_app.config['ADMIN_BASE_URL'],
|
||||||
current_app.config['BRANDING_PATH'],
|
|
||||||
service.organisation.logo
|
service.organisation.logo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import datetime
|
|||||||
from flask import url_for, current_app
|
from flask import url_for, current_app
|
||||||
|
|
||||||
from sqlalchemy.ext.associationproxy import association_proxy
|
from sqlalchemy.ext.associationproxy import association_proxy
|
||||||
from sqlalchemy.ext.hybrid import hybrid_property
|
|
||||||
from sqlalchemy.dialects.postgresql import (
|
from sqlalchemy.dialects.postgresql import (
|
||||||
UUID,
|
UUID,
|
||||||
JSON
|
JSON
|
||||||
@@ -1246,3 +1245,29 @@ class LetterRateDetail(db.Model):
|
|||||||
letter_rate = db.relationship('LetterRate', backref='letter_rates')
|
letter_rate = db.relationship('LetterRate', backref='letter_rates')
|
||||||
page_total = db.Column(db.Integer, nullable=False)
|
page_total = db.Column(db.Integer, nullable=False)
|
||||||
rate = db.Column(db.Numeric(), nullable=False)
|
rate = db.Column(db.Numeric(), nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class MonthlyBilling(db.Model):
|
||||||
|
__tablename__ = 'monthly_billing'
|
||||||
|
|
||||||
|
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'), index=True, nullable=False)
|
||||||
|
service = db.relationship('Service', backref='monthly_billing')
|
||||||
|
start_date = db.Column(db.DateTime, nullable=False)
|
||||||
|
end_date = db.Column(db.DateTime, nullable=False)
|
||||||
|
notification_type = db.Column(notification_types, nullable=False)
|
||||||
|
monthly_totals = db.Column(JSON, nullable=False)
|
||||||
|
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.datetime.utcnow)
|
||||||
|
|
||||||
|
__table_args__ = (
|
||||||
|
UniqueConstraint('service_id', 'start_date', 'notification_type', name='uix_monthly_billing'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def serialized(self):
|
||||||
|
return {
|
||||||
|
"start_date": self.start_date,
|
||||||
|
"end_date": self.end_date,
|
||||||
|
"service_id": str(self.service_id),
|
||||||
|
"notification_type": self.notification_type,
|
||||||
|
"monthly_totals": self.monthly_totals
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ def check_placeholders(template_object):
|
|||||||
|
|
||||||
|
|
||||||
def persist_notification(
|
def persist_notification(
|
||||||
|
*,
|
||||||
template_id,
|
template_id,
|
||||||
template_version,
|
template_version,
|
||||||
recipient,
|
recipient,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from notifications_utils.clients.redis import rate_limit_cache_key, daily_limit_
|
|||||||
|
|
||||||
from app.dao import services_dao, templates_dao
|
from app.dao import services_dao, templates_dao
|
||||||
from app.models import (
|
from app.models import (
|
||||||
INTERNATIONAL_SMS_TYPE, SMS_TYPE,
|
INTERNATIONAL_SMS_TYPE, SMS_TYPE, EMAIL_TYPE,
|
||||||
KEY_TYPE_TEST, KEY_TYPE_TEAM, SCHEDULE_NOTIFICATIONS
|
KEY_TYPE_TEST, KEY_TYPE_TEAM, SCHEDULE_NOTIFICATIONS
|
||||||
)
|
)
|
||||||
from app.service.utils import service_allowed_to_send_to
|
from app.service.utils import service_allowed_to_send_to
|
||||||
@@ -104,7 +104,7 @@ def validate_and_format_recipient(send_to, key_type, service, notification_type)
|
|||||||
number=send_to,
|
number=send_to,
|
||||||
international=international_phone_info.international
|
international=international_phone_info.international
|
||||||
)
|
)
|
||||||
else:
|
elif notification_type == EMAIL_TYPE:
|
||||||
return validate_and_format_email_address(email_address=send_to)
|
return validate_and_format_email_address(email_address=send_to)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ personalisation = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
letter_personalisation = dict(personalisation, required=["address_line_1", "postcode"])
|
||||||
|
|
||||||
|
|
||||||
https_url = {
|
https_url = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uri",
|
"format": "uri",
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ from app.schemas import (
|
|||||||
user_schema,
|
user_schema,
|
||||||
permission_schema,
|
permission_schema,
|
||||||
notification_with_template_schema,
|
notification_with_template_schema,
|
||||||
notification_with_personalisation_schema,
|
|
||||||
notifications_filter_schema,
|
notifications_filter_schema,
|
||||||
detailed_service_schema
|
detailed_service_schema
|
||||||
)
|
)
|
||||||
|
|||||||
45
app/v2/notifications/create_response.py
Normal file
45
app/v2/notifications/create_response.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
|
||||||
|
def create_post_sms_response_from_notification(notification, content, from_number, url_root, scheduled_for):
|
||||||
|
noti = __create_notification_response(notification, url_root, scheduled_for)
|
||||||
|
noti['content'] = {
|
||||||
|
'from_number': from_number,
|
||||||
|
'body': content
|
||||||
|
}
|
||||||
|
return noti
|
||||||
|
|
||||||
|
|
||||||
|
def create_post_email_response_from_notification(notification, content, subject, email_from, url_root, scheduled_for):
|
||||||
|
noti = __create_notification_response(notification, url_root, scheduled_for)
|
||||||
|
noti['content'] = {
|
||||||
|
"from_email": email_from,
|
||||||
|
"body": content,
|
||||||
|
"subject": subject
|
||||||
|
}
|
||||||
|
return noti
|
||||||
|
|
||||||
|
|
||||||
|
def create_post_letter_response_from_notification(notification, content, subject, url_root, scheduled_for):
|
||||||
|
noti = __create_notification_response(notification, url_root, scheduled_for)
|
||||||
|
noti['content'] = {
|
||||||
|
"body": content,
|
||||||
|
"subject": subject
|
||||||
|
}
|
||||||
|
return noti
|
||||||
|
|
||||||
|
|
||||||
|
def __create_notification_response(notification, url_root, scheduled_for):
|
||||||
|
return {
|
||||||
|
"id": notification.id,
|
||||||
|
"reference": notification.client_reference,
|
||||||
|
"uri": "{}v2/notifications/{}".format(url_root, str(notification.id)),
|
||||||
|
'template': {
|
||||||
|
"id": notification.template_id,
|
||||||
|
"version": notification.template_version,
|
||||||
|
"uri": "{}services/{}/templates/{}".format(
|
||||||
|
url_root,
|
||||||
|
str(notification.service_id),
|
||||||
|
str(notification.template_id)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
"scheduled_for": scheduled_for if scheduled_for else None
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
from app.models import NOTIFICATION_STATUS_TYPES, TEMPLATE_TYPES
|
from app.models import NOTIFICATION_STATUS_TYPES, TEMPLATE_TYPES
|
||||||
from app.schema_validation.definitions import (uuid, personalisation)
|
from app.schema_validation.definitions import (uuid, personalisation, letter_personalisation)
|
||||||
|
|
||||||
|
|
||||||
template = {
|
template = {
|
||||||
@@ -192,40 +192,43 @@ post_email_response = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def create_post_sms_response_from_notification(notification, body, from_number, url_root, service_id, scheduled_for):
|
post_letter_request = {
|
||||||
return {"id": notification.id,
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
"reference": notification.client_reference,
|
"description": "POST letter notification schema",
|
||||||
"content": {'body': body,
|
"type": "object",
|
||||||
'from_number': from_number},
|
"title": "POST v2/notifications/letter",
|
||||||
"uri": "{}v2/notifications/{}".format(url_root, str(notification.id)),
|
"properties": {
|
||||||
"template": __create_template_from_notification(notification=notification,
|
"reference": {"type": "string"},
|
||||||
url_root=url_root,
|
"template_id": uuid,
|
||||||
service_id=service_id),
|
"personalisation": letter_personalisation
|
||||||
"scheduled_for": scheduled_for if scheduled_for else None
|
},
|
||||||
}
|
"required": ["template_id", "personalisation"]
|
||||||
|
}
|
||||||
|
|
||||||
|
letter_content = {
|
||||||
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
|
"description": "Letter content for POST letter notification",
|
||||||
|
"type": "object",
|
||||||
|
"title": "notification letter content",
|
||||||
|
"properties": {
|
||||||
|
"body": {"type": "string"},
|
||||||
|
"subject": {"type": "string"}
|
||||||
|
},
|
||||||
|
"required": ["body", "subject"]
|
||||||
|
}
|
||||||
|
|
||||||
def create_post_email_response_from_notification(notification, content, subject, email_from, url_root, service_id,
|
post_letter_response = {
|
||||||
scheduled_for):
|
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||||
return {
|
"description": "POST sms notification response schema",
|
||||||
"id": notification.id,
|
"type": "object",
|
||||||
"reference": notification.client_reference,
|
"title": "response v2/notifications/letter",
|
||||||
"content": {
|
"properties": {
|
||||||
"from_email": email_from,
|
"id": uuid,
|
||||||
"body": content,
|
"reference": {"type": ["string", "null"]},
|
||||||
"subject": subject
|
"content": letter_content,
|
||||||
},
|
"uri": {"type": "string", "format": "uri"},
|
||||||
"uri": "{}v2/notifications/{}".format(url_root, str(notification.id)),
|
"template": template,
|
||||||
"template": __create_template_from_notification(notification=notification,
|
"scheduled_for": {"type": ["string", "null"]}
|
||||||
url_root=url_root,
|
},
|
||||||
service_id=service_id),
|
"required": ["id", "content", "uri", "template"]
|
||||||
"scheduled_for": scheduled_for if scheduled_for else None
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def __create_template_from_notification(notification, url_root, service_id):
|
|
||||||
return {
|
|
||||||
"id": notification.template_id,
|
|
||||||
"version": notification.template_version,
|
|
||||||
"uri": "{}services/{}/templates/{}".format(url_root, str(service_id), str(notification.template_id))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from flask import request, jsonify, current_app
|
import functools
|
||||||
|
|
||||||
|
from flask import request, jsonify, current_app, abort
|
||||||
|
|
||||||
from app import api_user, authenticated_service
|
from app import api_user, authenticated_service
|
||||||
from app.config import QueueNames
|
from app.config import QueueNames
|
||||||
from app.models import SMS_TYPE, EMAIL_TYPE, PRIORITY, SCHEDULE_NOTIFICATIONS
|
from app.models import SMS_TYPE, EMAIL_TYPE, LETTER_TYPE, PRIORITY
|
||||||
from app.notifications.process_notifications import (
|
from app.notifications.process_notifications import (
|
||||||
persist_notification,
|
persist_notification,
|
||||||
send_notification_to_queue,
|
send_notification_to_queue,
|
||||||
@@ -11,28 +13,34 @@ from app.notifications.process_notifications import (
|
|||||||
from app.notifications.validators import (
|
from app.notifications.validators import (
|
||||||
validate_and_format_recipient,
|
validate_and_format_recipient,
|
||||||
check_rate_limiting,
|
check_rate_limiting,
|
||||||
service_has_permission,
|
|
||||||
check_service_can_schedule_notification,
|
check_service_can_schedule_notification,
|
||||||
check_service_has_permission,
|
check_service_has_permission,
|
||||||
validate_template
|
validate_template
|
||||||
)
|
)
|
||||||
from app.schema_validation import validate
|
from app.schema_validation import validate
|
||||||
from app.utils import get_public_notify_type_text
|
|
||||||
from app.v2.notifications import v2_notification_blueprint
|
from app.v2.notifications import v2_notification_blueprint
|
||||||
from app.v2.notifications.notification_schemas import (
|
from app.v2.notifications.notification_schemas import (
|
||||||
post_sms_request,
|
post_sms_request,
|
||||||
create_post_sms_response_from_notification,
|
|
||||||
post_email_request,
|
post_email_request,
|
||||||
create_post_email_response_from_notification)
|
post_letter_request
|
||||||
from app.v2.errors import BadRequestError
|
)
|
||||||
|
from app.v2.notifications.create_response import (
|
||||||
|
create_post_sms_response_from_notification,
|
||||||
|
create_post_email_response_from_notification,
|
||||||
|
create_post_letter_response_from_notification
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@v2_notification_blueprint.route('/<notification_type>', methods=['POST'])
|
@v2_notification_blueprint.route('/<notification_type>', methods=['POST'])
|
||||||
def post_notification(notification_type):
|
def post_notification(notification_type):
|
||||||
if notification_type == EMAIL_TYPE:
|
if notification_type == EMAIL_TYPE:
|
||||||
form = validate(request.get_json(), post_email_request)
|
form = validate(request.get_json(), post_email_request)
|
||||||
else:
|
elif notification_type == SMS_TYPE:
|
||||||
form = validate(request.get_json(), post_sms_request)
|
form = validate(request.get_json(), post_sms_request)
|
||||||
|
elif notification_type == LETTER_TYPE:
|
||||||
|
form = validate(request.get_json(), post_letter_request)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
check_service_has_permission(notification_type, authenticated_service.permissions)
|
check_service_has_permission(notification_type, authenticated_service.permissions)
|
||||||
|
|
||||||
@@ -41,12 +49,6 @@ def post_notification(notification_type):
|
|||||||
|
|
||||||
check_rate_limiting(authenticated_service, api_user)
|
check_rate_limiting(authenticated_service, api_user)
|
||||||
|
|
||||||
form_send_to = form['phone_number'] if notification_type == SMS_TYPE else form['email_address']
|
|
||||||
send_to = validate_and_format_recipient(send_to=form_send_to,
|
|
||||||
key_type=api_user.key_type,
|
|
||||||
service=authenticated_service,
|
|
||||||
notification_type=notification_type)
|
|
||||||
|
|
||||||
template, template_with_content = validate_template(
|
template, template_with_content = validate_template(
|
||||||
form['template_id'],
|
form['template_id'],
|
||||||
form.get('personalisation', {}),
|
form.get('personalisation', {}),
|
||||||
@@ -54,20 +56,74 @@ def post_notification(notification_type):
|
|||||||
notification_type,
|
notification_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if notification_type == LETTER_TYPE:
|
||||||
|
notification = process_letter_notification(
|
||||||
|
form=form,
|
||||||
|
api_key=api_user,
|
||||||
|
template=template,
|
||||||
|
service=authenticated_service,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
notification = process_sms_or_email_notification(
|
||||||
|
form=form,
|
||||||
|
notification_type=notification_type,
|
||||||
|
api_key=api_user,
|
||||||
|
template=template,
|
||||||
|
service=authenticated_service
|
||||||
|
)
|
||||||
|
|
||||||
|
if notification_type == SMS_TYPE:
|
||||||
|
sms_sender = authenticated_service.sms_sender or current_app.config.get('FROM_NUMBER')
|
||||||
|
create_resp_partial = functools.partial(
|
||||||
|
create_post_sms_response_from_notification,
|
||||||
|
from_number=sms_sender
|
||||||
|
)
|
||||||
|
elif notification_type == EMAIL_TYPE:
|
||||||
|
create_resp_partial = functools.partial(
|
||||||
|
create_post_email_response_from_notification,
|
||||||
|
subject=template_with_content.subject,
|
||||||
|
email_from=authenticated_service.email_from
|
||||||
|
)
|
||||||
|
elif notification_type == LETTER_TYPE:
|
||||||
|
create_resp_partial = functools.partial(
|
||||||
|
create_post_letter_response_from_notification,
|
||||||
|
subject=template_with_content.subject,
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = create_resp_partial(
|
||||||
|
notification=notification,
|
||||||
|
content=str(template_with_content),
|
||||||
|
url_root=request.url_root,
|
||||||
|
scheduled_for=scheduled_for
|
||||||
|
)
|
||||||
|
return jsonify(resp), 201
|
||||||
|
|
||||||
|
|
||||||
|
def process_sms_or_email_notification(*, form, notification_type, api_key, template, service):
|
||||||
|
form_send_to = form['email_address'] if notification_type == EMAIL_TYPE else form['phone_number']
|
||||||
|
|
||||||
|
send_to = validate_and_format_recipient(send_to=form_send_to,
|
||||||
|
key_type=api_key.key_type,
|
||||||
|
service=service,
|
||||||
|
notification_type=notification_type)
|
||||||
|
|
||||||
# Do not persist or send notification to the queue if it is a simulated recipient
|
# Do not persist or send notification to the queue if it is a simulated recipient
|
||||||
simulated = simulated_recipient(send_to, notification_type)
|
simulated = simulated_recipient(send_to, notification_type)
|
||||||
|
|
||||||
notification = persist_notification(template_id=template.id,
|
notification = persist_notification(
|
||||||
template_version=template.version,
|
template_id=template.id,
|
||||||
recipient=form_send_to,
|
template_version=template.version,
|
||||||
service=authenticated_service,
|
recipient=form_send_to,
|
||||||
personalisation=form.get('personalisation', None),
|
service=service,
|
||||||
notification_type=notification_type,
|
personalisation=form.get('personalisation', None),
|
||||||
api_key_id=api_user.id,
|
notification_type=notification_type,
|
||||||
key_type=api_user.key_type,
|
api_key_id=api_key.id,
|
||||||
client_reference=form.get('reference', None),
|
key_type=api_key.key_type,
|
||||||
simulated=simulated)
|
client_reference=form.get('reference', None),
|
||||||
|
simulated=simulated
|
||||||
|
)
|
||||||
|
|
||||||
|
scheduled_for = form.get("scheduled_for", None)
|
||||||
if scheduled_for:
|
if scheduled_for:
|
||||||
persist_scheduled_notification(notification.id, form["scheduled_for"])
|
persist_scheduled_notification(notification.id, form["scheduled_for"])
|
||||||
else:
|
else:
|
||||||
@@ -75,26 +131,19 @@ def post_notification(notification_type):
|
|||||||
queue_name = QueueNames.PRIORITY if template.process_type == PRIORITY else None
|
queue_name = QueueNames.PRIORITY if template.process_type == PRIORITY else None
|
||||||
send_notification_to_queue(
|
send_notification_to_queue(
|
||||||
notification=notification,
|
notification=notification,
|
||||||
research_mode=authenticated_service.research_mode,
|
research_mode=service.research_mode,
|
||||||
queue=queue_name
|
queue=queue_name
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
current_app.logger.info("POST simulated notification for id: {}".format(notification.id))
|
current_app.logger.info("POST simulated notification for id: {}".format(notification.id))
|
||||||
|
|
||||||
if notification_type == SMS_TYPE:
|
return notification
|
||||||
sms_sender = authenticated_service.sms_sender or current_app.config.get('FROM_NUMBER')
|
|
||||||
resp = create_post_sms_response_from_notification(notification=notification,
|
|
||||||
body=str(template_with_content),
|
def process_letter_notification(*, form, api_key, template, service):
|
||||||
from_number=sms_sender,
|
# create job
|
||||||
url_root=request.url_root,
|
|
||||||
service_id=authenticated_service.id,
|
# create notification
|
||||||
scheduled_for=scheduled_for)
|
|
||||||
else:
|
# trigger build_dvla_file task
|
||||||
resp = create_post_email_response_from_notification(notification=notification,
|
raise NotImplementedError
|
||||||
content=str(template_with_content),
|
|
||||||
subject=template_with_content.subject,
|
|
||||||
email_from=authenticated_service.email_from,
|
|
||||||
url_root=request.url_root,
|
|
||||||
service_id=authenticated_service.id,
|
|
||||||
scheduled_for=scheduled_for)
|
|
||||||
return jsonify(resp), 201
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ manager.add_command('db', MigrateCommand)
|
|||||||
manager.add_command('create_provider_rate', commands.CreateProviderRateCommand)
|
manager.add_command('create_provider_rate', commands.CreateProviderRateCommand)
|
||||||
manager.add_command('purge_functional_test_data', commands.PurgeFunctionalTestDataCommand)
|
manager.add_command('purge_functional_test_data', commands.PurgeFunctionalTestDataCommand)
|
||||||
manager.add_command('custom_db_script', commands.CustomDbScript)
|
manager.add_command('custom_db_script', commands.CustomDbScript)
|
||||||
|
manager.add_command('populate_monthly_billing', commands.PopulateMonthlyBilling)
|
||||||
|
|
||||||
|
|
||||||
@manager.command
|
@manager.command
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
from app import notify_celery, create_app
|
|
||||||
from credstash import getAllSecrets
|
|
||||||
import os
|
|
||||||
|
|
||||||
# On AWS get secrets and export to env, skip this on Cloud Foundry
|
|
||||||
if os.getenv('VCAP_SERVICES') is None:
|
|
||||||
os.environ.update(getAllSecrets(region="eu-west-1"))
|
|
||||||
|
|
||||||
application = create_app("delivery")
|
|
||||||
application.app_context().push()
|
|
||||||
8
db.py
8
db.py
@@ -1,12 +1,8 @@
|
|||||||
from flask.ext.script import Manager, Server
|
from flask.ext.script import Manager, Server
|
||||||
from flask_migrate import Migrate, MigrateCommand
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
from app import create_app, db
|
|
||||||
from credstash import getAllSecrets
|
|
||||||
import os
|
|
||||||
|
|
||||||
# On AWS get secrets and export to env, skip this on Cloud Foundry
|
from app import create_app, db
|
||||||
if os.getenv('VCAP_SERVICES') is None:
|
|
||||||
os.environ.update(getAllSecrets(region="eu-west-1"))
|
|
||||||
|
|
||||||
application = create_app()
|
application = create_app()
|
||||||
|
|
||||||
|
|||||||
@@ -16,39 +16,39 @@ memory: 1G
|
|||||||
|
|
||||||
applications:
|
applications:
|
||||||
- name: notify-delivery-celery-beat
|
- name: notify-delivery-celery-beat
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery beat --loglevel=INFO
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery beat --loglevel=INFO
|
||||||
instances: 1
|
instances: 1
|
||||||
memory: 128M
|
memory: 128M
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-celery-beat
|
NOTIFY_APP_NAME: delivery-celery-beat
|
||||||
|
|
||||||
- name: notify-delivery-worker-database
|
- name: notify-delivery-worker-database
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q database-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q database-tasks
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker-database
|
NOTIFY_APP_NAME: delivery-worker-database
|
||||||
|
|
||||||
- name: notify-delivery-worker-research
|
- name: notify-delivery-worker-research
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=5 -Q research-mode-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=5 -Q research-mode-tasks
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker-research
|
NOTIFY_APP_NAME: delivery-worker-research
|
||||||
|
|
||||||
- name: notify-delivery-worker-sender
|
- name: notify-delivery-worker-sender
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q send-tasks,send-sms-tasks,send-email-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q send-sms-tasks,send-email-tasks
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker-sender
|
NOTIFY_APP_NAME: delivery-worker-sender
|
||||||
|
|
||||||
- name: notify-delivery-worker-periodic
|
- name: notify-delivery-worker-periodic
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=2 -Q periodic-tasks,statistics-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=2 -Q periodic-tasks,statistics-tasks
|
||||||
instances: 1
|
instances: 1
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker-periodic
|
NOTIFY_APP_NAME: delivery-worker-periodic
|
||||||
|
|
||||||
- name: notify-delivery-worker-priority
|
- name: notify-delivery-worker-priority
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=5 -Q priority-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=5 -Q priority-tasks
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker-priority
|
NOTIFY_APP_NAME: delivery-worker-priority
|
||||||
|
|
||||||
- name: notify-delivery-worker
|
- name: notify-delivery-worker
|
||||||
command: scripts/run_app_paas.sh celery -A aws_run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q job-tasks,retry-tasks,notify-internal-tasks
|
command: scripts/run_app_paas.sh celery -A run_celery.notify_celery worker --loglevel=INFO --concurrency=11 -Q job-tasks,retry-tasks,notify-internal-tasks
|
||||||
env:
|
env:
|
||||||
NOTIFY_APP_NAME: delivery-worker
|
NOTIFY_APP_NAME: delivery-worker
|
||||||
|
|||||||
38
migrations/versions/0110_monthly_billing.py
Normal file
38
migrations/versions/0110_monthly_billing.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 0110_monthly_billing
|
||||||
|
Revises: 0109_rem_old_noti_status
|
||||||
|
Create Date: 2017-07-13 14:35:03.183659
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0110_monthly_billing'
|
||||||
|
down_revision = '0109_rem_old_noti_status'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
|
||||||
|
op.create_table('monthly_billing',
|
||||||
|
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=False),
|
||||||
|
sa.Column('month', sa.String(), nullable=False),
|
||||||
|
sa.Column('year', sa.Float(), nullable=False),
|
||||||
|
sa.Column('notification_type',
|
||||||
|
postgresql.ENUM('email', 'sms', 'letter', name='notification_type', create_type=False),
|
||||||
|
nullable=False),
|
||||||
|
sa.Column('monthly_totals', postgresql.JSON(), nullable=False),
|
||||||
|
sa.Column('updated_at', sa.DateTime, nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['service_id'], ['services.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_monthly_billing_service_id'), 'monthly_billing', ['service_id'], unique=False)
|
||||||
|
op.create_index(op.f('uix_monthly_billing'), 'monthly_billing', ['service_id', 'month', 'year', 'notification_type'], unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_table('monthly_billing')
|
||||||
28
migrations/versions/0111_drop_old_service_flags.py
Normal file
28
migrations/versions/0111_drop_old_service_flags.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 0111_drop_old_service_flags
|
||||||
|
Revises: 0110_monthly_billing
|
||||||
|
Create Date: 2017-07-12 13:35:45.636618
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0111_drop_old_service_flags'
|
||||||
|
down_revision = '0110_monthly_billing'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.drop_column('services', 'can_send_letters')
|
||||||
|
op.drop_column('services', 'can_send_international_sms')
|
||||||
|
op.drop_column('services_history', 'can_send_letters')
|
||||||
|
op.drop_column('services_history', 'can_send_international_sms')
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.add_column('services_history', sa.Column('can_send_international_sms', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('services_history', sa.Column('can_send_letters', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('services', sa.Column('can_send_international_sms', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
|
||||||
|
op.add_column('services', sa.Column('can_send_letters', sa.BOOLEAN(), server_default=sa.text('false'), autoincrement=False, nullable=False))
|
||||||
43
migrations/versions/0112_add_start_end_dates.py
Normal file
43
migrations/versions/0112_add_start_end_dates.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 0112_add_start_end_dates
|
||||||
|
Revises: 0111_drop_old_service_flags
|
||||||
|
Create Date: 2017-07-12 13:35:45.636618
|
||||||
|
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from app.dao.date_util import get_month_start_end_date
|
||||||
|
|
||||||
|
down_revision = '0111_drop_old_service_flags'
|
||||||
|
revision = '0112_add_start_end_dates'
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.drop_index('uix_monthly_billing', 'monthly_billing')
|
||||||
|
op.alter_column('monthly_billing', 'month', nullable=True)
|
||||||
|
op.alter_column('monthly_billing', 'year', nullable=True)
|
||||||
|
op.add_column('monthly_billing', sa.Column('start_date', sa.DateTime))
|
||||||
|
op.add_column('monthly_billing', sa.Column('end_date', sa.DateTime))
|
||||||
|
conn = op.get_bind()
|
||||||
|
results = conn.execute("Select id, month, year from monthly_billing")
|
||||||
|
res = results.fetchall()
|
||||||
|
for x in res:
|
||||||
|
start_date, end_date = get_month_start_end_date(
|
||||||
|
datetime(int(x.year), datetime.strptime(x.month, '%B').month, 1))
|
||||||
|
conn.execute("update monthly_billing set start_date = '{}', end_date = '{}' where id = '{}'".format(start_date,
|
||||||
|
end_date,
|
||||||
|
x.id))
|
||||||
|
op.alter_column('monthly_billing', 'start_date', nullable=False)
|
||||||
|
op.alter_column('monthly_billing', 'end_date', nullable=False)
|
||||||
|
op.create_index(op.f('uix_monthly_billing'), 'monthly_billing', ['service_id', 'start_date', 'notification_type'],
|
||||||
|
unique=True)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
op.drop_column('monthly_billing', 'start_date')
|
||||||
|
op.drop_column('monthly_billing', 'end_date')
|
||||||
|
|
||||||
|
op.create_index(op.f('uix_monthly_billing'), 'monthly_billing',
|
||||||
|
['service_id', 'month', 'year', 'notification_type'], unique=True)
|
||||||
@@ -11,9 +11,7 @@ marshmallow==2.4.2
|
|||||||
marshmallow-sqlalchemy==0.8.0
|
marshmallow-sqlalchemy==0.8.0
|
||||||
flask-marshmallow==0.6.2
|
flask-marshmallow==0.6.2
|
||||||
Flask-Bcrypt==0.6.2
|
Flask-Bcrypt==0.6.2
|
||||||
credstash==1.8.0
|
|
||||||
boto3==1.4.4
|
boto3==1.4.4
|
||||||
celery==3.1.25
|
|
||||||
monotonic==1.3
|
monotonic==1.3
|
||||||
statsd==3.2.1
|
statsd==3.2.1
|
||||||
jsonschema==2.5.1
|
jsonschema==2.5.1
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
pycodestyle==2.3.1
|
pycodestyle==2.3.1
|
||||||
pytest==3.0.1
|
pytest==3.1.3
|
||||||
pytest-mock==1.2
|
pytest-mock==1.6.2
|
||||||
pytest-cov==2.3.1
|
pytest-cov==2.5.1
|
||||||
coveralls==1.1
|
coveralls==1.1
|
||||||
moto==0.4.25
|
moto==1.0.1
|
||||||
flex==5.8.0
|
flex==6.11.0
|
||||||
freezegun==0.3.7
|
freezegun==0.3.9
|
||||||
requests-mock==1.0.0
|
requests-mock==1.3.0
|
||||||
strict-rfc3339==0.7
|
strict-rfc3339==0.7
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
# notify_celery is referenced from manifest_delivery_base.yml, and cannot be removed
|
||||||
from app import notify_celery, create_app
|
from app import notify_celery, create_app
|
||||||
|
|
||||||
application = create_app('delivery')
|
application = create_app('delivery')
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ set -eo pipefail
|
|||||||
echo "Install dependencies"
|
echo "Install dependencies"
|
||||||
|
|
||||||
cd /home/notify-app/notifications-api;
|
cd /home/notify-app/notifications-api;
|
||||||
pip3 install --find-links=wheelhouse -r /home/notify-app/notifications-api/requirements.txt
|
pip3 install -r /home/notify-app/notifications-api/requirements.txt
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from flask.ext.script import Manager, Server
|
from flask.ext.script import Manager, Server
|
||||||
from flask_migrate import Migrate, MigrateCommand
|
from flask_migrate import Migrate, MigrateCommand
|
||||||
from app import (create_app, db, commands)
|
from app import (create_app, db, commands)
|
||||||
from credstash import getAllSecrets
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
default_env_file = '/home/ubuntu/environment'
|
default_env_file = '/home/ubuntu/environment'
|
||||||
@@ -11,10 +10,6 @@ if os.path.isfile(default_env_file):
|
|||||||
with open(default_env_file, 'r') as environment_file:
|
with open(default_env_file, 'r') as environment_file:
|
||||||
environment = environment_file.readline().strip()
|
environment = environment_file.readline().strip()
|
||||||
|
|
||||||
# On AWS get secrets and export to env, skip this on Cloud Foundry
|
|
||||||
if os.getenv('VCAP_SERVICES') is None:
|
|
||||||
os.environ.update(getAllSecrets(region="eu-west-1"))
|
|
||||||
|
|
||||||
from app.config import configs
|
from app.config import configs
|
||||||
|
|
||||||
os.environ['NOTIFY_API_ENVIRONMENT'] = configs[environment]
|
os.environ['NOTIFY_API_ENVIRONMENT'] = configs[environment]
|
||||||
|
|||||||
@@ -351,7 +351,7 @@ def test_reject_invalid_ips(restrict_ip_sms_app):
|
|||||||
assert exc_info.value.short_message == 'Unknown source IP address from the SMS provider'
|
assert exc_info.value.short_message == 'Unknown source IP address from the SMS provider'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.xfail(reason='Currently not blocking invalid IPs', strict=True)
|
@pytest.mark.xfail(reason='Currently not blocking invalid senders', strict=True)
|
||||||
def test_illegitimate_ips(restrict_ip_sms_app):
|
def test_illegitimate_ips(restrict_ip_sms_app):
|
||||||
with pytest.raises(AuthError) as exc_info:
|
with pytest.raises(AuthError) as exc_info:
|
||||||
restrict_ip_sms_app.get(
|
restrict_ip_sms_app.get(
|
||||||
@@ -361,4 +361,4 @@ def test_illegitimate_ips(restrict_ip_sms_app):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert exc_info.value.short_message == 'Unknown source IP address from the SMS provider'
|
assert exc_info.value.short_message == 'Unknown IP route not from known SMS provider'
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ from app.celery.scheduled_tasks import (
|
|||||||
send_scheduled_notifications,
|
send_scheduled_notifications,
|
||||||
switch_current_sms_provider_on_slow_delivery,
|
switch_current_sms_provider_on_slow_delivery,
|
||||||
timeout_job_statistics,
|
timeout_job_statistics,
|
||||||
timeout_notifications
|
timeout_notifications,
|
||||||
)
|
populate_monthly_billing)
|
||||||
from app.clients.performance_platform.performance_platform_client import PerformancePlatformClient
|
from app.clients.performance_platform.performance_platform_client import PerformancePlatformClient
|
||||||
from app.dao.jobs_dao import dao_get_job_by_id
|
from app.dao.jobs_dao import dao_get_job_by_id
|
||||||
from app.dao.notifications_dao import dao_get_scheduled_notifications
|
from app.dao.notifications_dao import dao_get_scheduled_notifications
|
||||||
@@ -36,10 +36,10 @@ from app.dao.provider_details_dao import (
|
|||||||
)
|
)
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Service, Template,
|
Service, Template,
|
||||||
SMS_TYPE, LETTER_TYPE
|
SMS_TYPE, LETTER_TYPE,
|
||||||
)
|
MonthlyBilling)
|
||||||
from app.utils import get_london_midnight_in_utc
|
from app.utils import get_london_midnight_in_utc
|
||||||
from tests.app.db import create_notification, create_service, create_template, create_job
|
from tests.app.db import create_notification, create_service, create_template, create_job, create_rate
|
||||||
from tests.app.conftest import (
|
from tests.app.conftest import (
|
||||||
sample_job as create_sample_job,
|
sample_job as create_sample_job,
|
||||||
sample_notification_history as create_notification_history,
|
sample_notification_history as create_notification_history,
|
||||||
@@ -98,6 +98,8 @@ def test_should_have_decorated_tasks_functions():
|
|||||||
'remove_transformed_dvla_files'
|
'remove_transformed_dvla_files'
|
||||||
assert delete_dvla_response_files_older_than_seven_days.__wrapped__.__name__ == \
|
assert delete_dvla_response_files_older_than_seven_days.__wrapped__.__name__ == \
|
||||||
'delete_dvla_response_files_older_than_seven_days'
|
'delete_dvla_response_files_older_than_seven_days'
|
||||||
|
assert populate_monthly_billing.__wrapped__.__name__ == \
|
||||||
|
'populate_monthly_billing'
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
@pytest.fixture(scope='function')
|
||||||
@@ -607,3 +609,30 @@ def test_delete_dvla_response_files_older_than_seven_days_does_not_remove_files(
|
|||||||
delete_dvla_response_files_older_than_seven_days()
|
delete_dvla_response_files_older_than_seven_days()
|
||||||
|
|
||||||
remove_s3_mock.assert_not_called()
|
remove_s3_mock.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@freeze_time("2017-07-12 02:00:00")
|
||||||
|
def test_populate_monthly_billing(sample_template):
|
||||||
|
yesterday = datetime(2017, 7, 11, 13, 30)
|
||||||
|
create_rate(datetime(2016, 1, 1), 0.0123, 'sms')
|
||||||
|
create_notification(template=sample_template, status='delivered', created_at=yesterday)
|
||||||
|
create_notification(template=sample_template, status='delivered', created_at=yesterday - timedelta(days=1))
|
||||||
|
create_notification(template=sample_template, status='delivered', created_at=yesterday + timedelta(days=1))
|
||||||
|
# not included in billing
|
||||||
|
create_notification(template=sample_template, status='delivered', created_at=yesterday - timedelta(days=30))
|
||||||
|
|
||||||
|
assert len(MonthlyBilling.query.all()) == 0
|
||||||
|
populate_monthly_billing()
|
||||||
|
|
||||||
|
monthly_billing = MonthlyBilling.query.all()
|
||||||
|
assert len(monthly_billing) == 1
|
||||||
|
assert monthly_billing[0].service_id == sample_template.service_id
|
||||||
|
assert monthly_billing[0].start_date == datetime(2017, 6, 30, 23)
|
||||||
|
assert monthly_billing[0].end_date == datetime(2017, 7, 31, 22, 59, 59, 99999)
|
||||||
|
assert monthly_billing[0].notification_type == 'sms'
|
||||||
|
assert len(monthly_billing[0].monthly_totals) == 1
|
||||||
|
assert sorted(monthly_billing[0].monthly_totals[0]) == sorted({'international': False,
|
||||||
|
'rate_multiplier': 1,
|
||||||
|
'billing_units': 3,
|
||||||
|
'rate': 0.0123,
|
||||||
|
'total_cost': 0.0369})
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
from app.dao.date_util import get_financial_year, get_april_fools
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.dao.date_util import get_financial_year, get_april_fools, get_month_start_end_date
|
||||||
|
|
||||||
|
|
||||||
def test_get_financial_year():
|
def test_get_financial_year():
|
||||||
@@ -11,3 +15,17 @@ def test_get_april_fools():
|
|||||||
april_fools = get_april_fools(2016)
|
april_fools = get_april_fools(2016)
|
||||||
assert str(april_fools) == '2016-03-31 23:00:00'
|
assert str(april_fools) == '2016-03-31 23:00:00'
|
||||||
assert april_fools.tzinfo is None
|
assert april_fools.tzinfo is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("month, year, expected_start, expected_end",
|
||||||
|
[
|
||||||
|
(7, 2017, datetime(2017, 6, 30, 23, 00, 00), datetime(2017, 7, 31, 22, 59, 59, 99999)),
|
||||||
|
(2, 2016, datetime(2016, 2, 1, 00, 00, 00), datetime(2016, 2, 29, 23, 59, 59, 99999)),
|
||||||
|
(2, 2017, datetime(2017, 2, 1, 00, 00, 00), datetime(2017, 2, 28, 23, 59, 59, 99999)),
|
||||||
|
(9, 2018, datetime(2018, 8, 31, 23, 00, 00), datetime(2018, 9, 30, 22, 59, 59, 99999)),
|
||||||
|
(12, 2019, datetime(2019, 12, 1, 00, 00, 00), datetime(2019, 12, 31, 23, 59, 59, 99999))])
|
||||||
|
def test_get_month_start_end_date(month, year, expected_start, expected_end):
|
||||||
|
month_year = datetime(year, month, 10, 13, 30, 00)
|
||||||
|
result = get_month_start_end_date(month_year)
|
||||||
|
assert result[0] == expected_start
|
||||||
|
assert result[1] == expected_end
|
||||||
|
|||||||
138
tests/app/dao/test_monthly_billing.py
Normal file
138
tests/app/dao/test_monthly_billing.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
|
from freezegun.api import FakeDatetime
|
||||||
|
|
||||||
|
from app.dao.monthly_billing_dao import (
|
||||||
|
create_or_update_monthly_billing_sms,
|
||||||
|
get_monthly_billing_sms,
|
||||||
|
get_service_ids_that_need_sms_billing_populated
|
||||||
|
)
|
||||||
|
from app.models import MonthlyBilling
|
||||||
|
from tests.app.db import create_notification, create_rate, create_service, create_template
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_monthly_billing(sample_template):
|
||||||
|
jan = datetime(2017, 1, 1)
|
||||||
|
feb = datetime(2017, 2, 15)
|
||||||
|
create_rate(start_date=jan, value=0.0158, notification_type='sms')
|
||||||
|
create_rate(start_date=datetime(2017, 3, 31, 23, 00, 00), value=0.123, notification_type='sms')
|
||||||
|
create_notification(template=sample_template, created_at=jan, billable_units=1, status='delivered')
|
||||||
|
create_notification(template=sample_template, created_at=feb, billable_units=2, status='delivered')
|
||||||
|
|
||||||
|
create_or_update_monthly_billing_sms(service_id=sample_template.service_id,
|
||||||
|
billing_month=jan)
|
||||||
|
create_or_update_monthly_billing_sms(service_id=sample_template.service_id,
|
||||||
|
billing_month=feb)
|
||||||
|
monthly_billing = MonthlyBilling.query.all()
|
||||||
|
assert len(monthly_billing) == 2
|
||||||
|
assert monthly_billing[0].start_date == datetime(2017, 1, 1)
|
||||||
|
assert monthly_billing[1].start_date == datetime(2017, 2, 1)
|
||||||
|
|
||||||
|
january = get_monthly_billing_sms(service_id=sample_template.service_id, billing_month=jan)
|
||||||
|
expected_jan = {"billing_units": 1,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.0158,
|
||||||
|
"total_cost": 1 * 0.0158}
|
||||||
|
assert_monthly_billing(january, sample_template.service_id, 1, expected_jan,
|
||||||
|
start_date=datetime(2017, 1, 1), end_date=datetime(2017, 1, 31))
|
||||||
|
|
||||||
|
february = get_monthly_billing_sms(service_id=sample_template.service_id, billing_month=feb)
|
||||||
|
expected_feb = {"billing_units": 2,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.0158,
|
||||||
|
"total_cost": 2 * 0.0158}
|
||||||
|
assert_monthly_billing(february, sample_template.service_id, 1, expected_feb,
|
||||||
|
start_date=datetime(2017, 2, 1), end_date=datetime(2017, 2, 28))
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_monthly_billing_multiple_rates_in_a_month(sample_template):
|
||||||
|
rate_1 = datetime(2016, 12, 1)
|
||||||
|
rate_2 = datetime(2017, 1, 15)
|
||||||
|
create_rate(start_date=rate_1, value=0.0158, notification_type='sms')
|
||||||
|
create_rate(start_date=rate_2, value=0.0124, notification_type='sms')
|
||||||
|
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 1, 1), billable_units=1, status='delivered')
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 1, 14, 23, 59), billable_units=1,
|
||||||
|
status='delivered')
|
||||||
|
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 1, 15), billable_units=2,
|
||||||
|
status='delivered')
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 1, 17, 13, 30, 57), billable_units=4,
|
||||||
|
status='delivered')
|
||||||
|
|
||||||
|
create_or_update_monthly_billing_sms(service_id=sample_template.service_id,
|
||||||
|
billing_month=rate_2)
|
||||||
|
monthly_billing = MonthlyBilling.query.all()
|
||||||
|
assert len(monthly_billing) == 1
|
||||||
|
assert monthly_billing[0].start_date == datetime(2017, 1, 1)
|
||||||
|
|
||||||
|
january = get_monthly_billing_sms(service_id=sample_template.service_id, billing_month=rate_2)
|
||||||
|
first_row = {"billing_units": 2,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.0158,
|
||||||
|
"total_cost": 3 * 0.0158}
|
||||||
|
assert_monthly_billing(january, sample_template.service_id, 2, first_row,
|
||||||
|
start_date=datetime(2017, 1, 1), end_date=datetime(2017, 1, 1))
|
||||||
|
second_row = {"billing_units": 6,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.0124,
|
||||||
|
"total_cost": 1 * 0.0124}
|
||||||
|
assert sorted(january.monthly_totals[1]) == sorted(second_row)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_monthly_billing_overwrites_old_totals(sample_template):
|
||||||
|
july = datetime(2017, 7, 1)
|
||||||
|
create_rate(july, 0.123, 'sms')
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 7, 2), billable_units=1, status='delivered')
|
||||||
|
with freeze_time('2017-07-20 02:30:00'):
|
||||||
|
create_or_update_monthly_billing_sms(sample_template.service_id, july)
|
||||||
|
first_update = get_monthly_billing_sms(sample_template.service_id, july)
|
||||||
|
expected = {"billing_units": 1,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.123,
|
||||||
|
"total_cost": 1 * 0.123}
|
||||||
|
assert_monthly_billing(first_update, sample_template.service_id, 1, expected,
|
||||||
|
start_date=datetime(2017, 6, 30, 23), end_date=datetime(2017, 7, 31, 23, 59, 59, 99999))
|
||||||
|
first_updated_at = first_update.updated_at
|
||||||
|
with freeze_time('2017-07-20 03:30:00'):
|
||||||
|
create_notification(template=sample_template, created_at=datetime(2017, 7, 5), billable_units=2,
|
||||||
|
status='delivered')
|
||||||
|
|
||||||
|
create_or_update_monthly_billing_sms(sample_template.service_id, july)
|
||||||
|
second_update = get_monthly_billing_sms(sample_template.service_id, july)
|
||||||
|
expected_update = {"billing_units": 3,
|
||||||
|
"rate_multiplier": 1,
|
||||||
|
"international": False,
|
||||||
|
"rate": 0.123,
|
||||||
|
"total_cost": 3 * 0.123}
|
||||||
|
assert_monthly_billing(second_update, sample_template.service_id, 1, expected_update,
|
||||||
|
start_date=datetime(2017, 6, 30, 23), end_date=datetime(2017, 7, 31, 23, 59, 59, 99999))
|
||||||
|
assert second_update.updated_at == FakeDatetime(2017, 7, 20, 3, 30)
|
||||||
|
assert first_updated_at != second_update.updated_at
|
||||||
|
|
||||||
|
|
||||||
|
def assert_monthly_billing(monthly_billing, service_id, expected_len, first_row, start_date, end_date):
|
||||||
|
assert monthly_billing.service_id == service_id
|
||||||
|
assert len(monthly_billing.monthly_totals) == expected_len
|
||||||
|
assert sorted(monthly_billing.monthly_totals[0]) == sorted(first_row)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_service_id(notify_db_session):
|
||||||
|
service_1 = create_service(service_name="Service One")
|
||||||
|
template_1 = create_template(service=service_1)
|
||||||
|
service_2 = create_service(service_name="Service Two")
|
||||||
|
template_2 = create_template(service=service_2)
|
||||||
|
create_notification(template=template_1, created_at=datetime(2017, 6, 30, 13, 30), status='delivered')
|
||||||
|
create_notification(template=template_1, created_at=datetime(2017, 7, 1, 14, 30), status='delivered')
|
||||||
|
create_notification(template=template_2, created_at=datetime(2017, 7, 15, 13, 30))
|
||||||
|
create_notification(template=template_2, created_at=datetime(2017, 7, 31, 13, 30))
|
||||||
|
services = get_service_ids_that_need_sms_billing_populated(start_date=datetime(2017, 7, 1),
|
||||||
|
end_date=datetime(2017, 7, 16))
|
||||||
|
expected_services = [service_1.id, service_2.id]
|
||||||
|
assert sorted([x.service_id for x in services]) == sorted(expected_services)
|
||||||
@@ -6,7 +6,7 @@ from flask import current_app
|
|||||||
|
|
||||||
from app.dao.date_util import get_financial_year
|
from app.dao.date_util import get_financial_year
|
||||||
from app.dao.notification_usage_dao import (
|
from app.dao.notification_usage_dao import (
|
||||||
get_rates_for_year,
|
get_rates_for_daterange,
|
||||||
get_yearly_billing_data,
|
get_yearly_billing_data,
|
||||||
get_monthly_billing_data,
|
get_monthly_billing_data,
|
||||||
get_total_billable_units_for_sent_sms_notifications_in_date_range,
|
get_total_billable_units_for_sent_sms_notifications_in_date_range,
|
||||||
@@ -24,22 +24,22 @@ from freezegun import freeze_time
|
|||||||
from tests.conftest import set_config
|
from tests.conftest import set_config
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year(notify_db, notify_db_session):
|
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(2016, 5, 18), 0.016)
|
||||||
set_up_rate(notify_db, datetime(2017, 3, 31, 23), 0.0158)
|
set_up_rate(notify_db, datetime(2017, 3, 31, 23), 0.0158)
|
||||||
start_date, end_date = get_financial_year(2017)
|
start_date, end_date = get_financial_year(2017)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert len(rates) == 1
|
assert len(rates) == 1
|
||||||
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-03-31 23:00:00"
|
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
|
assert rates[0].rate == 0.0158
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_multiple_result_per_year(notify_db, notify_db_session):
|
def test_get_rates_for_daterange_multiple_result_per_year(notify_db, notify_db_session):
|
||||||
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2016, 5, 18), 0.016)
|
set_up_rate(notify_db, datetime(2016, 5, 18), 0.016)
|
||||||
set_up_rate(notify_db, datetime(2017, 4, 1), 0.0158)
|
set_up_rate(notify_db, datetime(2017, 4, 1), 0.0158)
|
||||||
start_date, end_date = get_financial_year(2016)
|
start_date, end_date = get_financial_year(2016)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert len(rates) == 2
|
assert len(rates) == 2
|
||||||
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-04-01 00:00:00"
|
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
|
assert rates[0].rate == 0.015
|
||||||
@@ -47,12 +47,12 @@ def test_get_rates_for_year_multiple_result_per_year(notify_db, notify_db_sessio
|
|||||||
assert rates[1].rate == 0.016
|
assert rates[1].rate == 0.016
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_returns_correct_rates(notify_db, notify_db_session):
|
def test_get_rates_for_daterange_returns_correct_rates(notify_db, notify_db_session):
|
||||||
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
|
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
|
||||||
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
||||||
start_date, end_date = get_financial_year(2017)
|
start_date, end_date = get_financial_year(2017)
|
||||||
rates_2017 = get_rates_for_year(start_date, end_date, 'sms')
|
rates_2017 = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert len(rates_2017) == 2
|
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 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
|
assert rates_2017[0].rate == 0.016
|
||||||
@@ -60,43 +60,56 @@ def test_get_rates_for_year_returns_correct_rates(notify_db, notify_db_session):
|
|||||||
assert rates_2017[1].rate == 0.0175
|
assert rates_2017[1].rate == 0.0175
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_in_the_future(notify_db, notify_db_session):
|
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(2016, 4, 1), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
||||||
start_date, end_date = get_financial_year(2018)
|
start_date, end_date = get_financial_year(2018)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2017-06-01 00:00:00"
|
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
|
assert rates[0].rate == 0.0175
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_returns_empty_list_if_year_is_before_earliest_rate(notify_db, notify_db_session):
|
def test_get_rates_for_daterange_returns_empty_list_if_year_is_before_earliest_rate(notify_db, notify_db_session):
|
||||||
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
set_up_rate(notify_db, datetime(2016, 4, 1), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
||||||
start_date, end_date = get_financial_year(2015)
|
start_date, end_date = get_financial_year(2015)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert rates == []
|
assert rates == []
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_early_rate(notify_db, notify_db_session):
|
def test_get_rates_for_daterange_early_rate(notify_db, notify_db_session):
|
||||||
set_up_rate(notify_db, datetime(2015, 6, 1), 0.014)
|
set_up_rate(notify_db, datetime(2015, 6, 1), 0.014)
|
||||||
set_up_rate(notify_db, datetime(2016, 6, 1), 0.015)
|
set_up_rate(notify_db, datetime(2016, 6, 1), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
|
set_up_rate(notify_db, datetime(2016, 9, 1), 0.016)
|
||||||
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
set_up_rate(notify_db, datetime(2017, 6, 1), 0.0175)
|
||||||
start_date, end_date = get_financial_year(2016)
|
start_date, end_date = get_financial_year(2016)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert len(rates) == 3
|
assert len(rates) == 3
|
||||||
|
|
||||||
|
|
||||||
def test_get_rates_for_year_edge_case(notify_db, notify_db_session):
|
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(2016, 3, 31, 23, 00), 0.015)
|
||||||
set_up_rate(notify_db, datetime(2017, 3, 31, 23, 00), 0.0175)
|
set_up_rate(notify_db, datetime(2017, 3, 31, 23, 00), 0.0175)
|
||||||
start_date, end_date = get_financial_year(2016)
|
start_date, end_date = get_financial_year(2016)
|
||||||
rates = get_rates_for_year(start_date, end_date, 'sms')
|
rates = get_rates_for_daterange(start_date, end_date, 'sms')
|
||||||
assert len(rates) == 1
|
assert len(rates) == 1
|
||||||
assert datetime.strftime(rates[0].valid_from, '%Y-%m-%d %H:%M:%S') == "2016-03-31 23:00:00"
|
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
|
assert rates[0].rate == 0.015
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_rates_for_daterange_where_daterange_is_one_month_that_falls_between_rate_valid_from(
|
||||||
|
notify_db, notify_db_session
|
||||||
|
):
|
||||||
|
set_up_rate(notify_db, datetime(2017, 1, 1), 0.175)
|
||||||
|
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')
|
||||||
|
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):
|
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, 4, 1), 0.014)
|
||||||
set_up_rate(notify_db, datetime(2016, 6, 1), 0.0158)
|
set_up_rate(notify_db, datetime(2016, 6, 1), 0.0158)
|
||||||
@@ -254,8 +267,7 @@ def test_get_monthly_billing_data_with_multiple_rates(notify_db, notify_db_sessi
|
|||||||
assert results[3] == ('June', 4, 1, False, 'sms', 0.0175)
|
assert results[3] == ('June', 4, 1, False, 'sms', 0.0175)
|
||||||
|
|
||||||
|
|
||||||
def test_get_monthly_billing_data_with_no_notifications_for_year(notify_db, notify_db_session, sample_template,
|
def test_get_monthly_billing_data_with_no_notifications_for_daterange(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, 4, 1), 0.014)
|
||||||
results = get_monthly_billing_data(sample_template.service_id, 2016)
|
results = get_monthly_billing_data(sample_template.service_id, 2016)
|
||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from app.dao.provider_rates_dao import create_provider_rates
|
from app.dao.provider_rates_dao import create_provider_rates
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
from app import db
|
||||||
from app.dao.jobs_dao import dao_create_job
|
from app.dao.jobs_dao import dao_create_job
|
||||||
from app.dao.service_inbound_api_dao import save_service_inbound_api
|
from app.dao.service_inbound_api_dao import save_service_inbound_api
|
||||||
from app.models import (
|
from app.models import (
|
||||||
@@ -11,6 +11,7 @@ from app.models import (
|
|||||||
Notification,
|
Notification,
|
||||||
ScheduledNotification,
|
ScheduledNotification,
|
||||||
ServicePermission,
|
ServicePermission,
|
||||||
|
Rate,
|
||||||
Job,
|
Job,
|
||||||
InboundSms,
|
InboundSms,
|
||||||
Organisation,
|
Organisation,
|
||||||
@@ -239,3 +240,10 @@ def create_organisation(colour='blue', logo='test_x2.png', name='test_org_1'):
|
|||||||
dao_create_organisation(organisation)
|
dao_create_organisation(organisation)
|
||||||
|
|
||||||
return organisation
|
return organisation
|
||||||
|
|
||||||
|
|
||||||
|
def create_rate(start_date, value, notification_type):
|
||||||
|
rate = Rate(id=uuid.uuid4(), valid_from=start_date, rate=value, notification_type=notification_type)
|
||||||
|
db.session.add(rate)
|
||||||
|
db.session.commit()
|
||||||
|
return rate
|
||||||
|
|||||||
@@ -435,29 +435,22 @@ def test_get_html_email_renderer_prepends_logo_path(notify_api):
|
|||||||
|
|
||||||
renderer = send_to_providers.get_html_email_options(service)
|
renderer = send_to_providers.get_html_email_options(service)
|
||||||
|
|
||||||
assert renderer['brand_logo'] == 'http://localhost:6012/static/images/email-template/crests/justice-league.png'
|
assert renderer['brand_logo'] == 'http://static-logos.notify.tools/justice-league.png'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('base_url, expected_url', [
|
@pytest.mark.parametrize('base_url, expected_url', [
|
||||||
# don't change localhost to prevent errors when testing locally
|
# don't change localhost to prevent errors when testing locally
|
||||||
('http://localhost:6012', 'http://localhost:6012/static/sub-path/filename.png'),
|
('http://localhost:6012', 'http://static-logos.notify.tools/filename.png'),
|
||||||
# on other environments, replace www with staging
|
('https://www.notifications.service.gov.uk', 'https://static-logos.notifications.service.gov.uk/filename.png'),
|
||||||
('https://www.notifications.service.gov.uk', 'https://static.notifications.service.gov.uk/sub-path/filename.png'),
|
('https://notify.works', 'https://static-logos.notify.works/filename.png'),
|
||||||
|
('https://staging-notify.works', 'https://static-logos.staging-notify.works/filename.png'),
|
||||||
# staging and preview do not have cloudfront running, so should act as localhost
|
('https://www.notify.works', 'https://static-logos.notify.works/filename.png'),
|
||||||
pytest.mark.xfail(('https://www.notify.works', 'https://static.notify.works/sub-path/filename.png')),
|
('https://www.staging-notify.works', 'https://static-logos.staging-notify.works/filename.png'),
|
||||||
pytest.mark.xfail(('https://www.staging-notify.works', 'https://static.notify.works/sub-path/filename.png')),
|
|
||||||
pytest.mark.xfail(('https://notify.works', 'https://static.notify.works/sub-path/filename.png')),
|
|
||||||
pytest.mark.xfail(('https://staging-notify.works', 'https://static.notify.works/sub-path/filename.png')),
|
|
||||||
# these tests should be removed when cloudfront works on staging/preview
|
|
||||||
('https://www.notify.works', 'https://www.notify.works/static/sub-path/filename.png'),
|
|
||||||
('https://www.staging-notify.works', 'https://www.staging-notify.works/static/sub-path/filename.png'),
|
|
||||||
])
|
])
|
||||||
def test_get_logo_url_works_for_different_environments(base_url, expected_url):
|
def test_get_logo_url_works_for_different_environments(base_url, expected_url):
|
||||||
branding_path = '/sub-path/'
|
|
||||||
logo_file = 'filename.png'
|
logo_file = 'filename.png'
|
||||||
|
|
||||||
logo_url = send_to_providers.get_logo_url(base_url, branding_path, logo_file)
|
logo_url = send_to_providers.get_logo_url(base_url, logo_file)
|
||||||
|
|
||||||
assert logo_url == expected_url
|
assert logo_url == expected_url
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,18 @@ def test_persist_notification_creates_and_save_to_db(sample_template, sample_api
|
|||||||
|
|
||||||
assert Notification.query.count() == 0
|
assert Notification.query.count() == 0
|
||||||
assert NotificationHistory.query.count() == 0
|
assert NotificationHistory.query.count() == 0
|
||||||
notification = persist_notification(sample_template.id, sample_template.version, '+447111111111',
|
notification = persist_notification(
|
||||||
sample_template.service, {}, 'sms', sample_api_key.id,
|
template_id=sample_template.id,
|
||||||
sample_api_key.key_type, job_id=sample_job.id,
|
template_version=sample_template.version,
|
||||||
job_row_number=100, reference="ref")
|
recipient='+447111111111',
|
||||||
|
service=sample_template.service,
|
||||||
|
personalisation={},
|
||||||
|
notification_type='sms',
|
||||||
|
api_key_id=sample_api_key.id,
|
||||||
|
key_type=sample_api_key.key_type,
|
||||||
|
job_id=sample_job.id,
|
||||||
|
job_row_number=100,
|
||||||
|
reference="ref")
|
||||||
|
|
||||||
assert Notification.query.get(notification.id) is not None
|
assert Notification.query.get(notification.id) is not None
|
||||||
assert NotificationHistory.query.get(notification.id) is not None
|
assert NotificationHistory.query.get(notification.id) is not None
|
||||||
@@ -127,14 +135,14 @@ def test_persist_notification_does_not_increment_cache_if_test_key(
|
|||||||
assert Notification.query.count() == 0
|
assert Notification.query.count() == 0
|
||||||
assert NotificationHistory.query.count() == 0
|
assert NotificationHistory.query.count() == 0
|
||||||
persist_notification(
|
persist_notification(
|
||||||
sample_template.id,
|
template_id=sample_template.id,
|
||||||
sample_template.version,
|
template_version=sample_template.version,
|
||||||
'+447111111111',
|
recipient='+447111111111',
|
||||||
sample_template.service,
|
service=sample_template.service,
|
||||||
{},
|
personalisation={},
|
||||||
'sms',
|
notification_type='sms',
|
||||||
api_key.id,
|
api_key_id=api_key.id,
|
||||||
api_key.key_type,
|
key_type=api_key.key_type,
|
||||||
job_id=sample_job.id,
|
job_id=sample_job.id,
|
||||||
job_row_number=100,
|
job_row_number=100,
|
||||||
reference="ref",
|
reference="ref",
|
||||||
@@ -193,18 +201,33 @@ def test_persist_notification_increments_cache_if_key_exists(sample_template, sa
|
|||||||
mock_incr = mocker.patch('app.notifications.process_notifications.redis_store.incr')
|
mock_incr = mocker.patch('app.notifications.process_notifications.redis_store.incr')
|
||||||
mock_incr_hash_value = mocker.patch('app.notifications.process_notifications.redis_store.increment_hash_value')
|
mock_incr_hash_value = mocker.patch('app.notifications.process_notifications.redis_store.increment_hash_value')
|
||||||
|
|
||||||
persist_notification(sample_template.id, sample_template.version, '+447111111111',
|
persist_notification(
|
||||||
sample_template.service, {}, 'sms', sample_api_key.id,
|
template_id=sample_template.id,
|
||||||
sample_api_key.key_type, reference="ref")
|
template_version=sample_template.version,
|
||||||
|
recipient='+447111111111',
|
||||||
|
service=sample_template.service,
|
||||||
|
personalisation={},
|
||||||
|
notification_type='sms',
|
||||||
|
api_key_id=sample_api_key.id,
|
||||||
|
key_type=sample_api_key.key_type,
|
||||||
|
reference="ref"
|
||||||
|
)
|
||||||
mock_incr.assert_not_called()
|
mock_incr.assert_not_called()
|
||||||
mock_incr_hash_value.assert_not_called()
|
mock_incr_hash_value.assert_not_called()
|
||||||
|
|
||||||
mocker.patch('app.notifications.process_notifications.redis_store.get', return_value=1)
|
mocker.patch('app.notifications.process_notifications.redis_store.get', return_value=1)
|
||||||
mocker.patch('app.notifications.process_notifications.redis_store.get_all_from_hash',
|
mocker.patch('app.notifications.process_notifications.redis_store.get_all_from_hash',
|
||||||
return_value={sample_template.id, 1})
|
return_value={sample_template.id, 1})
|
||||||
persist_notification(sample_template.id, sample_template.version, '+447111111122',
|
persist_notification(
|
||||||
sample_template.service, {}, 'sms', sample_api_key.id,
|
template_id=sample_template.id,
|
||||||
sample_api_key.key_type, reference="ref2")
|
template_version=sample_template.version,
|
||||||
|
recipient='+447111111122',
|
||||||
|
service=sample_template.service,
|
||||||
|
personalisation={},
|
||||||
|
notification_type='sms',
|
||||||
|
api_key_id=sample_api_key.id,
|
||||||
|
key_type=sample_api_key.key_type,
|
||||||
|
reference="ref2")
|
||||||
mock_incr.assert_called_once_with(str(sample_template.service_id) + "-2016-01-01-count", )
|
mock_incr.assert_called_once_with(str(sample_template.service_id) + "-2016-01-01-count", )
|
||||||
mock_incr_hash_value.assert_called_once_with(cache_key_for_service_template_counter(sample_template.service_id),
|
mock_incr_hash_value.assert_called_once_with(cache_key_for_service_template_counter(sample_template.service_id),
|
||||||
sample_template.id)
|
sample_template.id)
|
||||||
|
|||||||
166
tests/app/v2/notifications/test_post_letter_notifications.py
Normal file
166
tests/app/v2/notifications/test_post_letter_notifications.py
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from flask import url_for, json
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.models import Job, Notification, SMS_TYPE, EMAIL_TYPE, LETTER_TYPE
|
||||||
|
from app.v2.errors import RateLimitError
|
||||||
|
|
||||||
|
from tests import create_authorization_header
|
||||||
|
from tests.app.db import create_service, create_template
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skip('Leters not currently implemented')
|
||||||
|
|
||||||
|
|
||||||
|
def letter_request(client, data, service_id, _expected_status=201):
|
||||||
|
resp = client.post(
|
||||||
|
url_for('v2_notifications.post_notification', notification_type='letter'),
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=[('Content-Type', 'application/json'), create_authorization_header(service_id=service_id)]
|
||||||
|
)
|
||||||
|
json_resp = json.loads(resp.get_data(as_text=True))
|
||||||
|
assert resp.status_code == _expected_status, json_resp
|
||||||
|
return json_resp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('reference', [None, 'reference_from_client'])
|
||||||
|
def test_post_letter_notification_returns_201(client, sample_letter_template, mocker, reference):
|
||||||
|
mocked = mocker.patch('app.celery.tasks.build_dvla_file.apply_async')
|
||||||
|
data = {
|
||||||
|
'template_id': str(sample_letter_template.id),
|
||||||
|
'personalisation': {
|
||||||
|
'address_line_1': 'Her Royal Highness Queen Elizabeth II',
|
||||||
|
'address_line_2': 'Buckingham Palace',
|
||||||
|
'address_line_3': 'London',
|
||||||
|
'postcode': 'SW1 1AA',
|
||||||
|
'name': 'Lizzie'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if reference:
|
||||||
|
data.update({'reference': reference})
|
||||||
|
|
||||||
|
resp_json = letter_request(client, data, service_id=sample_letter_template.service_id)
|
||||||
|
|
||||||
|
job = Job.query.one()
|
||||||
|
notification = Notification.query.all()
|
||||||
|
notification_id = notification.id
|
||||||
|
assert resp_json['id'] == str(notification_id)
|
||||||
|
assert resp_json['reference'] == reference
|
||||||
|
assert resp_json['content']['subject'] == sample_letter_template.subject
|
||||||
|
assert resp_json['content']['body'] == sample_letter_template.content
|
||||||
|
assert 'v2/notifications/{}'.format(notification_id) in resp_json['uri']
|
||||||
|
assert resp_json['template']['id'] == str(sample_letter_template.id)
|
||||||
|
assert resp_json['template']['version'] == sample_letter_template.version
|
||||||
|
assert (
|
||||||
|
'services/{}/templates/{}'.format(
|
||||||
|
sample_letter_template.service_id,
|
||||||
|
sample_letter_template.id
|
||||||
|
) in resp_json['template']['uri']
|
||||||
|
)
|
||||||
|
assert not resp_json['scheduled_for']
|
||||||
|
|
||||||
|
mocked.assert_called_once_with((str(job.id), ), queue='job-tasks')
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_letter_notification_returns_400_and_missing_template(
|
||||||
|
client,
|
||||||
|
sample_service
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
'template_id': str(uuid.uuid4()),
|
||||||
|
'personalisation': {'address_line_1': '', 'postcode': ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_json = letter_request(client, data, service_id=sample_service.id, _expected_status=400)
|
||||||
|
|
||||||
|
assert error_json['status_code'] == 400
|
||||||
|
assert error_json['errors'] == [{'error': 'BadRequestError', 'message': 'Template not found'}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_notification_returns_403_and_well_formed_auth_error(
|
||||||
|
client,
|
||||||
|
sample_letter_template
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
'template_id': str(sample_letter_template.id),
|
||||||
|
'personalisation': {'address_line_1': '', 'postcode': ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_json = letter_request(client, data, service_id=sample_letter_template.service_id, _expected_status=401)
|
||||||
|
|
||||||
|
assert error_json['status_code'] == 401
|
||||||
|
assert error_json['errors'] == [{
|
||||||
|
'error': 'AuthError',
|
||||||
|
'message': 'Unauthorized, authentication token must be provided'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_notification_returns_400_for_schema_problems(
|
||||||
|
client,
|
||||||
|
sample_service
|
||||||
|
):
|
||||||
|
data = {
|
||||||
|
'personalisation': {'address_line_1': '', 'postcode': ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_json = letter_request(client, data, service_id=sample_service.id, _expected_status=400)
|
||||||
|
|
||||||
|
assert error_json['status_code'] == 400
|
||||||
|
assert error_json['errors'] == [{
|
||||||
|
'error': 'ValidationError',
|
||||||
|
'message': 'template_id is a required property'
|
||||||
|
}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_returns_a_429_limit_exceeded_if_rate_limit_exceeded(
|
||||||
|
client,
|
||||||
|
sample_letter_template,
|
||||||
|
mocker
|
||||||
|
):
|
||||||
|
persist_mock = mocker.patch('app.v2.notifications.post_notifications.persist_notification')
|
||||||
|
mocker.patch(
|
||||||
|
'app.v2.notifications.post_notifications.check_rate_limiting',
|
||||||
|
side_effect=RateLimitError('LIMIT', 'INTERVAL', 'TYPE')
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'template_id': str(sample_letter_template.id),
|
||||||
|
'personalisation': {'address_line_1': '', 'postcode': ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_json = letter_request(client, data, service_id=sample_letter_template.service_id, _expected_status=429)
|
||||||
|
|
||||||
|
assert error_json['status_code'] == 429
|
||||||
|
assert error_json['errors'] == [{
|
||||||
|
'error': 'RateLimitError',
|
||||||
|
'message': 'Exceeded rate limit for key type TYPE of LIMIT requests per INTERVAL seconds'
|
||||||
|
}]
|
||||||
|
|
||||||
|
assert not persist_mock.called
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('service_args', [
|
||||||
|
{'service_permissions': [EMAIL_TYPE, SMS_TYPE]},
|
||||||
|
{'restricted': True}
|
||||||
|
])
|
||||||
|
def test_post_letter_notification_returns_403_if_not_allowed_to_send_notification(
|
||||||
|
client,
|
||||||
|
notify_db_session,
|
||||||
|
service_args
|
||||||
|
):
|
||||||
|
service = create_service(**service_args)
|
||||||
|
template = create_template(service, template_type=LETTER_TYPE)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'template_id': str(template.id),
|
||||||
|
'personalisation': {'address_line_1': '', 'postcode': ''}
|
||||||
|
}
|
||||||
|
|
||||||
|
error_json = letter_request(client, data, service_id=service.id, _expected_status=400)
|
||||||
|
assert error_json['status_code'] == 403
|
||||||
|
assert error_json['errors'] == [
|
||||||
|
{'error': 'BadRequestError', 'message': 'Cannot send letters'}
|
||||||
|
]
|
||||||
@@ -58,8 +58,8 @@ def test_post_sms_notification_returns_201(client, sample_template_with_placehol
|
|||||||
@pytest.mark.parametrize("notification_type, key_send_to, send_to",
|
@pytest.mark.parametrize("notification_type, key_send_to, send_to",
|
||||||
[("sms", "phone_number", "+447700900855"),
|
[("sms", "phone_number", "+447700900855"),
|
||||||
("email", "email_address", "sample@email.com")])
|
("email", "email_address", "sample@email.com")])
|
||||||
def test_post_sms_notification_returns_400_and_missing_template(client, sample_service,
|
def test_post_notification_returns_400_and_missing_template(client, sample_service,
|
||||||
notification_type, key_send_to, send_to):
|
notification_type, key_send_to, send_to):
|
||||||
data = {
|
data = {
|
||||||
key_send_to: send_to,
|
key_send_to: send_to,
|
||||||
'template_id': str(uuid.uuid4())
|
'template_id': str(uuid.uuid4())
|
||||||
@@ -434,3 +434,15 @@ def test_post_notification_raises_bad_request_if_service_not_invited_to_schedule
|
|||||||
error_json = json.loads(response.get_data(as_text=True))
|
error_json = json.loads(response.get_data(as_text=True))
|
||||||
assert error_json['errors'] == [
|
assert error_json['errors'] == [
|
||||||
{"error": "BadRequestError", "message": 'Cannot schedule notifications (this feature is invite-only)'}]
|
{"error": "BadRequestError", "message": 'Cannot schedule notifications (this feature is invite-only)'}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_notification_raises_bad_request_if_not_valid_notification_type(client, sample_service):
|
||||||
|
auth_header = create_authorization_header(service_id=sample_service.id)
|
||||||
|
response = client.post(
|
||||||
|
'/v2/notifications/foo',
|
||||||
|
data='{}',
|
||||||
|
headers=[('Content-Type', 'application/json'), auth_header]
|
||||||
|
)
|
||||||
|
assert response.status_code == 404
|
||||||
|
error_json = json.loads(response.get_data(as_text=True))
|
||||||
|
assert 'The requested URL was not found on the server.' in error_json['message']
|
||||||
|
|||||||
7
wsgi.py
7
wsgi.py
@@ -1,13 +1,6 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
from app import create_app
|
from app import create_app
|
||||||
from credstash import getAllSecrets
|
|
||||||
|
|
||||||
|
|
||||||
# On AWS get secrets and export to env, skip this on Cloud Foundry
|
|
||||||
if os.getenv('VCAP_SERVICES') is None:
|
|
||||||
os.environ.update(getAllSecrets(region="eu-west-1"))
|
|
||||||
|
|
||||||
application = create_app()
|
application = create_app()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user