using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.SignalR; using LittleShop.Data; using LittleShop.Models; using LittleShop.DTOs; using LittleShop.Enums; using LittleShop.Hubs; namespace LittleShop.Services; public class CryptoPaymentService : ICryptoPaymentService { private readonly LittleShopContext _context; private readonly ISilverPayService _silverPayService; private readonly ILogger _logger; private readonly IConfiguration _configuration; private readonly IPushNotificationService _pushNotificationService; private readonly ITeleBotMessagingService _teleBotMessagingService; private readonly IHubContext _notificationHub; public CryptoPaymentService( LittleShopContext context, ISilverPayService silverPayService, ILogger logger, IConfiguration configuration, IPushNotificationService pushNotificationService, ITeleBotMessagingService teleBotMessagingService, IHubContext notificationHub) { _context = context; _silverPayService = silverPayService; _logger = logger; _configuration = configuration; _pushNotificationService = pushNotificationService; _teleBotMessagingService = teleBotMessagingService; _notificationHub = notificationHub; _logger.LogInformation("CryptoPaymentService initialized with SilverPAY and SignalR notifications"); } public async Task 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); } try { // Use SilverPAY _logger.LogInformation("Creating SilverPAY order for {Currency}", currency); // Generate payment ID first to use as external_id var paymentId = Guid.NewGuid(); var silverPayOrder = await _silverPayService.CreateOrderAsync( paymentId.ToString(), // Use unique payment ID instead of order ID order.TotalAmount, currency, $"Order #{order.Id} - {order.Items.Count} items", _configuration["SilverPay:DefaultWebhookUrl"] ); var cryptoPayment = new CryptoPayment { Id = paymentId, // Use the same payment ID OrderId = orderId, Currency = currency, WalletAddress = silverPayOrder.PaymentAddress, RequiredAmount = silverPayOrder.CryptoAmount ?? order.TotalAmount, PaidAmount = 0, Status = PaymentStatus.Pending, SilverPayOrderId = silverPayOrder.Id, CreatedAt = DateTime.UtcNow, ExpiresAt = DateTime.UtcNow.AddHours(24) }; _context.CryptoPayments.Add(cryptoPayment); await _context.SaveChangesAsync(); _logger.LogInformation("Created SilverPAY payment - Order: {OrderId}, Address: {Address}, Amount: {Amount} {Currency}", silverPayOrder.Id, cryptoPayment.WalletAddress, cryptoPayment.RequiredAmount, currency); return MapToDto(cryptoPayment); } catch (Exception ex) { _logger.LogError(ex, "Failed to create payment for order {OrderId}", orderId); throw new InvalidOperationException($"Failed to create payment: {ex.Message}", ex); } } public async Task> GetAllPaymentsAsync() { var payments = await _context.CryptoPayments .Include(cp => cp.Order) .OrderByDescending(cp => cp.CreatedAt) .ToListAsync(); return payments.Select(MapToDto); } public async Task> 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 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 ProcessSilverPayWebhookAsync(string orderId, PaymentStatus status, decimal amount, string? transactionHash = null, int confirmations = 0) { var payment = await _context.CryptoPayments .FirstOrDefaultAsync(cp => cp.SilverPayOrderId == orderId); if (payment == null) { _logger.LogWarning("Received SilverPAY webhook for unknown order {OrderId}", orderId); return false; } payment.Status = status; payment.PaidAmount = amount; payment.TransactionHash = transactionHash; // Load order for status updates var order = await _context.Orders .Include(o => o.Customer) .FirstOrDefaultAsync(o => o.Id == payment.OrderId); // Handle expired payments - auto-cancel the order if (status == PaymentStatus.Expired && order != null) { if (order.Status == OrderStatus.PendingPayment) { order.Status = OrderStatus.Cancelled; order.UpdatedAt = DateTime.UtcNow; _logger.LogInformation("Auto-cancelled order {OrderId} due to payment expiration", orderId); } } // Determine if payment is confirmed (ready to fulfill order) var isPaymentConfirmed = status == PaymentStatus.Paid || status == PaymentStatus.Overpaid || (status == PaymentStatus.Completed && confirmations >= 3); if (isPaymentConfirmed && order != null) { payment.PaidAt = DateTime.UtcNow; order.Status = OrderStatus.PaymentReceived; order.PaidAt = DateTime.UtcNow; } await _context.SaveChangesAsync(); // Send notification only when payment is confirmed and order is updated if (isPaymentConfirmed) { await SendPaymentConfirmedNotification(payment.OrderId, amount); } _logger.LogInformation("Processed SilverPAY webhook for order {OrderId}, status: {Status}, confirmations: {Confirmations}", orderId, status, confirmations); return true; } // Remove old BTCPay webhook processor public async Task ProcessPaymentWebhookAsync(string invoiceId, PaymentStatus status, decimal amount, string? transactionHash = null, int confirmations = 0) { // This method is kept for interface compatibility but redirects to SilverPAY return await ProcessSilverPayWebhookAsync(invoiceId, status, amount, transactionHash, confirmations); } 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, SilverPayOrderId = payment.SilverPayOrderId, TransactionHash = payment.TransactionHash, CreatedAt = payment.CreatedAt, PaidAt = payment.PaidAt, ExpiresAt = payment.ExpiresAt }; } 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 SignalR real-time notification to connected admin users await _notificationHub.Clients.All.SendAsync("ReceiveNotification", new { title = title, message = body, type = "payment", orderId = orderId, amount = amount, timestamp = DateTime.UtcNow, icon = "💰", url = $"/Admin/Orders/Details/{orderId}" }); // Send push notification to admin users (may not work with custom CA) await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body); // Send TeleBot message to customer await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId); _logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (SignalR + Push + Telegram)", orderId); } catch (Exception ex) { _logger.LogError(ex, "Failed to send payment confirmation notification for order {OrderId}", orderId); } } }