Skip to content

Commit

Permalink
feat: Remove ability to receive text messages
Browse files Browse the repository at this point in the history
We are automating the following query

```
select * from service_sms_senders where service_id= 'service_id';
select * from inbound_numbers where service_id = 'service_id';

select * from service_permissions where service_id= 'service_id';

delete from service_permissions where service_id = 'service_id' and permission = 'inbound_sms';

delete from service_sms_senders where service_id = 'service_id' and inbound_number_id is not null;

-- run one of the two following queries
-- if we wish to release the number (if it has never been used)
update inbound_numbers set service_id = null, updated_at = now()
where service_id = 'service_id' and active;

-- if we wish to archive the number instead (if it has been used before so we dont want new services receiving the prev service's messages)
update inbound_numbers set service_id = null, updated_at = now(), active = false
where service_id = 'service_id' and active;
```

- Remove inbound_sms permission from service
- Remove service_sms_sender with inbound_number_id
- Disassociate inbound_number with service
- If the number is being release, leave 'active' status as True, if being archived, update 'active' to False
  • Loading branch information
joybytes committed Dec 19, 2024
1 parent bf78beb commit 3c78ed6
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 8 deletions.
32 changes: 31 additions & 1 deletion app/dao/inbound_numbers_dao.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from uuid import UUID

from app import db
from app.dao.dao_utils import autocommit
from app.constants import INBOUND_SMS_TYPE
from app.dao.dao_utils import autocommit, transaction
from app.models import InboundNumber


Expand Down Expand Up @@ -41,3 +44,30 @@ def dao_allocate_number_for_service(service_id, inbound_number_id):
if not updated:
raise Exception(f"Inbound number: {inbound_number_id} is not available")
return InboundNumber.query.get(inbound_number_id)


def archive_or_release_inbound_number_for_service(service_id: UUID, archive: bool, commit=True):
update_data = {
"service_id": None,
}

if archive:
update_data["active"] = False

result = InboundNumber.query.filter_by(service_id=service_id, active=True).update(
update_data, synchronize_session="fetch"
)

if commit:
db.session.commit()
return result


def dao_remove_inbound_sms_for_service(service_id, archive):
from app.dao.service_permissions_dao import dao_remove_service_permission
from app.dao.service_sms_sender_dao import dao_remove_inbound_sms_senders

with transaction():
dao_remove_service_permission(service_id, INBOUND_SMS_TYPE, commit=False)
dao_remove_inbound_sms_senders(service_id, commit=False)
archive_or_release_inbound_number_for_service(service_id, archive, commit=False)
10 changes: 6 additions & 4 deletions app/dao/service_permissions_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ def dao_add_service_permission(service_id, permission):
db.session.add(service_permission)


def dao_remove_service_permission(service_id, permission):
deleted = ServicePermission.query.filter(
def dao_remove_service_permission(service_id, permission, commit=True):
result = ServicePermission.query.filter(
ServicePermission.service_id == service_id, ServicePermission.permission == permission
).delete()
db.session.commit()
return deleted

if commit:
db.session.commit()
return result
14 changes: 14 additions & 0 deletions app/dao/service_sms_sender_dao.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from uuid import UUID

from sqlalchemy import desc

from app import db
Expand Down Expand Up @@ -106,3 +108,15 @@ def _raise_when_no_default(old_default):
# check that the update is not updating the only default to false
if not old_default:
raise Exception("You must have at least one SMS sender as the default.", 400)


def dao_remove_inbound_sms_senders(service_id: UUID, commit=True):
result = (
ServiceSmsSender.query.filter_by(service_id=service_id)
.filter(ServiceSmsSender.inbound_number_id.isnot(None))
.delete(synchronize_session="fetch")
)

if commit:
db.session.commit()
return result
14 changes: 14 additions & 0 deletions app/inbound_sms/inbound_sms_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,17 @@
"phone_number": {"type": "string"},
},
}

remove_inbound_sms_for_service_schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Schema for validating the input to remove inbound SMS capability",
"type": "object",
"properties": {
"archive": {
"type": "boolean",
"description": "Indicates whether to archive the inbound number or release it.",
},
},
"required": ["archive"],
"additionalProperties": False,
}
26 changes: 24 additions & 2 deletions app/inbound_sms/rest.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from flask import Blueprint, jsonify, request
from flask import Blueprint, current_app, jsonify, request
from notifications_utils.recipient_validation.phone_number import try_validate_and_format_phone_number

from app.dao.inbound_numbers_dao import (
dao_remove_inbound_sms_for_service,
)
from app.dao.inbound_sms_dao import (
dao_count_inbound_sms_for_service,
dao_get_inbound_sms_by_id,
Expand All @@ -9,7 +12,10 @@
)
from app.dao.service_data_retention_dao import fetch_service_data_retention_by_notification_type
from app.errors import register_errors
from app.inbound_sms.inbound_sms_schemas import get_inbound_sms_for_service_schema
from app.inbound_sms.inbound_sms_schemas import (
get_inbound_sms_for_service_schema,
remove_inbound_sms_for_service_schema,
)
from app.schema_validation import validate

inbound_sms = Blueprint("inbound_sms", __name__, url_prefix="/service/<uuid:service_id>/inbound-sms")
Expand Down Expand Up @@ -60,3 +66,19 @@ def get_inbound_by_id(service_id, inbound_sms_id):
message = dao_get_inbound_sms_by_id(service_id, inbound_sms_id)

return jsonify(message.serialize()), 200


@inbound_sms.route("/remove", methods=["POST"])
def remove_inbound_sms_for_service(service_id):
data = request.get_json()
validate(data, remove_inbound_sms_for_service_schema)

archive = data["archive"]

try:
dao_remove_inbound_sms_for_service(service_id, archive)
return jsonify({}), 200

except Exception as e:
current_app.logger.error("error removing inbound SMS for service %s: %s", service_id, e)
return jsonify({"message": str(e)}), 500
17 changes: 17 additions & 0 deletions tests/app/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from app.history_meta import create_history
from app.models import (
ApiKey,
InboundSmsHistory,
InvitedUser,
Job,
Notification,
Expand Down Expand Up @@ -1043,6 +1044,22 @@ def sample_inbound_numbers(sample_service):
return inbound_numbers


@pytest.fixture
def sample_inbound_sms_history(notify_db_session, sample_service):
inbound_sms = InboundSmsHistory(
id=uuid.uuid4(),
created_at=datetime.utcnow(),
service_id=sample_service.id,
notify_number="3",
provider_date=datetime.utcnow(),
provider_reference="reference-id",
provider="firetext",
)
notify_db_session.add(inbound_sms)
notify_db_session.commit()
return inbound_sms


@pytest.fixture
def sample_organisation(notify_db_session):
org = Organisation(name="sample organisation")
Expand Down
91 changes: 90 additions & 1 deletion tests/app/dao/test_inbound_numbers_dao.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import pytest
from sqlalchemy.exc import IntegrityError
from sqlalchemy.exc import IntegrityError, SQLAlchemyError

from app.constants import INBOUND_SMS_TYPE
from app.dao.inbound_numbers_dao import (
archive_or_release_inbound_number_for_service,
dao_allocate_number_for_service,
dao_get_available_inbound_numbers,
dao_get_inbound_number,
dao_get_inbound_number_for_service,
dao_get_inbound_numbers,
dao_remove_inbound_sms_for_service,
dao_set_inbound_number_active_flag,
dao_set_inbound_number_to_service,
)
from app.dao.service_sms_sender_dao import dao_add_sms_sender_for_service, dao_get_sms_senders_by_service_id
from app.models import InboundNumber
from tests.app.db import create_inbound_number, create_service

Expand Down Expand Up @@ -104,3 +109,87 @@ def test_dao_allocate_number_for_service_raises_if_invalid_inbound_number(notify
with pytest.raises(Exception) as exc:
dao_allocate_number_for_service(service_id=service.id, inbound_number_id=fake_uuid)
assert "is not available" in str(exc.value)


def test_archive_or_release_inbound_number_for_service_archive(sample_service, sample_inbound_numbers):
inbound = next((inbound for inbound in sample_inbound_numbers if inbound.service_id == sample_service.id), None)

archive_or_release_inbound_number_for_service(sample_service.id, True)

updated_inbound = InboundNumber.query.filter_by(number=inbound.number).one_or_none()

assert updated_inbound.service_id is None
assert updated_inbound.active is False


def test_archive_or_release_inbound_number_for_service_release(sample_service, sample_inbound_numbers):
inbound = next((inbound for inbound in sample_inbound_numbers if inbound.service_id == sample_service.id), None)

archive_or_release_inbound_number_for_service(sample_service.id, False)

updated_inbound = InboundNumber.query.filter_by(number=inbound.number).one_or_none()

assert updated_inbound.service_id is None
assert updated_inbound.active is True


@pytest.mark.parametrize(
"archive, inbound_number, expected_active_status",
[
(True, "7654321", False),
(False, "1234567", True),
],
)
def test_dao_remove_inbound_sms_for_service_success(
admin_request, sample_service_full_permissions, archive, inbound_number, expected_active_status
):
service = sample_service_full_permissions
service_inbound = dao_get_inbound_number_for_service(service.id)
dao_add_sms_sender_for_service(service.id, inbound_number, is_default=True, inbound_number_id=service_inbound.id)
sms_senders = dao_get_sms_senders_by_service_id(service.id)

assert (service.has_permission(INBOUND_SMS_TYPE)) is True
assert any(x.inbound_number_id is not None and x.sms_sender == inbound_number for x in sms_senders) is True
assert service_inbound.active is True
assert service_inbound.service_id is not None

dao_remove_inbound_sms_for_service(service.id, archive=archive)

sms_senders = dao_get_sms_senders_by_service_id(service.id)
updated_service_inbound = dao_get_inbound_number_for_service(service.id)
inbound = dao_get_inbound_number(service_inbound.id)

assert (service.has_permission(INBOUND_SMS_TYPE)) is False
assert any(x.inbound_number_id is not None and x.sms_sender == inbound_number for x in sms_senders) is False
assert updated_service_inbound is None
assert inbound.service_id is None
assert inbound.active is expected_active_status


def test_dao_remove_inbound_sms_for_service_failure(sample_service_full_permissions, mocker, notify_db_session):
inbound_number = "76543953521"
service = sample_service_full_permissions
service_inbound = dao_get_inbound_number_for_service(service.id)
dao_add_sms_sender_for_service(service.id, inbound_number, is_default=True, inbound_number_id=service_inbound.id)

sms_senders = dao_get_sms_senders_by_service_id(service.id)
assert service.has_permission(INBOUND_SMS_TYPE) is True
assert any(x.inbound_number_id is not None and x.sms_sender == inbound_number for x in sms_senders) is True
assert service_inbound.active is True
assert service_inbound.service_id is not None

with mocker.patch(
"app.dao.inbound_numbers_dao.archive_or_release_inbound_number_for_service", side_effect=SQLAlchemyError
):
with pytest.raises(SQLAlchemyError):
dao_remove_inbound_sms_for_service(service.id, archive=True)

sms_senders_after = dao_get_sms_senders_by_service_id(service.id)
updated_service_inbound = dao_get_inbound_number_for_service(service.id)
inbound = dao_get_inbound_number(service_inbound.id)

assert service.has_permission(INBOUND_SMS_TYPE) is True
assert any(x.inbound_number_id is not None and x.sms_sender == inbound_number for x in sms_senders_after) is True
assert updated_service_inbound is not None
assert inbound.service_id == service.id
assert inbound.active is True
28 changes: 28 additions & 0 deletions tests/app/dao/test_service_sms_sender_dao.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
dao_add_sms_sender_for_service,
dao_get_service_sms_senders_by_id,
dao_get_sms_senders_by_service_id,
dao_remove_inbound_sms_senders,
dao_update_service_sms_sender,
update_existing_sms_sender_with_inbound_number,
)
Expand Down Expand Up @@ -212,3 +213,30 @@ def test_archive_sms_sender_raises_an_error_if_attempting_to_archive_an_inbound_

assert "You cannot delete an inbound number" in str(e.value)
assert not inbound_number.archived


def test_dao_remove_inbound_sms_senders(notify_db_session):
inbound_number = "7654321"
service = create_service_with_inbound_number(inbound_number, service_name="inbound number service")
sms_senders = dao_get_sms_senders_by_service_id(service.id)
assert len(sms_senders) == 1
assert any(x.sms_sender == inbound_number for x in sms_senders) is True
assert sms_senders[0].inbound_number_id is not None

# adding null inbound_number_id for sms_sender
dao_add_sms_sender_for_service(service.id, "second", is_default=True)

sms_senders = dao_get_sms_senders_by_service_id(service.id)
assert len(sms_senders) == 2
assert any(x.inbound_number_id is None for x in sms_senders) is True

# removing only rows that are not null
dao_remove_inbound_sms_senders(service.id)

sms_senders = dao_get_sms_senders_by_service_id(service.id)
assert len(sms_senders) == 1

# check value with null inbound_number_id is still present
assert any(x.inbound_number_id is None for x in sms_senders) is True
# check value that had inbound_number_id is gone
assert any(x.inbound_number_id is not None for x in sms_senders) is False
Loading

0 comments on commit 3c78ed6

Please sign in to comment.