littleshop/TeleBot/TeleBot/UI/MessageFormatter.cs
SysAdmin 32d80e4b54 Fix: Currency display consistency and remove PGP security vulnerability
## Critical Bug Fixes

### Currency Display (£ vs $)
- Fix MenuBuilder.cs: Replace $ with £ for product prices (line 60) and order totals (line 329)
- Fix ProductCarouselService.cs: Replace $ with £ in product captions and multi-buy offers (lines 317, 325)
- Fix CallbackHandler.cs: Replace $ with £ in order confirmation message (line 800)

### Payment Amount Display Bug
- Fix MessageFormatter.cs: Remove flawed crypto detection logic (< 1.0m check)
- Bug: Order for £700 in ETH displayed as "£1.66" instead of "1.66 ETH"
- Root cause: RequiredAmount is always stored as crypto amount, not fiat
- Solution: Always display RequiredAmount with crypto symbol
- Impact: Fixes display for XMR, DOGE, LTC, and large ETH amounts

## Security: Remove PGP Encryption Feature

### Critical Security Issue Resolved
- PGP "encryption" was only Base64 encoding - NOT real encryption
- Shipping addresses stored as easily decoded text
- False sense of security for users

### Changes Made
- Mark EncryptWithPGP method as [Obsolete] in PrivacyService.cs
- Remove PGP encryption logic from order creation (LittleShopService.cs)
- Mark PGP properties as [Obsolete] in UserSession.cs models
- Disable EnablePGPEncryption feature flag in appsettings.json
- Add comments explaining feature removal

### Recommendation
Implement proper PGP encryption using BouncyCastle in future, or keep removed.

## Testing Required
- Verify all prices display with £ symbol
- Verify crypto payments show correct amount format (e.g., "1.66000000 ETH")
- Verify no PGP options appear in UI
- Test order creation without PGP encryption

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-03 17:36:10 +01:00

415 lines
16 KiB
C#
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Linq;
using System.Text;
using LittleShop.Client.Models;
using TeleBot.Models;
namespace TeleBot.UI
{
public static class MessageFormatter
{
public static string FormatWelcome(bool isReturning)
{
if (isReturning)
{
return $"🔒 *Welcome back to {BotConfig.BrandName}*\n\n" +
"Your privacy is our priority. All sessions are ephemeral by default.\n\n" +
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
"How can I help you today?";
}
return $"🔒 *Welcome to {BotConfig.BrandName}*\n\n" +
"🛡️ *Your Privacy Matters:*\n" +
"• No account required\n" +
"• Ephemeral sessions by default\n" +
"• Optional PGP encryption for shipping\n" +
"• Cryptocurrency payments only\n\n" +
"🖼️ *New Feature:* Browse products with beautiful image carousels!\n\n" +
"Use /help for available commands or choose from the menu below:";
}
public static string FormatCategories(List<Category> categories)
{
var sb = new StringBuilder();
sb.AppendLine("**PRODUCT CATEGORIES**\n");
sb.AppendLine("Choose a category to start shopping:");
return sb.ToString();
}
public static string FormatProductList(PagedResult<Product> products, string? categoryName = null)
{
var sb = new StringBuilder();
if (!string.IsNullOrEmpty(categoryName))
{
sb.AppendLine($"📦 *Products in {categoryName}*\n");
}
else
{
sb.AppendLine("📦 *All Products*\n");
}
if (!products.Items.Any())
{
sb.AppendLine("No products available in this category.");
return sb.ToString();
}
foreach (var product in products.Items)
{
sb.AppendLine($"*{product.Name}*");
sb.AppendLine($"💰 Price: £{product.Price:F2}");
if (!string.IsNullOrEmpty(product.Description))
{
var desc = product.Description.Length > 100
? product.Description.Substring(0, 97) + "..."
: product.Description;
sb.AppendLine($"_{desc}_");
}
sb.AppendLine();
}
if (products.TotalPages > 1)
{
sb.AppendLine($"Page {products.PageNumber} of {products.TotalPages}");
}
return sb.ToString();
}
public static string FormatSingleProduct(Product product)
{
var sb = new StringBuilder();
sb.AppendLine($"🛍️ *{product.Name}*");
// Show multi-buys if available
if (product.MultiBuys?.Any() == true)
{
var lowestPrice = product.MultiBuys.Min(mb => mb.PricePerUnit);
sb.AppendLine($"💰 From £{lowestPrice:F2}");
sb.AppendLine($"📦 _{product.MultiBuys.Count} multi-buy options_");
}
else
{
sb.AppendLine($"💰 £{product.Price:F2}");
}
// Show variants if available
if (product.Variants?.Any() == true)
{
var variantTypes = product.Variants.Select(v => v.VariantType).Distinct().FirstOrDefault() ?? "options";
sb.AppendLine($"🎨 _{product.Variants.Count} {variantTypes.ToLower()} available_");
}
if (!string.IsNullOrEmpty(product.Description))
{
// Truncate description for bubble format
var desc = product.Description.Length > 100
? product.Description.Substring(0, 100) + "..."
: product.Description;
sb.AppendLine($"\n_{desc}_");
}
return sb.ToString();
}
public static string FormatProductDetail(Product product)
{
var sb = new StringBuilder();
sb.AppendLine($"🛍️ *{product.Name}*\n");
sb.AppendLine($"💰 *Price:* £{product.Price:F2}");
sb.AppendLine($"⚖️ *Weight:* {product.Weight} {product.WeightUnit}");
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
if (!string.IsNullOrEmpty(product.Description))
{
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
}
if (product.Photos.Any())
{
sb.AppendLine($"\n🖼 _{product.Photos.Count} photo(s) available_");
}
sb.AppendLine("\nSelect quantity and add to cart:");
return sb.ToString();
}
public static string FormatProductWithMultiBuys(Product product)
{
var sb = new StringBuilder();
sb.AppendLine($"🛍️ *{product.Name}*\n");
if (product.MultiBuys?.Any() == true)
{
sb.AppendLine("📦 *Multi-Buy Options:*\n");
foreach (var multiBuy in product.MultiBuys.OrderBy(mb => mb.Quantity))
{
var savings = multiBuy.Quantity > 1
? $" (£{multiBuy.PricePerUnit:F2} each)"
: "";
sb.AppendLine($"• *{multiBuy.Name}*: £{multiBuy.Price:F2}{savings}");
if (!string.IsNullOrEmpty(multiBuy.Description))
{
sb.AppendLine($" _{multiBuy.Description}_");
}
}
}
else
{
sb.AppendLine($"💰 *Price:* £{product.Price:F2}");
}
if (product.Variants?.Any() == true)
{
var variantTypes = product.Variants.Select(v => v.VariantType).Distinct().FirstOrDefault() ?? "Variant";
sb.AppendLine($"\n🎨 *{variantTypes} Options:*");
var variantNames = string.Join(", ", product.Variants.OrderBy(v => v.SortOrder).Select(v => v.Name));
sb.AppendLine($"_{variantNames}_");
}
sb.AppendLine($"\n⚖ *Weight:* {product.Weight} {product.WeightUnit}");
sb.AppendLine($"📁 *Category:* {product.CategoryName ?? "Uncategorized"}");
if (!string.IsNullOrEmpty(product.Description))
{
sb.AppendLine($"\n📝 *Description:*\n{product.Description}");
}
sb.AppendLine("\n*Select an option to add to cart:*");
return sb.ToString();
}
public static string FormatCart(ShoppingCart cart)
{
var sb = new StringBuilder();
sb.AppendLine("🛒 *Shopping Cart*\n");
if (cart.IsEmpty())
{
sb.AppendLine("Your cart is empty.\n");
sb.AppendLine("Browse products to add items to your cart.");
return sb.ToString();
}
foreach (var item in cart.Items)
{
sb.AppendLine($"• *{item.ProductName}*");
sb.AppendLine($" Qty: {item.Quantity} × £{item.UnitPrice:F2} = *£{item.TotalPrice:F2}*");
}
sb.AppendLine($"\n📊 *Summary:*");
sb.AppendLine($"Items: {cart.GetTotalItems()}");
sb.AppendLine($"*Total: £{cart.GetTotalAmount():F2}*");
return sb.ToString();
}
public static string FormatOrderSummary(OrderFlowData orderFlow, ShoppingCart cart)
{
var sb = new StringBuilder();
sb.AppendLine("📋 *Order Summary*\n");
sb.AppendLine("*Shipping Information:*");
if (orderFlow.UsePGPEncryption)
{
sb.AppendLine("🔐 _Shipping details will be PGP encrypted_");
}
else
{
sb.AppendLine($"Name: {orderFlow.ShippingName}");
sb.AppendLine($"Address: {orderFlow.ShippingAddress}");
sb.AppendLine($"City: {orderFlow.ShippingCity}");
sb.AppendLine($"Post Code: {orderFlow.ShippingPostCode}");
sb.AppendLine($"Country: {orderFlow.ShippingCountry}");
}
if (!string.IsNullOrEmpty(orderFlow.Notes))
{
sb.AppendLine($"Notes: {orderFlow.Notes}");
}
sb.AppendLine($"\n*Order Total: £{cart.GetTotalAmount():F2}*");
sb.AppendLine($"Items: {cart.GetTotalItems()}");
sb.AppendLine("\nPlease confirm your order:");
return sb.ToString();
}
public static string FormatOrder(Order order)
{
var sb = new StringBuilder();
sb.AppendLine($"📦 *Order Details*\n");
sb.AppendLine($"*Order ID:* `{order.Id}`");
sb.AppendLine($"*Status:* {FormatOrderStatus(order.Status)}");
sb.AppendLine($"*Total:* £{order.TotalAmount:F2}");
sb.AppendLine($"*Created:* {order.CreatedAt:yyyy-MM-dd HH:mm} UTC");
if (order.PaidAt.HasValue)
{
sb.AppendLine($"*Paid:* {order.PaidAt.Value:yyyy-MM-dd HH:mm} UTC");
}
if (order.ShippedAt.HasValue)
{
sb.AppendLine($"*Shipped:* {order.ShippedAt.Value:yyyy-MM-dd HH:mm} UTC");
}
if (!string.IsNullOrEmpty(order.TrackingNumber))
{
sb.AppendLine($"*Tracking:* `{order.TrackingNumber}`");
}
if (order.Items.Any())
{
sb.AppendLine("\n*Items:*");
foreach (var item in order.Items)
{
sb.AppendLine($"• {item.ProductName} - Qty: {item.Quantity} - £{item.TotalPrice:F2}");
}
}
if (order.Payments.Any())
{
sb.AppendLine("\n*Payments:*");
foreach (var payment in order.Payments)
{
sb.AppendLine($"• {FormatCurrency(payment.Currency)}: {FormatPaymentStatus(payment.Status)}");
if (!string.IsNullOrEmpty(payment.WalletAddress))
{
sb.AppendLine($" Address: `{payment.WalletAddress}`");
}
// RequiredAmount is always stored as cryptocurrency amount
sb.AppendLine($" Amount: {payment.RequiredAmount:F8} {FormatCurrency(payment.Currency)}");
}
}
return sb.ToString();
}
public static string FormatPayment(CryptoPayment payment)
{
var sb = new StringBuilder();
sb.AppendLine($"💰 *Payment Instructions*\n");
sb.AppendLine($"*Currency:* {FormatCurrency(payment.Currency)}");
// RequiredAmount is always stored as cryptocurrency amount
sb.AppendLine($"*Amount:* `{payment.RequiredAmount:F8} {FormatCurrency(payment.Currency)}`");
sb.AppendLine($"*Status:* {FormatPaymentStatus(payment.Status)}");
sb.AppendLine($"*Expires:* {payment.ExpiresAt:yyyy-MM-dd HH:mm} UTC");
sb.AppendLine($"\n*Send exactly {payment.RequiredAmount:F8} {FormatCurrency(payment.Currency)} to:*");
sb.AppendLine($"`{payment.WalletAddress}`");
if (!string.IsNullOrEmpty(payment.BTCPayCheckoutUrl))
{
sb.AppendLine($"\n*Alternative Payment Link:*");
sb.AppendLine(payment.BTCPayCheckoutUrl);
}
sb.AppendLine("\n⚠ *Important:*");
sb.AppendLine("• Send the exact amount shown");
sb.AppendLine("• Payment must be received before expiry");
sb.AppendLine("• Save this information before closing");
return sb.ToString();
}
public static string FormatHelp()
{
return "*Available Commands:*\n\n" +
"/start - Start shopping\n" +
"/browse - Browse categories\n" +
"/products - View all products\n" +
"/cart - View shopping cart\n" +
"/orders - View your orders\n" +
"/review - Review delivered products\n" +
"/reviews - View all product reviews\n" +
"/support - Chat with support\n" +
"/privacy - Privacy policy\n" +
"/clear - Clear shopping cart\n" +
"/cancel - Cancel current operation\n" +
"/delete - Delete all your data\n" +
"/help - Show this help message\n\n";
}
public static string FormatPrivacyPolicy()
{
return "🔒 *Privacy Policy*\n\n" +
"*Data Collection:*\n" +
"• We store minimal data necessary for orders\n" +
"• No personal identifiers are stored\n" +
"• All sessions are ephemeral by default\n\n" +
"*Data Retention:*\n" +
"• Ephemeral sessions: 30 minutes\n" +
"• Order data: As required for fulfillment\n" +
"• No tracking or analytics without consent\n\n" +
"*Your Rights:*\n" +
"• Delete all data at any time (/delete)\n" +
"• Use PGP encryption for shipping\n" +
"• Access via Tor network\n" +
"• Cryptocurrency payments only\n\n" +
"*Security:*\n" +
"• All data encrypted at rest\n" +
"• Optional Tor routing\n" +
"• No third-party tracking\n" +
"• Open source and auditable";
}
private static string FormatOrderStatus(int status)
{
return status switch
{
0 => "⏳ Pending Payment",
1 => "💰 Payment Received",
2 => "⚙️ Processing",
3 => "📦 Picking & Packing",
4 => "🚚 Shipped",
5 => "✅ Delivered",
6 => "❌ Cancelled",
7 => "💸 Refunded",
_ => $"Status {status}"
};
}
private static string FormatPaymentStatus(int status)
{
return status switch
{
0 => "⏳ Pending",
1 => "✅ Paid",
2 => "❌ Failed",
3 => "⏰ Expired",
4 => "❌ Cancelled",
_ => $"Status {status}"
};
}
private static string FormatCurrency(int currency)
{
return currency switch
{
0 => "BTC",
1 => "XMR",
2 => "USDT",
3 => "LTC",
4 => "ETH",
5 => "ZEC",
6 => "DASH",
7 => "DOGE",
_ => $"Currency {currency}"
};
}
}
}