littleshop/LittleShop/Services/CryptoPaymentService.cs
SysAdmin 8b0e3e0611 Implement comprehensive notification system for LittleShop
- Add admin PWA push notifications for order management
- Integrate TeleBot customer messaging service
- Add push notification endpoints and VAPID key support
- Implement order status notifications throughout workflow
- Add notification UI components in admin panel
- Create TeleBotMessagingService for customer updates
- Add WebPush configuration to appsettings
- Fix compilation issues (BotStatus, BotContacts DbSet)
- Add comprehensive testing documentation

Features:
- Real-time admin notifications for new orders and status changes
- Customer order progress updates via TeleBot
- Graceful failure handling for notification services
- Test endpoints for notification system validation

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 16:17:24 +01:00

247 lines
9.1 KiB
C#

using Microsoft.EntityFrameworkCore;
using BTCPayServer.Client;
using BTCPayServer.Client.Models;
using LittleShop.Data;
using LittleShop.Models;
using LittleShop.DTOs;
using LittleShop.Enums;
namespace LittleShop.Services;
public class CryptoPaymentService : ICryptoPaymentService
{
private readonly LittleShopContext _context;
private readonly IBTCPayServerService _btcPayService;
private readonly ILogger<CryptoPaymentService> _logger;
private readonly IConfiguration _configuration;
private readonly IPushNotificationService _pushNotificationService;
private readonly ITeleBotMessagingService _teleBotMessagingService;
public CryptoPaymentService(
LittleShopContext context,
IBTCPayServerService btcPayService,
ILogger<CryptoPaymentService> logger,
IConfiguration configuration,
IPushNotificationService pushNotificationService,
ITeleBotMessagingService teleBotMessagingService)
{
_context = context;
_btcPayService = btcPayService;
_logger = logger;
_configuration = configuration;
_pushNotificationService = pushNotificationService;
_teleBotMessagingService = teleBotMessagingService;
}
public async Task<CryptoPaymentDto> CreatePaymentAsync(Guid orderId, CryptoCurrency currency)
{
var order = await _context.Orders
.Include(o => o.Items)
.ThenInclude(oi => oi.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
if (order == null)
throw new ArgumentException("Order not found", nameof(orderId));
// Check if payment already exists for this currency
var existingPayment = await _context.CryptoPayments
.FirstOrDefaultAsync(cp => cp.OrderId == orderId && cp.Currency == currency && cp.Status != PaymentStatus.Expired);
if (existingPayment != null)
{
return MapToDto(existingPayment);
}
// Create BTCPay Server invoice
var invoiceId = await _btcPayService.CreateInvoiceAsync(
order.TotalAmount,
currency,
order.Id.ToString(),
$"Order #{order.Id} - {order.Items.Count} items"
);
// Get the real wallet address from BTCPay Server
var invoice = await _btcPayService.GetInvoiceAsync(invoiceId);
if (invoice == null)
{
throw new InvalidOperationException($"Failed to retrieve invoice {invoiceId} from BTCPay Server");
}
// Extract the wallet address from the invoice
string walletAddress;
decimal cryptoAmount = 0;
try
{
// BTCPay Server v2 uses CheckoutLink for payment
// The actual wallet addresses are managed internally by BTCPay
// Customers should use the CheckoutLink to make payments
walletAddress = invoice.CheckoutLink ?? $"https://{_configuration["BTCPayServer:BaseUrl"]}/i/{invoiceId}";
// For display purposes, we can show the checkout link
// BTCPay handles all the wallet address generation internally
_logger.LogInformation("Created payment for {Currency} - Invoice: {InvoiceId}, Checkout: {CheckoutLink}",
currency, invoiceId, walletAddress);
// Set the amount from the invoice (will be in fiat)
cryptoAmount = invoice.Amount > 0 ? invoice.Amount : order.TotalAmount;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing invoice {InvoiceId}", invoiceId);
// Fallback to a generated checkout link
walletAddress = $"https://{_configuration["BTCPayServer:BaseUrl"]}/i/{invoiceId}";
cryptoAmount = order.TotalAmount;
}
var cryptoPayment = new CryptoPayment
{
Id = Guid.NewGuid(),
OrderId = orderId,
Currency = currency,
WalletAddress = walletAddress,
RequiredAmount = cryptoAmount > 0 ? cryptoAmount : order.TotalAmount, // Use crypto amount if available
PaidAmount = 0,
Status = PaymentStatus.Pending,
BTCPayInvoiceId = invoiceId, // This is the actual BTCPay invoice ID
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(24)
};
_context.CryptoPayments.Add(cryptoPayment);
await _context.SaveChangesAsync();
_logger.LogInformation("Created crypto payment {PaymentId} for order {OrderId} with currency {Currency}",
cryptoPayment.Id, orderId, currency);
return MapToDto(cryptoPayment);
}
public async Task<IEnumerable<CryptoPaymentDto>> GetPaymentsByOrderAsync(Guid orderId)
{
var payments = await _context.CryptoPayments
.Where(cp => cp.OrderId == orderId)
.OrderByDescending(cp => cp.CreatedAt)
.ToListAsync();
return payments.Select(MapToDto);
}
public async Task<PaymentStatusDto> GetPaymentStatusAsync(Guid paymentId)
{
var payment = await _context.CryptoPayments.FindAsync(paymentId);
if (payment == null)
throw new ArgumentException("Payment not found", nameof(paymentId));
return new PaymentStatusDto
{
PaymentId = payment.Id,
Status = payment.Status,
RequiredAmount = payment.RequiredAmount,
PaidAmount = payment.PaidAmount,
PaidAt = payment.PaidAt,
ExpiresAt = payment.ExpiresAt
};
}
public async Task<bool> ProcessPaymentWebhookAsync(string invoiceId, PaymentStatus status, decimal amount, string? transactionHash = null)
{
var payment = await _context.CryptoPayments
.FirstOrDefaultAsync(cp => cp.BTCPayInvoiceId == invoiceId);
if (payment == null)
{
_logger.LogWarning("Received webhook for unknown invoice {InvoiceId}", invoiceId);
return false;
}
payment.Status = status;
payment.PaidAmount = amount;
payment.TransactionHash = transactionHash;
if (status == PaymentStatus.Paid)
{
payment.PaidAt = DateTime.UtcNow;
// Update order status
var order = await _context.Orders
.Include(o => o.Customer)
.FirstOrDefaultAsync(o => o.Id == payment.OrderId);
if (order != null)
{
order.Status = OrderStatus.PaymentReceived;
order.PaidAt = DateTime.UtcNow;
}
}
await _context.SaveChangesAsync();
// Send notification for payment confirmation
if (status == PaymentStatus.Paid)
{
await SendPaymentConfirmedNotification(payment.OrderId, amount);
}
_logger.LogInformation("Processed payment webhook for invoice {InvoiceId}, status: {Status}",
invoiceId, status);
return true;
}
private static CryptoPaymentDto MapToDto(CryptoPayment payment)
{
return new CryptoPaymentDto
{
Id = payment.Id,
OrderId = payment.OrderId,
Currency = payment.Currency,
WalletAddress = payment.WalletAddress,
RequiredAmount = payment.RequiredAmount,
PaidAmount = payment.PaidAmount,
Status = payment.Status,
BTCPayInvoiceId = payment.BTCPayInvoiceId,
TransactionHash = payment.TransactionHash,
CreatedAt = payment.CreatedAt,
PaidAt = payment.PaidAt,
ExpiresAt = payment.ExpiresAt
};
}
private static string GetPaymentMethodId(CryptoCurrency currency)
{
return currency switch
{
CryptoCurrency.BTC => "BTC",
CryptoCurrency.XMR => "XMR",
CryptoCurrency.USDT => "USDT",
CryptoCurrency.LTC => "LTC",
CryptoCurrency.ETH => "ETH",
CryptoCurrency.ZEC => "ZEC",
CryptoCurrency.DASH => "DASH",
CryptoCurrency.DOGE => "DOGE",
_ => "BTC"
};
}
private async Task SendPaymentConfirmedNotification(Guid orderId, decimal amount)
{
try
{
var title = "💰 Payment Confirmed";
var body = $"Order #{orderId.ToString()[..8]} payment of £{amount:F2} confirmed. Ready for acceptance.";
// Send push notification to admin users
await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body);
// Send TeleBot message to customer
await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId);
_logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (Admin + Customer)", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send payment confirmation notification for order {OrderId}", orderId);
}
}
}