- BTCPay Server integration - TeleBot Telegram bot - Review system - Admin area - Docker deployment configuration 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
180 lines
6.4 KiB
C#
180 lines
6.4 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;
|
|
|
|
public CryptoPaymentService(
|
|
LittleShopContext context,
|
|
IBTCPayServerService btcPayService,
|
|
ILogger<CryptoPaymentService> logger)
|
|
{
|
|
_context = context;
|
|
_btcPayService = btcPayService;
|
|
_logger = logger;
|
|
}
|
|
|
|
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"
|
|
);
|
|
|
|
// 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<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 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]
|
|
};
|
|
}
|
|
} |