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 _logger; private readonly IConfiguration _configuration; public CryptoPaymentService( LittleShopContext context, IBTCPayServerService btcPayService, ILogger logger, IConfiguration configuration) { _context = context; _btcPayService = btcPayService; _logger = logger; _configuration = configuration; } 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); } // 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> 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 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.FindAsync(payment.OrderId); if (order != null) { order.Status = OrderStatus.PaymentReceived; order.PaidAt = DateTime.UtcNow; } } await _context.SaveChangesAsync(); _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" }; } }