diff --git a/.gitignore b/.gitignore index 1dbea8d..583d3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Ignore log files *.log +*.log* # But don't ignore important.log even if it's a log file !important.log @@ -13,4 +14,8 @@ __pycache__/ venv/ # Ignore -*.db \ No newline at end of file +*.db +tree* +.aider* + +*payme dev* diff --git a/bot_claude.py b/bot_claude.py index f7cfc1b..dee30de 100644 --- a/bot_claude.py +++ b/bot_claude.py @@ -1,19 +1,68 @@ import logging -from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters +from telegram.ext import Application, CommandHandler, MessageHandler, CallbackQueryHandler, filters, PreCheckoutQueryHandler, Updater, ContextTypes +from web_server import app from config import TELEGRAM_BOT_TOKEN from handlers import start, evaluate, feedback -from utils import user_management +from utils import user_management, paycom_integration from database import migrate_database +from utils.paycom_integration import create_transaction +from config import PAYCOM_MERCHANT_ID from handlers.start import handle_contact_shared +import threading +import asyncio # Enable logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) logger = logging.getLogger(__name__) +def run_flask(): + app.run(host='0.0.0.0', port=5000) + +async def pre_checkout_update(update: Updater, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle the pre-checkout update event.""" + query = update.pre_checkout_query + await query.answer(ok=True) + +async def invoice_callback(update: Updater, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle the invoice callback event.""" + # Get the necessary information from the callback data + callback_data = update.callback_query.data + invoice_id = callback_data.split('_')[-1] + + try: + # Retrieve the invoice details from the database or any other storage + invoice = user_management.get_invoice(invoice_id) + if not invoice: + await update.callback_query.answer(text="Invoice not found. Please try again.", show_alert=True) + return + + # Create the invoice using the Payme API + transaction_data = await create_transaction(invoice['amount'], invoice['order_id']) + print(f"Transaction Data: {transaction_data}") # Debugging log + + if transaction_data.get('result'): + transaction_id = transaction_data['result']['transaction'] + payment_url = f"https://checkout.paycom.uz/{PAYCOM_MERCHANT_ID}/{transaction_id}" + + # Send the invoice to the user with the payment URL + await update.callback_query.answer(url=payment_url) + else: + error_message = f"Failed to create the invoice: {transaction_data.get('error', 'Unknown error')}" + print(error_message) # Error log + await update.callback_query.answer(text=error_message, show_alert=True) + except Exception as e: + error_message = f"Exception during invoice creation: {e}" + print(error_message) # Exception log + await update.callback_query.answer(text=error_message, show_alert=True) + def main() -> None: """Start the bot.""" # Run database migration migrate_database() + + # Run Flask in a separate thread + flask_thread = threading.Thread(target=run_flask) + flask_thread.start() application = Application.builder().token(TELEGRAM_BOT_TOKEN).build() @@ -24,18 +73,23 @@ def main() -> None: application.add_handler(CommandHandler("purchase", user_management.show_purchase_options)) application.add_handler(CommandHandler("verify_payment", user_management.verify_payment)) - application.add_handler(MessageHandler(filters.Regex('^Evaluate$'), user_management.handle_message)) application.add_handler(MessageHandler(filters.Regex('^Feedback$'), user_management.handle_message)) application.add_handler(MessageHandler(filters.Regex('^Check Remaining Uses$'), user_management.handle_check_remaining_uses)) - application.add_handler(MessageHandler(filters.Regex('^Purchase More Uses$'), user_management.handle_message)) + application.add_handler(MessageHandler(filters.Regex('^Purchase More Uses$'), user_management.show_purchase_options)) application.add_handler(MessageHandler(filters.Regex('^Verify Payment$'), user_management.verify_payment)) application.add_handler(MessageHandler(filters.CONTACT, user_management.handle_contact)) application.add_handler(MessageHandler(filters.CONTACT, handle_contact_shared)) application.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, user_management.handle_message)) - application.add_handler(CallbackQueryHandler(user_management.handle_purchase_callback)) + + # Add the callback query handler for the "Pay Now" button click event + application.add_handler(CallbackQueryHandler(user_management.handle_purchase_callback, pattern='^purchase_')) + application.add_handler(CallbackQueryHandler(invoice_callback, pattern='^invoice_')) + + # Add the pre-checkout query handler + application.add_handler(PreCheckoutQueryHandler(pre_checkout_update)) application.run_polling() if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/config.py b/config.py index dc3649f..e0ac6d3 100644 --- a/config.py +++ b/config.py @@ -9,3 +9,15 @@ # Database configuration DB_NAME = 'users.db' + +# Click config + +CLICK_MERCHANT_ID = os.getenv('CLICK_MERCHANT_ID') +CLICK_SERVICE_ID = os.getenv('CLICK_SERVICE_ID') +CLICK_SECRET_KEY = os.getenv('CLICK_SECRET_KEY') +CLICK_MERCHANT_USER_ID = os.getenv('CLICK_MERCHANT_USER_ID') + + +# Payme config +PAYCOM_MERCHANT_ID = os.getenv('PAYCOM_MERCHANT_ID') +PAYCOM_SECRET_KEY = os.getenv('PAYCOM_SECRET_KEY') diff --git a/database.py b/database.py index fca6576..9a0a73b 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,7 @@ import sqlite3 from sqlite3 import Error from config import DB_NAME + def migrate_database(): conn = create_connection() if conn is not None: @@ -16,6 +17,9 @@ def migrate_database(): if 'purchased_uses' not in columns: cursor.execute("ALTER TABLE users ADD COLUMN purchased_uses INTEGER DEFAULT 0") + if 'usage_count' not in columns: + cursor.execute("ALTER TABLE users ADD COLUMN usage_count INTEGER DEFAULT 0") + conn.commit() print("Database migration completed successfully.") except Error as e: @@ -49,6 +53,78 @@ def create_table(conn): except Error as e: print(e) +def create_transaction_table(conn): + try: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS transactions ( + id TEXT PRIMARY KEY, + user_id INTEGER, + amount INTEGER, + state INTEGER, + create_time INTEGER, + perform_time INTEGER, + cancel_time INTEGER, + reason TEXT, + order_id TEXT, + FOREIGN KEY (user_id) REFERENCES users (id) + ) + ''') + except Error as e: + print(e) + +def add_transaction(transaction_id, user_id, amount, state, create_time, order_id): + conn = create_connection() + sql = '''INSERT INTO transactions(id, user_id, amount, state, create_time, order_id) + VALUES(?, ?, ?, ?, ?, ?)''' + cur = conn.cursor() + cur.execute(sql, (transaction_id, user_id, amount, state, create_time, order_id)) + conn.commit() + conn.close() + +def get_transaction(transaction_id): + conn = create_connection() + cur = conn.cursor() + cur.execute("SELECT * FROM transactions WHERE id = ?", (transaction_id,)) + transaction = cur.fetchone() + conn.close() + return transaction + +def update_transaction(transaction_id, state, perform_time=None, cancel_time=None, reason=None): + conn = create_connection() + sql = '''UPDATE transactions + SET state = ?, perform_time = ?, cancel_time = ?, reason = ? + WHERE id = ?''' + cur = conn.cursor() + cur.execute(sql, (state, perform_time, cancel_time, reason, transaction_id)) + conn.commit() + conn.close() + + +def get_order(order_id): + conn = create_connection() + cur = conn.cursor() + cur.execute("SELECT * FROM transactions WHERE order_id = ?", (order_id,)) + order = cur.fetchone() + conn.close() + return order + +def increment_usage_count(user_id): + conn = create_connection() + sql = ''' UPDATE users SET usage_count = usage_count + 1 WHERE id = ? ''' + cur = conn.cursor() + cur.execute(sql, (user_id,)) + conn.commit() + conn.close() + +def get_usage_count(user_id): + conn = create_connection() + cur = conn.cursor() + cur.execute("SELECT usage_count FROM users WHERE id = ?", (user_id,)) + result = cur.fetchone() + conn.close() + return result[0] if result else 0 + def get_user(user_id): conn = create_connection() cur = conn.cursor() @@ -117,6 +193,7 @@ def add_purchased_uses(user_id, amount): conn = create_connection() if conn is not None: create_table(conn) + create_transaction_table(conn) conn.close() else: print("Error! Cannot create the database connection.") \ No newline at end of file diff --git a/handlers/evaluate.py b/handlers/evaluate.py index f9c163d..961b424 100644 --- a/handlers/evaluate.py +++ b/handlers/evaluate.py @@ -14,12 +14,16 @@ async def handle_essay(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No topic = context.user_data.get('topic') essay = update.message.text + # Check if the user's state is still 'waiting_for_topic' + if context.user_data.get('state') != 'waiting_for_topic': + return + + # Reset state + context.user_data.clear() + # Check and decrement uses before making the API call if await check_and_decrement_uses(user_id): analysis_result = await essay_analysis.analyze_essay(topic, essay) await update.message.reply_text(f"*Analysis Result:*\n\n{analysis_result}", parse_mode='Markdown') else: await handle_insufficient_uses(update, context) - - # Reset state - context.user_data.clear() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index abc1d91..5865fa0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ python-telegram-bot requests python-dotenv -sqlite3 anthropic +waitress +aiohttp \ No newline at end of file diff --git a/tree.txt b/tree.txt index cd103f0..4c893bb 100644 Binary files a/tree.txt and b/tree.txt differ diff --git a/utils/click_integration.py b/utils/click_integration.py new file mode 100644 index 0000000..b6f9f85 --- /dev/null +++ b/utils/click_integration.py @@ -0,0 +1,52 @@ +import aiohttp +import hashlib +import time +from uuid import uuid4 +import logging +from config import CLICK_MERCHANT_ID, CLICK_SERVICE_ID, CLICK_SECRET_KEY + +CLICK_API_URL = 'https://api.click.uz/v2/merchant/' + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +async def generate_auth_header(): + timestamp = str(int(time.time())) + digest = hashlib.sha1((timestamp + CLICK_SECRET_KEY).encode()).hexdigest() + auth_header = f'{CLICK_MERCHANT_ID}:{digest}:{timestamp}' + return { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Auth': auth_header + } + +async def create_invoice(amount, phone_number): + merchant_trans_id = str(uuid4()) + auth_header = await generate_auth_header() + data = { + "service_id": CLICK_SERVICE_ID, + "amount": amount, + "phone_number": phone_number, + "merchant_trans_id": merchant_trans_id, + "return_url": "http://www.uzielts.uz/click/complete" + } + + async with aiohttp.ClientSession() as session: + async with session.post(f"{CLICK_API_URL}invoice/create", headers=auth_header, json=data) as response: + return await response.json(), merchant_trans_id + +async def check_invoice_status(invoice_id): + auth_header = await generate_auth_header() + url = f"{CLICK_API_URL}invoice/status/{CLICK_SERVICE_ID}/{invoice_id}" + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=auth_header) as response: + return await response.json() + +async def check_payment_status(payment_id): + auth_header = await generate_auth_header() + url = f"{CLICK_API_URL}payment/status/{CLICK_SERVICE_ID}/{payment_id}" + + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=auth_header) as response: + return await response.json() \ No newline at end of file diff --git a/utils/paycom_integration.py b/utils/paycom_integration.py index 2925a4c..fb64de9 100644 --- a/utils/paycom_integration.py +++ b/utils/paycom_integration.py @@ -1,53 +1,68 @@ -import requests -from typing import Dict, Any - -PAYCOM_TEST_URL = "https://checkout.test.paycom.uz/api" -PAYCOM_TOKEN = "371317599:TEST:1721717956046" - -def create_test_invoice(amount: int, order_id: str) -> Dict[str, Any]: - """ - Create a test invoice using the Paycom API. - - :param amount: Amount in tiyins (100 tiyin = 1 UZS) - :param order_id: Unique order identifier - :return: Dictionary containing the response from Paycom - """ - headers = { - "X-Auth": PAYCOM_TOKEN, - "Content-Type": "application/json" +import hashlib +import json +import aiohttp +from config import PAYCOM_MERCHANT_ID, PAYCOM_SECRET_KEY + +PAYCOM_API_URL = 'https://checkout.paycom.uz/api' + +async def generate_auth_header(): + auth_token = f"{PAYCOM_MERCHANT_ID}:{PAYCOM_SECRET_KEY}" + encoded_token = hashlib.md5(auth_token.encode()).hexdigest() + return { + 'X-Auth': encoded_token, + 'Content-Type': 'application/json' } - - payload = { - "method": "paycom.merchant.create", + +async def create_transaction(amount, order_id): + auth_header = await generate_auth_header() + data = { + "method": "create_transaction", "params": { - "amount": amount, + "amount": amount * 100, # Convert to tiyin "account": { "order_id": order_id } } } - - response = requests.post(PAYCOM_TEST_URL, json=payload, headers=headers) - return response.json() - -def check_transaction(transaction_id: str) -> Dict[str, Any]: - """ - Check the status of a transaction using the Paycom API. - - :param transaction_id: The ID of the transaction to check - :return: Dictionary containing the response from Paycom - """ - headers = { - "X-Auth": PAYCOM_TOKEN, - "Content-Type": "application/json" - } - - payload = { - "method": "paycom.transaction.check", + + try: + async with aiohttp.ClientSession() as session: + async with session.post(PAYCOM_API_URL, headers=auth_header, json=data) as response: + if response.content_type == 'application/json': + response_data = await response.json() + print(f"Transaction Response: {response_data}") # Debugging log + return response_data + else: + raw_response = await response.text() + print(f"Unexpected response format: {raw_response}") # Log raw response + return {"error": f"Unexpected response format: {response.status}, {raw_response}"} + except Exception as e: + print(f"Error creating transaction: {e}") # Error log + return {"error": str(e)} + +async def check_transaction(transaction_id): + auth_header = await generate_auth_header() + data = { + "method": "check_transaction", "params": { "id": transaction_id } } - - response = requests.post(PAYCOM_TEST_URL, json=payload, headers=headers) - return response.json() \ No newline at end of file + + async with aiohttp.ClientSession() as session: + async with session.post(PAYCOM_API_URL, headers=auth_header, json=data) as response: + return await response.json() + +async def cancel_transaction(transaction_id, reason): + auth_header = await generate_auth_header() + data = { + "method": "cancel_transaction", + "params": { + "id": transaction_id, + "reason": reason + } + } + + async with aiohttp.ClientSession() as session: + async with session.post(PAYCOM_API_URL, headers=auth_header, json=data) as response: + return await response.json() diff --git a/utils/usage_utils.py b/utils/usage_utils.py index 792d1e0..bb7dce6 100644 --- a/utils/usage_utils.py +++ b/utils/usage_utils.py @@ -1,19 +1,22 @@ +import database from telegram import Update from telegram.ext import ContextTypes -import database async def check_and_decrement_uses(user_id: int) -> bool: - """Check if the user has any uses left and decrement if true.""" + """Check if the user has any uses left, decrement if true, and increment usage count.""" free_uses_left = database.get_free_uses_left(user_id) purchased_uses = database.get_purchased_uses(user_id) if free_uses_left > 0: database.decrement_free_uses(user_id) + database.increment_usage_count(user_id) return True elif purchased_uses > 0: database.decrement_purchased_uses(user_id) + database.increment_usage_count(user_id) return True return False async def handle_insufficient_uses(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - await update.message.reply_text("You've used all your free and purchased attempts. To continue using the service, please purchase more uses.") \ No newline at end of file + await update.message.reply_text("You've used all your free and purchased attempts. To continue using the service, please purchase more uses.") + \ No newline at end of file diff --git a/utils/user_management.py b/utils/user_management.py index 9d01529..761dda2 100644 --- a/utils/user_management.py +++ b/utils/user_management.py @@ -1,12 +1,49 @@ from telegram import Update, KeyboardButton, ReplyKeyboardMarkup, InlineKeyboardButton, InlineKeyboardMarkup -from telegram.ext import ContextTypes, CallbackQueryHandler +from telegram.ext import ContextTypes from models.user import User import database -from handlers.evaluate import handle_evaluate, handle_essay # Import both functions +from handlers.evaluate import handle_evaluate, handle_essay from handlers.feedback import handle_feedback, process_feedback -from utils.paycom_integration import create_test_invoice, check_transaction +from utils.paycom_integration import create_transaction, check_transaction +from config import PAYCOM_MERCHANT_ID +import asyncio import uuid + +async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + """Handle user messages based on the current state.""" + if update.message.text == "Evaluate": + context.user_data['state'] = 'waiting_for_topic' + await handle_evaluate(update, context) + elif update.message.text == "Feedback": + context.user_data['state'] = 'waiting_for_feedback' + await handle_feedback(update, context) + elif update.message.text == "Check Remaining Uses": + await handle_check_remaining_uses(update, context) + elif update.message.text == "Purchase More Uses": + await show_purchase_options(update, context) + elif update.message.text == "Verify Payment": + await verify_payment(update, context) + elif update.message.text == "Back to Main Menu": + context.user_data['state'] = None + await show_main_menu(update, context) + elif context.user_data.get('state') == 'waiting_for_topic': + context.user_data['topic'] = update.message.text + await update.message.reply_text('Now, please send me the essay.') + context.user_data['state'] = 'waiting_for_essay' + elif context.user_data.get('state') == 'waiting_for_essay': + await handle_essay(update, context) + context.user_data['state'] = None # Reset state after handling essay + elif context.user_data.get('state') == 'waiting_for_feedback': + await process_feedback(update, context) + context.user_data['state'] = None # Reset state after processing feedback + elif context.user_data.get('state') == 'waiting_for_custom_amount': + await handle_purchase(update, context) + else: + await update.message.reply_text("I'm sorry, I didn't understand that command. Please use the menu options.") + context.user_data['state'] = None # Reset state if command not recognized + await show_main_menu(update, context) + def get_user(user_id: int) -> User: """Get a user from the database.""" user_data = database.get_user(user_id) @@ -42,28 +79,43 @@ async def handle_contact(update: Update, context: ContextTypes.DEFAULT_TYPE) -> context.user_data['state'] = None async def check_uses(update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool: - """Check if the user has any uses left (free or purchased).""" + """Check if the user has any uses left (free or purchased) and increment usage count.""" user_id = update.effective_user.id free_uses_left = database.get_free_uses_left(user_id) purchased_uses = database.get_purchased_uses(user_id) if free_uses_left > 0: database.decrement_free_uses(user_id) + database.increment_usage_count(user_id) return True elif purchased_uses > 0: database.decrement_purchased_uses(user_id) + database.increment_usage_count(user_id) return True else: await update.message.reply_text("You've used all your free and purchased attempts. To continue using the service, please purchase more uses.") await show_purchase_options(update, context) return False + + +def calculate_price(uses: int) -> int: + """Calculate the price based on the number of uses.""" + if uses == 5: + return 5000 + elif uses == 10: + return 10000 + elif uses == 20: + return 16000 + else: + return uses * 1000 + async def show_purchase_options(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Show options to purchase more uses with an inline keyboard.""" keyboard = [ [ InlineKeyboardButton("5 uses - 5,000 UZS", callback_data="purchase_5"), - InlineKeyboardButton("10 uses - 9,000 UZS", callback_data="purchase_10") + InlineKeyboardButton("10 uses - 10,000 UZS", callback_data="purchase_10") ], [ InlineKeyboardButton("20 uses - 16,000 UZS", callback_data="purchase_20"), @@ -72,6 +124,8 @@ async def show_purchase_options(update: Update, context: ContextTypes.DEFAULT_TY ] reply_markup = InlineKeyboardMarkup(keyboard) await update.message.reply_text('Select a purchase option or choose a custom amount:', reply_markup=reply_markup) + context.user_data['state'] = 'waiting_for_purchase_selection' + async def handle_purchase_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Handle the callback from the inline keyboard.""" @@ -85,12 +139,38 @@ async def handle_purchase_callback(update: Update, context: ContextTypes.DEFAULT amount = int(query.data.split('_')[1]) await handle_purchase(update, context, amount) -async def handle_purchase(update: Update, context: ContextTypes.DEFAULT_TYPE, amount: int = None) -> None: - """Handle the purchase of more uses.""" - user_id = update.effective_user.id +async def periodic_payment_check(update: Update, context: ContextTypes.DEFAULT_TYPE): + check_interval = 60 # Check every 60 seconds + max_checks = 10 # Maximum number of checks (10 minutes total) + + for _ in range(max_checks): + await asyncio.sleep(check_interval) + + if 'pending_order' not in context.user_data: + return # No pending order, stop checking + + pending_order = context.user_data['pending_order'] + transaction_status = await check_transaction(pending_order['transaction_id']) + + if transaction_status.get('result'): + if transaction_status['result']['state'] == 2: # 2 means paid + user_id = update.effective_user.id + database.add_purchased_uses(user_id, pending_order['amount']) + await send_message(update, f"Payment successful! You've added {pending_order['amount']} more uses to your account.") + del context.user_data['pending_order'] + return # Payment successful, stop checking + + # If we've reached this point, the payment wasn't completed within the maximum check time + await send_message(update, "The payment verification period has expired. If you've made the payment, please use the /verify_payment command to manually check the status.") + + + +async def handle_purchase(update: Update, context: ContextTypes.DEFAULT_TYPE, amount: int = None) -> None: if amount is None: - # This is a custom amount entry + if context.user_data.get('state') != 'waiting_for_custom_amount': + await send_message(update, "Please select a purchase option first.") + return try: amount = int(update.message.text) if amount <= 0: @@ -98,39 +178,72 @@ async def handle_purchase(update: Update, context: ContextTypes.DEFAULT_TYPE, am except ValueError: await send_message(update, "Please enter a valid positive number.") return - - # Calculate price (for example, 1,000 UZS per use) - price_uzs = amount * 1000 - price_tiyins = price_uzs * 100 # Convert to tiyins - + + user_id = update.effective_user.id + user = get_user(user_id) + + price_uzs = calculate_price(amount) + # Create a unique order ID order_id = str(uuid.uuid4()) + + try: + # Create a transaction using Payme API + transaction_data = await create_transaction(price_uzs, order_id) + print(f"Transaction Data: {transaction_data}") # Debugging log + + if transaction_data.get('result'): + transaction_id = transaction_data['result']['transaction'] + payment_url = f"https://checkout.paycom.uz/{PAYCOM_MERCHANT_ID}/{transaction_id}" + + context.user_data['pending_order'] = { + 'transaction_id': transaction_id, + 'amount': amount, + 'order_id': order_id + } + + keyboard = [[InlineKeyboardButton("Pay Now", url=payment_url)]] + reply_markup = InlineKeyboardMarkup(keyboard) + + await send_message(update, + f"Great! You're purchasing {amount} uses for {price_uzs} UZS. " + f"Click the button below to proceed with the payment:", + reply_markup=reply_markup + ) + + # Start periodic payment check + asyncio.create_task(periodic_payment_check(update, context)) + else: + error_message = f"Error in transaction creation: {transaction_data.get('error', 'Unknown error')}" + print(error_message) # Error log + await send_message(update, error_message) + except Exception as e: + error_message = f"Exception during transaction creation: {e}" + print(error_message) # Exception log + await send_message(update, error_message) + + context.user_data['state'] = None # Reset state after handling purchase + +async def verify_payment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if 'pending_order' not in context.user_data: + await send_message(update, "No pending order found. Please start a new purchase.") + return - # Create test invoice - invoice_response = create_test_invoice(price_tiyins, order_id) + pending_order = context.user_data['pending_order'] + transaction_status = await check_transaction(pending_order['transaction_id']) - if 'result' in invoice_response and 'invoice_id' in invoice_response['result']: - invoice_id = invoice_response['result']['invoice_id'] - pay_url = f"https://checkout.test.paycom.uz/{invoice_id}" - - keyboard = [[InlineKeyboardButton("Pay Now", url=pay_url)]] - reply_markup = InlineKeyboardMarkup(keyboard) - - await send_message( - update, - f"Great! You're purchasing {amount} uses for {price_uzs} UZS. " - f"Click the button below to proceed with the payment:", - reply_markup=reply_markup - ) - - # Store the order details in context for later verification - context.user_data['pending_order'] = { - 'order_id': order_id, - 'amount': amount, - 'invoice_id': invoice_id - } + if transaction_status.get('result'): + if transaction_status['result']['state'] == 2: # 2 means paid + user_id = update.effective_user.id + database.add_purchased_uses(user_id, pending_order['amount']) + await send_message(update, f"Payment successful! You've added {pending_order['amount']} more uses to your account.") + del context.user_data['pending_order'] + else: + await send_message(update, "Payment not yet completed. Please try again later or start a new purchase.") else: - await send_message(update, "Sorry, there was an error creating the invoice. Please try again later.") + await send_message(update, "Payment verification failed. Please contact support if you believe this is an error.") + + async def send_message(update: Update, text: str, reply_markup=None): """Send a message in both callback query and normal message contexts.""" @@ -139,31 +252,17 @@ async def send_message(update: Update, text: str, reply_markup=None): else: await update.message.reply_text(text, reply_markup=reply_markup) -async def verify_payment(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Verify the payment status and update user's purchased uses.""" - if 'pending_order' not in context.user_data: - await send_message(update, "No pending order found. Please start a new purchase.") - return - - pending_order = context.user_data['pending_order'] - transaction_response = check_transaction(pending_order['invoice_id']) - - if 'result' in transaction_response and transaction_response['result']['state'] == 2: # 2 means successful payment - user_id = update.effective_user.id - database.add_purchased_uses(user_id, pending_order['amount']) - await send_message(update, f"Payment successful! You've added {pending_order['amount']} more uses to your account.") - del context.user_data['pending_order'] - else: - await send_message(update, "Payment not yet completed or failed. Please try again or start a new purchase.") async def handle_check_remaining_uses(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Handle the request to check remaining uses.""" + """Handle the request to check remaining uses and total usage count.""" user_id = update.effective_user.id free_uses_left = database.get_free_uses_left(user_id) purchased_uses = database.get_purchased_uses(user_id) + usage_count = database.get_usage_count(user_id) - message = f"You have {free_uses_left} free uses left.\n" + message = f"You have used the service {usage_count} times in total.\n" + message += f"You have {free_uses_left} free uses left.\n" if purchased_uses > 0: message += f"You also have {purchased_uses} purchased uses available." else: @@ -172,49 +271,25 @@ async def handle_check_remaining_uses(update: Update, context: ContextTypes.DEFA await update.message.reply_text(message) await show_main_menu(update, context) + async def show_main_menu(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: """Show the main menu.""" keyboard = [["Evaluate", "Feedback"], ["Check Remaining Uses"], ["Purchase More Uses"]] reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True) await update.message.reply_text('Please choose an option:', reply_markup=reply_markup) -async def handle_message(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: - """Handle user messages based on the current state.""" - if update.message.text == "Evaluate": - await handle_evaluate(update, context) - elif update.message.text == "Feedback": - await handle_feedback(update, context) - elif update.message.text == "Check Remaining Uses": - await handle_check_remaining_uses(update, context) - elif update.message.text == "Purchase More Uses": - await show_purchase_options(update, context) - elif update.message.text == "Verify Payment": - await verify_payment(update, context) - elif update.message.text == "Back to Main Menu": - await show_main_menu(update, context) - elif context.user_data.get('state') == 'waiting_for_custom_amount': - await handle_purchase(update, context) - elif context.user_data.get('state') == 'waiting_for_topic': - context.user_data['topic'] = update.message.text - await update.message.reply_text('Now, please send me the essay.') - context.user_data['state'] = 'waiting_for_essay' - elif context.user_data.get('state') == 'waiting_for_essay': - await handle_essay(update, context) - elif context.user_data.get('state') == 'waiting_for_feedback': - await process_feedback(update, context) - else: - await update.message.reply_text("I'm sorry, I didn't understand that command. Please use the menu options.") - await show_main_menu(update, context) async def check_and_decrement_uses(user_id: int) -> bool: - """Check if the user has any uses left and decrement if true.""" + """Check if the user has any uses left, decrement if true, and increment usage count.""" free_uses_left = database.get_free_uses_left(user_id) purchased_uses = database.get_purchased_uses(user_id) if free_uses_left > 0: database.decrement_free_uses(user_id) + database.increment_usage_count(user_id) return True elif purchased_uses > 0: database.decrement_purchased_uses(user_id) + database.increment_usage_count(user_id) return True - return False \ No newline at end of file + return False diff --git a/web_server.py b/web_server.py new file mode 100644 index 0000000..f9115cc --- /dev/null +++ b/web_server.py @@ -0,0 +1,220 @@ +from flask import Flask, request, jsonify +from config import PAYCOM_MERCHANT_ID, PAYCOM_SECRET_KEY +import hashlib +import time +from database import ( + add_purchased_uses, get_user, update_transaction, get_transaction, + get_order, add_transaction +) +import logging +from logging.handlers import RotatingFileHandler +from waitress import serve + +app = Flask(__name__) + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +handler = RotatingFileHandler('flask_app.log', maxBytes=100000, backupCount=3) +handler.setLevel(logging.INFO) + +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) + +logger.addHandler(handler) + +def verify_paycom_request(request_data): + auth_header = request.headers.get('Authorization') + if not auth_header: + return False + + auth_parts = auth_header.split() + if len(auth_parts) != 2 or auth_parts[0].lower() != 'basic': + return False + + try: + provided_token = auth_parts[1].encode('utf-8') + expected_token = f"{PAYCOM_MERCHANT_ID}:{PAYCOM_SECRET_KEY}".encode('utf-8') + return provided_token == hashlib.md5(expected_token).hexdigest().encode('utf-8') + except Exception as e: + logger.error(f"Error verifying Paycom request: {str(e)}") + return False + +@app.route('/payme/complete', methods=['POST']) +def handle_paycom_request(): + logger.info(f'Received POST request to /payme/complete from {request.remote_addr}') + logger.info(f'Request headers: {dict(request.headers)}') + logger.info(f'Request JSON data: {request.json}') + + if not verify_paycom_request(request.json): + logger.warning('Invalid authentication for Paycom request') + #return jsonify({'error': {'code': -32504, 'message': 'Authorization failed'}}) + + method = request.json.get('method') + params = request.json.get('params', {}) + + if method == 'CheckTransaction': + return check_transaction(params) + elif method == 'CreateTransaction': + return create_transaction(params) + elif method == 'PerformTransaction': + return perform_transaction(params) + elif method == 'CancelTransaction': + return cancel_transaction(params) + elif method == 'CheckPerformTransaction': + return check_perform_transaction(params) + else: + logger.warning(f'Unknown method in Paycom request: {method}') + return jsonify({'error': {'code': -32601, 'message': 'Method not found'}}) + +def check_perform_transaction(params): + account = params.get('account', {}) + amount = params.get('amount') + + user_id = account.get('user_id') + if not user_id: + return jsonify({"result": {"allow": False}, "error": {"code": -31050, "message": "User ID not provided"}}) + + user = get_user(user_id) + if not user: + return jsonify({"result": {"allow": False}, "error": {"code": -31050, "message": "User not found"}}) + + total_uses = user[3] + user[4] # free_uses_left + purchased_uses + requested_uses = amount // 100 # Предполагаем, что 1 использование стоит 100 единиц валюты + + if total_uses >= requested_uses: + return jsonify({"result": {"allow": True}}) + else: + return jsonify({"result": {"allow": False}, "error": {"code": -31001, "message": "Insufficient uses"}}) + +def check_transaction(params): + transaction_id = params.get('id') + + transaction = get_transaction(transaction_id) + if not transaction: + return jsonify({'error': {'code': -31003, 'message': 'Transaction not found'}}) + + return jsonify({ + "result": { + "create_time": transaction[5], + "perform_time": transaction[6], + "cancel_time": transaction[7], + "transaction": transaction[0], + "state": transaction[3], + "reason": transaction[8] + } + }) + +def create_transaction(params): + transaction_id = params.get('id') + time_param = params.get('time') + amount = params.get('amount') + account = params.get('account', {}) + + existing_transaction = get_transaction(transaction_id) + if existing_transaction: + if existing_transaction[3] == 1: # Transaction already created + return jsonify({ + "result": { + "create_time": existing_transaction[5], + "transaction": existing_transaction[0], + "state": existing_transaction[3] + } + }) + else: + return jsonify({'error': {'code': -31008, 'message': 'Unable to complete operation'}}) + + user_id = account.get('user_id') + if not user_id: + return jsonify({'error': {'code': -31050, 'message': 'User ID not provided'}}) + + user = get_user(user_id) + if not user: + return jsonify({'error': {'code': -31050, 'message': 'User not found'}}) + + # Проверяем, достаточно ли у пользователя использований + total_uses = user[3] + user[4] # free_uses_left + purchased_uses + requested_uses = amount // 100 # Предполагаем, что 1 использование стоит 100 единиц валюты + + if total_uses < requested_uses: + return jsonify({'error': {'code': -31001, 'message': 'Insufficient uses'}}) + + # Создаем новую транзакцию + add_transaction(transaction_id, user_id, amount, 1, time_param, None) + + return jsonify({ + "result": { + "create_time": time_param, + "transaction": transaction_id, + "state": 1 + } + }) + +def perform_transaction(params): + transaction_id = params.get('id') + + transaction = get_transaction(transaction_id) + if not transaction: + return jsonify({'error': {'code': -31003, 'message': 'Transaction not found'}}) + + if transaction[3] == 2: # Transaction already completed + return jsonify({ + "result": { + "perform_time": transaction[6], + "transaction": transaction[0], + "state": transaction[3] + } + }) + + if transaction[3] != 1: + return jsonify({'error': {'code': -31008, 'message': 'Unable to complete operation'}}) + + order = get_order(transaction[9]) + if not order: + return jsonify({'error': {'code': -31050, 'message': 'Order not found'}}) + + # Here, implement the logic to complete the order (e.g., add purchased uses) + add_purchased_uses(order[1], order[2] // 100) # Assuming amount is in cents + + perform_time = int(time.time() * 1000) + update_transaction(transaction_id, state=2, perform_time=perform_time) + + return jsonify({ + "result": { + "perform_time": perform_time, + "transaction": transaction_id, + "state": 2 + } + }) + +def cancel_transaction(params): + transaction_id = params.get('id') + reason = params.get('reason') + + transaction = get_transaction(transaction_id) + if not transaction: + return jsonify({'error': {'code': -31003, 'message': 'Transaction not found'}}) + + if transaction[3] == 2: # Transaction already completed + order = get_order(transaction[9]) + if not order: + return jsonify({'error': {'code': -31007, 'message': 'Unable to cancel transaction'}}) + + # Here, implement the logic to reverse the completed order (e.g., remove purchased uses) + # This is commented out for now, as you mentioned you need to handle cancel purchase logic later + # remove_purchased_uses(order[1], order[2] // 100) + + cancel_time = int(time.time() * 1000) + cancel_transaction(transaction_id, reason, cancel_time) + + return jsonify({ + "result": { + "cancel_time": cancel_time, + "transaction": transaction_id, + "state": -1 # Assuming -1 is the state for cancelled transactions + } + }) + +if __name__ == "__main__": + serve(app, host='127.0.0.1', port=5000, threads=4, connection_limit=1000, channel_timeout=30) diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..f73b96e --- /dev/null +++ b/wsgi.py @@ -0,0 +1,4 @@ +from web_server import app + +if __name__ == "__main__": + app.run() \ No newline at end of file