"Fix-BUTTON_DATA_INVALID-and-add-multi-buy-buttons"
This commit is contained in:
parent
c961dfa47a
commit
694ce15549
@ -29,6 +29,8 @@ namespace TeleBot.Handlers
|
|||||||
private readonly IBotActivityTracker _activityTracker;
|
private readonly IBotActivityTracker _activityTracker;
|
||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<CallbackHandler> _logger;
|
private readonly ILogger<CallbackHandler> _logger;
|
||||||
|
private readonly MenuBuilder _menuBuilder;
|
||||||
|
private readonly CallbackDataMapper _mapper;
|
||||||
|
|
||||||
public CallbackHandler(
|
public CallbackHandler(
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
@ -37,7 +39,9 @@ namespace TeleBot.Handlers
|
|||||||
IProductCarouselService carouselService,
|
IProductCarouselService carouselService,
|
||||||
IBotActivityTracker activityTracker,
|
IBotActivityTracker activityTracker,
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<CallbackHandler> logger)
|
ILogger<CallbackHandler> logger,
|
||||||
|
MenuBuilder menuBuilder,
|
||||||
|
CallbackDataMapper mapper)
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_shopService = shopService;
|
_shopService = shopService;
|
||||||
@ -46,6 +50,8 @@ namespace TeleBot.Handlers
|
|||||||
_activityTracker = activityTracker;
|
_activityTracker = activityTracker;
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_menuBuilder = menuBuilder;
|
||||||
|
_mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery)
|
public async Task HandleCallbackAsync(ITelegramBotClient bot, CallbackQuery callbackQuery)
|
||||||
@ -115,7 +121,11 @@ namespace TeleBot.Handlers
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case "remove":
|
case "remove":
|
||||||
await HandleRemoveFromCart(bot, callbackQuery, session, Guid.Parse(data[1]));
|
{
|
||||||
|
var (_, productId, _, _) = _mapper.ParseCallback(callbackQuery.Data);
|
||||||
|
if (productId.HasValue)
|
||||||
|
await HandleRemoveFromCart(bot, callbackQuery, session, productId.Value);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "clear_cart":
|
case "clear_cart":
|
||||||
@ -372,32 +382,40 @@ namespace TeleBot.Handlers
|
|||||||
|
|
||||||
private async Task HandleQuantityChange(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
private async Task HandleQuantityChange(ITelegramBotClient bot, Message message, UserSession session, string[] data)
|
||||||
{
|
{
|
||||||
// Format: qty:productId:newQuantity
|
// Parse callback data using mapper
|
||||||
var productId = Guid.Parse(data[1]);
|
var (_, productId, quantity, _) = _mapper.ParseCallback(string.Join(":", data));
|
||||||
var quantity = int.Parse(data[2]);
|
|
||||||
|
|
||||||
var product = await _shopService.GetProductAsync(productId);
|
if (!productId.HasValue || !quantity.HasValue)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var product = await _shopService.GetProductAsync(productId.Value);
|
||||||
if (product == null)
|
if (product == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
session.TempData["current_quantity"] = quantity;
|
session.TempData["current_quantity"] = quantity.Value;
|
||||||
|
|
||||||
await bot.EditMessageReplyMarkupAsync(
|
await bot.EditMessageReplyMarkupAsync(
|
||||||
message.Chat.Id,
|
message.Chat.Id,
|
||||||
message.MessageId,
|
message.MessageId,
|
||||||
MenuBuilder.ProductDetailMenu(product, quantity)
|
_menuBuilder.ProductDetailMenu(product, quantity.Value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleAddToCart(ITelegramBotClient bot, CallbackQuery callbackQuery, UserSession session, string[] data)
|
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
|
// Parse callback data using mapper
|
||||||
var productId = Guid.Parse(data[1]);
|
var (_, productId, quantity, multiBuyId) = _mapper.ParseCallback(string.Join(":", data));
|
||||||
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 (!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)
|
if (product == null)
|
||||||
{
|
{
|
||||||
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
await bot.AnswerCallbackQueryAsync(callbackQuery.Id, "Product not found", showAlert: true);
|
||||||
@ -407,13 +425,15 @@ namespace TeleBot.Handlers
|
|||||||
// If product has variants but none selected, show variant selection
|
// If product has variants but none selected, show variant selection
|
||||||
if (selectedVariant == null && product.Variants?.Any() == true)
|
if (selectedVariant == null && product.Variants?.Any() == true)
|
||||||
{
|
{
|
||||||
await ShowVariantSelection(bot, callbackQuery.Message!, session, product, quantity, multiBuyId);
|
await ShowVariantSelection(bot, callbackQuery.Message!, session, product, quantity.Value, multiBuyId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get price based on multi-buy or base product
|
// Get price based on multi-buy or base product
|
||||||
decimal price = product.Price;
|
decimal price = product.Price;
|
||||||
string itemName = product.Name;
|
string itemName = product.Name;
|
||||||
|
int finalQuantity = quantity.Value;
|
||||||
|
|
||||||
if (multiBuyId.HasValue && product.MultiBuys != null)
|
if (multiBuyId.HasValue && product.MultiBuys != null)
|
||||||
{
|
{
|
||||||
var multiBuy = product.MultiBuys.FirstOrDefault(mb => mb.Id == multiBuyId);
|
var multiBuy = product.MultiBuys.FirstOrDefault(mb => mb.Id == multiBuyId);
|
||||||
@ -421,7 +441,7 @@ namespace TeleBot.Handlers
|
|||||||
{
|
{
|
||||||
price = multiBuy.Price;
|
price = multiBuy.Price;
|
||||||
itemName = $"{product.Name} ({multiBuy.Name})";
|
itemName = $"{product.Name} ({multiBuy.Name})";
|
||||||
quantity = multiBuy.Quantity; // Use multi-buy quantity
|
finalQuantity = multiBuy.Quantity; // Use multi-buy quantity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -431,7 +451,7 @@ namespace TeleBot.Handlers
|
|||||||
itemName += $" - {selectedVariant}";
|
itemName += $" - {selectedVariant}";
|
||||||
}
|
}
|
||||||
|
|
||||||
session.Cart.AddItem(productId, itemName, price, quantity, multiBuyId, selectedVariant);
|
session.Cart.AddItem(productId.Value, itemName, price, finalQuantity, multiBuyId, selectedVariant);
|
||||||
|
|
||||||
// Track add to cart action
|
// Track add to cart action
|
||||||
await _activityTracker.TrackActivityAsync(
|
await _activityTracker.TrackActivityAsync(
|
||||||
@ -439,13 +459,13 @@ namespace TeleBot.Handlers
|
|||||||
ActivityTypes.AddToCart,
|
ActivityTypes.AddToCart,
|
||||||
$"Added to cart: {itemName}",
|
$"Added to cart: {itemName}",
|
||||||
product,
|
product,
|
||||||
price * quantity,
|
price * finalQuantity,
|
||||||
quantity
|
finalQuantity
|
||||||
);
|
);
|
||||||
|
|
||||||
await bot.AnswerCallbackQueryAsync(
|
await bot.AnswerCallbackQueryAsync(
|
||||||
callbackQuery.Id,
|
callbackQuery.Id,
|
||||||
$"✅ Added {quantity}x {itemName} to cart",
|
$"✅ Added {finalQuantity}x {itemName} to cart",
|
||||||
showAlert: false
|
showAlert: false
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -469,7 +489,7 @@ namespace TeleBot.Handlers
|
|||||||
message.MessageId,
|
message.MessageId,
|
||||||
MessageFormatter.FormatCart(session.Cart),
|
MessageFormatter.FormatCart(session.Cart),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
session.State = SessionState.ViewingCart;
|
session.State = SessionState.ViewingCart;
|
||||||
}
|
}
|
||||||
@ -480,7 +500,7 @@ namespace TeleBot.Handlers
|
|||||||
chatId,
|
chatId,
|
||||||
MessageFormatter.FormatCart(session.Cart),
|
MessageFormatter.FormatCart(session.Cart),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
session.State = SessionState.ViewingCart;
|
session.State = SessionState.ViewingCart;
|
||||||
}
|
}
|
||||||
@ -565,7 +585,7 @@ namespace TeleBot.Handlers
|
|||||||
callbackQuery.Message!.Chat.Id,
|
callbackQuery.Message!.Chat.Id,
|
||||||
MessageFormatter.FormatCart(session.Cart),
|
MessageFormatter.FormatCart(session.Cart),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Immediately proceed to checkout
|
// Immediately proceed to checkout
|
||||||
@ -634,7 +654,7 @@ namespace TeleBot.Handlers
|
|||||||
callbackQuery.Message!.Chat.Id,
|
callbackQuery.Message!.Chat.Id,
|
||||||
MessageFormatter.FormatCart(session.Cart),
|
MessageFormatter.FormatCart(session.Cart),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Immediately proceed to checkout
|
// Immediately proceed to checkout
|
||||||
@ -738,7 +758,7 @@ namespace TeleBot.Handlers
|
|||||||
message.Chat.Id,
|
message.Chat.Id,
|
||||||
message.MessageId,
|
message.MessageId,
|
||||||
"❌ Failed to create order. Please try again.",
|
"❌ Failed to create order. Please try again.",
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -823,7 +843,7 @@ namespace TeleBot.Handlers
|
|||||||
$"• Network connectivity issues\n\n" +
|
$"• Network connectivity issues\n\n" +
|
||||||
$"Your cart has been restored. Please try again.",
|
$"Your cart has been restored. Please try again.",
|
||||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
MenuBuilder.CartMenu(session.Cart)
|
_menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -863,7 +883,7 @@ namespace TeleBot.Handlers
|
|||||||
$"Our payment system may be undergoing maintenance.\n" +
|
$"Our payment system may be undergoing maintenance.\n" +
|
||||||
$"Your cart has been restored. Please try again later.",
|
$"Your cart has been restored. Please try again later.",
|
||||||
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
MenuBuilder.CartMenu(session.Cart)
|
_menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,19 +20,22 @@ namespace TeleBot.Handlers
|
|||||||
private readonly IPrivacyService _privacyService;
|
private readonly IPrivacyService _privacyService;
|
||||||
private readonly IProductCarouselService _carouselService;
|
private readonly IProductCarouselService _carouselService;
|
||||||
private readonly ILogger<CommandHandler> _logger;
|
private readonly ILogger<CommandHandler> _logger;
|
||||||
|
private readonly MenuBuilder _menuBuilder;
|
||||||
|
|
||||||
public CommandHandler(
|
public CommandHandler(
|
||||||
ISessionManager sessionManager,
|
ISessionManager sessionManager,
|
||||||
ILittleShopService shopService,
|
ILittleShopService shopService,
|
||||||
IPrivacyService privacyService,
|
IPrivacyService privacyService,
|
||||||
IProductCarouselService carouselService,
|
IProductCarouselService carouselService,
|
||||||
ILogger<CommandHandler> logger)
|
ILogger<CommandHandler> logger,
|
||||||
|
MenuBuilder menuBuilder)
|
||||||
{
|
{
|
||||||
_sessionManager = sessionManager;
|
_sessionManager = sessionManager;
|
||||||
_shopService = shopService;
|
_shopService = shopService;
|
||||||
_privacyService = privacyService;
|
_privacyService = privacyService;
|
||||||
_carouselService = carouselService;
|
_carouselService = carouselService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_menuBuilder = menuBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task HandleCommandAsync(ITelegramBotClient bot, Message message, string command, string? args)
|
public async Task HandleCommandAsync(ITelegramBotClient bot, Message message, string command, string? args)
|
||||||
@ -202,7 +205,7 @@ namespace TeleBot.Handlers
|
|||||||
message.Chat.Id,
|
message.Chat.Id,
|
||||||
text,
|
text,
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.CartMenu(session.Cart)
|
replyMarkup: _menuBuilder.CartMenu(session.Cart)
|
||||||
);
|
);
|
||||||
|
|
||||||
session.State = Models.SessionState.ViewingCart;
|
session.State = Models.SessionState.ViewingCart;
|
||||||
|
|||||||
@ -16,6 +16,7 @@ using TeleBot;
|
|||||||
using TeleBot.Handlers;
|
using TeleBot.Handlers;
|
||||||
using TeleBot.Services;
|
using TeleBot.Services;
|
||||||
using TeleBot.Http;
|
using TeleBot.Http;
|
||||||
|
using TeleBot.UI;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var BrandName = "Little Shop";
|
var BrandName = "Little Shop";
|
||||||
@ -90,6 +91,10 @@ builder.Services.AddSingleton<ICommandHandler, CommandHandler>();
|
|||||||
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
|
builder.Services.AddSingleton<ICallbackHandler, CallbackHandler>();
|
||||||
builder.Services.AddSingleton<IMessageHandler, MessageHandler>();
|
builder.Services.AddSingleton<IMessageHandler, MessageHandler>();
|
||||||
|
|
||||||
|
// Callback Data Mapper (for short IDs to avoid Telegram's 64-byte limit)
|
||||||
|
builder.Services.AddSingleton<CallbackDataMapper>();
|
||||||
|
builder.Services.AddSingleton<MenuBuilder>();
|
||||||
|
|
||||||
// Bot Manager Service (for registration and metrics) - Single instance with direct connection (internal API)
|
// Bot Manager Service (for registration and metrics) - Single instance with direct connection (internal API)
|
||||||
builder.Services.AddHttpClient<BotManagerService>()
|
builder.Services.AddHttpClient<BotManagerService>()
|
||||||
.ConfigurePrimaryHttpMessageHandler(sp =>
|
.ConfigurePrimaryHttpMessageHandler(sp =>
|
||||||
|
|||||||
100
TeleBot/TeleBot/Services/CallbackDataMapper.cs
Normal file
100
TeleBot/TeleBot/Services/CallbackDataMapper.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace TeleBot.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps long GUIDs to short IDs for Telegram callback data (64-byte limit)
|
||||||
|
/// </summary>
|
||||||
|
public class CallbackDataMapper
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, Guid> _shortToGuid = new();
|
||||||
|
private readonly ConcurrentDictionary<Guid, string> _guidToShort = new();
|
||||||
|
private int _nextId = 1;
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get or create a short ID for a GUID
|
||||||
|
/// </summary>
|
||||||
|
public string GetShortId(Guid guid, string prefix = "id")
|
||||||
|
{
|
||||||
|
if (_guidToShort.TryGetValue(guid, out var existing))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
// Double-check after acquiring lock
|
||||||
|
if (_guidToShort.TryGetValue(guid, out existing))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
var shortId = $"{prefix}{_nextId++}";
|
||||||
|
_shortToGuid[shortId] = guid;
|
||||||
|
_guidToShort[guid] = shortId;
|
||||||
|
return shortId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decode a short ID back to GUID
|
||||||
|
/// </summary>
|
||||||
|
public Guid? DecodeShortId(string shortId)
|
||||||
|
{
|
||||||
|
return _shortToGuid.TryGetValue(shortId, out var guid) ? guid : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build callback data with short IDs
|
||||||
|
/// Format: action:shortProductId[:quantity[:shortMultiBuyId]]
|
||||||
|
/// </summary>
|
||||||
|
public string BuildCallback(string action, Guid productId, int? quantity = null, Guid? multiBuyId = null)
|
||||||
|
{
|
||||||
|
var parts = new List<string> { action, GetShortId(productId, "p") };
|
||||||
|
|
||||||
|
if (quantity.HasValue)
|
||||||
|
parts.Add(quantity.Value.ToString());
|
||||||
|
|
||||||
|
if (multiBuyId.HasValue)
|
||||||
|
parts.Add(GetShortId(multiBuyId.Value, "mb"));
|
||||||
|
|
||||||
|
var result = string.Join(":", parts);
|
||||||
|
|
||||||
|
// Ensure we don't exceed Telegram's 64-byte limit
|
||||||
|
if (result.Length > 63)
|
||||||
|
throw new InvalidOperationException($"Callback data too long ({result.Length} bytes): {result}");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse callback data with short IDs
|
||||||
|
/// </summary>
|
||||||
|
public (string action, Guid? productId, int? quantity, Guid? multiBuyId) ParseCallback(string callbackData)
|
||||||
|
{
|
||||||
|
var parts = callbackData.Split(':');
|
||||||
|
var action = parts[0];
|
||||||
|
|
||||||
|
Guid? productId = null;
|
||||||
|
int? quantity = null;
|
||||||
|
Guid? multiBuyId = null;
|
||||||
|
|
||||||
|
if (parts.Length > 1)
|
||||||
|
productId = DecodeShortId(parts[1]);
|
||||||
|
|
||||||
|
if (parts.Length > 2 && int.TryParse(parts[2], out var qty))
|
||||||
|
quantity = qty;
|
||||||
|
|
||||||
|
if (parts.Length > 3)
|
||||||
|
multiBuyId = DecodeShortId(parts[3]);
|
||||||
|
|
||||||
|
return (action, productId, quantity, multiBuyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear old mappings (call periodically to prevent memory buildup)
|
||||||
|
/// </summary>
|
||||||
|
public void ClearMappings()
|
||||||
|
{
|
||||||
|
_shortToGuid.Clear();
|
||||||
|
_guidToShort.Clear();
|
||||||
|
_nextId = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -29,15 +29,18 @@ namespace TeleBot.Services
|
|||||||
private readonly IConfiguration _configuration;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<ProductCarouselService> _logger;
|
private readonly ILogger<ProductCarouselService> _logger;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly MenuBuilder _menuBuilder;
|
||||||
private readonly string _imageCachePath;
|
private readonly string _imageCachePath;
|
||||||
|
|
||||||
public ProductCarouselService(
|
public ProductCarouselService(
|
||||||
IConfiguration configuration,
|
IConfiguration configuration,
|
||||||
ILogger<ProductCarouselService> logger,
|
ILogger<ProductCarouselService> logger,
|
||||||
HttpClient httpClient)
|
HttpClient httpClient,
|
||||||
|
MenuBuilder menuBuilder)
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_menuBuilder = menuBuilder;
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_imageCachePath = Path.Combine(Environment.CurrentDirectory, "image_cache");
|
_imageCachePath = Path.Combine(Environment.CurrentDirectory, "image_cache");
|
||||||
|
|
||||||
@ -208,7 +211,7 @@ namespace TeleBot.Services
|
|||||||
image,
|
image,
|
||||||
caption: FormatProductCaption(product),
|
caption: FormatProductCaption(product),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
replyMarkup: _menuBuilder.ProductDetailMenu(product)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -218,7 +221,7 @@ namespace TeleBot.Services
|
|||||||
chatId,
|
chatId,
|
||||||
MessageFormatter.FormatProductDetail(product),
|
MessageFormatter.FormatProductDetail(product),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
replyMarkup: _menuBuilder.ProductDetailMenu(product)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,7 +234,7 @@ namespace TeleBot.Services
|
|||||||
chatId,
|
chatId,
|
||||||
MessageFormatter.FormatProductDetail(product),
|
MessageFormatter.FormatProductDetail(product),
|
||||||
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
parseMode: Telegram.Bot.Types.Enums.ParseMode.Markdown,
|
||||||
replyMarkup: MenuBuilder.ProductDetailMenu(product)
|
replyMarkup: _menuBuilder.ProductDetailMenu(product)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,8 +20,10 @@ namespace TeleBot
|
|||||||
var config = new ConfigurationBuilder().Build();
|
var config = new ConfigurationBuilder().Build();
|
||||||
var logger = NullLogger<ProductCarouselService>.Instance;
|
var logger = NullLogger<ProductCarouselService>.Instance;
|
||||||
var httpClient = new System.Net.Http.HttpClient();
|
var httpClient = new System.Net.Http.HttpClient();
|
||||||
|
var mapper = new TeleBot.Services.CallbackDataMapper();
|
||||||
|
var menuBuilder = new TeleBot.UI.MenuBuilder(mapper);
|
||||||
|
|
||||||
var carouselService = new ProductCarouselService(config, logger, httpClient);
|
var carouselService = new ProductCarouselService(config, logger, httpClient, menuBuilder);
|
||||||
|
|
||||||
// Test image URL validation
|
// Test image URL validation
|
||||||
var validUrls = new[]
|
var validUrls = new[]
|
||||||
@ -43,8 +45,10 @@ namespace TeleBot
|
|||||||
var config = new ConfigurationBuilder().Build();
|
var config = new ConfigurationBuilder().Build();
|
||||||
var logger = NullLogger<ProductCarouselService>.Instance;
|
var logger = NullLogger<ProductCarouselService>.Instance;
|
||||||
var httpClient = new System.Net.Http.HttpClient();
|
var httpClient = new System.Net.Http.HttpClient();
|
||||||
|
var mapper = new TeleBot.Services.CallbackDataMapper();
|
||||||
|
var menuBuilder = new TeleBot.UI.MenuBuilder(mapper);
|
||||||
|
|
||||||
var carouselService = new ProductCarouselService(config, logger, httpClient);
|
var carouselService = new ProductCarouselService(config, logger, httpClient, menuBuilder);
|
||||||
|
|
||||||
// Create a test product with image
|
// Create a test product with image
|
||||||
var testProduct = new Product
|
var testProduct = new Product
|
||||||
|
|||||||
@ -4,11 +4,18 @@ using System.Linq;
|
|||||||
using LittleShop.Client.Models;
|
using LittleShop.Client.Models;
|
||||||
using Telegram.Bot.Types.ReplyMarkups;
|
using Telegram.Bot.Types.ReplyMarkups;
|
||||||
using TeleBot.Models;
|
using TeleBot.Models;
|
||||||
|
using TeleBot.Services;
|
||||||
|
|
||||||
namespace TeleBot.UI
|
namespace TeleBot.UI
|
||||||
{
|
{
|
||||||
public static class MenuBuilder
|
public class MenuBuilder
|
||||||
{
|
{
|
||||||
|
private readonly CallbackDataMapper _mapper;
|
||||||
|
|
||||||
|
public MenuBuilder(CallbackDataMapper mapper)
|
||||||
|
{
|
||||||
|
_mapper = mapper;
|
||||||
|
}
|
||||||
public static InlineKeyboardMarkup MainMenu()
|
public static InlineKeyboardMarkup MainMenu()
|
||||||
{
|
{
|
||||||
return new InlineKeyboardMarkup(new[]
|
return new InlineKeyboardMarkup(new[]
|
||||||
@ -90,7 +97,7 @@ namespace TeleBot.UI
|
|||||||
return new InlineKeyboardMarkup(buttons);
|
return new InlineKeyboardMarkup(buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InlineKeyboardMarkup ProductDetailMenu(Product product, int quantity = 1)
|
public InlineKeyboardMarkup ProductDetailMenu(Product product, int quantity = 1)
|
||||||
{
|
{
|
||||||
var buttons = new List<InlineKeyboardButton[]>();
|
var buttons = new List<InlineKeyboardButton[]>();
|
||||||
|
|
||||||
@ -106,14 +113,14 @@ namespace TeleBot.UI
|
|||||||
|
|
||||||
foreach (var multiBuy in product.MultiBuys.Where(mb => mb.IsActive).OrderBy(mb => mb.Quantity))
|
foreach (var multiBuy in product.MultiBuys.Where(mb => mb.IsActive).OrderBy(mb => mb.Quantity))
|
||||||
{
|
{
|
||||||
var label = $"{multiBuy.Name} - ${multiBuy.Price:F2}";
|
var label = $"{multiBuy.Name} - £{multiBuy.Price:F2}";
|
||||||
if (multiBuy.Quantity > 1)
|
if (multiBuy.Quantity > 1)
|
||||||
label += $" (${multiBuy.PricePerUnit:F2}/each)";
|
label += $" (£{multiBuy.PricePerUnit:F2}/each)";
|
||||||
|
|
||||||
// If has variants, need variant selection first
|
// Use short callback data
|
||||||
var callbackData = hasVariants
|
var callbackData = hasVariants
|
||||||
? $"selectvar:{product.Id}:{multiBuy.Quantity}:{multiBuy.Id}"
|
? _mapper.BuildCallback("selectvar", product.Id, multiBuy.Quantity, multiBuy.Id)
|
||||||
: $"add:{product.Id}:{multiBuy.Quantity}:{multiBuy.Id}";
|
: _mapper.BuildCallback("add", product.Id, multiBuy.Quantity, multiBuy.Id);
|
||||||
|
|
||||||
buttons.Add(new[]
|
buttons.Add(new[]
|
||||||
{
|
{
|
||||||
@ -123,12 +130,12 @@ namespace TeleBot.UI
|
|||||||
|
|
||||||
// Add regular single item option
|
// Add regular single item option
|
||||||
var singleCallbackData = hasVariants
|
var singleCallbackData = hasVariants
|
||||||
? $"selectvar:{product.Id}:1"
|
? _mapper.BuildCallback("selectvar", product.Id, 1)
|
||||||
: $"add:{product.Id}:1";
|
: _mapper.BuildCallback("add", product.Id, 1);
|
||||||
|
|
||||||
buttons.Add(new[] {
|
buttons.Add(new[] {
|
||||||
InlineKeyboardButton.WithCallbackData(
|
InlineKeyboardButton.WithCallbackData(
|
||||||
$"🛒 Single Item - ${product.Price:F2}",
|
$"🛒 Single Item - £{product.Price:F2}",
|
||||||
singleCallbackData
|
singleCallbackData
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -138,21 +145,21 @@ namespace TeleBot.UI
|
|||||||
// No multi-buys, show quantity selector
|
// No multi-buys, show quantity selector
|
||||||
var quantityButtons = new List<InlineKeyboardButton>();
|
var quantityButtons = new List<InlineKeyboardButton>();
|
||||||
if (quantity > 1)
|
if (quantity > 1)
|
||||||
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("➖", $"qty:{product.Id}:{quantity - 1}"));
|
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("➖", _mapper.BuildCallback("qty", product.Id, quantity - 1)));
|
||||||
quantityButtons.Add(InlineKeyboardButton.WithCallbackData($"Qty: {quantity}", "noop"));
|
quantityButtons.Add(InlineKeyboardButton.WithCallbackData($"Qty: {quantity}", "noop"));
|
||||||
if (quantity < 10)
|
if (quantity < 10)
|
||||||
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("➕", $"qty:{product.Id}:{quantity + 1}"));
|
quantityButtons.Add(InlineKeyboardButton.WithCallbackData("➕", _mapper.BuildCallback("qty", product.Id, quantity + 1)));
|
||||||
|
|
||||||
buttons.Add(quantityButtons.ToArray());
|
buttons.Add(quantityButtons.ToArray());
|
||||||
|
|
||||||
// Add to cart button
|
// Add to cart button
|
||||||
var addCallbackData = hasVariants
|
var addCallbackData = hasVariants
|
||||||
? $"selectvar:{product.Id}:{quantity}"
|
? _mapper.BuildCallback("selectvar", product.Id, quantity)
|
||||||
: $"add:{product.Id}:{quantity}";
|
: _mapper.BuildCallback("add", product.Id, quantity);
|
||||||
|
|
||||||
buttons.Add(new[] {
|
buttons.Add(new[] {
|
||||||
InlineKeyboardButton.WithCallbackData(
|
InlineKeyboardButton.WithCallbackData(
|
||||||
$"🛒 Add to Cart - ${product.Price * quantity:F2}",
|
$"🛒 Add to Cart - £{product.Price * quantity:F2}",
|
||||||
addCallbackData
|
addCallbackData
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
@ -184,7 +191,7 @@ namespace TeleBot.UI
|
|||||||
return new InlineKeyboardMarkup(buttons);
|
return new InlineKeyboardMarkup(buttons);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InlineKeyboardMarkup CartMenu(ShoppingCart cart)
|
public InlineKeyboardMarkup CartMenu(ShoppingCart cart)
|
||||||
{
|
{
|
||||||
var buttons = new List<InlineKeyboardButton[]>();
|
var buttons = new List<InlineKeyboardButton[]>();
|
||||||
|
|
||||||
@ -196,7 +203,7 @@ namespace TeleBot.UI
|
|||||||
buttons.Add(new[] {
|
buttons.Add(new[] {
|
||||||
InlineKeyboardButton.WithCallbackData(
|
InlineKeyboardButton.WithCallbackData(
|
||||||
$"❌ Remove {item.ProductName}",
|
$"❌ Remove {item.ProductName}",
|
||||||
$"remove:{item.ProductId}"
|
_mapper.BuildCallback("remove", item.ProductId)
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user