byceps/byceps

View on GitHub
byceps/blueprints/site/shop/orders/views.py

Summary

Maintainability
A
0 mins
Test Coverage
F
31%
"""
byceps.blueprints.site.shop.orders.views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2014-2024 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from decimal import Decimal

from flask import abort, g, request
from flask_babel import gettext
import structlog

from byceps.services.brand import brand_service
from byceps.services.email import (
    email_config_service,
    email_footer_service,
    email_service,
)
from byceps.services.shop.cancellation_request import (
    cancellation_request_service,
)
from byceps.services.shop.order import order_payment_service, order_service
from byceps.services.shop.order.email import order_email_service
from byceps.services.shop.order.errors import OrderAlreadyCanceledError
from byceps.services.shop.order.models.order import PaymentState
from byceps.services.shop.storefront import storefront_service
from byceps.services.user import user_service
from byceps.signals import shop as shop_signals
from byceps.util.framework.blueprint import create_blueprint
from byceps.util.framework.flash import flash_error, flash_success
from byceps.util.framework.templating import templated
from byceps.util.l10n import get_user_locale
from byceps.util.views import login_required, redirect_to

from .forms import CancelForm, RequestFullRefundForm, RequestPartialRefundForm


log = structlog.get_logger()


blueprint = create_blueprint('shop_orders', __name__)


@blueprint.get('')
@login_required
@templated
def index():
    """List orders placed by the current user in the storefront assigned
    to the current site.
    """
    storefront_id = g.site.storefront_id
    if storefront_id is not None:
        storefront = storefront_service.get_storefront(storefront_id)
        orders = order_service.get_orders_placed_by_user_for_storefront(
            g.user.id, storefront.id
        )
    else:
        orders = []

    return {
        'orders': orders,
        'PaymentState': PaymentState,
    }


@blueprint.get('/<uuid:order_id>')
@login_required
@templated
def view(order_id):
    """Show a single order (if it belongs to the current user and
    current site's storefront).
    """
    order = order_service.find_order_with_details(order_id)

    if order is None:
        abort(404)

    if not _is_order_placed_by_current_user(order):
        abort(404)

    storefront = storefront_service.get_storefront(g.site.storefront_id)
    if order.storefront_id != storefront.id:
        # Order does not belong to the current site's storefront.
        abort(404)

    cancellation_request = cancellation_request_service.get_request_for_order(
        order.id
    )

    template_context = {
        'order': order,
        'render_order_payment_method': _find_order_payment_method_label,
        'cancellation_request': cancellation_request,
    }

    if order.is_open:
        template_context['payment_instructions'] = _get_payment_instructions(
            order
        )

    return template_context


def _find_order_payment_method_label(payment_method):
    return order_service.find_payment_method_label(payment_method)


def _get_payment_instructions(order) -> str | None:
    language_code = get_user_locale(g.user)

    result = order_payment_service.get_html_payment_instructions(
        order, language_code
    )
    if result.is_err():
        log.error(
            'Sending refund request confirmation email failed',
            error=result.unwrap_err(),
        )
        return None

    return result.unwrap()


@blueprint.get('/<uuid:order_id>/cancel')
@login_required
@templated
def cancel_form(order_id, erroneous_form=None):
    """Show form to cancel an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if order.is_paid:
        flash_error(
            gettext(
                'The order has already been paid. You cannot cancel it yourself anymore.'
            )
        )
        return redirect_to('.view', order_id=order.id)

    form = erroneous_form if erroneous_form else CancelForm()

    return {
        'order': order,
        'form': form,
    }


@blueprint.post('/<uuid:order_id>/cancel')
@login_required
def cancel(order_id):
    """Set the payment state of a single order to 'canceled' and
    release the respective article quantities.
    """
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if order.is_paid:
        flash_error(
            gettext(
                'The order has already been paid. You cannot cancel it yourself anymore.'
            )
        )
        return redirect_to('.view', order_id=order.id)

    form = CancelForm(request.form)
    if not form.validate():
        return cancel_form(order_id, form)

    reason = form.reason.data.strip()

    cancellation_result = order_service.cancel_order(order.id, g.user, reason)
    if cancellation_result.is_err():
        err = cancellation_result.unwrap_err()
        if isinstance(err, OrderAlreadyCanceledError):
            flash_error(
                gettext(
                    'The order has already been canceled. The payment state cannot be changed anymore.'
                )
            )
        else:
            flash_error(gettext('An unexpected error occurred.'))
        return redirect_to('.view', order_id=order.id)

    canceled_order, event = cancellation_result.unwrap()

    flash_success(gettext('Order has been canceled.'))

    order_email_service.send_email_for_canceled_order_to_orderer(canceled_order)

    shop_signals.order_canceled.send(None, event=event)

    return redirect_to('.view', order_id=canceled_order.id)


@blueprint.get('/<uuid:order_id>/request_cancellation')
@login_required
@templated
def request_cancellation_choices(order_id):
    """Show choices to request cancellation of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    return {
        'order': order,
    }


@blueprint.get('/<uuid:order_id>/request_cancellation/donate_everything')
@login_required
@templated
def donate_everything_form(order_id, erroneous_form=None):
    """Show form to donate the full amount of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    return {
        'order': order,
    }


@blueprint.post('/<uuid:order_id>/request_cancellation')
@login_required
def donate_everything(order_id):
    """Donate the full amount of an order, then cancel the order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    amount_donation = order.total_amount.amount

    cancellation_request = (
        cancellation_request_service.create_request_for_full_donation(
            order.shop_id,
            order.id,
            order.order_number,
            amount_donation,
        )
    )

    reason = 'Ticketrückgabe und Spende des Bestellbetrags in voller Höhe wie angefordert'

    cancellation_result = order_service.cancel_order(order.id, g.user, reason)
    if cancellation_result.is_err():
        err = cancellation_result.unwrap_err()
        if isinstance(err, OrderAlreadyCanceledError):
            flash_error(
                gettext(
                    'The order has already been canceled. The payment state cannot be changed anymore.'
                )
            )
        else:
            flash_error(gettext('An unexpected error occurred.'))
        return redirect_to('.view', order_id=order.id)

    canceled_order, event = cancellation_result.unwrap()

    cancellation_request_service.accept_request(cancellation_request.id)

    flash_success(gettext('Order has been canceled.'))

    order_email_service.send_email_for_canceled_order_to_orderer(canceled_order)

    shop_signals.order_canceled.send(None, event=event)

    return redirect_to('.view', order_id=canceled_order.id)


@blueprint.get('/<uuid:order_id>/request_partial_refund')
@login_required
@templated
def request_partial_refund_form(order_id, erroneous_form=None):
    """Show form to request a partial refund of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    form = erroneous_form if erroneous_form else RequestPartialRefundForm(order)

    return {
        'order': order,
        'form': form,
    }


@blueprint.post('/<uuid:order_id>/request_partial_refund')
@login_required
def request_partial_refund(order_id):
    """Request a partial refund of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    form = RequestPartialRefundForm(order, request.form)
    if not form.validate():
        return request_partial_refund_form(order_id, form)

    amount_donation = form.amount_donation.data
    amount_refund = order.total_amount.amount - amount_donation
    recipient_name = form.recipient_name.data
    recipient_iban = form.recipient_iban.data

    cancellation_request_service.create_request_for_partial_refund(
        order.shop_id,
        order.id,
        order.order_number,
        amount_refund,
        amount_donation,
        recipient_name,
        recipient_iban,
    )

    _send_refund_request_confirmation_email(order.order_number, amount_refund)

    flash_success('Die Stornierungsanfrage wurde übermittelt.')

    return redirect_to('.view', order_id=order.id)


@blueprint.get('/<uuid:order_id>/request_full_refund')
@login_required
@templated
def request_full_refund_form(order_id, erroneous_form=None):
    """Show form to request a full refund of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    form = erroneous_form if erroneous_form else RequestFullRefundForm()

    return {
        'order': order,
        'form': form,
    }


@blueprint.post('/<uuid:order_id>/request_full_refund')
@login_required
def request_full_refund(order_id):
    """Request a full refund of an order."""
    order = _get_order_by_current_user_or_404(order_id)

    if order.is_canceled:
        flash_error(gettext('The order has already been canceled.'))
        return redirect_to('.view', order_id=order.id)

    if not order.is_paid:
        flash_error(gettext('The order has not been paid.'))
        return redirect_to('.view', order_id=order.id)

    request_for_order_number = (
        cancellation_request_service.get_request_for_order(order.id)
    )
    if request_for_order_number:
        flash_error('Es liegt bereits eine Stornierungsanfrage vor.')
        return redirect_to('.view', order_id=order.id)

    form = RequestFullRefundForm(request.form)
    if not form.validate():
        return request_full_refund_form(order_id, form)

    amount_refund = order.total_amount.amount
    recipient_name = form.recipient_name.data
    recipient_iban = form.recipient_iban.data

    cancellation_request_service.create_request_for_full_refund(
        order.shop_id,
        order.id,
        order.order_number,
        amount_refund,
        recipient_name,
        recipient_iban,
    )

    _send_refund_request_confirmation_email(order.order_number, amount_refund)

    flash_success('Die Stornierungsanfrage wurde übermittelt.')

    return redirect_to('.view', order_id=order.id)


def _send_refund_request_confirmation_email(
    order_number, amount_refund: Decimal
) -> None:
    email_config = email_config_service.get_config(g.brand_id)

    email_address = user_service.get_email_address_data(g.user.id)
    if (email_address is None) or not email_address.verified:
        # Ignore this situation for now.
        return

    screen_name = g.user.screen_name or 'User'

    brand = brand_service.get_brand(g.brand_id)
    language_code = get_user_locale(g.user)

    footer_result = email_footer_service.get_footer(brand, language_code)
    if footer_result.is_err():
        log.error(
            'Sending refund request confirmation email failed',
            error=footer_result.unwrap_err(),
        )
        return

    footer = footer_result.unwrap()

    sender = email_config.sender
    recipients = [email_address.address]
    subject = 'Eingang deiner Anfrage zur Rückerstattung von Tickets'
    body = (
        f'Hallo {screen_name},\n\n'
        'wir haben deine Anfrage zur Rückerstattung deiner Bestellung '
        f'{order_number} in Höhe von {amount_refund} € erhalten.\n\n'
        'Bitte beachte, dass die Abwicklung der Rückzahlung einige Zeit '
        'in Anspruch nehmen kann. Danke für dein Verständnis.'
        '\n\n'
    ) + footer

    email_service.enqueue_email(sender, recipients, subject, body)


# helpers


def _get_order_by_current_user_or_404(order_id):
    order = order_service.find_order(order_id)

    if order is None:
        abort(404)

    if not _is_order_placed_by_current_user(order):
        abort(404)

    return order


def _is_order_placed_by_current_user(order) -> bool:
    return order.placed_by.id == g.user.id