littleshop/LittleShop/Services/CryptoPaymentService.cs
SysAdmin a2247d7c02
Some checks failed
Build and Deploy LittleShop / Build TeleBot Docker Image (push) Failing after 11s
Build and Deploy LittleShop / Build LittleShop Docker Image (push) Failing after 15s
Build and Deploy LittleShop / Deploy to Production VPS (Manual Only) (push) Has been skipped
Build and Deploy LittleShop / Deploy to Pre-Production (CT109) (push) Has been skipped
feat: Add customer management, payments, and push notifications with security enhancements
Major Feature Additions:
- Customer management: Full CRUD with data export and privacy compliance
- Payment management: Centralized payment tracking and administration
- Push notification subscriptions: Manage and track web push subscriptions

Security Enhancements:
- IP whitelist middleware for administrative endpoints
- Data retention service with configurable policies
- Enhanced push notification security documentation
- Security fixes progress tracking (2025-11-14)

UI/UX Improvements:
- Enhanced navigation with improved mobile responsiveness
- Updated admin dashboard with order status counts
- Improved product CRUD forms
- New customer and payment management interfaces

Backend Improvements:
- Extended customer service with data export capabilities
- Enhanced order service with status count queries
- Improved crypto payment service with better error handling
- Updated validators and configuration

Documentation:
- DEPLOYMENT_NGINX_GUIDE.md: Nginx deployment instructions
- IP_STORAGE_ANALYSIS.md: IP storage security analysis
- PUSH_NOTIFICATION_SECURITY.md: Push notification security guide
- UI_UX_IMPROVEMENT_PLAN.md: Planned UI/UX enhancements
- UI_UX_IMPROVEMENTS_COMPLETED.md: Completed improvements

Cleanup:
- Removed temporary database WAL files
- Removed stale commit message file

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 19:33:02 +00:00

259 lines
10 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.SignalR;
using LittleShop.Data;
using LittleShop.Models;
using LittleShop.DTOs;
using LittleShop.Enums;
using LittleShop.Hubs;
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;
private readonly IHubContext<NotificationHub> _notificationHub;
public CryptoPaymentService(
LittleShopContext context,
ISilverPayService silverPayService,
ILogger<CryptoPaymentService> logger,
IConfiguration configuration,
IPushNotificationService pushNotificationService,
ITeleBotMessagingService teleBotMessagingService,
IHubContext<NotificationHub> notificationHub)
{
_context = context;
_silverPayService = silverPayService;
_logger = logger;
_configuration = configuration;
_pushNotificationService = pushNotificationService;
_teleBotMessagingService = teleBotMessagingService;
_notificationHub = notificationHub;
_logger.LogInformation("CryptoPaymentService initialized with SilverPAY and SignalR notifications");
}
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>> GetAllPaymentsAsync()
{
var payments = await _context.CryptoPayments
.Include(cp => cp.Order)
.OrderByDescending(cp => cp.CreatedAt)
.ToListAsync();
return payments.Select(MapToDto);
}
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 SignalR real-time notification to connected admin users
await _notificationHub.Clients.All.SendAsync("ReceiveNotification", new
{
title = title,
message = body,
type = "payment",
orderId = orderId,
amount = amount,
timestamp = DateTime.UtcNow,
icon = "💰",
url = $"/Admin/Orders/Details/{orderId}"
});
// Send push notification to admin users (may not work with custom CA)
await _pushNotificationService.SendOrderNotificationAsync(orderId, title, body);
// Send TeleBot message to customer
await _teleBotMessagingService.SendPaymentConfirmedAsync(orderId);
_logger.LogInformation("Sent payment confirmation notifications for order {OrderId} (SignalR + Push + Telegram)", orderId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send payment confirmation notification for order {OrderId}", orderId);
}
}
}