diff --git a/database.py b/database.py index b778dd8..93b6264 100644 --- a/database.py +++ b/database.py @@ -148,6 +148,8 @@ class Transaction(TableDeclarativeBase): user = relationship("User") # The value of this transaction. Can be both negative and positive. value = Column(Integer, nullable=False) + # Refunded status: if True, ignore the value of this transaction when recalculating + refunded = Column(Boolean, default=False) # Extra notes on the transaction notes = Column(Text) @@ -190,8 +192,9 @@ class Admin(TableDeclarativeBase): user_id = Column(BigInteger, ForeignKey("users.user_id"), primary_key=True) user = relationship("User") # Permissions + edit_products = Column(Boolean, default=True) receive_orders = Column(Boolean, default=True) - # TODO: unfinished + view_transactions = Column(Boolean, default=True) # Extra table parameters __tablename__ = "admins" @@ -211,8 +214,12 @@ class Order(TableDeclarativeBase): user = relationship("User") # Date of creation creation_date = Column(DateTime, nullable=False) - # Date of delivery, None if the item hasn't been delivered yet + # Date of delivery delivery_date = Column(DateTime) + # Date of refund: if null, product hasn't been refunded + refund_date = Column(DateTime) + # Refund reason: if null, product hasn't been refunded + refund_reason = Column(Text) # List of items in the order items = relationship("OrderItem") # Extra details specified by the purchasing user diff --git a/strings.py b/strings.py index a64f630..b42aade 100644 --- a/strings.py +++ b/strings.py @@ -113,6 +113,15 @@ menu_done = "✅️ Fatto" # Menu: pay invoice menu_pay = "💳 Paga" +# Menu: complete +menu_complete = "✅ Completa" + +# Menu: refund +menu_refund = "✴️ Rimborsa" + +# Menu: stop +menu_stop = "🛑 Interrompi" + # Menu: add to cart menu_add_to_cart = "➕ Aggiungi" @@ -136,6 +145,10 @@ ask_product_image = "Che immagine vuoi che abbia il prodotto?" ask_order_notes = "Vuoi lasciare un messaggio insieme all'ordine?\n" \ "Sarà visibile al negoziante." +# Refund product: reason? +ask_refund_reason = "Allega una motivazione a questo rimborso.\n" \ + "Sarà visibile al cliente." + # 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" \ @@ -168,6 +181,17 @@ payment_invoice_fee_label = "Supplemento carta" notification_order_placed = "*️⃣ E' stato piazzato un nuovo ordine:\n" \ "{order}" +# Notification: order has been completed +notification_order_completed = "✅ Un tuo ordine è stato completato!\n" \ + "{order}" + +# Notification: order has been refunded +notification_order_refunded = "✴️ Un tuo ordine è stato rimborsato!\n" \ + "{order}\n" \ + "\n" \ + "Motivazione data dal negoziante:\n" \ + "{reason}" + # Info: informazioni sul bot bot_info = 'Questo bot utilizza greed,' \ ' un framework di @Steffo per i pagamenti su Telegram rilasciato sotto la' \ @@ -180,15 +204,15 @@ success_product_edited = "✅ Il prodotto è stato aggiunto/modificato con succe # Success: product has been added/edited to the database success_product_deleted = "✅ Il prodotto è stato eliminato con successo!" -# Order: set as complete -order_complete = "✅ Completa" - -# Order: set as refunded -order_refund = "✴️ Rimborsa" - # Success: order has been created success_order_created = "✅ L'ordine è stato inviato con successo!" +# Success: order was marked as completed +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." + # Error: message received not in a private chat error_nonprivate_chat = "⚠️ Questo bot funziona solo in chat private." diff --git a/worker.py b/worker.py index cd6658a..3fa3b2d 100644 --- a/worker.py +++ b/worker.py @@ -184,11 +184,15 @@ class ChatWorker(threading.Thread): # Return the photo array return update.message.photo - def __wait_for_inlinekeyboard_callback(self) -> telegram.CallbackQuery: + def __wait_for_inlinekeyboard_callback(self, cancellable:bool=True) -> typing.Union[telegram.CallbackQuery, CancelSignal]: """Continue getting updates until an inline keyboard callback is received, then return it.""" while True: # Get the next update update = self.__receive_next_update() + # Ensure the update isn't a CancelSignal + if cancellable and isinstance(update, CancelSignal): + # Return the CancelSignal + return update # Ensure the update is a CallbackQuery if update.callback_query is None: continue @@ -685,17 +689,70 @@ class ChatWorker(threading.Thread): def __orders_menu(self): """Display a live flow of orders.""" + # Create a cancel and a stop keyboard + stop_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_stop, callback_data="cmd_cancel")]]) + cancel_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_cancel, callback_data="cmd_cancel")]]) # Send a small intro message on the Live Orders mode - self.bot.send_message(self.chat.id, strings.conversation_live_orders_start) + self.bot.send_message(self.chat.id, strings.conversation_live_orders_start, reply_markup=stop_keyboard) # Create the order keyboard - order_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.order_complete)], - [telegram.InlineKeyboardButton(strings.order_refund)]]) + order_keyboard = telegram.InlineKeyboardMarkup([[telegram.InlineKeyboardButton(strings.menu_complete, callback_data="order_complete")], + [telegram.InlineKeyboardButton(strings.menu_refund, callback_data="order_refund")]]) # Display the past pending orders - orders = self.session.query(db.Order).filter(db.Order.delivery_date == None).all() + orders = self.session.query(db.Order).filter_by(delivery_date=None, refund_date=None).join(db.Transaction).join(db.User).all() + # Create a dict linking the message ids to the orders + order_dict: typing.Dict[int, db.Order] = {} # Create a message for every one of them for order in orders: # Send the created message - self.bot.send_message(self.chat.id, order.get_text(session=self.session), reply_markup=order_keyboard) + message = self.bot.send_message(self.chat.id, order.get_text(session=self.session), reply_markup=order_keyboard) + # Add the message to the order_dict + order_dict[message.message_id] = order + # Set the listening mode flag to True + self.listening_mode = True + while True: + # Wait for any message to stop the listening mode + update = self.__wait_for_inlinekeyboard_callback(cancellable=True) + # If the user pressed the stop button, exit listening mode + if isinstance(update, CancelSignal): + # Stop the listening mode + self.listening_mode = False + # If the user pressed the complete order button, complete the order + elif update.data == "order_complete": + # Find the order + order = order_dict[update.message.message_id] + # Mark the order as complete + order.completition_date = datetime.datetime.now() + # Commit the transaction + self.session.commit() + # Notify the admin of the completition + self.bot.send_message(self.chat.id, strings.success_order_completed.format(order_id=order.order_id)) + # Notify the user of the completition + self.bot.send_message(order.user_id, strings.notification_order_completed.format(order=order.get_text(self.session))) + # If the user pressed the refund order button, refund the order... + elif update.data == "order_refund": + # Find the order + order = order_dict[update.message.message_id] + # Ask for a refund reason + self.bot.send_message(self.chat.id, strings.ask_refund_reason, reply_markup=cancel_keyboard) + # Wait for a reply + reply = self.__wait_for_regex("(.*)", cancellable=True) + # If the user pressed the cancel button, cancel the refund + if isinstance(reply, CancelSignal): + continue + # Mark the order as refunded + order.refund_date = datetime.datetime.now() + # Save the refund reason + order.refund_reason = reply + # Refund the credit, reverting the old transaction + order.transaction.refunded = True + # Restore the credit to the user + order.user.credit -= order.transaction.value + # Commit the changes + self.session.commit() + # Notify the shopkeeper of the successful refund + self.bot.send_message(self.chat.id, strings.success_order_refunded.format(order_id=order.order_id)) + # Notify the user of the refund + self.bot.send_message(order.user_id, strings.notification_order_refunded.format(order=order.get_text(self.session), reason=reply)) def __graceful_stop(self):