byceps.blueprints.site.shop.orders.views   F
last analyzed

Complexity

Total Complexity 74

Size/Duplication

Total Lines 600
Duplicated Lines 29.67 %

Test Coverage

Coverage 30.89%

Importance

Changes 0
Metric Value
wmc 74
eloc 380
dl 178
loc 600
ccs 80
cts 259
cp 0.3089
rs 2.48
c 0
b 0
f 0

17 Functions

Rating   Name   Duplication   Size   Complexity  
A index() 0 19 2
A _find_order_payment_method_label() 0 2 1
B view() 0 66 7
B request_cancellation_choices() 27 27 5
A _send_refund_request_confirmation_email() 0 38 4
A cancel_form() 0 24 4
B request_partial_refund() 48 48 6
A _is_cancellation_requesting_enabled() 0 10 3
B request_full_refund_form() 30 30 6
A _is_order_placed_by_current_user() 0 2 1
B donate_everything_form() 27 27 5
A _get_payment_instructions() 0 14 2
A _get_order_by_current_user_or_404() 0 10 3
B cancel() 0 50 6
B request_partial_refund_form() 0 30 6
B request_full_refund() 46 46 6
B donate_everything() 0 63 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like byceps.blueprints.site.shop.orders.views 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
"""
2
byceps.blueprints.site.shop.orders.views
3
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
5
:Copyright: 2014-2024 Jochen Kupperschmidt
6
:License: Revised BSD (see `LICENSE` file for details)
7
"""
8
9 1
from decimal import Decimal
10
11 1
from flask import abort, current_app, g, request
12 1
from flask_babel import gettext
13
import structlog
14 1
15 1
from byceps.services.brand import brand_service
16
from byceps.services.email import (
17
    email_config_service,
18
    email_footer_service,
19
    email_service,
20 1
)
21 1
from byceps.services.party import party_setting_service
22 1
from byceps.services.shop.cancellation_request import (
23 1
    cancellation_request_service,
24 1
)
25 1
from byceps.services.shop.order import (
26 1
    order_command_service,
27 1
    order_payment_service,
28 1
    order_service,
29 1
)
30 1
from byceps.services.shop.order.email import order_email_service
31 1
from byceps.services.shop.order.errors import OrderAlreadyCanceledError
32 1
from byceps.services.shop.order.models.order import PaymentState
33
from byceps.services.shop.payment import payment_gateway_service
34 1
from byceps.services.shop.shop import shop_service
35
from byceps.services.shop.storefront import storefront_service
36
from byceps.services.user import user_service
37 1
from byceps.signals import shop as shop_signals
38
from byceps.util.framework.blueprint import create_blueprint
39
from byceps.util.framework.flash import flash_error, flash_success
40 1
from byceps.util.framework.templating import templated
41 1
from byceps.util.l10n import get_user_locale
42 1
from byceps.util.views import login_required, redirect_to
43 1
44
from .forms import CancelForm, RequestFullRefundForm, RequestPartialRefundForm
45
46
47
log = structlog.get_logger()
48
49
50
blueprint = create_blueprint('shop_orders', __name__)
51
52
53
@blueprint.get('')
54
@login_required
55
@templated
56
def index():
57
    """List orders placed by the current user in the storefront assigned
58
    to the current site.
59
    """
60
    storefront_id = g.site.storefront_id
61
    if storefront_id is not None:
62 1
        storefront = storefront_service.get_storefront(storefront_id)
63 1
        orders = order_service.get_orders_placed_by_user_for_storefront(
64 1
            g.user.id, storefront.id
65 1
        )
66
    else:
67
        orders = []
68
69 1
    return {
70
        'orders': orders,
71 1
        'PaymentState': PaymentState,
72
    }
73
74 1
75 1
@blueprint.get('/<uuid:order_id>')
76
@login_required
77 1
@templated
78 1
def view(order_id):
79
    """Show a single order (if it belongs to the current user and
80 1
    current site's storefront).
81
    """
82 1
    order = order_service.find_order_with_details(order_id)
83
84
    if order is None:
85
        abort(404)
86
87
    if not _is_order_placed_by_current_user(order):
88 1
        abort(404)
89
90
    storefront = storefront_service.get_storefront(g.site.storefront_id)
91
    if order.storefront_id != storefront.id:
92
        # Order does not belong to the current site's storefront.
93
        abort(404)
94 1
95 1
    shop = shop_service.get_shop(order.shop_id)
96
97
    paypal_enabled = (
98
        payment_gateway_service.is_payment_gateway_enabled_for_storefront(
99 1
            'paypal', storefront.id
100
        )
101
    )
102 1
    if paypal_enabled:
103
        paypal_client_id = current_app.config.get('PAYPAL_CLIENT_ID')
104
    else:
105
        paypal_client_id = None
106 1
107 1
    stripe_enabled = (
108
        payment_gateway_service.is_payment_gateway_enabled_for_storefront(
109 1
            'stripe', storefront.id
110
        )
111
    )
112
    if stripe_enabled:
113
        stripe_publishable_key = current_app.config.get(
114 1
            'STRIPE_PUBLISHABLE_KEY'
115 1
        )
116 1
    else:
117 1
        stripe_publishable_key = None
118
119
    cancellation_request = cancellation_request_service.get_request_for_order(
120
        order.id
121
    )
122
123
    template_context = {
124
        'order': order,
125
        'shop_title': shop.title,
126
        'paypal_enabled': paypal_enabled,
127
        'paypal_client_id': paypal_client_id,
128
        'stripe_enabled': stripe_enabled,
129
        'stripe_publishable_key': stripe_publishable_key,
130
        'render_order_payment_method': _find_order_payment_method_label,
131
        'cancellation_requesting_enabled': _is_cancellation_requesting_enabled(),
132
        'cancellation_request': cancellation_request,
133
    }
134
135
    if order.is_open:
136
        template_context['payment_instructions'] = _get_payment_instructions(
137
            order
138
        )
139
140
    return template_context
141 1
142 1
143 1
def _find_order_payment_method_label(payment_method):
144
    return order_service.find_payment_method_label(payment_method)
145
146
147
def _is_cancellation_requesting_enabled() -> bool:
148
    party_id = g.party.id if g.party else None
149
    if party_id is None:
150
        return False
151
152
    cancellation_permitted = party_setting_service.find_setting_value(
153
        party_id, 'order_cancellation_requesting_enabled'
154
    )
155
156
    return cancellation_permitted == 'true'
157
158
159
def _get_payment_instructions(order) -> str | None:
160
    language_code = get_user_locale(g.user)
161
162
    result = order_payment_service.get_html_payment_instructions(
163
        order, language_code
164
    )
165
    if result.is_err():
166
        log.error(
167
            'Sending refund request confirmation email failed',
168
            error=result.unwrap_err(),
169
        )
170
        return None
171
172
    return result.unwrap()
173
174
175
@blueprint.get('/<uuid:order_id>/cancel')
176
@login_required
177
@templated
178
def cancel_form(order_id, erroneous_form=None):
179
    """Show form to cancel an order."""
180
    order = _get_order_by_current_user_or_404(order_id)
181
182
    if order.is_canceled:
183
        flash_error(gettext('The order has already been canceled.'))
184
        return redirect_to('.view', order_id=order.id)
185
186
    if order.is_paid:
187
        flash_error(
188
            gettext(
189
                'The order has already been paid. You cannot cancel it yourself anymore.'
190
            )
191 1
        )
192 1
        return redirect_to('.view', order_id=order.id)
193 1
194 1
    form = erroneous_form if erroneous_form else CancelForm()
195
196
    return {
197
        'order': order,
198
        'form': form,
199
    }
200
201
202
@blueprint.post('/<uuid:order_id>/cancel')
203
@login_required
204
def cancel(order_id):
205
    """Set the payment state of a single order to 'canceled' and
206
    release the respective product quantities.
207
    """
208
    order = _get_order_by_current_user_or_404(order_id)
209
210
    if order.is_canceled:
211
        flash_error(gettext('The order has already been canceled.'))
212
        return redirect_to('.view', order_id=order.id)
213
214
    if order.is_paid:
215
        flash_error(
216
            gettext(
217
                'The order has already been paid. You cannot cancel it yourself anymore.'
218
            )
219
        )
220 1
        return redirect_to('.view', order_id=order.id)
221 1
222 1
    form = CancelForm(request.form)
223 1
    if not form.validate():
224
        return cancel_form(order_id, form)
225
226
    reason = form.reason.data.strip()
227
228
    cancellation_result = order_command_service.cancel_order(
229
        order.id, g.user, reason
230
    )
231
    if cancellation_result.is_err():
232
        err = cancellation_result.unwrap_err()
233
        if isinstance(err, OrderAlreadyCanceledError):
234
            flash_error(
235
                gettext(
236
                    'The order has already been canceled. The payment state cannot be changed anymore.'
237
                )
238
            )
239
        else:
240
            flash_error(gettext('An unexpected error occurred.'))
241
        return redirect_to('.view', order_id=order.id)
242
243
    canceled_order, event = cancellation_result.unwrap()
244
245
    flash_success(gettext('Order has been canceled.'))
246
247
    order_email_service.send_email_for_canceled_order_to_orderer(canceled_order)
248
249 1
    shop_signals.order_canceled.send(None, event=event)
250 1
251 1
    return redirect_to('.view', order_id=canceled_order.id)
252
253
254 View Code Duplication
@blueprint.get('/<uuid:order_id>/request_cancellation')
255
@login_required
256
@templated
257
def request_cancellation_choices(order_id):
258
    """Show choices to request cancellation of an order."""
259
    if not _is_cancellation_requesting_enabled():
260
        abort(403)
261
262
    order = _get_order_by_current_user_or_404(order_id)
263
264
    if order.is_canceled:
265
        flash_error(gettext('The order has already been canceled.'))
266
        return redirect_to('.view', order_id=order.id)
267
268
    if not order.is_paid:
269
        flash_error(gettext('The order has not been paid.'))
270
        return redirect_to('.view', order_id=order.id)
271
272
    request_for_order_number = (
273
        cancellation_request_service.get_request_for_order(order.id)
274
    )
275
    if request_for_order_number:
276
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
277
        return redirect_to('.view', order_id=order.id)
278
279
    return {
280
        'order': order,
281
    }
282
283
284 View Code Duplication
@blueprint.get('/<uuid:order_id>/request_cancellation/donate_everything')
285
@login_required
286
@templated
287
def donate_everything_form(order_id, erroneous_form=None):
288
    """Show form to donate the full amount of an order."""
289
    if not _is_cancellation_requesting_enabled():
290
        abort(403)
291
292
    order = _get_order_by_current_user_or_404(order_id)
293
294
    if order.is_canceled:
295
        flash_error(gettext('The order has already been canceled.'))
296
        return redirect_to('.view', order_id=order.id)
297
298
    if not order.is_paid:
299
        flash_error(gettext('The order has not been paid.'))
300
        return redirect_to('.view', order_id=order.id)
301
302
    request_for_order_number = (
303
        cancellation_request_service.get_request_for_order(order.id)
304
    )
305
    if request_for_order_number:
306
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
307
        return redirect_to('.view', order_id=order.id)
308
309
    return {
310 1
        'order': order,
311 1
    }
312 1
313 1
314
@blueprint.post('/<uuid:order_id>/request_cancellation')
315
@login_required
316
def donate_everything(order_id):
317
    """Donate the full amount of an order, then cancel the order."""
318
    if not _is_cancellation_requesting_enabled():
319
        abort(403)
320
321
    order = _get_order_by_current_user_or_404(order_id)
322
323
    if order.is_canceled:
324
        flash_error(gettext('The order has already been canceled.'))
325
        return redirect_to('.view', order_id=order.id)
326
327
    if not order.is_paid:
328
        flash_error(gettext('The order has not been paid.'))
329
        return redirect_to('.view', order_id=order.id)
330
331
    request_for_order_number = (
332
        cancellation_request_service.get_request_for_order(order.id)
333
    )
334
    if request_for_order_number:
335
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
336
        return redirect_to('.view', order_id=order.id)
337
338
    amount_donation = order.total_amount.amount
339
340
    cancellation_request = (
341
        cancellation_request_service.create_request_for_full_donation(
342 1
            order.shop_id,
343 1
            order.id,
344 1
            order.order_number,
345
            amount_donation,
346
        )
347
    )
348
349
    reason = 'Ticketrückgabe und Spende des Bestellbetrags in voller Höhe wie angefordert'
350
351
    cancellation_result = order_command_service.cancel_order(
352
        order.id, g.user, reason
353
    )
354
    if cancellation_result.is_err():
355
        err = cancellation_result.unwrap_err()
356
        if isinstance(err, OrderAlreadyCanceledError):
357
            flash_error(
358
                gettext(
359
                    'The order has already been canceled. The payment state cannot be changed anymore.'
360
                )
361
            )
362
        else:
363
            flash_error(gettext('An unexpected error occurred.'))
364
        return redirect_to('.view', order_id=order.id)
365
366
    canceled_order, event = cancellation_result.unwrap()
367
368
    cancellation_request_service.accept_request(cancellation_request.id)
369
370
    flash_success(gettext('Order has been canceled.'))
371
372
    order_email_service.send_email_for_canceled_order_to_orderer(canceled_order)
373
374
    shop_signals.order_canceled.send(None, event=event)
375
376
    return redirect_to('.view', order_id=canceled_order.id)
377
378
379
@blueprint.get('/<uuid:order_id>/request_partial_refund')
380
@login_required
381
@templated
382
def request_partial_refund_form(order_id, erroneous_form=None):
383
    """Show form to request a partial refund of an order."""
384
    if not _is_cancellation_requesting_enabled():
385
        abort(403)
386
387
    order = _get_order_by_current_user_or_404(order_id)
388
389
    if order.is_canceled:
390 1
        flash_error(gettext('The order has already been canceled.'))
391 1
        return redirect_to('.view', order_id=order.id)
392 1
393 1
    if not order.is_paid:
394
        flash_error(gettext('The order has not been paid.'))
395
        return redirect_to('.view', order_id=order.id)
396
397
    request_for_order_number = (
398
        cancellation_request_service.get_request_for_order(order.id)
399
    )
400
    if request_for_order_number:
401
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
402
        return redirect_to('.view', order_id=order.id)
403
404
    form = erroneous_form if erroneous_form else RequestPartialRefundForm(order)
405
406
    return {
407
        'order': order,
408
        'form': form,
409
    }
410
411
412 View Code Duplication
@blueprint.post('/<uuid:order_id>/request_partial_refund')
413
@login_required
414
def request_partial_refund(order_id):
415
    """Request a partial refund of an order."""
416
    if not _is_cancellation_requesting_enabled():
417
        abort(403)
418
419
    order = _get_order_by_current_user_or_404(order_id)
420
421
    if order.is_canceled:
422 1
        flash_error(gettext('The order has already been canceled.'))
423 1
        return redirect_to('.view', order_id=order.id)
424 1
425
    if not order.is_paid:
426
        flash_error(gettext('The order has not been paid.'))
427
        return redirect_to('.view', order_id=order.id)
428
429
    request_for_order_number = (
430
        cancellation_request_service.get_request_for_order(order.id)
431
    )
432
    if request_for_order_number:
433
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
434
        return redirect_to('.view', order_id=order.id)
435
436
    form = RequestPartialRefundForm(order, request.form)
437
    if not form.validate():
438
        return request_partial_refund_form(order_id, form)
439
440
    amount_donation = form.amount_donation.data
441
    amount_refund = order.total_amount.amount - amount_donation
442
    recipient_name = form.recipient_name.data
443
    recipient_iban = form.recipient_iban.data
444
445
    cancellation_request_service.create_request_for_partial_refund(
446
        order.shop_id,
447
        order.id,
448
        order.order_number,
449
        amount_refund,
450
        amount_donation,
451
        recipient_name,
452
        recipient_iban,
453
    )
454
455
    _send_refund_request_confirmation_email(order.order_number, amount_refund)
456
457
    flash_success('Die Stornierungsanfrage wurde übermittelt.')
458
459
    return redirect_to('.view', order_id=order.id)
460
461
462 View Code Duplication
@blueprint.get('/<uuid:order_id>/request_full_refund')
463
@login_required
464
@templated
465
def request_full_refund_form(order_id, erroneous_form=None):
466
    """Show form to request a full refund of an order."""
467
    if not _is_cancellation_requesting_enabled():
468 1
        abort(403)
469
470
    order = _get_order_by_current_user_or_404(order_id)
471
472
    if order.is_canceled:
473
        flash_error(gettext('The order has already been canceled.'))
474
        return redirect_to('.view', order_id=order.id)
475
476
    if not order.is_paid:
477
        flash_error(gettext('The order has not been paid.'))
478
        return redirect_to('.view', order_id=order.id)
479
480
    request_for_order_number = (
481
        cancellation_request_service.get_request_for_order(order.id)
482
    )
483
    if request_for_order_number:
484
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
485
        return redirect_to('.view', order_id=order.id)
486
487
    form = erroneous_form if erroneous_form else RequestFullRefundForm()
488
489
    return {
490
        'order': order,
491
        'form': form,
492
    }
493
494
495 View Code Duplication
@blueprint.post('/<uuid:order_id>/request_full_refund')
496
@login_required
497
def request_full_refund(order_id):
498
    """Request a full refund of an order."""
499
    if not _is_cancellation_requesting_enabled():
500
        abort(403)
501
502 1
    order = _get_order_by_current_user_or_404(order_id)
503
504
    if order.is_canceled:
505
        flash_error(gettext('The order has already been canceled.'))
506
        return redirect_to('.view', order_id=order.id)
507
508
    if not order.is_paid:
509
        flash_error(gettext('The order has not been paid.'))
510
        return redirect_to('.view', order_id=order.id)
511
512
    request_for_order_number = (
513
        cancellation_request_service.get_request_for_order(order.id)
514 1
    )
515 1
    if request_for_order_number:
516
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
517
        return redirect_to('.view', order_id=order.id)
518
519
    form = RequestFullRefundForm(request.form)
520
    if not form.validate():
521
        return request_full_refund_form(order_id, form)
522
523
    amount_refund = order.total_amount.amount
524
    recipient_name = form.recipient_name.data
525
    recipient_iban = form.recipient_iban.data
526
527
    cancellation_request_service.create_request_for_full_refund(
528
        order.shop_id,
529
        order.id,
530
        order.order_number,
531
        amount_refund,
532
        recipient_name,
533
        recipient_iban,
534
    )
535
536
    _send_refund_request_confirmation_email(order.order_number, amount_refund)
537
538
    flash_success('Die Stornierungsanfrage wurde übermittelt.')
539
540
    return redirect_to('.view', order_id=order.id)
541
542
543
def _send_refund_request_confirmation_email(
544
    order_number, amount_refund: Decimal
545
) -> None:
546
    email_config = email_config_service.get_config(g.brand_id)
547
548
    email_address = user_service.get_email_address_data(g.user.id)
549
    if (email_address is None) or not email_address.verified:
550
        # Ignore this situation for now.
551
        return
552
553
    screen_name = g.user.screen_name or 'User'
554
555
    brand = brand_service.get_brand(g.brand_id)
556
    language_code = get_user_locale(g.user)
557
558
    footer_result = email_footer_service.get_footer(brand, language_code)
559
    if footer_result.is_err():
560
        log.error(
561
            'Sending refund request confirmation email failed',
562
            error=footer_result.unwrap_err(),
563
        )
564
        return
565
566
    footer = footer_result.unwrap()
567
568
    sender = email_config.sender
569
    recipients = [email_address.address]
570
    subject = 'Eingang deiner Anfrage zur Rückerstattung von Tickets'
571
    body = (
572
        f'Hallo {screen_name},\n\n'
573
        'wir haben deine Anfrage zur Rückerstattung deiner Bestellung '
574
        f'{order_number} in Höhe von {amount_refund} € erhalten.\n\n'
575
        'Bitte beachte, dass die Abwicklung der Rückzahlung einige Zeit '
576
        'in Anspruch nehmen kann. Danke für dein Verständnis.'
577
        '\n\n'
578
    ) + footer
579
580
    email_service.enqueue_email(sender, recipients, subject, body)
581
582
583
# helpers
584
585
586
def _get_order_by_current_user_or_404(order_id):
587
    order = order_service.find_order(order_id)
588
589
    if order is None:
590
        abort(404)
591
592
    if not _is_order_placed_by_current_user(order):
593
        abort(404)
594
595
    return order
596
597
598
def _is_order_placed_by_current_user(order) -> bool:
599
    return order.placed_by.id == g.user.id
600