byceps/byceps

View on GitHub
byceps/services/shop/order/order_action_service.py

Summary

Maintainability
A
0 mins
Test Coverage
C
79%
"""
byceps.services.shop.order.order_action_service
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

from collections.abc import Callable
from uuid import UUID

from sqlalchemy import delete, select

from byceps.database import db
from byceps.services.shop.article import article_service
from byceps.services.shop.article.models import ArticleID
from byceps.services.user.models.user import User

from .actions.award_badge import award_badge
from .actions.create_ticket_bundles import create_ticket_bundles
from .actions.create_tickets import create_tickets
from .actions.revoke_ticket_bundles import revoke_ticket_bundles
from .actions.revoke_tickets import revoke_tickets
from .dbmodels.order_action import DbOrderAction
from .models.action import Action, ActionParameters
from .models.order import LineItem, Order, PaymentState


OrderActionType = Callable[[Order, LineItem, User, ActionParameters], None]


PROCEDURES_BY_NAME: dict[str, OrderActionType] = {
    'award_badge': award_badge,
    'create_ticket_bundles': create_ticket_bundles,
    'revoke_ticket_bundles': revoke_ticket_bundles,
    'create_tickets': create_tickets,
    'revoke_tickets': revoke_tickets,
}


# -------------------------------------------------------------------- #
# creation/removal


def create_action(
    article_id: ArticleID,
    payment_state: PaymentState,
    procedure_name: str,
    parameters: ActionParameters,
) -> None:
    """Create an order action."""
    db_action = DbOrderAction(
        article_id, payment_state, procedure_name, parameters
    )

    db.session.add(db_action)
    db.session.commit()


def delete_action(action_id: UUID) -> None:
    """Delete the order action."""
    db.session.execute(delete(DbOrderAction).filter_by(id=action_id))
    db.session.commit()


def delete_actions_for_article(article_id: ArticleID) -> None:
    """Delete all order actions for an article."""
    db.session.execute(delete(DbOrderAction).filter_by(article_id=article_id))
    db.session.commit()


def find_action(action_id: UUID) -> Action | None:
    """Return the action with that ID, if found."""
    db_action = db.session.get(DbOrderAction, action_id)

    if db_action is None:
        return None

    return _db_entity_to_action(db_action)


# -------------------------------------------------------------------- #
# retrieval


def get_actions_for_article(article_id: ArticleID) -> list[Action]:
    """Return the order actions defined for that article."""
    db_actions = db.session.scalars(
        select(DbOrderAction).filter_by(article_id=article_id)
    ).all()

    return [_db_entity_to_action(db_action) for db_action in db_actions]


def _db_entity_to_action(db_action: DbOrderAction) -> Action:
    return Action(
        id=db_action.id,
        article_id=db_action.article_id,
        payment_state=db_action.payment_state,
        procedure_name=db_action.procedure,
        parameters=db_action.parameters,
    )


# -------------------------------------------------------------------- #
# execution


def execute_creation_actions(order: Order, initiator: User) -> None:
    """Execute item creation actions for this order."""
    _execute_actions(order, PaymentState.paid, initiator)


def execute_revocation_actions(order: Order, initiator: User) -> None:
    """Execute item revocation actions for this order."""
    _execute_actions(order, PaymentState.canceled_after_paid, initiator)


def _execute_actions(
    order: Order, payment_state: PaymentState, initiator: User
) -> None:
    """Execute relevant actions for this order in its new payment state."""
    article_ids = {line_item.article_id for line_item in order.line_items}

    actions = _get_actions(article_ids, payment_state)
    actions_by_article_id = {action.article_id: action for action in actions}

    for line_item in order.line_items:
        action = actions_by_article_id.get(line_item.article_id)
        if action is None:
            continue

        procedure = _get_procedure(action.procedure_name, action.article_id)
        procedure(order, line_item, initiator, action.parameters)


def _get_actions(
    article_ids: set[ArticleID], payment_state: PaymentState
) -> list[Action]:
    """Return the order actions for those article IDs."""
    if not article_ids:
        return []

    db_actions = db.session.scalars(
        select(DbOrderAction)
        .filter(DbOrderAction.article_id.in_(article_ids))
        .filter_by(_payment_state=payment_state.name)
    ).all()

    return [_db_entity_to_action(db_action) for db_action in db_actions]


def _get_procedure(name: str, article_id: ArticleID) -> OrderActionType:
    """Return procedure with that name, or raise an exception if the
    name is not registered.
    """
    procedure = PROCEDURES_BY_NAME.get(name)

    if procedure is None:
        article = article_service.get_article(article_id)
        raise Exception(
            f"Unknown procedure '{name}' configured "
            f"for article number '{article.item_number}'."
        )

    return procedure