Problem:
- Selecting Size then Color would reset Size selection
- Users could never get "Add to Cart" button with multiple variant types
- Each selection created a NEW list, wiping previous choices
Root Cause:
- HandleSetVariant created: new List<string> { variantName }
- This replaced all previous selections instead of accumulating
Fix:
1. Get existing selected variants from session
2. Find the variant type of newly selected variant
3. Remove any previous selection from the SAME type (allow changing choice)
4. Add the new selection
5. Save accumulated list back to session
Example behavior now:
- Select "Red" (Color) → selectedVariants = ["Red"]
- Select "Large" (Size) → selectedVariants = ["Red", "Large"] ✅
- Select "Blue" (Color) → selectedVariants = ["Blue", "Large"] ✅
- Can now confirm when all types selected
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
1541 lines
64 KiB
C#
1541 lines
64 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 IBotActivityTracker _activityTracker;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly ILogger<CallbackHandler> _logger;
|
|
private readonly MenuBuilder _menuBuilder;
|
|
private readonly CallbackDataMapper _mapper;
|
|
|
|
public CallbackHandler(
|
|
ISessionManager sessionManager,
|
|
ILittleShopService shopService,
|
|
IPrivacyService privacyService,
|
|
IProductCarouselService carouselService,
|
|
IBotActivityTracker activityTracker,
|
|
IConfiguration configuration,
|
|
ILogger<CallbackHandler> logger,
|
|
MenuBuilder menuBuilder,
|
|
CallbackDataMapper mapper)
|
|
{
|
|
_sessionManager = sessionManager;
|
|
_shopService = shopService;
|
|
_privacyService = privacyService;
|
|
_carouselService = carouselService;
|
|
_activityTracker = activityTracker;
|
|
_configuration = configuration;
|
|
_logger = logger;
|
|
_menuBuilder = menuBuilder;
|
|
_mapper = mapper;
|
|
}
|
|
|
|
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":
|
|
{
|
|
var (_, productId, _, _) = _mapper.ParseCallback(callbackQuery.Data);
|
|
if (productId.HasValue)
|
|
await HandleProductDetail(bot, callbackQuery.Message, session, productId.Value);
|
|
}
|
|
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":
|
|
{
|
|
var (_, productId, _, _) = _mapper.ParseCallback(callbackQuery.Data);
|
|
if (productId.HasValue)
|
|
await HandleRemoveFromCart(bot, callbackQuery, session, productId.Value);
|
|
}
|
|
break;
|
|
|
|
case "clear_cart":
|
|
await HandleClearCart(bot, callbackQuery, session);
|
|
break;
|
|
|
|
case "checkout":
|
|
await HandleCheckout(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "use_saved_address":
|
|
await HandleUseSavedAddress(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "enter_new_address":
|
|
await HandleEnterNewAddress(bot, callbackQuery.Message, session);
|
|
break;
|
|
|
|
case "save_address_yes":
|
|
await HandleSaveAddressPreference(bot, callbackQuery.Message, session, true);
|
|
break;
|
|
|
|
case "save_address_no":
|
|
await HandleSaveAddressPreference(bot, callbackQuery.Message, session, false);
|
|
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 "reviews":
|
|
await HandleViewReviews(bot, callbackQuery.Message, session);
|
|
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;
|
|
|
|
case "selectvar":
|
|
await HandleSelectVariant(bot, callbackQuery.Message, session, data);
|
|
break;
|
|
|
|
case "setvariant":
|
|
await HandleSetVariant(bot, callbackQuery.Message, session, data);
|
|
break;
|
|
|
|
case "addvariant":
|
|
await HandleAddVariant(bot, callbackQuery.Message, session, data);
|
|
break;
|
|
|
|
case "confirmvar":
|
|
await HandleConfirmVariant(bot, callbackQuery, session, data);
|
|
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)
|
|
{
|
|
// Send new message at bottom instead of editing old one
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
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)
|
|
{
|
|
// Track browsing activity
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.Browse,
|
|
"Browsing categories"
|
|
);
|
|
|
|
var categories = await _shopService.GetCategoriesAsync();
|
|
|
|
// Send new message at bottom for navigation
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
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);
|
|
|
|
// Track category view
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.ViewProduct,
|
|
$"Viewing category: {category?.Name ?? "Unknown"}"
|
|
);
|
|
|
|
// 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)
|
|
{
|
|
// Create Buy button with short ID format
|
|
var buyButton = new InlineKeyboardMarkup(new[]
|
|
{
|
|
new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData(
|
|
$"🛒 Buy {product.Name}",
|
|
_mapper.BuildCallback("product", product.Id)
|
|
)
|
|
}
|
|
});
|
|
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
MessageFormatter.FormatSingleProduct(product),
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: buyButton
|
|
);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Track product view
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.ViewProduct,
|
|
$"Viewing product: {product.Name}",
|
|
product
|
|
);
|
|
|
|
// 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)
|
|
{
|
|
// Parse callback data using mapper
|
|
var (_, productId, quantity, _) = _mapper.ParseCallback(string.Join(":", data));
|
|
|
|
if (!productId.HasValue || !quantity.HasValue)
|
|
return;
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
if (product == null)
|
|
return;
|
|
|
|
session.TempData["current_quantity"] = quantity.Value;
|
|
|
|
await bot.EditMessageReplyMarkupAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
_menuBuilder.ProductDetailMenu(product, quantity.Value)
|
|
);
|
|
}
|
|
|
|
private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
|
{
|
|
// Parse callback data using mapper
|
|
var (_, productId, quantity, multiBuyId) = _mapper.ParseCallback(string.Join(":", data));
|
|
|
|
if (!productId.HasValue || !quantity.HasValue)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Invalid product data", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Check for variant in data (old format compatibility)
|
|
string? selectedVariant = data.Length > 4 ? data[4] : null;
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
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.Value, multiBuyId);
|
|
return;
|
|
}
|
|
|
|
// Get price based on multi-buy or base product
|
|
decimal price = product.Price;
|
|
string itemName = product.Name;
|
|
int finalQuantity = quantity.Value;
|
|
|
|
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})";
|
|
finalQuantity = multiBuy.Quantity; // Use multi-buy quantity
|
|
}
|
|
}
|
|
|
|
// Add variant to item name if selected
|
|
if (!string.IsNullOrEmpty(selectedVariant))
|
|
{
|
|
itemName += $" - {selectedVariant}";
|
|
}
|
|
|
|
session.Cart.AddItem(productId.Value, itemName, price, finalQuantity, multiBuyId, selectedVariant);
|
|
|
|
// Track add to cart action
|
|
await _activityTracker.TrackActivityAsync(
|
|
callbackQuery.Message!.Chat,
|
|
ActivityTypes.AddToCart,
|
|
$"Added to cart: {itemName}",
|
|
product,
|
|
price * finalQuantity,
|
|
finalQuantity
|
|
);
|
|
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"✅ Added {finalQuantity}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)
|
|
{
|
|
// Track cart view
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.ViewCart,
|
|
$"Viewing cart with {session.Cart.Items.Count} items",
|
|
null,
|
|
session.Cart.GetTotalAmount()
|
|
);
|
|
|
|
// Send new message at bottom instead of editing
|
|
await SendNewCartMessage(bot, message.Chat.Id, session);
|
|
}
|
|
|
|
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);
|
|
|
|
// Track quick buy action
|
|
await _activityTracker.TrackActivityAsync(
|
|
callbackQuery.Message!.Chat,
|
|
ActivityTypes.AddToCart,
|
|
$"Quick buy: {product.Name}",
|
|
product,
|
|
product.Price * quantity,
|
|
quantity
|
|
);
|
|
|
|
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))
|
|
{
|
|
var displayName = variant.Name;
|
|
// Show price if it's different from base product price
|
|
if (variant.Price.HasValue && variant.Price.Value != product.Price)
|
|
{
|
|
displayName += $" - £{variant.Price.Value:F2}";
|
|
}
|
|
|
|
buttons.Add(new[]
|
|
{
|
|
InlineKeyboardButton.WithCallbackData(displayName, $"quickbuyvar:{product.Id}:{quantity}:{variant.Id}")
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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:variantId
|
|
var productId = Guid.Parse(data[1]);
|
|
var quantity = int.Parse(data[2]);
|
|
var variantId = Guid.Parse(data[3]);
|
|
|
|
var product = await _shopService.GetProductAsync(productId);
|
|
if (product == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Find the variant
|
|
var variant = product.Variants?.FirstOrDefault(v => v.Id == variantId);
|
|
if (variant == null)
|
|
{
|
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Variant not found", showAlert: true);
|
|
return;
|
|
}
|
|
|
|
// Add to cart with variant ID
|
|
var itemName = $"{product.Name} - {variant.Name}";
|
|
session.Cart.AddItem(product, quantity, multiBuyId: null, variantId: variantId);
|
|
|
|
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;
|
|
}
|
|
|
|
// Track checkout initiation
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.Checkout,
|
|
$"Starting checkout with {session.Cart.Items.Count} items",
|
|
null,
|
|
session.Cart.GetTotalAmount()
|
|
);
|
|
|
|
// Initialize order flow
|
|
session.OrderFlow = new OrderFlowData
|
|
{
|
|
UsePGPEncryption = session.Privacy.RequirePGP
|
|
};
|
|
|
|
session.State = SessionState.CheckoutFlow;
|
|
|
|
// Check if user has saved address
|
|
if (session.SavedAddress != null && !string.IsNullOrWhiteSpace(session.SavedAddress.Name))
|
|
{
|
|
// Offer to use saved address
|
|
var savedAddressPreview = $"{session.SavedAddress.Name}\n" +
|
|
$"{session.SavedAddress.Address}\n" +
|
|
$"{session.SavedAddress.City}\n" +
|
|
$"{session.SavedAddress.PostCode}\n" +
|
|
$"{session.SavedAddress.Country}";
|
|
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
$"📦 *Checkout - Delivery Details*\n\n" +
|
|
$"Use your saved address?\n\n" +
|
|
$"```\n{savedAddressPreview}\n```",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: _menuBuilder.UseSavedAddressMenu()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
// Send new message for checkout - collect all details at once
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
"📦 *Checkout - Delivery Details*\n\n" +
|
|
"Please provide all delivery details in one message:\n\n" +
|
|
"• Full Name\n" +
|
|
"• Street Address\n" +
|
|
"• City\n" +
|
|
"• Post/Zip Code\n" +
|
|
"• Country (or leave blank for UK)\n\n" +
|
|
"_Example:_\n" +
|
|
"`John Smith\n" +
|
|
"123 Main Street\n" +
|
|
"London\n" +
|
|
"SW1A 1AA\n" +
|
|
"United Kingdom`",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
}
|
|
}
|
|
|
|
private async Task HandleUseSavedAddress(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
if (session.SavedAddress == null || session.OrderFlow == null)
|
|
{
|
|
await HandleEnterNewAddress(bot, message, session);
|
|
return;
|
|
}
|
|
|
|
// Copy saved address to order flow
|
|
session.OrderFlow.ShippingName = session.SavedAddress.Name;
|
|
session.OrderFlow.ShippingAddress = session.SavedAddress.Address;
|
|
session.OrderFlow.ShippingCity = session.SavedAddress.City;
|
|
session.OrderFlow.ShippingPostCode = session.SavedAddress.PostCode;
|
|
session.OrderFlow.ShippingCountry = session.SavedAddress.Country;
|
|
session.OrderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
|
|
|
|
// Show order summary
|
|
var summary = MessageFormatter.FormatOrderSummary(session.OrderFlow, session.Cart);
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
summary,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CheckoutConfirmMenu()
|
|
);
|
|
}
|
|
|
|
private async Task HandleEnterNewAddress(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"📦 *Checkout - Delivery Details*\n\n" +
|
|
"Please provide all delivery details in one message:\n\n" +
|
|
"• Full Name\n" +
|
|
"• Street Address\n" +
|
|
"• City\n" +
|
|
"• Post/Zip Code\n" +
|
|
"• Country (or leave blank for UK)\n\n" +
|
|
"_Example:_\n" +
|
|
"`John Smith\n" +
|
|
"123 Main Street\n" +
|
|
"London\n" +
|
|
"SW1A 1AA\n" +
|
|
"United Kingdom`",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown
|
|
);
|
|
}
|
|
|
|
private async Task HandleSaveAddressPreference(ITelegramBotClient bot, Message message, UserSession session, bool saveAddress)
|
|
{
|
|
// Save user preference
|
|
session.Privacy.SaveShippingAddress = saveAddress;
|
|
|
|
if (saveAddress && session.OrderFlow != null)
|
|
{
|
|
// Save the address
|
|
session.SavedAddress = new SavedShippingAddress
|
|
{
|
|
Name = session.OrderFlow.ShippingName,
|
|
Address = session.OrderFlow.ShippingAddress,
|
|
City = session.OrderFlow.ShippingCity,
|
|
PostCode = session.OrderFlow.ShippingPostCode,
|
|
Country = session.OrderFlow.ShippingCountry,
|
|
SavedAt = DateTime.UtcNow
|
|
};
|
|
}
|
|
|
|
// Continue to order summary
|
|
if (session.OrderFlow != null)
|
|
{
|
|
session.OrderFlow.CurrentStep = OrderFlowStep.ReviewingOrder;
|
|
var summary = MessageFormatter.FormatOrderSummary(session.OrderFlow, session.Cart);
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
summary,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.CheckoutConfirmMenu()
|
|
);
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
// Track view orders action
|
|
await _activityTracker.TrackActivityAsync(
|
|
message.Chat,
|
|
ActivityTypes.ViewOrders,
|
|
"Viewing order history"
|
|
);
|
|
|
|
// Use new customer-based order lookup
|
|
var orders = await _shopService.GetCustomerOrdersAsync(
|
|
telegramUser.Id,
|
|
telegramUser.Username ?? "",
|
|
$"{telegramUser.FirstName} {telegramUser.LastName}".Trim(),
|
|
telegramUser.FirstName ?? "",
|
|
telegramUser.LastName ?? ""
|
|
);
|
|
|
|
// Send new message at bottom for navigation
|
|
if (!orders.Any())
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
"📦 *Your Orders*\n\nYou have no orders yet.",
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
else
|
|
{
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
$"📦 *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;
|
|
}
|
|
|
|
// Send new message at bottom for navigation
|
|
await bot.SendTextMessageAsync(
|
|
message.Chat.Id,
|
|
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);
|
|
}
|
|
|
|
private async Task HandleViewReviews(ITelegramBotClient bot, Message message, UserSession session)
|
|
{
|
|
try
|
|
{
|
|
// TODO: Fetch actual reviews from API
|
|
var reviewsText = "⭐ *Customer Reviews*\n\n" +
|
|
"Recent product reviews from verified customers:\n\n" +
|
|
"⭐⭐⭐⭐⭐ *Amazing quality!*\n" +
|
|
"_Product: Premium Item_\n" +
|
|
"Fast delivery, excellent product. Will order again!\n" +
|
|
"- Verified Customer, 2 days ago\n\n" +
|
|
"⭐⭐⭐⭐ *Good value*\n" +
|
|
"_Product: Standard Item_\n" +
|
|
"Product as described, good packaging.\n" +
|
|
"- Verified Customer, 5 days ago\n\n" +
|
|
"_Use /review after receiving your order to share your experience!_";
|
|
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
reviewsText,
|
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error viewing reviews");
|
|
await bot.EditMessageTextAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
"❌ Error loading reviews. Please try again.",
|
|
replyMarkup: MenuBuilder.MainMenu()
|
|
);
|
|
}
|
|
}
|
|
|
|
private async Task HandleSelectVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: selectvar:productId:quantity:multiBuyId
|
|
// Use mapper to decode short IDs back to GUIDs
|
|
var (_, productId, quantity, multiBuyId) = _mapper.ParseCallback(string.Join(":", data));
|
|
|
|
if (!productId.HasValue || !quantity.HasValue)
|
|
{
|
|
_logger.LogError("Invalid callback data for selectvar: {Data}", string.Join(":", data));
|
|
return;
|
|
}
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
if (product == null)
|
|
{
|
|
await bot.SendTextMessageAsync(message.Chat.Id, "Product not found.");
|
|
return;
|
|
}
|
|
|
|
// Clear any previous selections
|
|
session.TempData["selected_variants"] = new List<string>();
|
|
session.TempData["current_product"] = productId.Value;
|
|
session.TempData["current_quantity"] = quantity.Value;
|
|
session.TempData["current_multibuy"] = multiBuyId?.ToString();
|
|
|
|
await bot.EditMessageReplyMarkupAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
replyMarkup: _menuBuilder.VariantSelectionMenu(product, quantity.Value, multiBuyId?.ToString())
|
|
);
|
|
|
|
session.State = SessionState.SelectingVariants;
|
|
}
|
|
|
|
private async Task HandleSetVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: setvariant:productId:variantName (for single item)
|
|
// Use mapper to decode short IDs back to GUIDs
|
|
var (_, productId, _, _) = _mapper.ParseCallback(string.Join(":", data.Take(2)));
|
|
var variantName = data[2];
|
|
|
|
if (!productId.HasValue)
|
|
{
|
|
_logger.LogError("Invalid callback data for setvariant: {Data}", string.Join(":", data));
|
|
return;
|
|
}
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
if (product == null) return;
|
|
|
|
// Get existing selections
|
|
var selectedVariants = session.TempData.ContainsKey("selected_variants")
|
|
? session.TempData["selected_variants"] as List<string> ?? new List<string>()
|
|
: new List<string>();
|
|
|
|
// Find the variant type for the selected variant
|
|
var selectedVariantObj = product.Variants?.FirstOrDefault(v => v.Name == variantName);
|
|
if (selectedVariantObj != null)
|
|
{
|
|
// Remove any previous selection from the same variant type
|
|
var variantsOfSameType = product.Variants
|
|
.Where(v => v.VariantType == selectedVariantObj.VariantType)
|
|
.Select(v => v.Name)
|
|
.ToList();
|
|
|
|
selectedVariants.RemoveAll(v => variantsOfSameType.Contains(v));
|
|
|
|
// Add the new selection
|
|
selectedVariants.Add(variantName);
|
|
}
|
|
|
|
session.TempData["selected_variants"] = selectedVariants;
|
|
|
|
var quantity = session.TempData.ContainsKey("current_quantity")
|
|
? (int)session.TempData["current_quantity"]
|
|
: 1;
|
|
|
|
var multiBuyId = session.TempData.ContainsKey("current_multibuy")
|
|
? session.TempData["current_multibuy"] as string
|
|
: null;
|
|
|
|
await bot.EditMessageReplyMarkupAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
replyMarkup: _menuBuilder.VariantSelectionMenu(product, quantity, multiBuyId, selectedVariants)
|
|
);
|
|
}
|
|
|
|
private async Task HandleAddVariant(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
|
{
|
|
// Format: addvariant:productId:quantity:variantName:multiBuyId (for multi-buy)
|
|
// Use mapper to decode short IDs back to GUIDs
|
|
var (_, productId, quantity, multiBuyId) = _mapper.ParseCallback(string.Join(":", data.Take(4)));
|
|
var variantName = data[3];
|
|
|
|
if (!productId.HasValue || !quantity.HasValue)
|
|
{
|
|
_logger.LogError("Invalid callback data for addvariant: {Data}", string.Join(":", data));
|
|
return;
|
|
}
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
if (product == null) return;
|
|
|
|
// Get current selections
|
|
var selectedVariants = session.TempData.ContainsKey("selected_variants")
|
|
? (List<string>)session.TempData["selected_variants"]
|
|
: new List<string>();
|
|
|
|
// Add variant if not at quantity limit
|
|
if (selectedVariants.Count < quantity.Value)
|
|
{
|
|
selectedVariants.Add(variantName);
|
|
session.TempData["selected_variants"] = selectedVariants;
|
|
}
|
|
|
|
await bot.EditMessageReplyMarkupAsync(
|
|
message.Chat.Id,
|
|
message.MessageId,
|
|
replyMarkup: _menuBuilder.VariantSelectionMenu(product, quantity.Value, multiBuyId?.ToString(), selectedVariants)
|
|
);
|
|
}
|
|
|
|
private async Task HandleConfirmVariant(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
|
{
|
|
// Format: confirmvar:productId:quantity:multiBuyId:variantList
|
|
// Use mapper to decode short IDs back to GUIDs
|
|
var (_, productId, quantity, multiBuyId) = _mapper.ParseCallback(string.Join(":", data.Take(4)));
|
|
var variantString = data[4];
|
|
var selectedVariants = variantString.Split(',').ToList();
|
|
|
|
if (!productId.HasValue || !quantity.HasValue)
|
|
{
|
|
_logger.LogError("Invalid callback data for confirmvar: {Data}", string.Join(":", data));
|
|
return;
|
|
}
|
|
|
|
var product = await _shopService.GetProductAsync(productId.Value);
|
|
if (product == null) return;
|
|
|
|
// Add to cart with selected variants
|
|
var cartItem = session.Cart.AddItem(product, quantity.Value, multiBuyId, variantId: null, selectedVariants);
|
|
|
|
// Show success message
|
|
await bot.AnswerCallbackQueryAsync(
|
|
callbackQuery.Id,
|
|
$"✅ Added {quantity.Value}x {product.Name} with {string.Join(", ", selectedVariants)} to cart!",
|
|
showAlert: true
|
|
);
|
|
|
|
// Return to product view
|
|
await HandleProductDetail(bot, callbackQuery.Message!, session, productId.Value);
|
|
|
|
// Clear temp data
|
|
session.TempData.Remove("selected_variants");
|
|
session.TempData.Remove("current_multibuy");
|
|
}
|
|
}
|
|
} |