Commit f96676a0 authored by Jakob Moser's avatar Jakob Moser
Browse files

Merge branch 'frontend/add-customer-ui' into 'master'

Add customer-facing UI

See merge request moser/poolpay!16
parents 5025619a f154cffb
Loading
Loading
Loading
Loading
+22 −10
Original line number Diff line number Diff line
@@ -4,7 +4,10 @@ from sqlalchemy import create_engine
from sqlalchemy.orm import Session

from poolpay import db
from poolpay.card.Pirc522CardReader import Pirc522CardReader
from poolpay.director.Director import Director
from poolpay.model.Retrievable import Retrievable
from poolpay.ui.Play import Play
from poolpay.vault.Vault import Vault

proj_dir = Path(__file__).parent.parent.resolve()
@@ -12,24 +15,33 @@ instance_dir = proj_dir / "instance"
vault_file = proj_dir / "instance.img"
db_file = instance_dir / "poolpay.db"

# TODO Start UI (with a basic message indicating the application is locked and a password is required)
# TODO Show a message indicating the application is locked and a password is required?
password = "hunter2"  # TODO Prompt for this instead of hardcoding it into the file

vault = Vault(vault_file, instance_dir)

if not vault_file.exists():
    vault.create(password)

play = Play(
    # TODO Insert types here
    Idle=...,
    PaymentSuccess=...,
    Balance=...,
    Reversal=...,
    WaitForCard=...,
)
director = Director(play)

with vault.open(password), Pirc522CardReader() as card_reader:
    try:
    vault.open(password)

        engine = create_engine(f"sqlite:///{db_file}")
        Retrievable.metadata.create_all(engine)
        db.session = Session(engine)

    # TODO Initialize card reader (on card presented: Update UI, book transaction)
    # TODO Change UI to a "ready" state
        card_reader.on_card_presented(
            lambda reader: director.card_presented(reader.read_id())
        )
        card_reader.on_card_removed(
            lambda reader: director.card_removed()
        )
        director.start()
    finally:
        db.session.close()
        engine.dispose()
    vault.close()
+10 −0
Original line number Diff line number Diff line
@@ -27,6 +27,16 @@ class CardReader(AbstractContextManager, ABC):
        """
        ...

    @abstractmethod
    def on_card_removed(self, handle_removal: Callable[[CardReader], None]) -> None:
        """
        Register the function handle_removal so that it is called whenever a MIFARE Ultralight card was presented and removed again.

        If this is called multiple times, the CardReader may decide whether it only remembers and calls the
        last registered function, or all of them.
        """
        ...

    @abstractmethod
    def close(self) -> None:
        """
+97 −0
Original line number Diff line number Diff line
import time
from dataclasses import dataclass, field

from poolpay.model.Card import Card
from poolpay.price.constants import DRINK
from poolpay.ui.Play import Play
from poolpay.ui.scenes.Scene import Scene


@dataclass
class Director:
    """
    Connects the scenes of a play with the underlying model.

    The director knows when which scene needs to be presented and what changes need to
    be done to the model at what time.

    This class is usually called a Controller in most design patterns, but I found
    the name director funnier in this context.
    """

    play: Play
    _next_scene_on_card_presented: type[Scene] = field(init=False)

    def start(self) -> None:
        self.nothing_requested()

        # Wait for the action to happen in other threads
        while True:
            time.sleep(0.1)

    def card_presented(self, card_id: int) -> None:
        card = Card.get_only(card_id)

        if not card:
            # TODO Show error that this card is not enrolled
            return

        match self._next_scene_on_card_presented:
            case self.play.Reversal:
                self._do_reversal(card)
            case self.play.Balance:
                self._do_balance(card)
            case self.play.PaymentSuccess:
                self._do_payment(card)

    def card_removed(self) -> None:
        self.nothing_requested()

    def nothing_requested(self) -> None:
        self._next_scene_on_card_presented = self.play.PaymentSuccess
        self.play.Idle(self, price_cents=DRINK).present()

    def reversal_requested(self) -> None:
        self._next_scene_on_card_presented = self.play.Reversal
        self.play.WaitForCard(self).present()

    def balance_requested(self) -> None:
        self._next_scene_on_card_presented = self.play.Balance
        self.play.WaitForCard(self).present()

    def _do_payment(self, card: Card) -> None:
        # We just pretend that this is certainly atomic and there is no way that we will ever have a race condition here
        old_balance_cents = card.owner.balance_cents
        card.owner.pay(DRINK)
        new_balance_cents = card.owner.balance_cents

        # TODO Commit to the database

        self.play.PaymentSuccess(
            self,
            old_balance_cents=old_balance_cents,
            new_balance_cents=new_balance_cents,
            person_name=card.owner.name,
        ).present()

    def _do_reversal(self, card: Card) -> None:
        # We just pretend that this is certainly atomic and there is no way that we will ever have a race condition here
        old_balance_cents = card.owner.balance_cents
        card.owner.pay(-DRINK)
        new_balance_cents = card.owner.balance_cents

        # TODO Commit to the database

        self.play.Reversal(
            self,
            old_balance_cents=old_balance_cents,
            new_balance_cents=new_balance_cents,
            person_name=card.owner.name,
        ).present()

    def _do_balance(self, card: Card) -> None:
        self.play.Balance(
            self,
            balance_cents=card.owner.balance_cents,
            person_name=card.owner.name,
        ).present()
+0 −0

Empty file added.

+0 −0

Empty file added.

Loading