diff --git a/database.py b/database.py index cf72499..9b8c44e 100644 --- a/database.py +++ b/database.py @@ -1,5 +1,5 @@ -from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint, CheckConstraint -from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary +from sqlalchemy import create_engine, Column, ForeignKey, UniqueConstraint +from sqlalchemy import Integer, BigInteger, String, Text, LargeBinary, DateTime from sqlalchemy.orm import sessionmaker, relationship from sqlalchemy.ext.declarative import declarative_base import configloader @@ -74,12 +74,14 @@ class Product(TableDeclarativeBase): stock = Column(Integer) # Extra table parameters - __tablename__ = "product" + __tablename__ = "products" # No __init__ is needed, the default one is sufficient - def __str__(self): + def __str__(self, one_row=False): """Return the product details formatted with Telegram HTML. The image is omitted.""" + if one_row: + return f"{escape(self.name)} - {strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price)}" return f"{escape(self.name)}\n" \ f"{escape(self.description)}\n\n" \ f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \ @@ -88,7 +90,7 @@ class Product(TableDeclarativeBase): def __repr__(self): return f"" - def send_as_message(self, chat_id: int) -> dict: + def send_as_message(self, chat_id: int) -> requests.Response: """Send a message containing the product data.""" r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto", files={"photo": self.image}, @@ -147,7 +149,7 @@ class Transaction(TableDeclarativeBase): return string def __repr__(self): - return f"" + return f"" class Admin(TableDeclarativeBase): @@ -166,6 +168,49 @@ class Admin(TableDeclarativeBase): return f"" +class Order(TableDeclarativeBase): + """An order which has been placed by an user. + It may include multiple products, available in the OrderItem table.""" + + # The unique order id + order_id = Column(Integer, primary_key=True) + # The user who placed the order + user_id = Column(BigInteger, ForeignKey("users.user_id")) + user = relationship("User") + # Date of creation + creation_date = Column(DateTime, nullable=False) + # Date of delivery, None if the item hasn't been delivered yet + delivery_date = Column(DateTime) + # List of items in the order + items = relationship("OrderItem") + # Extra details specified by the purchasing user + notes = Column(Text) + + # Extra table parameters + __tablename__ = "orders" + + def __repr__(self): + return f"" + + +class OrderItem(TableDeclarativeBase): + """A product that has been purchased as part of an order.""" + + # The unique item id + item_id = Column(Integer, primary_key=True) + # The product that is being ordered + product_id = Column(Integer, ForeignKey("products.id"), nullable=False) + product = relationship("Product") + # The order in which this item is being purchased + order_id = Column(Integer, ForeignKey("orders.order_id"), nullable=False) + + # Extra table parameters + __tablename__ = "orderitems" + + def __repr__(self): + return f"" + + # If this script is ran as main, try to create all the tables in the database if __name__ == "__main__": TableDeclarativeBase.metadata.create_all() \ No newline at end of file diff --git a/strings.py b/strings.py index 9daec4b..c17466a 100644 --- a/strings.py +++ b/strings.py @@ -31,6 +31,16 @@ conversation_payment_method = "Come vuoi aggiungere fondi al tuo portafoglio?" # Conversation: select a product to edit conversation_admin_select_product = "Che prodotto vuoi modificare?" +# Conversation: add extra notes to the order +conversation_extra_notes = "Che messaggio vuoi lasciare insieme al tuo ordine?" + +# Conversation: confirm the cart contents +conversation_confirm_cart = "Il tuo carrello contiene questi prodotti:\n" \ + "{product_list}\n" \ + "\n" \ + "Totale: {total_cost}\n" \ + "Procedere?" + # Notification: the conversation has expired conversation_expired = "🕐 Il bot non ha ricevuto messaggi per un po' di tempo, quindi ha chiuso la conversazione.\n" \ "Per riavviarne una nuova, invia il comando /start." @@ -68,6 +78,9 @@ menu_add_product = "✨ Nuovo prodotto" # Menu: cancel menu_cancel = "🔙 Annulla" +# Menu: done +menu_done = "✅️ Fatto" + # Add product: name? ask_product_name = "Come si deve chiamare il prodotto?" @@ -139,5 +152,3 @@ error_invoice_expired = "⚠️ Questo pagamento è scaduto ed è stato annullat # Error: a product with that name already exists error_duplicate_name = "️⚠ Esiste già un prodotto con questo nome." - - diff --git a/worker.py b/worker.py index 126db57..b213994 100644 --- a/worker.py +++ b/worker.py @@ -8,7 +8,7 @@ import sys import queue as queuem import database as db import re -import requests +import datetime from html import escape class StopSignal: @@ -194,7 +194,77 @@ class ChatWorker(threading.Thread): self.__bot_info() def __order_menu(self): - raise NotImplementedError() + """User menu to order products from the shop.""" + # Create a list with the requested items + order_items = [] + # Get the products list from the db + products = self.session.query(db.Product).all() + # TODO: this should be changed + # Loop exit reason + exit_reason = None + # Ask for a list of products to order + while True: + # Create a list of product names + product_names = [product.name for product in products] + # Add a Cancel button at the end of the keyboard + product_names.append(strings.menu_cancel) + # If at least 1 product has been ordered, add a Done button at the start of the keyboard + if len(order_items) > 0: + product_names.insert(0, strings.menu_done) + # Create a keyboard using the product names + keyboard = [[telegram.KeyboardButton(product_name)] for product_name in product_names] + # Wait for an answer + selection = self.__wait_for_specific_message(product_names) + # If the user selected the Cancel option... + if selection == strings.menu_cancel: + exit_reason = "Cancel" + break + # If the user selected the Done option... + elif selection == strings.menu_done: + exit_reason = "Done" + break + # If the user selected a product... + else: + # Find the selected product + product = self.session.query(db.Product).filter_by(name=selection).one() + # Add the product to the order_items list + order_items.append(product) + # Ask for extra notes + self.bot.send_message(self.chat.id, strings.conversation_extra_notes) + # Wait for an answer + notes = self.__wait_for_regex("(.+)") + # Create the confirmation message and find the total cost + total_cost = 0 + product_list_string = "" + for item in order_items: + # Add to the string and the cost + product_list_string += f"{str(item)}\n" + total_cost += item.price + # Send the confirmation message + self.bot.send_message(self.chat.id, strings.conversation_confirm_cart.format(product_list=product_list_string, total_cost=strings.currency_format_string.format(symbol=strings.currency_symbol, value=(total_cost / (10 ** int(configloader.config["Payments"]["currency_exp"])))))) + # TODO: wait for an answer + # TODO: create a new transaction + # TODO: test the code + # TODO: everything + # Create the order record and add it to the session + order = db.Order(user=self.user, + creation_date=datetime.datetime.now(), + notes=notes) + self.session.add(order) + # Commit the session so the order record gets an id + self.session.commit() + # Create the orderitems for the selected products + for item in order_items: + item_record = db.OrderItem(product=item, + order_id=order.order_id) + # Add the created item to the session + self.session.add(item_record) + # Commit the session + self.session.commit() + # Send a confirmation to the user + self.bot.send_message(self.chat.id, strings.success_order_created) + + def __order_status(self): raise NotImplementedError()