diff --git a/database.py b/database.py index 061a409..e0ee891 100644 --- a/database.py +++ b/database.py @@ -57,6 +57,10 @@ class User(TableDeclarativeBase): else: return self.first_name + def identifiable_str(self): + """Describe the user in the best way possible, ensuring a way back to the database record exists.""" + return f"user_{self.user_id} ({str(self)})" + def mention(self): """Mention the user in the best way possible given the available data.""" if self.username is not None: @@ -107,18 +111,11 @@ class Product(TableDeclarativeBase): cart = strings.in_cart_format_string.format(quantity=cart_qty) else: cart = '' - return strings.order_format_string.format(name=escape(self.name), - description=escape(self.description), - stock=stock, - price=str(Price(self.price)), - cart=cart) - elif style == "image": - print("WARNING: image text is obsolete and shouldn't be used anymore") - 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 ''}" + return strings.product_format_string.format(name=escape(self.name), + description=escape(self.description), + stock=stock, + price=str(Price(self.price)), + cart=cart) else: raise ValueError("style is not an accepted value") @@ -136,7 +133,7 @@ class Product(TableDeclarativeBase): r = requests.post(f"https://api.telegram.org/bot{configloader.config['Telegram']['token']}/sendPhoto", files={"photo": self.image}, params={"chat_id": chat_id, - "caption": self.text(style="image"), + "caption": self.text(), "parse_mode": "HTML"}) return r.json() @@ -186,12 +183,13 @@ class Transaction(TableDeclarativeBase): __table_args__ = (UniqueConstraint("provider", "provider_charge_id"),) def __str__(self): - """Return the correctly formatted transaction value""" - # Add a plus symbol if the value is positive - string = "+" if self.value > 0 else "" - # Add the correctly formatted value - string += strings.currency_format_string.format(symbol=strings.currency_symbol, value=self.value) - # Return the created string + string = f"T{self.transaction_id} | {str(self.user)} | {Price(self.value)}" + if self.refunded: + string += f" | {strings.emoji_refunded}" + if self.provider: + string += f" | {self.provider}" + if self.notes: + string += f" | {self.notes}" return string def __repr__(self): @@ -207,7 +205,7 @@ class Admin(TableDeclarativeBase): # Permissions edit_products = Column(Boolean, default=True) receive_orders = Column(Boolean, default=True) - view_transactions = Column(Boolean, default=True) + create_transactions = Column(Boolean, default=True) # Live mode enabled live_mode = Column(Boolean, default=False) diff --git a/strings.py b/strings.py index 2770a22..1b5c8a7 100644 --- a/strings.py +++ b/strings.py @@ -57,6 +57,9 @@ conversation_admin_select_product = "✏️ Che prodotto vuoi modificare?" # Conversation: select a product to delete conversation_admin_select_product_to_delete = "❌ Che prodotto vuoi eliminare?" +# Conversation: select a user to edit +conversation_admin_select_user = "✏️ Che utente vuoi modificare?" + # Conversation: add extra notes to the order conversation_extra_notes = "Che messaggio vuoi lasciare insieme al tuo ordine?" @@ -105,6 +108,9 @@ menu_orders = "📦 Ordini" # Menu: transactions menu_transactions = "💳 Transazioni" +# Menu: edit credit +menu_edit_credit = "💰 Crea transazione" + # Admin menu: go to user mode menu_user_mode = "👤 Passa alla modalità utente" @@ -142,7 +148,7 @@ menu_add_to_cart = "➕ Aggiungi" menu_remove_from_cart = "➖ Rimuovi" # Emoji: unprocessed order -emoji_not_processed = "*️⃣ " +emoji_not_processed = "*️⃣" # Emoji: completed order emoji_completed = "✅" @@ -171,6 +177,14 @@ ask_order_notes = "Vuoi lasciare un messaggio insieme all'ordine?\n" \ ask_refund_reason = "Allega una motivazione a questo rimborso.\n" \ "Sarà visibile al cliente." +# Edit credit: notes? +ask_transaction_notes = "Allega una nota a questa transazione.\n" \ + "Sarà visibile al cliente in seguito all'accredito / addebito e nel registro delle transazioni." + +# Edit credit: amount? +ask_credit = "Di quanto vuoi modificare il credito dell'utente?\n" \ + "(Se vuoi addebitargli soldi, aggiungi un - davanti al numero.)" + # Thread has started downloading an image and might be unresponsive downloading_image = "Sto scaricando la tua foto!\n" \ "Potrei metterci un po'... Abbi pazienza!\n" \ @@ -211,6 +225,10 @@ notification_order_completed = "Un tuo ordine è stato completato!\n" \ notification_order_refunded = "Un tuo ordine è stato rimborsato!\n" \ "{order}" +# Notification: a manual transaction was applied +notification_transaction_created = "E' stata applicata una nuova transazione al tuo portafoglio:\n" \ + "{transaction}" + # Refund reason refund_reason = "Motivazione del rimborso:\n" \ "{reason}" @@ -236,6 +254,9 @@ success_order_completed = "✅ Hai segnato l'ordine #{order_id} come completato. # Success: order was refunded successfully success_order_refunded = "✴️ L'ordine #{order_id} è stato rimborsato con successo." +# Success: transaction was created successfully +success_transaction_created = "✅ La transazione è stata creata con successo!" + # Error: message received not in a private chat error_nonprivate_chat = "⚠️ Questo bot funziona solo in chat private." @@ -267,3 +288,6 @@ error_order_already_cleared = "⚠️ Questo ordine è già stato processato." # Error: no orders have been placed, so none can be shown error_no_orders = "⚠️ Non hai ancora piazzato ordini, quindi non c'è niente da visualizzare!" + +# Error: selected user does not exist +error_user_does_not_exist = "⚠️ L'utente selezionato non esiste." \ No newline at end of file diff --git a/worker.py b/worker.py index f9f3e32..e50d8cc 100644 --- a/worker.py +++ b/worker.py @@ -273,7 +273,7 @@ class ChatWorker(threading.Thread): else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=message['result']['message_id'], - caption=product.text(style="image"), + caption=product.text(), parse_mode="HTML", reply_markup=inline_keyboard) # Create the keyboard with the cancel button @@ -317,8 +317,7 @@ class ChatWorker(threading.Thread): else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(style="image", - cart_qty=cart[callback.message.message_id][1]), + caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) # Create the cart summary @@ -364,8 +363,7 @@ class ChatWorker(threading.Thread): else: self.bot.edit_message_caption(chat_id=self.chat.id, message_id=callback.message.message_id, - caption=product.text(style="image", - cart_qty=cart[callback.message.message_id][1]), + caption=product.text(cart_qty=cart[callback.message.message_id][1]), parse_mode="HTML", reply_markup=product_inline_keyboard) # Create the cart summary @@ -444,7 +442,7 @@ class ChatWorker(threading.Thread): orders = self.session.query(db.Order)\ .filter(db.Order.user == self.user)\ .order_by(db.Order.creation_date.desc())\ - .limit(5)\ + .limit(20)\ .all() # Ensure there is at least one order to display if len(orders) == 0: @@ -601,14 +599,14 @@ class ChatWorker(threading.Thread): keyboard.append([strings.menu_products]) if self.admin.receive_orders: keyboard.append([strings.menu_orders]) - if self.admin.view_transactions: - keyboard.append([strings.menu_transactions]) + if self.admin.create_transactions: + keyboard.append([strings.menu_edit_credit]) # Send the previously created keyboard to the user (ensuring it can be clicked only 1 time) self.bot.send_message(self.chat.id, strings.conversation_open_admin_menu, reply_markup=telegram.ReplyKeyboardMarkup(keyboard, one_time_keyboard=True)) # Wait for a reply from the user selection = self.__wait_for_specific_message([strings.menu_products, strings.menu_orders, - strings.menu_user_mode]) + strings.menu_user_mode, strings.menu_edit_credit]) # If the user has selected the Products option... if selection == strings.menu_products: # Open the products menu @@ -617,6 +615,10 @@ class ChatWorker(threading.Thread): elif selection == strings.menu_orders: # Open the orders menu self.__orders_menu() + # If the user has selected the Transactions option... + elif selection == strings.menu_edit_credit: + # Open the edit credit menu + self.__create_transaction() # If the user has selected the User mode option... elif selection == strings.menu_user_mode: # Start the bot in user mode @@ -862,6 +864,66 @@ class ChatWorker(threading.Thread): self.bot.send_message(order.user_id, strings.notification_order_refunded.format(order=order.get_text(self.session))) + def __create_transaction(self): + """Edit manually the credit of an user.""" + # Find all the users in the database + users = self.session.query(db.User).all() + # Create a list containing all the keyboard button strings + keyboard_buttons = [[strings.menu_cancel]] + # Add to the list all the users + for user in users: + keyboard_buttons.append([user.identifiable_str()]) + # TODO: handle more than 99 users + # Create the keyboard + keyboard = telegram.ReplyKeyboardMarkup(keyboard_buttons, one_time_keyboard=True) + # Send the keyboard + self.bot.send_message(self.chat.id, strings.conversation_admin_select_user, reply_markup=keyboard) + # Wait for a reply + reply = self.__wait_for_regex("user_([0-9]+)", cancellable=True) + # Allow the cancellation of the operation + if reply == strings.menu_cancel: + return + # Find the user in the database + user = self.session.query(db.User).filter_by(user_id=int(reply)).one_or_none() + # Ensure the user exists + if not user: + self.bot.send_message(self.chat.id, strings.error_user_does_not_exist) + # Create an inline keyboard with a single cancel button + cancel = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, + callback_data="cmd_cancel")]]) + # Request from the user the amount of money to be credited manually + self.bot.send_message(self.chat.id, strings.ask_credit, reply_markup=cancel) + # Wait for an answer + reply = self.__wait_for_regex(r"(-? ?[0-9]{1,3}(?:[.,][0-9]{1,2})?)", cancellable=True) + # Allow the cancellation of the operation + if isinstance(reply, CancelSignal): + return + # Convert the reply to a price object + price = utils.Price(reply) + # Ask the user for notes + self.bot.send_message(self.chat.id, strings.ask_transaction_notes, reply_markup=cancel) + # Wait for an answer + reply = self.__wait_for_regex(r"(.*)", cancellable=True) + # Allow the cancellation of the operation + if isinstance(reply, CancelSignal): + return + # Create a new transaction + transaction = db.Transaction(user=user, + value=int(price), + provider="Manual", + notes=reply) + self.session.add(transaction) + # Change the user credit + user.credit += int(price) + # Commit the changes + self.session.commit() + # Notify the user of the credit/debit + self.bot.send_message(user.user_id, + strings.notification_transaction_created.format(transaction=str(transaction)), + parse_mode="HTML") + # Notify the admin of the success + self.bot.send_message(self.chat.id, strings.success_transaction_created) + def __graceful_stop(self): """Handle the graceful stop of the thread.""" # Notify the user that the session has expired and remove the keyboard