littleshop/LittleShop/Services/CryptoPaymentService.cs
SysAdmin 5138242a99 Fix SilverPay payment integration JSON serialization
- Changed JSON naming policy from CamelCase to SnakeCaseLower for SilverPay API compatibility
- Fixed field name from 'fiat_amount' to 'amount' in request body
- Used unique payment ID instead of order ID to avoid duplicate external_id conflicts
- Modified SilverPayApiResponse to handle string amounts from API
- Added [JsonIgnore] attributes to computed properties to prevent JSON serialization conflicts
- Fixed test compilation errors (mock service and enum casting issues)
- Updated SilverPay endpoint to http://10.0.0.52:8001/

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-22 04:38:28 +01:00

218 lines
8.3 KiB
C#

using Microsoft.EntityFrameworkCore;
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 ISilverPayService _silverPayService;
private readonly ILogger<CryptoPaymentService> _logger;
private readonly IConfiguration _configuration;
private readonly IPushNotificationService _pushNotificationService;
private readonly ITeleBotMessagingService _teleBotMessagingService;
public CryptoPaymentService(
LittleShopContext context,
ISilverPayService silverPayService,
ILogger<CryptoPaymentService> logger,
IConfiguration configuration,
IPushNotificationService pushNotificationService,
ITeleBotMessagingService teleBotMessagingService)
{
_context = context;
_silverPayService = silverPayService;
_logger = logger;
_configuration = configuration;
_pushNotificationService = pushNotificationService;
_teleBotMessagingService = teleBotMessagingService;
_logger.LogInformation("CryptoPaymentService initialized with SilverPAY");
}
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);
}
try
{
// Use SilverPAY
_logger.LogInformation("Creating SilverPAY order for {Currency}", currency);
// Generate payment ID first to use as external_id
var paymentId = Guid.NewGuid();
var silverPayOrder = await _silverPayService.CreateOrderAsync(
paymentId.ToString(), // Use unique payment ID instead of order ID
order.TotalAmount,
currency,
$"Order #{order.Id} - {order.Items.Count} items",
_configuration["SilverPay:DefaultWebhookUrl"]
);
var cryptoPayment = new CryptoPayment
{
Id = paymentId, // Use the same payment ID
OrderId = orderId,
Currency = currency,
WalletAddress = silverPayOrder.PaymentAddress,
RequiredAmount = silverPayOrder.CryptoAmount ?? order.TotalAmount,
PaidAmount = 0,
Status = PaymentStatus.Pending,
SilverPayOrderId = silverPayOrder.Id,
CreatedAt = DateTime.UtcNow,
ExpiresAt = DateTime.UtcNow.AddHours(24)
};
_context.CryptoPayments.Add(cryptoPayment);
await _context.SaveChangesAsync();
_logger.LogInformation("Created SilverPAY payment - Order: {OrderId}, Address: {Address}, Amount: {Amount} {Currency}",
silverPayOrder.Id, cryptoPayment.WalletAddress, cryptoPayment.RequiredAmount, currency);
return MapToDto(cryptoPayment);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create payment for order {OrderId}", orderId);
throw new InvalidOperationException($"Failed to create payment: {ex.Message}", ex);
}
}
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> ProcessSilverPayWebhookAsync(string orderId, PaymentStatus status, decimal amount, string? transactionHash = null, int confirmations = 0)
{
var payment = await _context.CryptoPayments
.FirstOrDefaultAsync(cp => cp.SilverPayOrderId == orderId);
if (payment == null)
{
_logger.LogWarning("Received SilverPAY webhook for unknown order {OrderId}", orderId);
return false;
}
payment.Status = status;
payment.PaidAmount = amount;
payment.TransactionHash = transactionHash;
if (status == PaymentStatus.Paid || (status == PaymentStatus.Completed && confirmations >= 3))
{
payment.PaidAt = DateTime.UtcNow;
// Update order status
var order = await _context.Orders
.Include(o => o.Customer)
.FirstOrDefaultAsync(o => o.Id == payment.OrderId);
if (order != null)
{
order.Status = OrderStatus.PaymentReceived;
order.PaidAt = DateTime.UtcNow;
}
}
await _context.SaveChangesAsync();
// Send notification for payment confirmation
if (status == PaymentStatus.Paid || status == PaymentStatus.Completed)
{
await SendPaymentConfirmedNotification(payment.OrderId, amount);
}
_logger.LogInformation("Processed SilverPAY webhook for order {OrderId}, status: {Status}, confirmations: {Confirmations}",
orderId, status, confirmations);
return true;
}
// Remove old BTCPay webhook processor
public async Task<bool> ProcessPaymentWebhookAsync(string invoiceId, PaymentStatus status, decimal amount, string? transactionHash = null)
{
// This method is kept for interface compatibility but redirects to SilverPAY
return await ProcessSilverPayWebhookAsync(invoiceId, status, amount, transactionHash);
}
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,
SilverPayOrderId = payment.SilverPayOrderId,
TransactionHash = payment.TransactionHash,
CreatedAt = payment.CreatedAt,
PaidAt = payment.PaidAt,
ExpiresAt = payment.ExpiresAt
};
}
private async Task SendPaymentConfirmedNotification(Guid orderId, decimal amount)
{
try
{
var title = "💰 Payment Confirmed";
var body = $"Order #{orderId.ToString()[..8]} payment of £{amount:F2} confirmed. Ready for acceptance.";
// Send push notification to admin users
await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body);
// Send TeleBot message to customer
await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId);
_logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (Admin + Customer)", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send payment confirmation notification for order {OrderId}", orderId);
}
}
}