littleshop/LittleShop/Services/CryptoPaymentService.cs
SysAdmin 6c95ed3145 Fix: Add TeleBot integration and expired payment handling
TeleBot Configuration:
- Added TeleBot API URL and API key to docker-compose.yml
- Configured to connect to telebot-service:5000 internally
- Enables customer notifications via Telegram bot

Expired Payment Handling:
- Auto-cancel orders when payment status is Expired
- Only cancels orders in PendingPayment status
- Logs cancellation for audit trail

Customer View Improvements:
- Hide cancelled orders from customer order lists
- Filters applied to both GetOrdersByIdentityAsync and GetOrdersByCustomerIdAsync
- Prevents confusion from displaying cancelled/expired orders

This resolves:
- No notifications to customers (TeleBot not configured)
- No notifications to admin (TeleBot connection failed)
- Expired orders remaining visible to customers
- Orders not auto-cancelled when payment expires

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 16:55:19 +01:00

231 lines
9.0 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;
// Load order for status updates
var order = await _context.Orders
.Include(o => o.Customer)
.FirstOrDefaultAsync(o => o.Id == payment.OrderId);
// Handle expired payments - auto-cancel the order
if (status == PaymentStatus.Expired && order != null)
{
if (order.Status == OrderStatus.PendingPayment)
{
order.Status = OrderStatus.Cancelled;
order.UpdatedAt = DateTime.UtcNow;
_logger.LogInformation("Auto-cancelled order {OrderId} due to payment expiration", orderId);
}
}
// Determine if payment is confirmed (ready to fulfill order)
var isPaymentConfirmed = status == PaymentStatus.Paid ||
status == PaymentStatus.Overpaid ||
(status == PaymentStatus.Completed && confirmations >= 3);
if (isPaymentConfirmed && order != null)
{
payment.PaidAt = DateTime.UtcNow;
order.Status = OrderStatus.PaymentReceived;
order.PaidAt = DateTime.UtcNow;
}
await _context.SaveChangesAsync();
// Send notification only when payment is confirmed and order is updated
if (isPaymentConfirmed)
{
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, int confirmations = 0)
{
// This method is kept for interface compatibility but redirects to SilverPAY
return await ProcessSilverPayWebhookAsync(invoiceId, status, amount, transactionHash, confirmations);
}
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);
}
}
}