diff --git a/database.py b/database.py index 3472c87..6daab40 100644 --- a/database.py +++ b/database.py @@ -7,6 +7,7 @@ import telegram import strings import requests from html import escape +from utils import Price # Create a (lazy) database engine engine = create_engine(configloader.config["Database"]["engine"]) @@ -83,15 +84,24 @@ class Product(TableDeclarativeBase): def __str__(self): return self.text() - def text(self, one_row:bool=False, cart_qty:int=None): + def text(self, style:str="full", cart_qty:int=None): """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 / (10 ** int(configloader.config['Payments']['currency_exp'])))}" - return f"{escape(self.name)}\n" \ - f"{escape(self.description)}\n" \ - f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \ - f"{strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price / (10 ** int(configloader.config['Payments']['currency_exp'])))}\n" \ - f"{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}" + if style == "short": + return f"{cart_qty}x {escape(self.name)} - {str(Price(self.price) * cart_qty)}" + elif style == "full": + return f"{escape(self.name)}\n" \ + f"{escape(self.description)}\n" \ + f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \ + f"{str(Price(self.price))}\n" \ + f"{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}" + elif style == "image": + return f"{escape(self.name)}\n" \ + f"{escape(self.description)}\n" \ + f"{strings.in_stock_format_string.format(quantity=self.stock) if self.stock is not None else ''}\n" \ + f"{strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.price / (10 ** int(configloader.config['Payments']['currency_exp'])))}\n" \ + f"{strings.in_cart_format_string.format(quantity=cart_qty) if cart_qty is not None else ''}" + else: + raise ValueError("style is not an accepted value") def __repr__(self): return f"" @@ -101,13 +111,13 @@ class Product(TableDeclarativeBase): if self.image is None: r = requests.get(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendMessage", params={"chat_id": chat_id, - "text": str(self), + "text": self.text(), "parse_mode": "HTML"}) else: r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto", files={"photo": self.image}, params={"chat_id": chat_id, - "caption": str(self), + "caption": self.text(style="image"), "parse_mode": "HTML"}) return r.json() diff --git a/strings.py b/strings.py index 7963d0a..9decc26 100644 --- a/strings.py +++ b/strings.py @@ -42,7 +42,7 @@ conversation_cart_actions = "Quando hai finito di aggiungere prodotti al carrell # Conversation: confirm the cart contents conversation_confirm_cart = "Il tuo carrello contiene questi prodotti:\n" \ - "{product_list}\n" \ + "{product_list}" \ "\n" \ "Totale: {total_cost}\n" \ "Procedere?" diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..5bc3d31 --- /dev/null +++ b/utils.py @@ -0,0 +1,80 @@ +from configloader import config +from strings import currency_format_string, currency_symbol +import typing + +class Price: + def __init__(self, value:typing.Union[int, float, str, "Price"]=0): + if isinstance(value, int): + # Keep the value as it is + self.value = int(value) + elif isinstance(value, float): + # Convert the value to minimum units + self.value = int(value * (10 ** int(config["Payments"]["currency_exp"]))) + elif isinstance(value, str): + # Remove decimal points, then cast to int + self.value = int(value.replace(".", "").replace(",", "")) + elif isinstance(value, Price): + # Copy self + self.value = value.value + + def __str__(self): + return currency_format_string.format(symbol=currency_symbol, value="{0:.2f}".format(self.value / (10 ** int(config["Payments"]["currency_exp"])))) + + def __int__(self): + return self.value + + def __float__(self): + return self.value / (10 ** int(config["Payments"]["currency_exp"])) + + def __ge__(self, other): + return self.value >= Price(other).value + + def __le__(self, other): + return self.value <= Price(other).value + + def __eq__(self, other): + return self.value == Price(other).value + + def __gt__(self, other): + return self.value > Price(other).value + + def __lt__(self, other): + return self.value < Price(other).value + + def __add__(self, other): + return Price(self.value + Price(other).value) + + def __sub__(self, other): + return Price(self.value - Price(other).value) + + def __mul__(self, other): + return Price(self.value * Price(other).value) + + def __floordiv__(self, other): + return Price(self.value // Price(other).value) + + def __radd__(self, other): + self.__add__(other) + + def __rsub__(self, other): + return Price(Price(other).value - self.value) + + def __rmul__(self, other): + self.__mul__(other) + + def __iadd__(self, other): + self.value += Price(other).value + return self + + def __isub__(self, other): + self.value -= Price(other).value + return self + + def __imul__(self, other): + self.value *= Price(other).value + return self + + def __ifloordiv__(self, other): + self.value //= Price(other).value + return self + diff --git a/worker.py b/worker.py index a563470..99ee18e 100644 --- a/worker.py +++ b/worker.py @@ -1,7 +1,6 @@ import threading import typing import uuid - import datetime import telegram import strings @@ -10,6 +9,7 @@ import sys import queue as queuem import database as db import re +from utils import Price from html import escape class StopSignal: @@ -232,7 +232,7 @@ class ChatWorker(threading.Thread): products = self.session.query(db.Product).all() # Create a dict to be used as 'cart' # The key is the message id of the product list - cart = {} # type: typing.Dict[typing.List[db.Product, int]] + cart: typing.Dict[typing.List[db.Product, int]] = {} # Initialize the products list for product in products: # If the product is not for sale, don't display it @@ -279,13 +279,16 @@ class ChatWorker(threading.Thread): text=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) - try: - self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id, - text=strings.conversation_cart_actions, reply_markup=final_inline_keyboard) - except telegram.error.BadRequest: - # Telegram returns an error if the message is not edited - pass + caption=product.text(style="image", cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) + # Create the cart summary + product_list = "" + total_cost = Price(0) + for product_id in cart: + if cart[product_id][1] > 0: + product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n" + total_cost += cart[product_id][0].price * cart[product_id][1] + self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id, + text=strings.conversation_confirm_cart.format(product_list=product_list, total_cost=str(total_cost), reply_markup=final_inline_keyboard)) # If the Remove from cart button has been pressed... elif callback.data == "cart_remove": # Get the selected product @@ -313,13 +316,18 @@ class ChatWorker(threading.Thread): text=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) - try: - self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id, - text=strings.conversation_cart_actions, reply_markup=final_inline_keyboard) - except telegram.error.BadRequest: - # Telegram returns an error if the message is not edited - pass + caption=product.text(style="image", cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) + # Create the cart summary + product_list = "" + total_cost = Price(0) + for product_id in cart: + if cart[product_id][1] > 0: + product_list += cart[product_id][0].text(style="short", cart_qty=cart[product_id][1]) + "\n" + total_cost += cart[product_id][0].price * cart[product_id][1] + self.bot.edit_message_text(chat_id=self.chat.id, message_id=final.message_id, + text=strings.conversation_confirm_cart.format(product_list=product_list, + total_cost=str(total_cost), + reply_markup=final_inline_keyboard)) # If the done button has been pressed... elif callback.data == "cart_done": # End the loop @@ -364,7 +372,6 @@ class ChatWorker(threading.Thread): # Notify the user of the order result self.bot.send_message(self.chat.id, strings.success_order_created) - def __order_status(self): raise NotImplementedError() @@ -406,10 +413,10 @@ class ChatWorker(threading.Thread): def __add_credit_cc(self): """Add money to the wallet through a credit card payment.""" # Create a keyboard to be sent later - keyboard = [[telegram.KeyboardButton(strings.currency_format_string.format(symbol=strings.currency_symbol, value="10"))], - [telegram.KeyboardButton(strings.currency_format_string.format(symbol=strings.currency_symbol, value="25"))], - [telegram.KeyboardButton(strings.currency_format_string.format(symbol=strings.currency_symbol, value="50"))], - [telegram.KeyboardButton(strings.currency_format_string.format(symbol=strings.currency_symbol, value="100"))], + keyboard = [[telegram.KeyboardButton(str(Price("10.00")))], + [telegram.KeyboardButton(str(Price("25.00")))], + [telegram.KeyboardButton(str(Price("50.00")))], + [telegram.KeyboardButton(str(Price("100.00")))], [telegram.KeyboardButton(strings.menu_cancel)]] # Boolean variable to check if the user has cancelled the action cancelled = False @@ -427,13 +434,13 @@ class ChatWorker(threading.Thread): cancelled = True continue # Convert the amount to an integer - value = int(float(selection.replace(",", ".")) * (10 ** int(configloader.config["Payments"]["currency_exp"]))) + value = Price(selection) # Ensure the amount is within the range if value > int(configloader.config["Payments"]["max_amount"]): - self.bot.send_message(self.chat.id, strings.error_payment_amount_over_max.format(max_amount=strings.currency_format_string.format(symbol=strings.currency_symbol, value=configloader.config["Payments"]["max_amount"]))) + self.bot.send_message(self.chat.id, strings.error_payment_amount_over_max.format(max_amount=Price(configloader.config["Payments"]["max_amount"]))) continue elif value < int(configloader.config["Payments"]["min_amount"]): - self.bot.send_message(self.chat.id, strings.error_payment_amount_under_min.format(min_amount=strings.currency_format_string.format(symbol=strings.currency_symbol, value=configloader.config["Payments"]["min_amount"]))) + self.bot.send_message(self.chat.id, strings.error_payment_amount_under_min.format(min_amount=Price(configloader.config["Payments"]["min_amount"]))) continue break # If the user cancelled the action... @@ -443,11 +450,11 @@ class ChatWorker(threading.Thread): # Set the invoice active invoice payload self.invoice_payload = str(uuid.uuid4()) # Create the price array - prices = [telegram.LabeledPrice(label=strings.payment_invoice_label, amount=value)] + prices = [telegram.LabeledPrice(label=strings.payment_invoice_label, amount=int(value))] # If the user has to pay a fee when using the credit card, add it to the prices list - fee_percentage = float(configloader.config["Credit Card"]["fee_percentage"]) / 100 - fee_fixed = int(configloader.config["Credit Card"]["fee_fixed"]) - total_fee = int(value * fee_percentage) + fee_fixed + fee_percentage = Price(configloader.config["Credit Card"]["fee_percentage"]) // 100 + fee_fixed = Price(configloader.config["Credit Card"]["fee_fixed"]) + total_fee = value * fee_percentage + fee_fixed if total_fee > 0: prices.append(telegram.LabeledPrice(label=strings.payment_invoice_fee_label, amount=int(total_fee))) else: @@ -459,7 +466,7 @@ class ChatWorker(threading.Thread): # The amount is valid, send the invoice self.bot.send_invoice(self.chat.id, title=strings.payment_invoice_title, - description=strings.payment_invoice_description.format(amount=strings.currency_format_string.format(symbol=strings.currency_symbol, value=value / (10 ** int(configloader.config["Payments"]["currency_exp"])))), + description=strings.payment_invoice_description.format(amount=str(value)), payload=self.invoice_payload, provider_token=configloader.config["Credit Card"]["credit_card_token"], start_parameter="tempdeeplink", # TODO: no idea on how deeplinks should work @@ -490,7 +497,7 @@ class ChatWorker(threading.Thread): transaction.payment_email = successfulpayment.order_info.email transaction.payment_phone = successfulpayment.order_info.phone_number # Add the credit to the user account - self.user.credit += successfulpayment.total_amount - total_fee + self.user.credit += successfulpayment.total_amount - int(total_fee) # Add and commit the transaction self.session.add(transaction) self.session.commit() @@ -587,7 +594,7 @@ class ChatWorker(threading.Thread): self.bot.send_message(self.chat.id, strings.ask_product_price, parse_mode="HTML") # Display the current name if you're editing an existing product if product: - self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=(strings.currency_format_string.format(symbol=strings.currency_symbol, value=(product.price / (10 ** int(configloader.config["Payments"]["currency_exp"]))))) if product.price is not None else 'Non in vendita'), parse_mode="HTML", reply_markup=cancel) + self.bot.send_message(self.chat.id, strings.edit_current_value.format(value=(str(Price(product.price)) if product.price is not None else 'Non in vendita')), parse_mode="HTML", reply_markup=cancel) # Wait for an answer price = self.__wait_for_regex(r"([0-9]{1,3}(?:[.,][0-9]{1,2})?|[Xx])", cancellable=True) # If the price is skipped @@ -596,19 +603,17 @@ class ChatWorker(threading.Thread): elif price.lower() == "x": price = None else: - price = int(float(price.replace(",", ".")) * (10 ** int(configloader.config["Payments"]["currency_exp"]))) + price = Price(price) # Ask for the product image self.bot.send_message(self.chat.id, strings.ask_product_image, reply_markup=cancel) # Wait for an answer photo_list = self.__wait_for_photo(cancellable=True) - # TODO: ask for boolean status # If a new product is being added... if not product: # Create the db record for the product - # TODO: add the boolean status product = db.Product(name=name, description=description, - price=price, + price=int(price), boolean_product=False) # Add the record to the database self.session.add(product)