- Add admin PWA push notifications for order management
- Integrate TeleBot customer messaging service
- Add push notification endpoints and VAPID key support
- Implement order status notifications throughout workflow
- Add notification UI components in admin panel
- Create TeleBotMessagingService for customer updates
- Add WebPush configuration to appsettings
- Fix compilation issues (BotStatus, BotContacts DbSet)
- Add comprehensive testing documentation
Features:
- Real-time admin notifications for new orders and status changes
- Customer order progress updates via TeleBot
- Graceful failure handling for notification services
- Test endpoints for notification system validation
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
247 lines
9.1 KiB
C#
247 lines
9.1 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;
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IPushNotificationService _pushNotificationService;
|
|
private readonly ITeleBotMessagingService _teleBotMessagingService;
|
|
|
|
public CryptoPaymentService(
|
|
LittleShopContext context,
|
|
IBTCPayServerService btcPayService,
|
|
ILogger<CryptoPaymentService> logger,
|
|
IConfiguration configuration,
|
|
IPushNotificationService pushNotificationService,
|
|
ITeleBotMessagingService teleBotMessagingService)
|
|
{
|
|
_context = context;
|
|
_btcPayService = btcPayService;
|
|
_logger = logger;
|
|
_configuration = configuration;
|
|
_pushNotificationService = pushNotificationService;
|
|
_teleBotMessagingService = teleBotMessagingService;
|
|
}
|
|
|
|
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"
|
|
);
|
|
|
|
// Get the real wallet address from BTCPay Server
|
|
var invoice = await _btcPayService.GetInvoiceAsync(invoiceId);
|
|
if (invoice == null)
|
|
{
|
|
throw new InvalidOperationException($"Failed to retrieve invoice {invoiceId} from BTCPay Server");
|
|
}
|
|
|
|
// Extract the wallet address from the invoice
|
|
string walletAddress;
|
|
decimal cryptoAmount = 0;
|
|
|
|
try
|
|
{
|
|
// BTCPay Server v2 uses CheckoutLink for payment
|
|
// The actual wallet addresses are managed internally by BTCPay
|
|
// Customers should use the CheckoutLink to make payments
|
|
walletAddress = invoice.CheckoutLink ?? $"https://{_configuration["BTCPayServer:BaseUrl"]}/i/{invoiceId}";
|
|
|
|
// For display purposes, we can show the checkout link
|
|
// BTCPay handles all the wallet address generation internally
|
|
_logger.LogInformation("Created payment for {Currency} - Invoice: {InvoiceId}, Checkout: {CheckoutLink}",
|
|
currency, invoiceId, walletAddress);
|
|
|
|
// Set the amount from the invoice (will be in fiat)
|
|
cryptoAmount = invoice.Amount > 0 ? invoice.Amount : order.TotalAmount;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error processing invoice {InvoiceId}", invoiceId);
|
|
|
|
// Fallback to a generated checkout link
|
|
walletAddress = $"https://{_configuration["BTCPayServer:BaseUrl"]}/i/{invoiceId}";
|
|
cryptoAmount = order.TotalAmount;
|
|
}
|
|
|
|
var cryptoPayment = new CryptoPayment
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OrderId = orderId,
|
|
Currency = currency,
|
|
WalletAddress = walletAddress,
|
|
RequiredAmount = cryptoAmount > 0 ? cryptoAmount : order.TotalAmount, // Use crypto amount if available
|
|
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
|
|
.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)
|
|
{
|
|
await SendPaymentConfirmedNotification(payment.OrderId, amount);
|
|
}
|
|
|
|
_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 GetPaymentMethodId(CryptoCurrency currency)
|
|
{
|
|
return currency switch
|
|
{
|
|
CryptoCurrency.BTC => "BTC",
|
|
CryptoCurrency.XMR => "XMR",
|
|
CryptoCurrency.USDT => "USDT",
|
|
CryptoCurrency.LTC => "LTC",
|
|
CryptoCurrency.ETH => "ETH",
|
|
CryptoCurrency.ZEC => "ZEC",
|
|
CryptoCurrency.DASH => "DASH",
|
|
CryptoCurrency.DOGE => "DOGE",
|
|
_ => "BTC"
|
|
};
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
} |