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; public CryptoPaymentService( LittleShopContext context, IBTCPayServerService btcPayService, ILogger logger) { _context = context; _btcPayService = btcPayService; _logger = logger; } 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" ); // For now, generate a placeholder wallet address // In a real implementation, this would come from BTCPay Server var walletAddress = GenerateWalletAddress(currency); var cryptoPayment = new CryptoPayment { Id = Guid.NewGuid(), OrderId = orderId, Currency = currency, WalletAddress = walletAddress, RequiredAmount = order.TotalAmount, // This should be converted to crypto amount 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 GenerateWalletAddress(CryptoCurrency currency) { // Placeholder wallet addresses - in production these would come from BTCPay Server var guid = Guid.NewGuid().ToString("N"); // 32 characters return currency switch { CryptoCurrency.BTC => "bc1q" + guid[..26], CryptoCurrency.XMR => "4" + guid + guid[..32], // XMR needs ~95 chars, use double GUID CryptoCurrency.USDT => "0x" + guid[..32], // ERC-20 address CryptoCurrency.LTC => "ltc1q" + guid[..26], CryptoCurrency.ETH => "0x" + guid[..32], CryptoCurrency.ZEC => "zs1" + guid + guid[..29], // Shielded address CryptoCurrency.DASH => "X" + guid[..30], CryptoCurrency.DOGE => "D" + guid[..30], _ => "placeholder_" + guid[..20] }; } }