-
Notifications
You must be signed in to change notification settings - Fork 0
/
pangeanetwork.py
495 lines (430 loc) · 15.8 KB
/
pangeanetwork.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
from app import create_app, db
import os
import os.path as op
from datetime import datetime
from flask import Flask
from flask import request
from flask_sqlalchemy import SQLAlchemy
from flask import Response
from app.models import User, CoOp, Role, Loan, Transaction
import africastalking
import json
from flask import Flask
from flask_mail import Mail
from flask_mail import Message
from flask_bcrypt import Bcrypt
import random
import string
app = create_app()
bcrypt = Bcrypt(app)
username = "sandbox"
api_key = os.environ.get('AT_API_KEY')
test_number = "+254456923994"
africastalking.initialize(username, api_key)
sms = africastalking.SMS
# initialize mail app
mail = Mail()
def test_text():
africastalking.initialize(username, api_key)
# Initialize a service e.g. SMS
sms = africastalking.SMS
# Use the service synchronously
try:
response = sms.send("~*~*~Testing text send from Africa's Talking API~*~*~", [test_number])
print(response)
except Exception as e:
print('Encountered an error while sending: %s' % str(e))
def get_members(from_user):
user_phone = int(from_user)
print('User phone: ' + str(user_phone))
officer_role = Role.query.filter(Role.name == "officer").first()
valid_user = User.query.filter(User.role_id == officer_role.id, User.phone == user_phone).first()
valid = False
if valid_user is not None:
valid = True
if not valid:
return "Error: you are not an officer"
coop = CoOp.query.filter(CoOp.id == valid_user.co_op_id).first()
users = User.query.filter(User.co_op_id == coop.id)
members = 'Members in your coop:\n'
for user in users:
members += user.first_name + ' ' + user.last_name + ': (id = ' + str(user.id) + ')\n'
return members
def add_transaction(from_user, msg):
africastalking.initialize(username, api_key)
sms = africastalking.SMS
from_phone = int(from_user)
print(from_phone)
member_id = msg[1]
member = User.query.filter(User.id == member_id).first()
if member == None:
return send_missing_user_error(from_phone)
amount = int(msg[2])
if member.loan == None:
officer_text = sms.send('ERROR: Given member does not have a current loan', [from_phone])
print(officer_text)
return 'Error: Member does not have a current loan'
loan_balance = member.loan.balance
new_user_transaction = Transaction(
loan=[member.loan],
amount=amount,
previous_balance=loan_balance,
new_balance=loan_balance - amount,
state='initiated',
user_id=member.id
)
member.transactions.append(new_user_transaction)
db.session.add(member)
db.session.add(new_user_transaction)
db.session.commit()
officer_role = Role.query.filter(Role.name == "officer").first()
co_op_officer = User.query.filter(User.role_id == officer_role.id, User.co_op_id == member.co_op_id).first()
try:
response_text = sms.send("Transaction added. Requesting confirmation from Co-Op Leader", ['+' + str(from_phone)])
leader_text = sms.send("A new repayment transaction has been added for member " + str(member.first_name) + " (ID: " + str(member.id) + ") in the amount of " + str(amount) + ". \
Please respond with 'Y " + str(member.id) + "' to confirm this transaction is accurate or 'N " + str(member.id) + "' to reject it. Their new balance will be: " +
str(loan_balance - amount), ['+' + co_op_officer.phone])
print(response_text)
print(leader_text)
return 'success: added loan transaction'
except Exception as e:
print('Encountered an error while sending: %s' % str(e))
return 'Encountered an error while sending: %s' % str(e)
def send_missing_user_error(sender_phone):
error_text = sms.send(
"The user you are trying to update does not exist in our database. Please text <ADD {user number}> if you would like to add them", ['+' + str(sender_phone)])
print(error_text)
return 'error: user does not exist'
def send_not_officer_error(sender_phone):
error_text = sms.send(
"The user, {user number}, you have specified does not have the required role of officer in order to complete the transaction.", ['+' + str(sender_phone)])
print(error_text)
return 'error: user not an officer'
def confirm_transaction(from_user, msg):
officer_role = Role.query.filter(Role.name == 'officer').first()
officer = User.query.filter(User.phone == int(from_user), User.role_id == officer_role.id).first()
if officer is None:
return send_not_officer_error(from_user)
# TODO: return error message if user does not exist
# TODO: return error message if user is not an officer
member_id = msg[1]
co_op = CoOp.query.filter(CoOp.id == officer.co_op_id).first()
member = User.query.filter(User.co_op_id == co_op.id, User.id == member_id).first()
if member == None:
return send_missing_user_error(from_user)
user_transaction = member.transactions.all()[-1]
if user_transaction == None:
error_text = sms.send('ERROR: There is no transaction available to confirm for user ' + str(member.first_name))
print(error_text)
return 'error: transaction does not exist'
elif user_transaction.state == 'initiated':
user_transaction.state = 'confirmed'
member.loan.balance = user_transaction.new_balance
co_op.current_balance -= user_transaction.amount
db.session.add(member)
db.session.add(co_op)
db.session.add(user_transaction)
db.session.commit()
try:
officer_text = sms.send("Transaction complete. New balance for member " + str(member.id) + ' - ' + str(member.first_name) + " is " + str(member.loan.balance) + '. \
New balance for Co-Op ' + co_op.name + ' is ' + str(co_op.current_balance), [from_user])
if member.phone:
member_text = sms.send('Transaction confirmed. Your new balance is ' + str(member.loan.balance), [member.phone])
print(member_text)
print(officer_text)
except Exception as e:
print('Encountered an error while sending: %s' % str(e))
def create_new_user(user_data):
# create password hash
pw_hash = bcrypt.generate_password_hash(user_data['password'])
new_user = User(
first_name=user_data['first_name'],
last_name=user_data['last_name'],
email=user_data['email'],
phone=user_data['phone'],
active=True,
password=pw_hash,
co_op_id=user_data['co_op_id'],
role_id=user_data['role_id']
)
db.session.add(new_user)
db.session.commit()
return new_user.id
# Routes
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return 'success'
if request.method == 'POST': # text receive
data = request.form.to_dict()
# expected data format:
# {'linkId': 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
# 'text': 'loan 2 34',
# 'to': '<short code>',
# 'id': 'xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx',
# 'date': '2019-10-02 15:12:34',
# 'from': '+11234567890}
try:
from_user = data['from']
print(from_user)
except:
from_user = None
if from_user == None:
return 'failure: request error'
msg = data['text'].split()
cmd = msg[0].lower()
print(msg)
print(cmd)
if cmd == 'loan':
if len(msg) != 3:
return 'error'
return add_transaction(from_user, msg)
elif cmd == 'y':
confirm_transaction(from_user, msg)
elif cmd == 'n':
return 'transaction denied'
# TODO: remove transaction
elif cmd == 'members':
list_of_members = get_members(from_user)
print(list_of_members)
response = sms.send(list_of_members, [from_user])
print(response)
# TODO: support 'new' command for new members and funds
else:
return 'error'
# TODO: handle error
return 'success'
@app.route('/transactions', methods=['GET'])
def transactions():
data = []
transactions = Transaction.query.order_by(Transaction.timestamp.desc()).all()
for transaction in transactions:
user = User.query.filter(User.id == transaction.user_id).first()
data.append(
{
"amount": transaction.amount,
"previous_balance": transaction.previous_balance,
"new_balance": transaction.new_balance,
"state": transaction.state,
"user_name": user.first_name + ' ' + user.last_name,
"timestamp": transaction.timestamp
}
)
results = {"data": data}
return Response(json.dumps(results, default=str), mimetype='application/json')
@app.route('/members', methods=['GET', 'POST', 'PUT'])
def members():
'''
PUT:
Json data:
- name
- coop
- phone
- role
- loan_balance
'''
if (request.method == 'GET'):
data = []
users = User.query.all()
for user in users:
data.append(
{
"name": user.first_name + ' ' + user.last_name,
"coop": CoOp.query.filter(CoOp.id == user.co_op_id).first().name,
"phone": user.phone,
"role": Role.query.filter(Role.id == user.role_id).first().name,
"loan_balance": user.loan.balance if user.loan else 'N/A'
}
)
results = {"data": data}
return Response(json.dumps(results, default=str), mimetype='application/json')
elif (request.method == 'POST'):
data = request.form.to_dict()
id = create_new_user(data)
result = { 'status': 200, 'user_id': id }
# TODO: prompt loan creation if loan_id was not given
# TODO: request email confirmation if email was given
return Response(json.dumps(result, default=str), mimetype='application/json')
elif (request.method == 'PUT'):
content = request.json
# assume email field is unchanged
# may need to refactor later on
user = User.query.filter(User.email == content['email']).first()
if(not user):
results = {'status': 'bad request, incorrect json data', 'code': '400'}
return Response(json.dumps(results, default=str), mimetype="application/json")
user.first_name = content['first_name']
user.last_name = content['last_name']
user.phone = content['phone']
user.active = content['active']
# update db
db.session.commit()
results = {'status': 'ok', 'code': 200}
return Response(json.dumps(results, default=str), mimetype="application/json")
@app.route('/member/<memberid>', methods=['GET'])
def member(memberid):
'''
GET: queries the database for all the users and returns the member that matches the id sent as a parameter
parameter:
id: the id of the member
'''
memberid = int(memberid)
if(request.method == 'GET'):
data = []
user = User.query.filter(memberid == User.id).first()
if(user):
data.append(
{
"name": user.first_name + ' ' + user.last_name,
"coop": CoOp.query.filter(CoOp.id == user.co_op_id).first().name,
"phone": user.phone,
"role": Role.query.filter(Role.id == user.role_id).first().name,
"loan_balance": user.loan.balance if user.loan else 'N/A'
}
)
results = {"data": data, "code": 200, "status": "ok"}
return Response(json.dumps(results, default=str), mimetype="application/json")
else:
results = {"code": "404", "status": "member not found"}
return Response(json.dumps(results, default=str), mimetype="application/json")
@app.route('/coops', methods=['GET', 'PUT'])
def coops():
'''
GET: queries for all coops
PUT: updates an existing coop based on the name
Json data:
name (str): the name of the coop
start_date (str): the start date of the coop
end_date (str): the end date of the coop
location (str): the location of the coop
interest (float): the interest of the coop
intial_balance (float): the initial balance of the coop
current_balance (float): the current balance of the coop
expected_repayment (str): the expected repayment of the coop
'''
data = []
coops = CoOp.query.all()
if(request.method == 'GET'):
for coop in coops:
data.append(
{
"name": coop.name,
"start_date": coop.start_date,
"end_date": coop.end_date,
"location": coop.location,
"interest": coop.interest,
"initial_balance": coop.initial_balance,
"current_balance": coop.current_balance,
"expected_repayment": coop.expected_repayment
}
)
results = {"data": data}
return Response(json.dumps(results), mimetype='application/json')
elif(request.method == 'PUT'):
content = request.json
results = {}
isCoopExist = False
for coop in coops:
if(content['name'] == coop.name):
coop.name = content['name']
coop.start_date = content['start_date']
coop.end_date = content['end_date']
coop.location = content['location']
coop.initial_balance = content['initial_balance']
coop.current_balance = content['current_balance']
coop.expected_repayment = content['expected_repayment']
db.session.commit()
isCoopExist = True
results = {"code": 200, "status": "ok"}
return Response(json.dumps(results), mimetype='application/json')
if(isCoopExist == False):
results = {"code": 404, "status": "coop not found"}
return Response(json.dumps(results), mimetype='application/json')
@app.route('/loans', methods=['GET'])
def loans():
loans = Loan.query.all()
data = []
for loan in loans:
user = User.query.filter(loan.user_id == User.id).first()
data.append(
{
'name': user.first_name + ' ' + user.last_name,
'start': loan.loan_start,
'end': loan.loan_end,
'interest': loan.interest,
'initial': loan.initial_balance,
'remaining': loan.balance
}
)
results = {'data': data}
return Response(json.dumps(results), mimetype='application/json')
@app.route('/loan/<loanid>', methods=['GET'])
def loan(loanid):
loanid = int(loanid)
loan = Loan.query.filter(loanid == Loan.id).first()
if(loan):
user = User.query.filter(loan.user_id == User.id).first()
data = []
data.append(
{
'name': user.first_name + ' ' + user.last_name,
'start': loan.loan_start,
'end': loan.loan_end,
'interest': loan.interest,
'initial': loan.initial_balance,
'remaining': loan.balance
}
)
results = {'data': data, 'code': 200, 'status': 'ok'}
return Response(json.dumps(results), mimetype='application/json')
else:
results = {'code':404, 'status': 'loan not found'}
return Response(json.dumps(results), mimetype='application/json')
@app.route('/forgotpassword', methods=['POST'])
def forgotPassword():
'''
Fill in sender email fields
'''
body = request.json
encoded_jwt = jwt.encode({'payload': body['email']}, 'elixir', algorithm='HS256')
port = 587
smtp_server = "smtp.gmail.com"
msg = MIMEMultipart("alternative")
msg['Subject'] = "Pangea Network Password Reset"
msg['From'] = ''
msg['To'] = body['email']
html = """\
Hello this is your password reset token<br/>
"""
html += str(encoded_jwt)
msg.attach(MIMEText(html, 'html'))
context = ssl.create_default_context()
server = smtplib.SMTP(smtp_server, port)
server.ehlo()
server.starttls(context=context)
# server.ehlo()
server.login(msg['From'],'')
server.sendmail(msg['From'],msg['To'],msg.as_string())
server.close()
return Response(json.dumps({'status':'ok', 'email': body['email']}),mimetype='application/json')
@app.route('/passwordreset', methods=['POST'])
def passwordReset():
'''
Updates user's password based on json data sent from the client
Json data:
email (str): the email of the user
password (str): the password of the user
'''
content = request.json
data = []
users = User.query.all()
for user in users:
if(user.email == content['email']):
# hash password
pw_hash = bcrypt.generate_password_hash(content['password'])
user.password = pw_hash
db.session.commit()
results = {"code": 200, "status": "ok"}
return Response(json.dumps(results, default=str), mimetype='application/json')
if __name__ == '__main__':
app.run()