Completed
Push — master ( 997812...107d83 )
by Pat
56s
created

chezbetty.purchase()   F

Complexity

Conditions 12

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 12
dl 0
loc 52
rs 2.9839

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like chezbetty.purchase() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
from pyramid.events import subscriber
2
from pyramid.events import BeforeRender
3
from pyramid.httpexceptions import HTTPFound
4
from pyramid.renderers import render
5
from pyramid.renderers import render_to_response
6
from pyramid.response import Response
7
from pyramid.view import view_config, forbidden_view_config
8
9
from pyramid.i18n import TranslationStringFactory
10
_ = TranslationStringFactory('betty')
11
12
from sqlalchemy.sql import func
13
from sqlalchemy.exc import DBAPIError
14
from sqlalchemy.orm.exc import NoResultFound
15
16
from .models import *
17
from .models.model import *
18
from .models import user as __user
19
from .models.user import User
20
from .models.item import Item
21
from .models.box import Box
22
from .models.transaction import Transaction, BTCDeposit, PurchaseLineItem
23
from .models.account import Account, VirtualAccount, CashAccount
24
from .models.event import Event
25
from .models.announcement import Announcement
26
from .models.btcdeposit import BtcPendingDeposit
27
from .models.pool import Pool
28
29
from .utility import user_password_reset
30
from .utility import send_email
31
32
from pyramid.security import Allow, Everyone, remember, forget
33
34
import chezbetty.datalayer as datalayer
35
from .btc import Bitcoin, BTCException
36
import binascii
37
import transaction
38
39
import traceback
40
41
class DepositException(Exception):
42
    pass
43
44
45
46
47
@view_config(route_name='umid_check',
48
             request_method='POST',
49
             renderer='json',
50
             permission='service')
51
def umid_check(request):
52
    try:
53
        User.from_umid(request.POST['umid'])
54
        return {'status': 'success'}
55
    except:
56
        return {'status': 'error'}
57
58
59
60
### Post mcard swipe
61
62
@view_config(route_name='swipe', permission='service')
63
def swipe(request):
64
    try:
65
        user = User.from_umid(request.matchdict['umid'], create_if_never_seen=True)
66
    except __user.InvalidUserException as e:
67
        request.session.flash(_(
68
            'mcard-error',
69
            default='Failed to read M-Card. Please try swiping again.',
70
            ), 'error')
71
        return HTTPFound(location=request.route_url('index'))
72
    return HTTPFound(location=request.route_url('purchase', umid=request.matchdict['umid']))
73
74
@view_config(route_name='purchase', renderer='templates/terminal/purchase.jinja2', permission='service')
75
def purchase(request):
76
    try:
77
        if len(request.matchdict['umid']) != 8:
78
            raise __user.InvalidUserException()
79
80
        with transaction.manager:
81
            user = User.from_umid(request.matchdict['umid'])
82
        user = DBSession.merge(user)
83
        if not user.enabled:
84
            request.session.flash(_(
85
                'user-not-enabled',
86
                default='User is not enabled. Please contact ${email}.',
87
                mapping={'email':request.registry.settings['chezbetty.email']},
88
                ), 'error')
89
            return HTTPFound(location=request.route_url('index'))
90
91
        # For Demo mode:
92
        items = DBSession.query(Item)\
93
                         .filter(Item.enabled == True)\
94
                         .order_by(Item.name)\
95
                         .limit(6).all()
96
97
        # Pre-populate cart if returning from undone transaction
98
        existing_items = ''
99
        if len(request.GET) != 0:
100
            for (item_id, quantity) in request.GET.items():
101
                item = Item.from_id(int(item_id))
102
                existing_items += render('templates/terminal/purchase_item_row.jinja2',
103
                    {'item': item, 'quantity': int(quantity)})
104
105
        # Figure out if any pools can be used to pay for this purchase
106
        pools = []
107
        for pool in Pool.all_by_owner(user, True):
108
            if pool.balance > (pool.credit_limit * -1):
109
                pools.append(pool)
110
111
        for pu in user.pools:
112
            if pu.pool.enabled and pu.pool.balance > (pu.pool.credit_limit * -1):
113
                pools.append(pu.pool)
114
115
        return {'user': user,
116
                'items': items,
117
                'existing_items': existing_items,
118
                'pools': pools}
119
120
    except __user.InvalidUserException as e:
121
        request.session.flash(_(
122
            'mcard-error',
123
            default='Failed to read M-Card. Please try swiping again.',
124
            ), 'error')
125
        return HTTPFound(location=request.route_url('index'))
126
127
128
@view_config(route_name='user', renderer='templates/terminal/user.jinja2', permission='service')
129
def user(request):
130
    try:
131
        user = User.from_umid(request.matchdict['umid'])
132
        if not user.enabled:
133
            request.session.flash('User not permitted to purchase items.', 'error')
134
            return HTTPFound(location=request.route_url('index'))
135
136
        transactions,count = limitable_request(
137
                request, user.get_transactions, limit=20, count=True)
138
        return {'user': user,
139
                'transactions': transactions,
140
                'transactions_count': count,
141
                }
142
143
    except __user.InvalidUserException as e:
144
        request.session.flash('Invalid User ID.', 'error')
145
        return HTTPFound(location=request.route_url('index'))
146
147
148
@view_config(route_name='deposit', renderer='templates/terminal/deposit.jinja2', permission='service')
149
def deposit(request):
150
    try:
151
        user = User.from_umid(request.matchdict['umid'])
152
153
        # Record the deposit limit so we can show the user
154
        if user.total_deposits > 10.0 and user.total_purchases > 10.0:
155
            user.deposit_limit = 100.0
156
        else:
157
            user.deposit_limit = 20.0
158
159
        try:
160
            auth_key = binascii.b2a_hex(open("/dev/urandom", "rb").read(32))[:-3].decode("ascii")
161
            btc_addr = Bitcoin.get_new_address(user.umid, auth_key)
162
            btc_html = render('templates/terminal/btc.jinja2', {'addr': btc_addr})
163
164
            e = BtcPendingDeposit(user, auth_key, btc_addr)
165
            DBSession.add(e)
166
            DBSession.flush()
167
        except BTCException as e:
168
            print('BTC error: %s' % str(e))
169
            btc_html = ""
170
171
        # Get pools the user can deposit to
172
        pools = Pool.all_accessable(user, True)
173
174
        return {'user' : user,
175
                'btc'  : btc_html, 
176
                'pools': pools}
177
178
    except __user.InvalidUserException as e:
179
        request.session.flash('Invalid User ID.', 'error')
180
        return HTTPFound(location=request.route_url('index'))
181
182
183
@view_config(route_name='deposit_edit',
184
             renderer='templates/terminal/deposit_edit.jinja2',
185
             permission='service')
186
def deposit_edit(request):
187
    try:
188
        user = User.from_umid(request.matchdict['umid'])
189
        event = Event.from_id(request.matchdict['event_id'])
190
191
        if event.type != 'deposit' or event.transactions[0].type != 'cashdeposit':
192
            request.session.flash('Can only edit a cash deposit.', 'error')
193
            return HTTPFound(location=request.route_url('index'))
194
195
        # Get pools the user can deposit to
196
        pools = []
197
        for pool in Pool.all_by_owner(user, True):
198
            pools.append(pool)
199
200
        for pu in user.pools:
201
            if pu.pool.enabled:
202
                pools.append(pu.pool)
203
204
        return {'user': user,
205
                'old_event': event,
206
                'old_deposit': event.transactions[0], 
207
                'pools': pools}
208
209
    except __user.InvalidUserException as e:
210
        request.session.flash('Invalid User ID.', 'error')
211
        return HTTPFound(location=request.route_url('index'))
212
213
    except Exception as e:
214
        if request.debug: raise(e)
215
        request.session.flash('Error.', 'error')
216
        return HTTPFound(location=request.route_url('index'))
217
218
219
220
@view_config(route_name='event', permission='service')
221
def event(request):
222
    try:
223
        event = Event.from_id(request.matchdict['event_id'])
224
        transaction = event.transactions[0]
225
        user = User.from_id(event.user_id)
226
227
        # Choose which page to show based on the type of event
228
        if event.type == 'deposit':
229
            # View the deposit success page
230
            prev_balance = user.balance - transaction.amount
231
232
            if transaction.to_account_virt_id == user.id:
233
                account_type = 'user'
234
                pool = None
235
            else:
236
                account_type = 'pool'
237
                pool = Pool.from_id(transaction.to_account_virt_id)
238
239
            return render_to_response('templates/terminal/deposit_complete.jinja2',
240
                {'deposit': transaction,
241
                 'user': user,
242
                 'event': event,
243
                 'prev_balance': prev_balance, 
244
                 'account_type': account_type,
245
                 'pool': pool}, request)
246
247
        elif event.type == 'purchase':
248
            # View the purchase success page
249
            order = {'total': transaction.amount,
250
                     'discount': transaction.discount,
251
                     'items': []}
252
            for subtrans in transaction.subtransactions:
253
                item = {}
254
                item['name'] = subtrans.item.name
255
                item['quantity'] = subtrans.quantity
256
                item['price'] = subtrans.item.price
257
                item['total_price'] = subtrans.amount
258
                order['items'].append(item)
259
260
            if transaction.fr_account_virt_id == user.id:
261
                account_type = 'user'
262
                pool = None
263
            else:
264
                account_type = 'pool'
265
                pool = Pool.from_id(transaction.fr_account_virt_id)
266
267
            request.session.flash('Success! The purchase was added successfully', 'success')
268
            return render_to_response('templates/terminal/purchase_complete.jinja2',
269
                {'user': user,
270
                 'event': event,
271
                 'order': order,
272
                 'transaction': transaction,
273
                 'account_type': account_type,
274
                 'pool': pool}, request)
275
276
    except NoResultFound as e:
277
        # TODO: add generic failure page
278
        pass
279
    except Exception as e:
280
        if request.debug: raise(e)
281
        return HTTPFound(location=request.route_url('index'))
282
283
284
@view_config(route_name='event_undo', permission='service')
285
def event_undo(request):
286
    # Lookup the transaction that the user wants to undo
287
    try:
288
        event = Event.from_id(request.matchdict['event_id'])
289
    except:
290
        request.session.flash('Error: Could not find transaction to undo.', 'error')
291
        return HTTPFound(location=request.route_url('index'))
292
293
    for transaction in event.transactions:
294
295
        # Make sure transaction is a deposit, the only one the user is allowed
296
        # to undo
297
        if transaction.type not in ('cashdeposit', 'purchase'):
298
            request.session.flash('Error: Only deposits and purchases may be undone.', 'error')
299
            return HTTPFound(location=request.route_url('index'))
300
301
        # Make sure that the user who is requesting the deposit was the one who
302
        # actually placed the deposit.
303
        try:
304
            user = User.from_id(event.user_id)
305
        except:
306
            request.session.flash('Error: Invalid user for transaction.', 'error')
307
            return HTTPFound(location=request.route_url('index'))
308
309
        if user.umid != request.matchdict['umid']:
310
            request.session.flash('Error: Transaction does not belong to specified user', 'error')
311
            return HTTPFound(location=request.route_url('user', umid=request.matchdict['umid']))
312
313
    # If the checks pass, actually revert the transaction
314
    try:
315
        line_items = datalayer.undo_event(event, user)
316
        if event.type == 'deposit':
317
            request.session.flash('Deposit successfully undone.', 'success')
318
        elif event.type == 'purchase':
319
            request.session.flash('Purchase undone. Please edit it as needed.', 'success')
320
    except:
321
        request.session.flash('Error: Failed to undo transaction.', 'error')
322
        return HTTPFound(location=request.route_url('purchase', umid=user.umid))
323
324
    if event.type == 'deposit':
325
        return HTTPFound(location=request.route_url('user', umid=user.umid))
326
    elif event.type == 'purchase':
327
        return HTTPFound(location=request.route_url('purchase', umid=user.umid, _query=line_items))
328
    else:
329
        assert(False and "Should not be able to get here?")
330
331
332
###
333
### JSON Requests
334
###
335
336
@view_config(route_name='purchase_item_row', renderer='json', permission='service')
337
def item(request):
338
    try:
339
        item = Item.from_barcode(request.matchdict['barcode'])
340
    except:
341
        # Could not find the item. Check to see if the user scanned a box
342
        # instead. This could lead to two cases: a) the box only has 1 item in it
343
        # in which case we just add that item to the cart. This likely occurred
344
        # because the individual items do not have barcodes so we just use
345
        # the box. b) The box has multiple items in it in which case we throw
346
        # an error for now.
347
        try:
348
            box = Box.from_barcode(request.matchdict['barcode'])
349
            if box.subitem_number == 1:
350
                item = box.items[0].item
351
            else:
352
                return {'status': 'scanned_box_with_multiple_items'}
353
        except:
354
            return {'status': 'unknown_barcode'}
355
    if item.enabled:
356
        status = 'success'
357
    else:
358
        status = 'disabled'
359
    item_html = render('templates/terminal/purchase_item_row.jinja2', {'item': item})
360
    return {'status': status, 'id':item.id, 'item_row_html' : item_html}
361
362
###
363
### POST Handlers
364
###
365
366
@view_config(route_name='item_request_new', request_method='POST')
367
def item_request_new(request):
368
    try:
369
        request_text = request.POST['request']
370
        if len(request_text) < 5:
371
            raise ValueError()
372
373
        datalayer.new_request(None, request.POST['request'])
374
375
        request.session.flash('Request added successfully', 'success')
376
        return HTTPFound(location=request.route_url('index'))
377
378
    except ValueError:
379
        request.session.flash('If you are making a request, it should probably contain some characters.', 'error')
380
        return HTTPFound(location=request.route_url('item_request'))
381
382
    except:
383
        request.session.flash('Error adding request.', 'error')
384
        return HTTPFound(location=request.route_url('index'))
385
386
@view_config(route_name='purchase_new',
387
             request_method='POST',
388
             renderer='json',
389
             permission='service')
390
def purchase_new(request):
391
    try:
392
        user = User.from_umid(request.POST['umid'])
393
394
        ignored_keys = ['umid', 'account', 'pool_id']
395
396
        # Bundle all purchase items
397
        items = {}
398
        for item_id,quantity in request.POST.items():
399
            if item_id in ignored_keys:
400
                continue
401
            item = Item.from_id(int(item_id))
402
            items[item] = int(quantity)
403
404
        # What should pay for this?
405
        # Note: should do a bunch of checking to make sure all of this
406
        # is kosher. But given our locked down single terminal, we're just
407
        # going to skip all of that.
408
        if request.POST['account'] == 'user':
409
            purchase = datalayer.purchase(user, user, items)
410
        elif request.POST['account'] == 'pool':
411
            pool = Pool.from_id(int(request.POST['pool_id']))
412
            purchase = datalayer.purchase(user, pool, items)
413
414
        # Return the committed transaction ID
415
        return {'event_id': purchase.event.id}
416
417
    except __user.InvalidUserException as e:
418
        request.session.flash('Invalid user error. Please try again.', 'error')
419
        return {'redirect_url': '/'}
420
421
    except ValueError as e:
422
        return {'error': 'Unable to parse Item ID or quantity'}
423
424
    except NoResultFound as e:
425
        # Could not find an item
426
        return {'error': 'Unable to identify an item.'}
427
428
429
# Handle the POST from coinbase saying Chez Betty got a btc deposit.
430
# Store the bitcoin record in the DB
431
@view_config(route_name='btc_deposit', request_method='POST', renderer='json')
432
def btc_deposit(request):
433
434
    user = User.from_umid(request.matchdict['umid'])
435
    auth_key = request.matchdict['auth_key']
436
437
    addr       = request.json_body['address']
438
    amount_btc = request.json_body['amount']
439
    txid       = request.json_body['transaction']['id']
440
    created_at = request.json_body['transaction']['created_at']
441
    txhash     = request.json_body['transaction']['hash']
442
443
    try:
444
        pending = BtcPendingDeposit.from_auth_key(auth_key)
445
    except NoResultFound as e:
446
        print("No result for auth_key %s" % auth_key)
447
        return {}
448
449
450
    if (pending.user_id != user.id or pending.address != addr):
451
        print("Mismatch of BtcPendingDeposit userid or address: (%d/%d), (%s/%s)" % (pending.user_id, user.id, pending.address, addr))
452
        return {}
453
454
    #try:
455
    usd_per_btc = Bitcoin.get_spot_price()
456
    #except BTCException as e:
457
    #    # unknown exchange rate?
458
    #    print('Could not get exchange rate for addr %s txhash %s; failing...' % (addr, txhash))
459
    #    return {}
460
461
    amount_usd = Decimal(amount_btc) * usd_per_btc
462
463
    # round down to nearest cent
464
    amount_usd = Decimal(int(amount_usd*100))/Decimal(100)
465
466
    ret = "addr: %s, amount: %s, txid: %s, created_at: %s, txhash: %s, exchange = $%s/BTC"\
467
           % (addr, amount_btc, txid, created_at, txhash, usd_per_btc)
468
    datalayer.bitcoin_deposit(user, amount_usd, txhash, addr, amount_btc)
469
    DBSession.delete(pending)
470
    print(ret)
471
472
    return {}
473
474
475
@view_config(route_name='btc_check', request_method='GET', renderer='json')
476
def btc_check(request):
477
    try:
478
        deposit = BTCDeposit.from_address(request.matchdict['addr'])
479
        return {"event_id": deposit.event.id}
480
    except:
481
        return {}
482
483
484
@view_config(route_name='deposit_emailinfo',
485
             renderer='json',
486
             permission='service')
487
def deposit_emailinfo(request):
488
    try:
489
        user = User.from_id(int(request.matchdict['user_id']))
490
        if not user.has_password:
491
            return deposit_password_create(request)
492
        send_email(TO=user.uniqname+'@umich.edu',
493
                   SUBJECT='Chez Betty Credit Card Instructions',
494
                   body=render('templates/terminal/email_userinfo.jinja2', {'user': user}))
495
        return {'status': 'success',
496
                'msg': 'Instructions emailed to {}@umich.edu.'.format(user.uniqname)}
497
    except NoResultFound:
498
        return {'status': 'error',
499
                'msg': 'Could not find user.'}
500
    except Exception as e:
501
        if request.debug: raise(e)
502
        return {'status': 'error',
503
                'msg': 'Error.'}
504
505
506
@view_config(route_name='deposit_password_create',
507
             renderer='json',
508
             permission='service')
509
def deposit_password_create(request):
510
    try:
511
        user = User.from_id(int(request.matchdict['user_id']))
512
        if user.has_password:
513
            return {'status': 'error',
514
                    'msg': 'Error: User already has password.'}
515
        user_password_reset(user)
516
        return {'status': 'success',
517
                'msg': 'Password set and emailed to {}@umich.edu.'.format(user.uniqname)}
518
    except NoResultFound:
519
        return {'status': 'error',
520
                'msg': 'Could not find user.'}
521
    except Exception as e:
522
        if request.debug: raise(e)
523
        return {'status': 'error',
524
                'msg': 'Error.'}
525
526
@view_config(route_name='deposit_password_reset',
527
        renderer='json',
528
        permission='service')
529
def deposit_password_reset(request):
530
    try:
531
        user = User.from_id(int(request.matchdict['user_id']))
532
        user_password_reset(user)
533
        return {'status': 'success',
534
                'msg': 'Password set and emailed to {}@umich.edu.'.format(user.uniqname)}
535
    except NoResultFound:
536
        return {'status': 'error',
537
                'msg': 'Could not find user.'}
538
    except Exception as e:
539
        if request.debug: raise(e)
540
        return {'status': 'error',
541
                'msg': 'Error.'}
542
543
@view_config(route_name='deposit_new',
544
             request_method='POST',
545
             renderer='json',
546
             permission='service')
547
def deposit_new(request):
548
    try:
549
        user = User.from_umid(request.POST['umid'])
550
        amount = Decimal(request.POST['amount'])
551
        account = request.POST['account']
552
553
        # Check if the deposit amount is too great.
554
        # This if block could be tighter, but this is easier to understand
555
        if amount > 100.0:
556
            # Anything above 100 is blocked
557
            raise DepositException('Deposit amount of ${:,.2f} exceeds the limit'.format(amount))
558
559
        if amount < 100.0 and amount > 20.0 and (user.total_deposits < 10.0 or user.total_purchases < 10.0):
560
            # If the deposit is between 20 and 100 and the user hasn't done much
561
            # with betty. Block the deposit. We do allow deposits up to 100 for
562
            # customers that have shown they know how to scan/purchase and
563
            # deposit
564
            raise DepositException('Deposit amount of ${:,.2f} exceeds the limit'.format(amount))
565
566
        if amount <= 0.0:
567
            raise DepositException('Deposit amount must be greater than $0.00')
568
569
        if account == 'user':
570
            deposit = datalayer.deposit(user, user, amount)
571
        elif account == 'pool':
572
            deposit = datalayer.deposit(user, Pool.from_id(request.POST['pool_id']), amount)
573
574
        # Return a JSON blob of the transaction ID so the client can redirect to
575
        # the deposit success page
576
        return {'event_id': deposit['event'].id}
577
578
    except __user.InvalidUserException as e:
579
        request.session.flash('Invalid user error. Please try again.', 'error')
580
        return {'error': 'Error finding user.',
581
                'redirect_url': '/'}
582
583
    except ValueError as e:
584
        return {'error': 'Error understanding deposit amount.'}
585
586
    except DepositException as e:
587
        return {'error': str(e)}
588
589
590
@view_config(route_name='deposit_edit_submit',
591
             request_method='POST',
592
             renderer='json',
593
             permission='service')
594
def deposit_edit_submit(request):
595
    try:
596
        user = User.from_umid(request.POST['umid'])
597
        amount = Decimal(request.POST['amount'])
598
        old_event = Event.from_id(request.POST['old_event_id'])
599
600
        if old_event.type != 'deposit' or \
601
           old_event.transactions[0].type != 'cashdeposit' or \
602
           (old_event.transactions[0].to_account_virt_id != user.id and \
603
            old_event.user_id != user.id):
604
           # Something went wrong, can't undo this deposit
605
           raise DepositException('Cannot undo that deposit')
606
607
        new_deposit = deposit_new(request)
608
609
        if 'error' in new_deposit and new_deposit['error'] != 'success':
610
            # Error occurred, do not delete old event
611
            return new_deposit
612
613
        # Now undo old deposit
614
        datalayer.undo_event(old_event, user)
615
616
        return new_deposit
617
618
    except __user.InvalidUserException as e:
619
        request.session.flash('Invalid user error. Please try again.', 'error')
620
        return {'redirect_url': '/'}
621
622
    except DepositException as e:
623
        return {'error': str(e)}
624
625
    except Exception as e:
626
        if request.debug: raise(e)
627
        return {'error': 'Error.'}
628
629