Skip to content

Commit 65dc570

Browse files
Merge pull request #472 from tutorcruncher/message_bird_pricing_through_webhooks
2 parents bd5b9c2 + 811c0fe commit 65dc570

File tree

8 files changed

+114
-184
lines changed

8 files changed

+114
-184
lines changed

src/schemas/webhooks.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,19 @@ class MessageBirdWebHook(BaseWebhook):
9898
status: MessageBirdMessageStatus
9999
message_id: IDStr
100100
error_code: str = None
101+
price_amount: float
101102

102103
def extra_json(self, sort_keys=False):
103104
return json.dumps({'error_code': self.error_code} if self.error_code else {}, sort_keys=sort_keys)
104105

105106
class Config:
106107
ignore_extra = True
107-
fields = {'message_id': 'id', 'ts': 'statusDatetime', 'error_code': 'statusErrorCode'}
108+
fields = {
109+
'message_id': 'id',
110+
'ts': 'statusDatetime',
111+
'error_code': 'statusErrorCode',
112+
'price_amount': 'price[amount]',
113+
}
108114

109115

110116
ID_REGEX = re.compile(r'[/<>= ]')

src/views/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ async def messages_list(
9797
data['previous'] = f"{this_url}?offset={max(offset - LIST_PAGE_SIZE, 0)}"
9898
if 'sms' in method:
9999
start, end = month_interval()
100-
data['spend'] = await get_sms_spend(conn, company_id=company_id, start=start, end=end, method=method)
100+
data['spend'] = await get_sms_spend(conn, company_id=company_id, start=start, end=end, method=method) or 0
101101
return data
102102

103103

src/views/webhooks.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,8 @@ async def messagebird_webhook_view(request: Request):
5656
event = MessageBirdWebHook(**request.query_params)
5757
except ValidationError as e:
5858
raise HttpUnprocessableEntity(e.args[0])
59-
await glove.redis.enqueue_job('update_message_status', SendMethod.sms_messagebird, event)
59+
method = SendMethod.sms_messagebird
60+
if (test := request.query_params.get('test')) and test.lower() == 'true':
61+
method = SendMethod.sms_test
62+
await glove.redis.enqueue_job('update_message_status', method, event)
6063
return 'message status updated\n'

src/worker/sms.py

Lines changed: 6 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from dataclasses import asdict, dataclass
22

3-
import asyncio
43
import chevron
54
import json
65
import logging
@@ -20,7 +19,7 @@
2019
from phonenumbers.geocoder import country_name_for_number, description_for_number
2120
from typing import Optional
2221

23-
from src.ext import ApiError, MessageBird
22+
from src.ext import MessageBird
2423
from src.render.main import MessageTooLong, SmsLength, apply_short_links, sms_length
2524
from src.schemas.messages import MessageStatus, SmsRecipientModel, SmsSendMethod, SmsSendModel
2625
from src.settings import Settings
@@ -127,7 +126,6 @@ async def _test_send_sms(self, sms_data: SmsData):
127126
# remove the + from the beginning of the number
128127
msg_id = f'{self.m.uid}-{sms_data.number.number[1:]}'
129128
send_ts = utcnow()
130-
cost = 0.012 * sms_data.length.parts
131129
output = (
132130
f'to: {sms_data.number}\n'
133131
f'msg id: {msg_id}\n'
@@ -136,7 +134,6 @@ async def _test_send_sms(self, sms_data: SmsData):
136134
f'tags: {self.tags}\n'
137135
f'company_code: {self.m.company_code}\n'
138136
f'from_name: {self.from_name}\n'
139-
f'cost: {cost}\n'
140137
f'length: {sms_data.length}\n'
141138
f'message:\n'
142139
f'{sms_data.message}\n'
@@ -146,85 +143,12 @@ async def _test_send_sms(self, sms_data: SmsData):
146143
save_path = self.settings.test_output / f'{msg_id}.txt'
147144
test_logger.info('sending message: %s (saved to %s)', output, save_path)
148145
save_path.write_text(output)
149-
await self._store_sms(msg_id, send_ts, sms_data, cost)
150-
151-
async def _messagebird_get_mcc_cost(self, redis, mcc):
152-
rates_key = 'messagebird-rates'
153-
if not await redis.exists(rates_key):
154-
# get fresh data on rates by mcc
155-
main_logger.info('getting fresh pricing data from messagebird...')
156-
r = await self.messagebird.get('pricing/sms/outbound')
157-
if r.status_code != 200:
158-
response = r.text
159-
main_logger.error(
160-
'error getting messagebird api', extra={'status': r.status_code, 'response': response}
161-
)
162-
raise MessageBirdExternalError((r.status_code, response))
163-
data = r.json()
164-
prices = data['prices']
165-
if not next((1 for g in prices if g['mcc'] == '0'), None):
166-
main_logger.error('no default messagebird pricing with mcc "0"', extra={'prices': prices})
167-
prices = {g['mcc']: f'{float(g["price"]):0.5f}' for g in prices}
168-
await asyncio.gather(redis.hmset_dict(rates_key, prices), redis.expire(rates_key, ONE_DAY))
169-
rate = await redis.hget(rates_key, mcc, encoding='utf8')
170-
if not rate:
171-
main_logger.warning('no rate found for mcc: "%s", using default', mcc)
172-
rate = await redis.hget(rates_key, '0', encoding='utf8')
173-
assert rate, f'no rate found for mcc: {mcc}'
174-
return float(rate)
175-
176-
async def _messagebird_get_number_cost(self, number: Number):
177-
cc_mcc_key = f'messagebird-cc:{number.country_code}'
178-
with await self.ctx['redis'] as redis:
179-
mcc = await redis.get(cc_mcc_key)
180-
if mcc is None:
181-
main_logger.info('no mcc for %s, doing HLR lookup...', number.number)
182-
api_number = number.number.replace('+', '')
183-
184-
data = {'msisdn': api_number}
185-
response = await self.messagebird.post('hlr', **data)
186-
hlr_data = response.json()
187-
hlr_id = hlr_data.get('id')
188-
189-
network, hlr = None, None
190-
for i in range(60):
191-
try:
192-
r = await self.messagebird.get(f'hlr/{hlr_id}')
193-
except ApiError:
194-
main_logger.info('Error retrieving HLR data for %s, attempt %d', number.number, i)
195-
continue
196-
hlr = r.json()
197-
if hlr.get('errors'):
198-
continue
199-
network = hlr.get('network')
200-
if not network:
201-
continue
202-
elif hlr['status'] == 'active':
203-
main_logger.info(
204-
'found result for %s after %d attempts %s', number.number, i, json.dumps(hlr, indent=2)
205-
)
206-
break
207-
await asyncio.sleep(1)
208-
if not hlr or not network:
209-
main_logger.warning('No HLR result found for %s after 30 attempts', number.number, extra=hlr)
210-
return
211-
mcc = str(network)[:3]
212-
await redis.setex(cc_mcc_key, ONE_YEAR, mcc)
213-
return await self._messagebird_get_mcc_cost(redis, mcc)
146+
await self._store_sms(msg_id, send_ts, sms_data)
214147

215148
async def _messagebird_send_sms(self, sms_data: SmsData):
216-
try:
217-
msg_cost = await self._messagebird_get_number_cost(sms_data.number)
218-
except MessageBirdExternalError:
219-
msg_cost = 0 # Set to SMS cost to 0 until cost API is working/changed
220-
if msg_cost is None:
221-
return
222-
223-
cost = sms_data.length.parts * msg_cost
224149
send_ts = utcnow()
225-
main_logger.info(
226-
'sending SMS to %s, parts: %d, cost: %0.2fp', sms_data.number.number, sms_data.length.parts, cost * 100
227-
)
150+
main_logger.info('sending SMS to %s, parts: %d', sms_data.number.number, sms_data.length.parts)
151+
228152
r = await self.messagebird.post(
229153
'messages',
230154
originator=self.from_name,
@@ -237,9 +161,9 @@ async def _messagebird_send_sms(self, sms_data: SmsData):
237161
data = r.json()
238162
if data['recipients']['totalCount'] != 1:
239163
main_logger.error('not one recipients in send response', extra={'data': data})
240-
await self._store_sms(data['id'], send_ts, sms_data, cost)
164+
await self._store_sms(data['id'], send_ts, sms_data)
241165

242-
async def _store_sms(self, external_id, send_ts, sms_data: SmsData, cost: float):
166+
async def _store_sms(self, external_id, send_ts, sms_data: SmsData):
243167
message_id = await glove.pg.fetchval_b(
244168
'insert into messages (:values__names) values :values returning id',
245169
values=Values(
@@ -255,7 +179,6 @@ async def _store_sms(self, external_id, send_ts, sms_data: SmsData, cost: float)
255179
to_address=sms_data.number.number_formatted,
256180
tags=self.tags,
257181
body=sms_data.message,
258-
cost=cost,
259182
extra=json.dumps(asdict(sms_data.length)),
260183
),
261184
)

src/worker/webhooks.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import asyncio
12
import hashlib
23
import json
34
import logging
@@ -10,7 +11,7 @@
1011
from ua_parser.user_agent_parser import Parse as ParseUserAgent
1112

1213
from src.schemas.messages import SendMethod
13-
from src.schemas.webhooks import BaseWebhook, MandrillWebhook
14+
from src.schemas.webhooks import BaseWebhook, MandrillWebhook, MessageBirdWebHook
1415

1516
main_logger = logging.getLogger('worker.webhooks')
1617

@@ -81,6 +82,7 @@ async def update_message_status(ctx, send_method: SendMethod, m: BaseWebhook, lo
8182
message_id = await glove.pg.fetchval_b(
8283
'select id from messages where :where', where=(V('external_id') == m.message_id) & (V('method') == send_method)
8384
)
85+
8486
if not message_id:
8587
return UpdateStatus.missing
8688

@@ -90,8 +92,17 @@ async def update_message_status(ctx, send_method: SendMethod, m: BaseWebhook, lo
9092
if log_each:
9193
main_logger.info('adding event %s, ts: %s, status: %s', m.message_id, m.ts, m.status)
9294

93-
await glove.pg.execute_b(
94-
'insert into events (:values__names) values :values',
95-
values=Values(message_id=message_id, status=m.status, ts=m.ts, extra=m.extra_json()),
96-
)
95+
qs = [
96+
glove.pg.execute_b(
97+
'insert into events (:values__names) values :values',
98+
values=Values(message_id=message_id, status=m.status, ts=m.ts, extra=m.extra_json()),
99+
),
100+
]
101+
if isinstance(m, MessageBirdWebHook):
102+
cost = m.price_amount
103+
qs.append(
104+
glove.pg.execute_b('update messages set cost=:cost where id=:message_id', cost=cost, message_id=message_id)
105+
)
106+
await asyncio.gather(*qs)
107+
97108
return UpdateStatus.added

tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pathlib import Path
1515
from starlette.testclient import TestClient
1616
from typing import Any, Callable
17+
from urllib.parse import urlencode
1718

1819
from src.schemas.messages import EmailSendModel, SendMethod
1920
from src.settings import Settings
@@ -231,6 +232,27 @@ def _send_message(**extra):
231232
return _send_message
232233

233234

235+
@pytest.fixture
236+
def send_webhook(cli, worker, loop):
237+
def _send_webhook(ext_id, price, **extra):
238+
url_args = {
239+
'id': ext_id,
240+
'reference': 'morpheus',
241+
'recipient': '447896541236',
242+
'status': 'delivered',
243+
'statusDatetime': '2032-06-06T12:00:00',
244+
'price[amount]': price,
245+
'test': True,
246+
}
247+
248+
url_args.update(**extra)
249+
r = cli.get(f'/webhook/messagebird/?{urlencode(url_args)}')
250+
assert r.status_code == 200
251+
worker.test_run()
252+
253+
return _send_webhook
254+
255+
234256
@pytest.fixture(name='call_send_emails')
235257
def _fix_call_send_emails(glove, sync_db):
236258
def run(**kwargs):

0 commit comments

Comments
 (0)