diff --git a/app/dao/inbound_numbers_dao.py b/app/dao/inbound_numbers_dao.py new file mode 100644 index 000000000..1c184b7f6 --- /dev/null +++ b/app/dao/inbound_numbers_dao.py @@ -0,0 +1,30 @@ +from app import db +from app.dao.dao_utils import transactional +from app.models import InboundNumber + + +def dao_get_inbound_numbers(): + return InboundNumber.query.all() + + +def dao_get_available_inbound_numbers(): + return InboundNumber.query.filter(InboundNumber.active, InboundNumber.service_id.is_(None)).all() + + +def dao_get_inbound_number_for_service(service_id): + return InboundNumber.query.filter(InboundNumber.service_id == service_id).first() + + +@transactional +def dao_set_inbound_number_to_service(service_id, inbound_number): + inbound_number.service_id = service_id + + db.session.add(inbound_number) + + +@transactional +def dao_set_inbound_number_active_flag(inbound_number_id, active): + inbound_number = InboundNumber.query.filter(InboundNumber.id == inbound_number_id).first() + inbound_number.active = active + + db.session.add(inbound_number) diff --git a/app/models.py b/app/models.py index 4ab486276..0f4fddf57 100644 --- a/app/models.py +++ b/app/models.py @@ -242,6 +242,36 @@ class Service(db.Model, Versioned): return cls(**fields) +class InboundNumber(db.Model): + __tablename__ = "inbound_numbers" + + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + number = db.Column(db.String(11), unique=True, nullable=False) + provider = db.Column(db.String(), nullable=False) + service_id = db.Column(UUID(as_uuid=True), db.ForeignKey('services.id'), unique=True, index=True, nullable=True) + service = db.relationship(Service, backref=db.backref("inbound_number", uselist=False)) + active = db.Column(db.Boolean, index=False, unique=False, nullable=False, default=True) + created_at = db.Column(db.DateTime, default=datetime.datetime.utcnow, nullable=False) + updated_at = db.Column(db.DateTime, nullable=True, onupdate=datetime.datetime.utcnow) + + def serialize(self): + def serialize_service(): + return { + "id": str(self.service_id), + "name": self.service.name + } + + return { + "id": str(self.id), + "number": self.number, + "provider": self.provider, + "service": serialize_service() if self.service else None, + "active": self.active, + "created_at": self.created_at.strftime(DATETIME_FORMAT), + "updated_at": self.updated_at.strftime(DATETIME_FORMAT) if self.updated_at else None, + } + + class ServicePermission(db.Model): __tablename__ = "service_permissions" diff --git a/migrations/versions/0115_add_inbound_numbers.py b/migrations/versions/0115_add_inbound_numbers.py new file mode 100644 index 000000000..18644d312 --- /dev/null +++ b/migrations/versions/0115_add_inbound_numbers.py @@ -0,0 +1,35 @@ +"""empty message + +Revision ID: 0115_add_inbound_numbers +Revises: 0014_drop_monthly_billing_cols +Create Date: 2017-08-10 17:30:01.507694 + +""" + +# revision identifiers, used by Alembic. +revision = '0115_add_inbound_numbers' +down_revision = '0014_drop_monthly_billing_cols' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +def upgrade(): + op.create_table('inbound_numbers', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('number', sa.String(length=11), nullable=False), + sa.Column('provider', sa.String(), nullable=False), + sa.Column('service_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['service_id'], ['services.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('number') + ) + op.create_index(op.f('ix_inbound_numbers_service_id'), 'inbound_numbers', ['service_id'], unique=True) + + +def downgrade(): + op.drop_index(op.f('ix_inbound_numbers_service_id'), table_name='inbound_numbers') + op.drop_table('inbound_numbers') diff --git a/tests/app/conftest.py b/tests/app/conftest.py index 30a9d4210..d111d8d3c 100644 --- a/tests/app/conftest.py +++ b/tests/app/conftest.py @@ -39,7 +39,14 @@ from app.dao.invited_user_dao import save_invited_user from app.dao.provider_rates_dao import create_provider_rates from app.clients.sms.firetext import FiretextClient from tests import create_authorization_header -from tests.app.db import create_user, create_template, create_notification, create_api_key +from tests.app.db import ( + create_user, + create_template, + create_notification, + create_service, + create_api_key, + create_inbound_number +) @pytest.yield_fixture @@ -983,6 +990,16 @@ def sample_provider_rate(notify_db, notify_db_session, valid_from=None, rate=Non ) +@pytest.fixture +def sample_inbound_numbers(notify_db, notify_db_session, sample_service): + service = create_service(service_name='sample service 2') + inbound_numbers = [] + inbound_numbers.append(create_inbound_number(number='1', provider='mmg')) + inbound_numbers.append(create_inbound_number(number='2', provider='mmg', active=False, service_id=service.id)) + inbound_numbers.append(create_inbound_number(number='3', provider='firetext', service_id=sample_service.id)) + return inbound_numbers + + @pytest.fixture def restore_provider_details(notify_db, notify_db_session): """ diff --git a/tests/app/dao/test_inbound_numbers_dao.py b/tests/app/dao/test_inbound_numbers_dao.py new file mode 100644 index 000000000..9d59a8110 --- /dev/null +++ b/tests/app/dao/test_inbound_numbers_dao.py @@ -0,0 +1,81 @@ +import pytest +from sqlalchemy.exc import IntegrityError + +from app.dao.inbound_numbers_dao import ( + dao_get_inbound_numbers, + dao_get_inbound_number_for_service, + dao_get_available_inbound_numbers, + dao_set_inbound_number_to_service, + dao_set_inbound_number_active_flag +) +from app.models import InboundNumber + +from tests.app.db import create_service, create_inbound_number + + +def test_get_inbound_numbers(notify_db, notify_db_session, sample_inbound_numbers): + res = dao_get_inbound_numbers() + + assert len(res) == len(sample_inbound_numbers) + assert res == sample_inbound_numbers + + +def test_get_available_inbound_numbers(notify_db, notify_db_session): + inbound_number = create_inbound_number(number='1') + + res = dao_get_available_inbound_numbers() + + assert len(res) == 1 + assert res[0] == inbound_number + + +def test_set_service_id_on_inbound_number(notify_db, notify_db_session, sample_inbound_numbers): + service = create_service(service_name='test service') + numbers = dao_get_available_inbound_numbers() + + dao_set_inbound_number_to_service(service.id, numbers[0]) + + res = InboundNumber.query.filter(InboundNumber.service_id == service.id).all() + + assert len(res) == 1 + assert res[0].service_id == service.id + + +def test_after_setting_service_id_that_inbound_number_is_unavailable( + notify_db, notify_db_session, sample_inbound_numbers): + service = create_service(service_name='test service') + numbers = dao_get_available_inbound_numbers() + + assert len(numbers) == 1 + + dao_set_inbound_number_to_service(service.id, numbers[0]) + + res = dao_get_available_inbound_numbers() + + assert len(res) == 0 + + +def test_setting_a_service_twice_will_raise_an_error(notify_db, notify_db_session): + create_inbound_number(number='1') + create_inbound_number(number='2') + service = create_service(service_name='test service') + numbers = dao_get_available_inbound_numbers() + + dao_set_inbound_number_to_service(service.id, numbers[0]) + + with pytest.raises(IntegrityError) as e: + dao_set_inbound_number_to_service(service.id, numbers[1]) + + assert 'duplicate key value violates unique constraint' in str(e.value) + + +@pytest.mark.parametrize("active", [True, False]) +def test_set_inbound_number_active_flag(notify_db, notify_db_session, sample_service, active): + inbound_number = create_inbound_number(number='1') + dao_set_inbound_number_to_service(sample_service.id, inbound_number) + + dao_set_inbound_number_active_flag(inbound_number.id, active=active) + + inbound_number = dao_get_inbound_number_for_service(sample_service.id) + + assert inbound_number.active is active diff --git a/tests/app/dao/test_services_dao.py b/tests/app/dao/test_services_dao.py index ee4f4f912..b60767289 100644 --- a/tests/app/dao/test_services_dao.py +++ b/tests/app/dao/test_services_dao.py @@ -7,6 +7,10 @@ from sqlalchemy.orm.exc import FlushError, NoResultFound from sqlalchemy.exc import IntegrityError from freezegun import freeze_time from app import db +from app.dao.inbound_numbers_dao import ( + dao_set_inbound_number_to_service, + dao_get_available_inbound_numbers +) from app.dao.services_dao import ( dao_create_service, dao_add_user_to_service, @@ -896,3 +900,13 @@ def test_dao_fetch_services_by_sms_sender(notify_db_session): services = dao_fetch_services_by_sms_sender('foo') assert {foo1.id, foo2.id} == {x.id for x in services} + + +def test_dao_allocating_inbound_number_shows_on_service(notify_db_session, sample_inbound_numbers): + inbound_numbers = dao_get_available_inbound_numbers() + + service = create_service(service_name='test service') + + dao_set_inbound_number_to_service(service.id, inbound_numbers[0]) + + assert service.inbound_number.number == inbound_numbers[0].number diff --git a/tests/app/db.py b/tests/app/db.py index 71bf29bcb..6a4f45cbf 100644 --- a/tests/app/db.py +++ b/tests/app/db.py @@ -15,6 +15,7 @@ from app.models import ( Rate, Job, InboundSms, + InboundNumber, Organisation, EMAIL_TYPE, SMS_TYPE, @@ -276,3 +277,16 @@ def create_api_key(service, key_type=KEY_TYPE_NORMAL): db.session.add(api_key) db.session.commit() return api_key + + +def create_inbound_number(number, provider='mmg', active=True, service_id=None): + inbound_number = InboundNumber( + id=uuid.uuid4(), + number=number, + provider=provider, + active=active, + service_id=service_id + ) + db.session.add(inbound_number) + db.session.commit() + return inbound_number diff --git a/tests/app/inbound_number/__init__.py b/tests/app/inbound_number/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/app/test_model.py b/tests/app/test_model.py index 66b96ba4e..b8ffcf9b4 100644 --- a/tests/app/test_model.py +++ b/tests/app/test_model.py @@ -19,7 +19,7 @@ from tests.app.conftest import ( sample_template as create_sample_template, sample_notification_with_job as create_sample_notification_with_job ) -from tests.app.db import create_notification +from tests.app.db import create_notification, create_service, create_inbound_number @pytest.mark.parametrize('mobile_number', [ @@ -218,3 +218,12 @@ def test_email_notification_serializes_with_subject(client, sample_email_templat def test_letter_notification_serializes_with_subject(client, sample_letter_template): res = sample_letter_template.serialize() assert res['subject'] == 'Template subject' + + +def test_inbound_number_serializes_with_service(client, notify_db_session): + service = create_service() + inbound_number = create_inbound_number(number='1', service_id=service.id) + serialized_inbound_number = inbound_number.serialize() + assert serialized_inbound_number.get('id') == str(inbound_number.id) + assert serialized_inbound_number.get('service').get('id') == str(inbound_number.service.id) + assert serialized_inbound_number.get('service').get('name') == inbound_number.service.name