Features Added: - Standard e-commerce properties (Price, Weight, shipping fields) - Order management with Create/Edit views and shipping information - ShippingRates system for weight-based shipping calculations - Comprehensive test coverage with JWT authentication tests - Sample data seeder with 5 orders demonstrating full workflow - Photo upload functionality for products - Multi-cryptocurrency payment support (BTC, XMR, USDT, etc.) Database Changes: - Added ShippingRates table - Added shipping fields to Orders (Name, Address, City, PostCode, Country) - Renamed properties to standard names (BasePrice to Price, ProductWeight to Weight) - Added UpdatedAt timestamps to models UI Improvements: - Added Create/Edit views for Orders - Added ShippingRates management UI - Updated navigation menu with Shipping option - Enhanced Order Details view with shipping information Sample Data: - 3 Categories (Electronics, Clothing, Books) - 5 Products with various prices - 5 Shipping rates (Royal Mail options) - 5 Orders in different statuses (Pending to Delivered) - 3 Crypto payments demonstrating payment flow Security: - All API endpoints secured with JWT authentication - No public endpoints - client apps must authenticate - Privacy-focused design with minimal data collection Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
176 lines
6.1 KiB
C#
176 lines
6.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;
|
|
|
|
public CryptoPaymentService(
|
|
LittleShopContext context,
|
|
IBTCPayServerService btcPayService,
|
|
ILogger<CryptoPaymentService> logger)
|
|
{
|
|
_context = context;
|
|
_btcPayService = btcPayService;
|
|
_logger = logger;
|
|
}
|
|
|
|
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"
|
|
);
|
|
|
|
// For now, generate a placeholder wallet address
|
|
// In a real implementation, this would come from BTCPay Server
|
|
var walletAddress = GenerateWalletAddress(currency);
|
|
|
|
var cryptoPayment = new CryptoPayment
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
OrderId = orderId,
|
|
Currency = currency,
|
|
WalletAddress = walletAddress,
|
|
RequiredAmount = order.TotalAmount, // This should be converted to crypto amount
|
|
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.FindAsync(payment.OrderId);
|
|
if (order != null)
|
|
{
|
|
order.Status = OrderStatus.PaymentReceived;
|
|
order.PaidAt = DateTime.UtcNow;
|
|
}
|
|
}
|
|
|
|
await _context.SaveChangesAsync();
|
|
|
|
_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 GenerateWalletAddress(CryptoCurrency currency)
|
|
{
|
|
// Placeholder wallet addresses - in production these would come from BTCPay Server
|
|
return currency switch
|
|
{
|
|
CryptoCurrency.BTC => "bc1q" + Guid.NewGuid().ToString("N")[..26],
|
|
CryptoCurrency.XMR => "4" + Guid.NewGuid().ToString("N")[..94],
|
|
CryptoCurrency.USDT => "0x" + Guid.NewGuid().ToString("N")[..38],
|
|
CryptoCurrency.LTC => "ltc1q" + Guid.NewGuid().ToString("N")[..26],
|
|
CryptoCurrency.ETH => "0x" + Guid.NewGuid().ToString("N")[..38],
|
|
_ => "placeholder_" + Guid.NewGuid().ToString("N")[..20]
|
|
};
|
|
}
|
|
} |