- 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>
218 lines
8.3 KiB
C#
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);
|
|
}
|
|
}
|
|
} |