Skip to content
This repository was archived by the owner on Sep 11, 2025. It is now read-only.

Commit ad7274d

Browse files
authored
Merge pull request #21 from Worth-NL/feature/clients/sms/spryng
Spryng client
2 parents 5a5a187 + 630aa4f commit ad7274d

File tree

8 files changed

+147
-17
lines changed

8 files changed

+147
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ target/
6868
# Mac
6969
*.DS_Store
7070
environment.sh
71+
environment.fish
7172
.envrc
7273

7374
celerybeat-schedule

app/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from app.clients.letter.dvla import DVLAClient
3838
from app.clients.sms.firetext import FiretextClient
3939
from app.clients.sms.mmg import MMGClient
40+
from app.clients.sms.spryng import SpryngClient
4041

4142
db = SQLAlchemy()
4243
migrate = Migrate()
@@ -47,6 +48,7 @@
4748
aws_ses_client = AwsSesClient()
4849
aws_ses_stub_client = AwsSesStubClient()
4950
dvla_client = DVLAClient()
51+
spryng_client = SpryngClient()
5052
signing = Signing()
5153
zendesk_client = ZendeskClient()
5254
statsd_client = StatsdClient()
@@ -90,14 +92,17 @@ def create_app(application):
9092
logging.init_app(application, statsd_client)
9193
firetext_client.init_app(application, statsd_client=statsd_client)
9294
mmg_client.init_app(application, statsd_client=statsd_client)
95+
spryng_client.init_app(application, statsd_client=statsd_client)
9396
dvla_client.init_app(application, statsd_client=statsd_client)
9497
aws_ses_client.init_app(application.config["AWS_REGION"], statsd_client=statsd_client)
9598
aws_ses_stub_client.init_app(
9699
application.config["AWS_REGION"], statsd_client=statsd_client, stub_url=application.config["SES_STUB_URL"]
97100
)
98101
# If a stub url is provided for SES, then use the stub client rather than the real SES boto client
99102
email_clients = [aws_ses_stub_client] if application.config["SES_STUB_URL"] else [aws_ses_client]
100-
notification_provider_clients.init_app(sms_clients=[firetext_client, mmg_client], email_clients=email_clients)
103+
notification_provider_clients.init_app(
104+
sms_clients=[firetext_client, mmg_client, spryng_client], email_clients=email_clients
105+
)
101106

102107
notify_celery.init_app(application)
103108
signing.init_app(application)
@@ -411,8 +416,7 @@ def checkout(dbapi_connection, connection_record, connection_proxy):
411416
elif current_task:
412417
connection_record.info["request_data"] = {
413418
"method": "celery",
414-
# worker name
415-
"host": current_app.config["NOTIFY_APP_NAME"],
419+
"host": current_app.config["NOTIFY_APP_NAME"], # worker name
416420
"url_rule": current_task.name, # task name
417421
}
418422
# anything else. migrations possibly, or flask cli commands.

app/celery/process_sms_client_response_tasks.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,15 @@
88
from app.clients import ClientException
99
from app.clients.sms.firetext import get_firetext_responses
1010
from app.clients.sms.mmg import get_mmg_responses
11+
from app.clients.sms.spryng import get_spryng_responses
1112
from app.constants import NOTIFICATION_PENDING
1213
from app.dao import notifications_dao
1314
from app.dao.templates_dao import dao_get_template_by_id
1415
from app.notifications.notifications_ses_callback import (
1516
check_and_queue_callback_task,
1617
)
1718

18-
sms_response_mapper = {
19-
"MMG": get_mmg_responses,
20-
"Firetext": get_firetext_responses,
21-
}
19+
sms_response_mapper = {"MMG": get_mmg_responses, "Firetext": get_firetext_responses, "Spryng": get_spryng_responses}
2220

2321

2422
@notify_celery.task(bind=True, name="process-sms-client-response", max_retries=5, default_retry_delay=300)

app/clients/sms/spryng.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import json
2+
import logging
3+
4+
from requests import RequestException, request
5+
6+
from app.clients.sms import SmsClient, SmsClientResponseException
7+
8+
logger = logging.getLogger(__name__)
9+
10+
spryng_response_map = {
11+
"10": {"status": "delivered", "reasoncode": {"0": "No error"}},
12+
"20": {
13+
"status": "permanent-failure",
14+
"reasoncode": {
15+
"20": "Recipient number unreachable",
16+
"21": "Recipient number incorrect",
17+
"22": "Delivery failure",
18+
"31": "The recipient is blacklisted",
19+
"32": "The originator is not registered for this country",
20+
},
21+
},
22+
}
23+
24+
25+
def get_spryng_responses(status, detailed_status_code=None):
26+
return (
27+
spryng_response_map[status]["status"],
28+
spryng_response_map[status]["reasoncode"].get(detailed_status_code, None),
29+
)
30+
31+
32+
class SpryngClient(SmsClient):
33+
"""
34+
Spryng sms client.
35+
"""
36+
37+
def init_app(self, *args, **kwargs):
38+
super().init_app(*args, **kwargs)
39+
self.api_key = self.current_app.config.get("SPRYNG_API_KEY")
40+
self.url = self.current_app.config.get("SPRYNG_URL")
41+
42+
@property
43+
def name(self):
44+
return "spryng"
45+
46+
def try_send_sms(self, to, content, reference, international, sender):
47+
data = {
48+
"originator": sender,
49+
"recipients": [to.replace("+", "")],
50+
"body": content,
51+
"reference": reference,
52+
"route": "business",
53+
"encoding": "unicode",
54+
}
55+
56+
try:
57+
response = request(
58+
"POST",
59+
self.url,
60+
data=json.dumps(data),
61+
timeout=60,
62+
headers={"Content-Type": "application/json", "Authorization": "Bearer {}".format(self.api_key)},
63+
)
64+
65+
response.raise_for_status()
66+
67+
try:
68+
json.loads(response.text)
69+
70+
if response.status_code != 200:
71+
raise ValueError("Expected 'status code' to be '200'")
72+
except (ValueError, AttributeError) as e:
73+
raise SmsClientResponseException("Invalid response JSON") from e
74+
except RequestException as e:
75+
raise SmsClientResponseException("Request failed") from e
76+
77+
return response

app/config.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ class Config(object):
102102
FIRETEXT_API_KEY = os.getenv("FIRETEXT_API_KEY")
103103
FIRETEXT_INTERNATIONAL_API_KEY = os.getenv("FIRETEXT_INTERNATIONAL_API_KEY", "placeholder")
104104

105+
# Spryng API key
106+
SPRYNG_API_KEY = os.environ.get("SPRYNG_API_KEY")
107+
105108
# Prefix to identify queues in SQS
106109
NOTIFICATION_QUEUE_PREFIX = os.getenv("NOTIFICATION_QUEUE_PREFIX")
107110

@@ -154,7 +157,7 @@ class Config(object):
154157
CHECK_PROXY_HEADER = False
155158

156159
# these should always add up to 100%
157-
SMS_PROVIDER_RESTING_POINTS = {"mmg": 51, "firetext": 49}
160+
SMS_PROVIDER_RESTING_POINTS = {"mmg": 0, "firetext": 0, "spryng": 100}
158161

159162
NOTIFY_SERVICE_ID = "d6aa2c68-a2d9-4437-ab19-3ae8eb202553"
160163
NOTIFY_USER_ID = "6af522d0-2915-4e52-83a3-3690455a5fe6"
@@ -275,12 +278,14 @@ class Config(object):
275278
},
276279
"create-nightly-notification-status": {
277280
"task": "create-nightly-notification-status",
278-
"schedule": crontab(hour=0, minute=30), # after 'timeout-sending-notifications'
281+
# after 'timeout-sending-notifications'
282+
"schedule": crontab(hour=0, minute=30),
279283
"options": {"queue": QueueNames.REPORTING},
280284
},
281285
"delete-notifications-older-than-retention": {
282286
"task": "delete-notifications-older-than-retention",
283-
"schedule": crontab(hour=3, minute=0), # after 'create-nightly-notification-status'
287+
# after 'create-nightly-notification-status'
288+
"schedule": crontab(hour=3, minute=0),
284289
"options": {"queue": QueueNames.REPORTING},
285290
},
286291
"delete-inbound-sms": {
@@ -416,6 +421,7 @@ class Config(object):
416421
# these environment vars aren't defined in the manifest so to set them on paas use `cf set-env`
417422
MMG_URL = os.environ.get("MMG_URL", "https://api.mmg.co.uk/jsonv2a/api.php")
418423
FIRETEXT_URL = os.environ.get("FIRETEXT_URL", "https://www.firetext.co.uk/api/sendsms/json")
424+
SPRYNG_URL = os.environ.get("SPRYNG_URL", "https://rest.spryngsms.com/v1/messages")
419425
SES_STUB_URL = os.environ.get("SES_STUB_URL")
420426

421427
AWS_REGION = "eu-west-1"
@@ -446,8 +452,6 @@ class Config(object):
446452
######################
447453
# Config overrides ###
448454
######################
449-
450-
451455
class Development(Config):
452456
DEBUG = True
453457
SQLALCHEMY_ECHO = False

app/dao/provider_details_dao.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,9 @@ def get_provider_details_by_identifier(identifier):
2424

2525

2626
def get_alternative_sms_provider(identifier):
27-
if identifier == "firetext":
28-
return "mmg"
29-
elif identifier == "mmg":
30-
return "firetext"
31-
raise ValueError("Unrecognised sms provider {}".format(identifier))
27+
if identifier in ["firetext", "mmg", "spryng"]:
28+
return identifier
29+
raise ValueError(f"Unrecognised sms provider {identifier}")
3230

3331

3432
def dao_get_provider_versions(provider_id):

app/notifications/notifications_sms_callback.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,24 @@ def process_firetext_response():
5050
return jsonify(result="success"), 200
5151

5252

53+
@sms_callback_blueprint.route("/spryng", methods=["GET"])
54+
def process_spryng_response():
55+
client_name = "Spryng"
56+
errors = validate_callback_data(data=request.args, fields=["STATUS", "REASONCODE"], client_name=client_name)
57+
if errors:
58+
raise InvalidRequest(errors, status_code=400)
59+
60+
status = request.args.get("STATUS")
61+
detailed_status_code = request.args.get("REASONCODE")
62+
provider_reference = request.args.get("REFERENCE")
63+
64+
process_sms_client_response.apply_async(
65+
[status, provider_reference, client_name, detailed_status_code], queue=QueueNames.SMS_CALLBACKS
66+
)
67+
68+
return jsonify(result="success"), 200
69+
70+
5371
def validate_callback_data(data, fields, client_name):
5472
errors = []
5573
for f in fields:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""
2+
3+
Revision ID: 0443_add_spryng_provider
4+
Revises: 0442_new_sms_allowance_n_rate
5+
Create Date: 2024-05-07 13:05:00.000000
6+
7+
"""
8+
9+
import uuid
10+
from datetime import datetime
11+
12+
from alembic import op
13+
14+
15+
revision = "0443_add_spryng_provider"
16+
down_revision = "0442_new_sms_allowance_n_rate"
17+
18+
19+
identifier = "spryng"
20+
provider_id = str(uuid.uuid4())
21+
22+
23+
def upgrade():
24+
op.execute(
25+
f"INSERT INTO provider_details (id, display_name, identifier, priority, notification_type, active, version, supports_international) values ('{provider_id}', '{identifier.capitalize()}', '{identifier}', 30, 'sms', true, 1, true)"
26+
)
27+
28+
29+
def downgrade():
30+
op.execute(f"DELETE FROM provider_details WHERE identifier = '{identifier}'")

0 commit comments

Comments
 (0)