Initial commit of LittleShop project (excluding large archives)
- 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>
This commit is contained in:
@@ -1,180 +1,180 @@
|
||||
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]
|
||||
};
|
||||
}
|
||||
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]
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user