- Fixed invoice creation to use GBP (fiat) instead of cryptocurrency amounts - BTCPay Server now handles automatic crypto conversion - Updated payment flow to use checkout links instead of raw wallet addresses - Added comprehensive logging for debugging payment issues - Created diagnostic endpoints for testing BTCPay connection and payments - Added documentation for deployment and troubleshooting The key issue was that BTCPay v2 manages wallet addresses internally and provides checkout links for customers to complete payments, rather than exposing raw crypto addresses. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
212 lines
7.6 KiB
C#
212 lines
7.6 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;
|
|
|
|
public CryptoPaymentService(
|
|
LittleShopContext context,
|
|
IBTCPayServerService btcPayService,
|
|
ILogger<CryptoPaymentService> logger,
|
|
IConfiguration configuration)
|
|
{
|
|
_context = context;
|
|
_btcPayService = btcPayService;
|
|
_logger = logger;
|
|
_configuration = configuration;
|
|
}
|
|
|
|
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.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"
|
|
};
|
|
}
|
|
} |