- Fix admin panel to show all pending orders (PendingPayment + PaymentReceived) - Fix currency display from USD ($) to GBP (£) throughout TeleBot - Update payment methods to use dynamic SilverPay currency list - Consolidate shipping address collection into single message - Implement cart backup/restore on payment failure - Remove unsupported XMR from TeleBot config 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1079 lines
45 KiB
C#
1079 lines
45 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
using QRCoder;
|
|
using Telegram.Bot;
|
|
using Telegram.Bot.Exceptions;
|
|
using Telegram.Bot.Types;
|
|
using Telegram.Bot.Types.ReplyMarkups;
|
|
using TeleBot.Models;
|
|
using TeleBot.Services;
|
|
using TeleBot.UI;
|
|
using LittleShop.Client.Models;
|
|
|
|
namespace TeleBot.Handlers
|
|
{
|
|
public interface ICallbackHandler
|
|
{
|
|
Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery);
|
|
}
|
|
|
|
public class CallbackHandler : ICallbackHandler
|
|
{
|
|
private readonly ISessionManager _sessionManager;
|
|
private readonly ILittleShopService _shopService;
|
|
private readonly IPrivacyService _privacyService;
|
|
private readonly IProductCarouselService _carouselService;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<CallbackHandler> _logger;
|
|
|
|
public CallbackHandler(
|
|
ISessionManager sessionManager,
|
|
ILittleShopService shopService,
|
|
IPrivacyService privacyService,
|
|
IProductCarouselService carouselService,
|
|
IConfiguration configuration,
|
|
ILogger<CallbackHandler> logger)
|
|
{
|
|
_sessionManager = sessionManager;
|
|
_shopService = shopService;
|
|
_privacyService = privacyService;
|
|
_carouselService = carouselService;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery)
|
|
{
|
|
if (callbackQuery.Message == null || callbackQuery.Data == null)
|
|
return;
|
|
|
|
var session = await _sessionManager.GetOrCreateSessionAsync(callbackQuery.From.Id);
|
|
bool callbackAnswered = false;
|
|
|
|
try
|
|
{
|
|
// Answer callback immediately to prevent timeout
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
|
|
callbackAnswered = true;
|
|
|
|
var data = callbackQuery.Data.Split(':');
|
|
var action = data[0];
|
|
|
|
switch (action)
|
|
{
|
|
case "menu":
|
|
await HandleMainMenu(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "browse":
|
|
await HandleBrowse(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "category":
|
|
await HandleCategory(bot, callbackQuery.Message, session, Guid.Parse(data[1]));
|
|
break;
|
|
|
|
case "products":
|
|
if (data.Length > 1 && data[1] == "page")
|
|
{
|
|
await HandleProductsPage(bot, callbackQuery.Message, session, data);
|
|
}
|
|
else
|
|
{
|
|
await HandleProductList(bot, callbackQuery.Message, session, data);
|
|
}
|
|
break;
|
|
|
|
case "product":
|
|
await HandleProductDetail(bot, callbackQuery.Message, session, Guid.Parse(data[1]));
|
|
break;
|
|
|
|
case "qty":
|
|
await HandleQuantityChange(bot, callbackQuery.Message, session, data);
|
|
break;
|
|
|
|
case "add":
|
|
await HandleAddToCart(bot, callbackQuery, session, data);
|
|
break;
|
|
|
|
case "quickbuy":
|
|
await HandleQuickBuy(bot, callbackQuery, session, data);
|
|
break;
|
|
|
|
case "quickbuyvar":
|
|
await HandleQuickBuyWithVariation(bot, callbackQuery, session, data);
|
|
break;
|
|
|
|
case "cart":
|
|
await HandleViewCart(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "remove":
|
|
await HandleRemoveFromCart(bot, callbackQuery, session, Guid.Parse(data[1]));
|
|
break;
|
|
|
|
case "clear_cart":
|
|
await HandleClearCart(bot, callbackQuery, session);
|
|
break;
|
|
|
|
case "checkout":
|
|
await HandleCheckout(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "confirm_order":
|
|
await HandleConfirmOrder(bot, callbackQuery.Message, session, callbackQuery.From);
|
|
break;
|
|
|
|
case "pay":
|
|
await HandlePayment(bot, callbackQuery.Message, session, data[1]);
|
|
break;
|
|
|
|
case "orders":
|
|
await HandleViewOrders(bot, callbackQuery.Message, session, callbackQuery.From);
|
|
break;
|
|
|
|
case "order":
|
|
await HandleViewOrder(bot, callbackQuery.Message, session, Guid.Parse(data[1]), callbackQuery.From);
|
|
break;
|
|
|
|
case "privacy":
|
|
await HandlePrivacySettings(bot, callbackQuery.Message, session, data.Length > 1 ? data[1] : null);
|
|
break;
|
|
|
|
case "help":
|
|
await HandleHelp(bot, callbackQuery.Message);
|
|
break;
|
|
|
|
case "support":
|
|
await HandleSupportCallback(bot, callbackQuery, session);
|
|
break;
|
|
|
|
case "refresh_conversation":
|
|
await HandleRefreshConversation(bot, callbackQuery, session);
|
|
break;
|
|
|
|
case "exit_chat":
|
|
await HandleExitChat(bot, callbackQuery, session);
|
|
break;
|
|
|
|
case "noop":
|
|
// No operation - used for display-only buttons
|
|
break;
|
|
|
|
case "cancel_support":
|
|
await HandleCancelSupport(bot, callbackQuery, session);
|
|
break;
|
|
|
|
default:
|
|
_logger.LogWarning("Unknown callback action: {Action}", action);
|
|
break;
|
|
}
|
|
|
|
await _sessionManager.UpdateSessionAsync(session);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error handling callback {Data}", callbackQuery.Data);
|
|
|
|
// Only try to answer callback if not already answered
|
|
if (!callbackAnswered)
|
|
{
|
|
try
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
"An error occurred. Please try again.",
|
|
showAlert: true
|
|
);
|
|
}
|
|
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("query is too old"))
|
|
{
|
|
// Callback already expired, ignore
|
|
_logger.LogDebug("Callback query already expired");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task HandleMainMenu(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatWelcome(true),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
session.State = SessionState.MainMenu;
|
|
}
|
|
|
|
private async Task HandleBrowse(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
var categories = await _shopService.GetCategoriesAsync();
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatCategories(categories),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CategoryMenu(categories)
|
|
);
|
|
session.State = SessionState.BrowsingCategories;
|
|
}
|
|
|
|
private async Task HandleCategory(ITelegramBotClient bot, Message message, UserSession session, Guid categoryId)
|
|
{
|
|
var categories = await _shopService.GetCategoriesAsync();
|
|
var category = categories.FirstOrDefault(c => c.Id == categoryId);
|
|
var products = await _shopService.GetProductsAsync(categoryId, 1);
|
|
|
|
// Edit the original message to show category header (bigger, more prominent)
|
|
var headerText = $"**{category?.Name ?? "Unknown Category"} PRODUCTS**\n\nBrowse individual products below:";
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
headerText,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CategoryNavigationMenu(categoryId)
|
|
);
|
|
|
|
// Send individual product bubbles
|
|
if (products.Items.Any())
|
|
{
|
|
foreach (var product in products.Items)
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
MessageFormatter.FormatSingleProduct(product),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.SingleProductMenu(product.Id)
|
|
);
|
|
}
|
|
|
|
// Send navigation buttons after all products
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
".",
|
|
replyMarkup: MenuBuilder.ProductNavigationMenu(categoryId)
|
|
);
|
|
}
|
|
else
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
"No products available in this category.",
|
|
replyMarkup: MenuBuilder.BackToCategoriesMenu()
|
|
);
|
|
}
|
|
|
|
session.State = SessionState.ViewingProducts;
|
|
}
|
|
|
|
private async Task HandleProductList(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: products:categoryId:page or products:all:page
|
|
var page = int.Parse(data[2]);
|
|
Guid? categoryId = data[1] != "all" ? Guid.Parse(data[1]) : null;
|
|
|
|
var products = await _shopService.GetProductsAsync(categoryId, page);
|
|
string? categoryName = null;
|
|
|
|
if (categoryId.HasValue)
|
|
{
|
|
var categories = await _shopService.GetCategoriesAsync();
|
|
categoryName = categories.FirstOrDefault(c => c.Id == categoryId)?.Name;
|
|
}
|
|
|
|
// Use carousel service to send products with images
|
|
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, categoryName, page);
|
|
session.State = SessionState.ViewingProducts;
|
|
}
|
|
|
|
private async Task HandleProductsPage(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: products:page:pageNumber
|
|
var page = int.Parse(data[2]);
|
|
|
|
// Get products for all categories (no specific category filter)
|
|
var products = await _shopService.GetProductsAsync(null, page);
|
|
|
|
// Use carousel service to send products with images
|
|
await _carouselService.SendProductCarouselAsync(bot, message.Chat.Id, products, "All Categories", page);
|
|
session.State = SessionState.ViewingProducts;
|
|
}
|
|
|
|
private async Task HandleProductDetail(ITelegramBotClient bot, Message message, UserSession session, Guid productId)
|
|
{
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync("", "Product not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Store current product in temp data for quantity selection
|
|
session.TempData["current_product"] = productId;
|
|
session.TempData["current_quantity"] = 1;
|
|
|
|
// Use carousel service to send product with image
|
|
await _carouselService.SendSingleProductWithImageAsync(bot, message.Chat.Id, product);
|
|
session.State = SessionState.ViewingProduct;
|
|
}
|
|
|
|
private async Task HandleQuantityChange(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: qty:productId:newQuantity
|
|
var productId = Guid.Parse(data[1]);
|
|
var quantity = int.Parse(data[2]);
|
|
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
return;
|
|
|
|
session.TempData["current_quantity"] = quantity;
|
|
|
|
await bot.EditMessageReplyMarkupAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MenuBuilder.ProductDetailMenu(product, quantity)
|
|
);
|
|
}
|
|
|
|
private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
|
{
|
|
// Format: add:productId:quantity or add:productId:quantity:multiBuyId or add:productId:quantity:multiBuyId:variant
|
|
var productId = Guid.Parse(data[1]);
|
|
var quantity = int.Parse(data[2]);
|
|
Guid? multiBuyId = data.Length > 3 && !data[3].Contains(":") ? Guid.Parse(data[3]) : null;
|
|
string? selectedVariant = data.Length > 4 ? data[4] : (data.Length > 3 && data[3].Contains(":") ? data[3] : null);
|
|
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// If product has variants but none selected, show variant selection
|
|
if (selectedVariant == null && product.Variants?.Any() == true)
|
|
{
|
|
await ShowVariantSelection(bot, callbackQuery.Message!, session, product, quantity, multiBuyId);
|
|
return;
|
|
}
|
|
|
|
// Get price based on multi-buy or base product
|
|
decimal price = product.Price;
|
|
string itemName = product.Name;
|
|
if (multiBuyId.HasValue && product.MultiBuys != null)
|
|
{
|
|
var multiBuy = product.MultiBuys.FirstOrDefault(mb => mb.Id == multiBuyId);
|
|
if (multiBuy != null)
|
|
{
|
|
price = multiBuy.Price;
|
|
itemName = $"{product.Name} ({multiBuy.Name})";
|
|
quantity = multiBuy.Quantity; // Use multi-buy quantity
|
|
}
|
|
}
|
|
|
|
// Add variant to item name if selected
|
|
if (!string.IsNullOrEmpty(selectedVariant))
|
|
{
|
|
itemName += $" - {selectedVariant}";
|
|
}
|
|
|
|
session.Cart.AddItem(productId, itemName, price, quantity, multiBuyId, selectedVariant);
|
|
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"✅ Added {quantity}x {itemName} to cart",
|
|
showAlert: false
|
|
);
|
|
|
|
// Send new cart message instead of editing
|
|
await SendNewCartMessage(bot, callbackQuery.Message!.Chat.Id, session);
|
|
}
|
|
|
|
private async Task HandleViewCart(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatCart(session.Cart),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
session.State = SessionState.ViewingCart;
|
|
}
|
|
|
|
private async Task SendNewCartMessage(ITelegramBotClient bot, long chatId, UserSession session)
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
chatId,
|
|
MessageFormatter.FormatCart(session.Cart),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
session.State = SessionState.ViewingCart;
|
|
}
|
|
|
|
private async Task ShowVariantSelection(ITelegramBotClient bot, Message message, UserSession session, Product product, int quantity, Guid? multiBuyId)
|
|
{
|
|
var text = $"**{product.Name}**\n\n";
|
|
text += "Please select a variant:\n";
|
|
|
|
var buttons = new List<InlineKeyboardButton[]>();
|
|
|
|
if (product.Variants?.Any() == true)
|
|
{
|
|
foreach (var variant in product.Variants.OrderBy(v => v.SortOrder))
|
|
{
|
|
var callbackData = multiBuyId.HasValue
|
|
? $"add:{product.Id}:{quantity}:{multiBuyId}:{variant.Name}"
|
|
: $"add:{product.Id}:{quantity}::{variant.Name}";
|
|
|
|
buttons.Add(new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData(variant.Name, callbackData)
|
|
});
|
|
}
|
|
}
|
|
|
|
buttons.Add(new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData("« Back", $"product:{product.Id}")
|
|
});
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
text,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: new InlineKeyboardMarkup(buttons)
|
|
);
|
|
}
|
|
|
|
private async Task HandleQuickBuy(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
|
{
|
|
// Format: quickbuy:productId:quantity
|
|
var productId = Guid.Parse(data[1]);
|
|
var quantity = int.Parse(data[2]);
|
|
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// If variants exist, show variant selection with quickbuy flow
|
|
if (product.Variants?.Any() == true)
|
|
{
|
|
await ShowVariantSelectionForQuickBuy(bot, callbackQuery.Message!, session, product, quantity);
|
|
return;
|
|
}
|
|
|
|
// Add to cart with base product
|
|
session.Cart.AddItem(productId, product.Name, product.Price, quantity, null, null);
|
|
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"✅ Added {quantity}x {product.Name} to cart",
|
|
showAlert: false
|
|
);
|
|
|
|
// Send cart summary in new message
|
|
await bot.SendTextMessageAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
MessageFormatter.FormatCart(session.Cart),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
|
|
// Immediately proceed to checkout
|
|
await Task.Delay(500); // Small delay for better UX
|
|
await HandleCheckout(bot, callbackQuery.Message, session);
|
|
}
|
|
|
|
private async Task ShowVariantSelectionForQuickBuy(ITelegramBotClient bot, Message message, UserSession session, Product product, int quantity)
|
|
{
|
|
var text = $"**Quick Buy: {product.Name}**\n\n";
|
|
text += "Please select a variant:\n";
|
|
var buttons = new List<InlineKeyboardButton[]>();
|
|
|
|
if (product.Variants?.Any() == true)
|
|
{
|
|
// Add buttons for each variant with quickbuy flow
|
|
foreach (var variant in product.Variants.OrderBy(v => v.SortOrder))
|
|
{
|
|
buttons.Add(new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData(variant.Name, $"quickbuyvar:{product.Id}:{quantity}:{variant.Name}")
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add back button
|
|
buttons.Add(new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData("⬅️ Back", "menu")
|
|
});
|
|
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
text + "\n\n*Select an option for quick checkout:*",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: new InlineKeyboardMarkup(buttons)
|
|
);
|
|
}
|
|
|
|
private async Task HandleQuickBuyWithVariation(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
|
{
|
|
// Format: quickbuyvar:productId:quantity:variantName
|
|
var productId = Guid.Parse(data[1]);
|
|
var quantity = int.Parse(data[2]);
|
|
var variantName = data[3];
|
|
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Add to cart with variant
|
|
var itemName = $"{product.Name} - {variantName}";
|
|
session.Cart.AddItem(productId, itemName, product.Price, quantity, null, variantName);
|
|
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"✅ Added {quantity}x {itemName} to cart",
|
|
showAlert: false
|
|
);
|
|
|
|
// Send cart summary in new message
|
|
await bot.SendTextMessageAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
MessageFormatter.FormatCart(session.Cart),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
|
|
// Immediately proceed to checkout
|
|
await Task.Delay(500); // Small delay for better UX
|
|
await HandleCheckout(bot, callbackQuery.Message, session);
|
|
}
|
|
|
|
private async Task HandleRemoveFromCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, Guid productId)
|
|
{
|
|
var item = session.Cart.Items.FirstOrDefault(i => i.ProductId == productId);
|
|
if (item != null)
|
|
{
|
|
session.Cart.RemoveItem(productId);
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"❌ Removed {item.ProductName} from cart",
|
|
showAlert: false
|
|
);
|
|
}
|
|
|
|
await HandleViewCart(bot, callbackQuery.Message!, session);
|
|
}
|
|
|
|
private async Task HandleClearCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
session.Cart.Clear();
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
"🗑️ Cart cleared",
|
|
showAlert: false
|
|
);
|
|
|
|
await HandleViewCart(bot, callbackQuery.Message!, session);
|
|
}
|
|
|
|
private async Task HandleCheckout(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
if (session.Cart.IsEmpty())
|
|
{
|
|
await bot.AnswerCallbackQueryAsync("", "Your cart is empty", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Initialize order flow
|
|
session.OrderFlow = new OrderFlowData
|
|
{
|
|
UsePGPEncryption = session.Privacy.RequirePGP
|
|
};
|
|
|
|
session.State = SessionState.CheckoutFlow;
|
|
|
|
// Send new message for checkout instead of editing
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
"📦 *Checkout - Step 1/5*\n\n" +
|
|
"Please enter your shipping name:\n\n" +
|
|
"_Reply to this message with your name_",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
}
|
|
|
|
private async Task HandleConfirmOrder(ITelegramBotClient bot, Message message, UserSession session, User telegramUser)
|
|
{
|
|
if (session.OrderFlow == null || session.Cart.IsEmpty())
|
|
{
|
|
await bot.AnswerCallbackQueryAsync("", "Invalid order state", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Create the order with customer information
|
|
var order = await _shopService.CreateOrderAsync(
|
|
session,
|
|
telegramUser.Id,
|
|
telegramUser.Username ?? "",
|
|
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
|
|
telegramUser.FirstName ?? "",
|
|
telegramUser.LastName ?? "");
|
|
|
|
if (order == null)
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"❌ Failed to create order. Please try again.",
|
|
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Backup cart in case payment fails - we'll need to restore it
|
|
var cartBackup = System.Text.Json.JsonSerializer.Serialize(session.Cart);
|
|
session.TempData["cart_backup"] = cartBackup;
|
|
|
|
// Clear cart after successful order (will restore if payment fails)
|
|
session.Cart.Clear();
|
|
|
|
// Store order ID for payment
|
|
session.TempData["current_order_id"] = order.Id;
|
|
|
|
// Show payment options - get currencies dynamically from SilverPay support + admin settings
|
|
var currencies = await _shopService.GetAvailableCurrenciesAsync();
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
$"✅ *Order Created Successfully!*\n\n" +
|
|
$"Order ID: `{order.Id}`\n" +
|
|
$"Total: ${order.TotalAmount:F2}\n\n" +
|
|
"Select your preferred payment method:",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.PaymentMethodMenu(currencies)
|
|
);
|
|
}
|
|
|
|
private async Task HandlePayment(ITelegramBotClient bot, Message message, UserSession session, string currency)
|
|
{
|
|
if (!session.TempData.TryGetValue("current_order_id", out var orderIdObj) || orderIdObj is not Guid orderId)
|
|
{
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"❌ Order not found. Please start a new order.",
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
MenuBuilder.MainMenu()
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Show processing message
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
$"🔄 Creating {currency} payment...\n\nPlease wait...",
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
|
|
try
|
|
{
|
|
var payment = await _shopService.CreatePaymentAsync(orderId, currency);
|
|
|
|
if (payment == null)
|
|
{
|
|
// Restore cart from backup since payment failed
|
|
if (session.TempData.TryGetValue("cart_backup", out var cartBackupObj) && cartBackupObj is string cartBackupJson)
|
|
{
|
|
try
|
|
{
|
|
session.Cart = System.Text.Json.JsonSerializer.Deserialize<ShoppingCart>(cartBackupJson) ?? new ShoppingCart();
|
|
session.TempData.Remove("cart_backup");
|
|
}
|
|
catch
|
|
{
|
|
// If restoration fails, cart remains empty
|
|
}
|
|
}
|
|
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
$"❌ *Payment Creation Failed*\n\n" +
|
|
$"Unable to create {currency} payment.\n" +
|
|
$"This might be due to:\n" +
|
|
$"• Payment gateway temporarily unavailable\n" +
|
|
$"• Network connectivity issues\n\n" +
|
|
$"Your cart has been restored. Please try again.",
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Payment created successfully, remove cart backup
|
|
session.TempData.Remove("cart_backup");
|
|
|
|
// Continue with display
|
|
var paymentText = MessageFormatter.FormatPayment(payment);
|
|
|
|
await DisplayPaymentInfo(bot, message, payment, paymentText);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to create payment for order {OrderId} with currency {Currency}", orderId, currency);
|
|
|
|
// Restore cart from backup since payment failed
|
|
if (session.TempData.TryGetValue("cart_backup", out var cartBackupObj) && cartBackupObj is string cartBackupJson)
|
|
{
|
|
try
|
|
{
|
|
session.Cart = System.Text.Json.JsonSerializer.Deserialize<ShoppingCart>(cartBackupJson) ?? new ShoppingCart();
|
|
session.TempData.Remove("cart_backup");
|
|
}
|
|
catch
|
|
{
|
|
// If restoration fails, cart remains empty
|
|
}
|
|
}
|
|
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
$"❌ *Payment System Error*\n\n" +
|
|
$"Sorry, there was a technical issue creating your {currency} payment.\n\n" +
|
|
$"Our payment system may be undergoing maintenance.\n" +
|
|
$"Your cart has been restored. Please try again later.",
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
MenuBuilder.CartMenu(session.Cart)
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Safely edit a message only if the content has changed
|
|
/// </summary>
|
|
private async Task SafeEditMessageAsync(ITelegramBotClient bot, ChatId chatId, int messageId, string newText,
|
|
Telegram.Bot.Types.Enums.ParseMode parseMode = Telegram.Bot.Types.Enums.ParseMode.Html,
|
|
InlineKeyboardMarkup? replyMarkup = null)
|
|
{
|
|
try
|
|
{
|
|
await bot.EditMessageTextAsync(chatId, messageId, newText, parseMode: parseMode, replyMarkup: replyMarkup);
|
|
}
|
|
catch (ApiRequestException apiEx) when (apiEx.Message.Contains("message is not modified"))
|
|
{
|
|
// Message content hasn't changed, this is fine
|
|
_logger.LogDebug("Attempted to edit message with identical content");
|
|
}
|
|
}
|
|
|
|
private async Task DisplayPaymentInfo(ITelegramBotClient bot, Message message, dynamic payment, string paymentText)
|
|
{
|
|
|
|
// Generate QR code if enabled
|
|
if (_configuration.GetValue<bool>("Features:EnableQRCodes"))
|
|
{
|
|
try
|
|
{
|
|
using var qrGenerator = new QRCodeGenerator();
|
|
var qrCodeData = qrGenerator.CreateQrCode(payment.WalletAddress, QRCodeGenerator.ECCLevel.Q);
|
|
using var qrCode = new PngByteQRCode(qrCodeData);
|
|
var qrCodeBytes = qrCode.GetGraphic(10);
|
|
|
|
using var stream = new System.IO.MemoryStream(qrCodeBytes);
|
|
await bot.SendPhotoAsync(
|
|
message.Chat.Id,
|
|
InputFile.FromStream(stream, "payment_qr.png"),
|
|
caption: paymentText,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
|
|
// Delete the original message
|
|
await bot.DeleteMessageAsync(message.Chat.Id, message.MessageId);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to generate QR code");
|
|
// Fall back to text-only
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
paymentText,
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
await SafeEditMessageAsync(
|
|
bot,
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
paymentText,
|
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
}
|
|
|
|
private async Task HandleViewOrders(ITelegramBotClient bot, Message message, UserSession session, User telegramUser)
|
|
{
|
|
// Use new customer-based order lookup
|
|
var orders = await _shopService.GetCustomerOrdersAsync(
|
|
telegramUser.Id,
|
|
telegramUser.Username ?? "",
|
|
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
|
|
telegramUser.FirstName ?? "",
|
|
telegramUser.LastName ?? ""
|
|
);
|
|
|
|
if (!orders.Any())
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"📦 *Your Orders*\n\nYou have no orders yet.",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
$"📦 *Your Orders*\n\nFound {orders.Count} order(s):",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.OrderListMenu(orders)
|
|
);
|
|
}
|
|
|
|
session.State = SessionState.ViewingOrders;
|
|
}
|
|
|
|
private async Task HandleViewOrder(ITelegramBotClient bot, Message message, UserSession session, Guid orderId, User telegramUser)
|
|
{
|
|
var order = await _shopService.GetCustomerOrderAsync(
|
|
orderId,
|
|
telegramUser.Id,
|
|
telegramUser.Username ?? "",
|
|
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
|
|
telegramUser.FirstName ?? "",
|
|
telegramUser.LastName ?? ""
|
|
);
|
|
|
|
if (order == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync("", "Order not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatOrder(order),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
|
|
session.State = SessionState.ViewingOrder;
|
|
}
|
|
|
|
private async Task HandlePrivacySettings(ITelegramBotClient bot, Message message, UserSession session, string? setting)
|
|
{
|
|
if (setting != null)
|
|
{
|
|
switch (setting)
|
|
{
|
|
case "ephemeral":
|
|
session.Privacy.UseEphemeralMode = !session.Privacy.UseEphemeralMode;
|
|
session.IsEphemeral = session.Privacy.UseEphemeralMode;
|
|
break;
|
|
|
|
case "tor":
|
|
session.Privacy.UseTorOnly = !session.Privacy.UseTorOnly;
|
|
break;
|
|
|
|
case "pgp":
|
|
session.Privacy.RequirePGP = !session.Privacy.RequirePGP;
|
|
if (session.Privacy.RequirePGP && string.IsNullOrEmpty(session.Privacy.PGPPublicKey))
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
"Please set your PGP public key using:\n`/pgpkey YOUR_PUBLIC_KEY`",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
}
|
|
break;
|
|
|
|
case "analytics":
|
|
session.Privacy.DisableAnalytics = !session.Privacy.DisableAnalytics;
|
|
break;
|
|
|
|
case "disappearing":
|
|
session.Privacy.EnableDisappearingMessages = !session.Privacy.EnableDisappearingMessages;
|
|
break;
|
|
|
|
case "delete":
|
|
await _sessionManager.DeleteUserDataAsync(message.From!.Id);
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"✅ *All your data has been deleted*\n\nYou can start fresh with /start",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatPrivacyPolicy(),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.PrivacyMenu(session.Privacy)
|
|
);
|
|
|
|
session.State = SessionState.PrivacySettings;
|
|
}
|
|
|
|
private async Task HandleHelp(ITelegramBotClient bot, Message message)
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
MessageFormatter.FormatHelp(),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
|
|
private async Task HandleSupportCallback(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
session.State = SessionState.CustomerSupport;
|
|
|
|
await ShowCustomerConversationInCallback(bot, callbackQuery, session);
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
|
|
}
|
|
|
|
private async Task HandleRefreshConversation(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
await ShowCustomerConversationInCallback(bot, callbackQuery, session);
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Messages refreshed");
|
|
}
|
|
|
|
private async Task HandleExitChat(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
session.State = SessionState.MainMenu;
|
|
|
|
await bot.EditMessageTextAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
callbackQuery.Message.MessageId,
|
|
"Chat ended. How can I help you today?",
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
|
|
}
|
|
|
|
private async Task ShowCustomerConversationInCallback(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
try
|
|
{
|
|
var user = callbackQuery.From;
|
|
|
|
// Get conversation history for this customer
|
|
var messages = await _shopService.GetCustomerConversationAsync(
|
|
user.Id,
|
|
user.Username ?? "",
|
|
$"{user.FirstName} {user.LastName}".Trim(),
|
|
user.FirstName ?? "",
|
|
user.LastName ?? ""
|
|
);
|
|
|
|
var conversationText = "💬 *Your Messages*\n\n";
|
|
|
|
if (messages?.Any() == true)
|
|
{
|
|
conversationText += "Recent conversation:\n\n";
|
|
|
|
foreach (var msg in messages.OrderBy(m => m.CreatedAt).TakeLast(8)) // Show last 8 messages
|
|
{
|
|
var isFromBusiness = msg.Direction == 0; // AdminToCustomer
|
|
var sender = isFromBusiness ? "🏪 Shop" : "👤 You";
|
|
var time = msg.CreatedAt.ToString("MMM dd, HH:mm");
|
|
|
|
conversationText += $"*{sender}* _{time}_\n{msg.Content}\n\n";
|
|
}
|
|
}
|
|
else
|
|
{
|
|
conversationText += "No messages yet. Start a conversation by typing below!\n\n";
|
|
}
|
|
|
|
conversationText += "_Type your message or use buttons below._";
|
|
|
|
await bot.EditMessageTextAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
callbackQuery.Message.MessageId,
|
|
conversationText,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.ConversationMenu()
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error showing customer conversation in callback");
|
|
await bot.EditMessageTextAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
callbackQuery.Message.MessageId,
|
|
"💬 *Customer Messages*\n\nReady to chat with our support team!\n\n_Type your message below._",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.ConversationMenu()
|
|
);
|
|
}
|
|
}
|
|
|
|
private async Task HandleCancelSupport(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session)
|
|
{
|
|
session.State = SessionState.MainMenu;
|
|
|
|
await bot.EditMessageTextAsync(
|
|
callbackQuery.Message!.Chat.Id,
|
|
callbackQuery.Message.MessageId,
|
|
"Customer support cancelled. How can I help you today?",
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id);
|
|
}
|
|
}
|
|
} |