Refactor payment verification to manual workflow and add comprehensive cleanup tools
Major changes: • Remove BTCPay Server integration in favor of SilverPAY manual verification • Add test data cleanup mechanisms (API endpoints and shell scripts) • Fix compilation errors in TestController (IdentityReference vs CustomerIdentity) • Add deployment automation scripts for Hostinger VPS • Enhance integration testing with comprehensive E2E validation • Add Blazor components and mobile-responsive CSS for admin interface • Create production environment configuration scripts Key Features Added: • Manual payment verification through Admin panel Order Details • Bulk test data cleanup with proper cascade handling • Deployment automation with systemd service configuration • Comprehensive E2E testing suite with SilverPAY integration validation • Mobile-first admin interface improvements Security & Production: • Environment variable configuration for production secrets • Proper JWT and VAPID key management • SilverPAY API integration with live credentials • Database cleanup and maintenance tools 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,279 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/btcpay-test")]
|
||||
[Authorize(AuthenticationSchemes = "Cookies", Roles = "Admin")]
|
||||
public class BTCPayTestController : ControllerBase
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IBTCPayServerService _btcPayService;
|
||||
private readonly ILogger<BTCPayTestController> _logger;
|
||||
|
||||
public BTCPayTestController(
|
||||
IConfiguration configuration,
|
||||
IBTCPayServerService btcPayService,
|
||||
ILogger<BTCPayTestController> logger)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_btcPayService = btcPayService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet("connection")]
|
||||
public async Task<IActionResult> TestConnection()
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = _configuration["BTCPayServer:BaseUrl"];
|
||||
var apiKey = _configuration["BTCPayServer:ApiKey"];
|
||||
|
||||
if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
return BadRequest(new { error = "BTCPay Server configuration missing" });
|
||||
}
|
||||
|
||||
// Create HttpClient with certificate bypass for internal networks
|
||||
var httpClient = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
|
||||
});
|
||||
|
||||
var client = new BTCPayServerClient(new Uri(baseUrl), apiKey, httpClient);
|
||||
|
||||
// Test basic connection by getting server info
|
||||
var serverInfo = await client.GetServerInfo();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
status = "Connected",
|
||||
baseUrl = baseUrl,
|
||||
serverVersion = serverInfo?.Version,
|
||||
supportedPaymentMethods = serverInfo?.SupportedPaymentMethods,
|
||||
message = "BTCPay Server connection successful"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
error = ex.Message,
|
||||
type = ex.GetType().Name,
|
||||
baseUrl = _configuration["BTCPayServer:BaseUrl"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("stores")]
|
||||
public async Task<IActionResult> GetStores()
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = _configuration["BTCPayServer:BaseUrl"];
|
||||
var apiKey = _configuration["BTCPayServer:ApiKey"];
|
||||
|
||||
// Create HttpClient with certificate bypass for internal networks
|
||||
var httpClient = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
|
||||
});
|
||||
|
||||
var client = new BTCPayServerClient(new Uri(baseUrl), apiKey, httpClient);
|
||||
|
||||
// Get available stores
|
||||
var stores = await client.GetStores();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
stores = stores.Select(s => new
|
||||
{
|
||||
id = s.Id,
|
||||
name = s.Name,
|
||||
website = s.Website,
|
||||
defaultCurrency = s.DefaultCurrency
|
||||
}).ToList(),
|
||||
message = "Stores retrieved successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
error = ex.Message,
|
||||
type = ex.GetType().Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("invoice/{invoiceId}")]
|
||||
public async Task<IActionResult> GetInvoiceDetails(string invoiceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoice = await _btcPayService.GetInvoiceAsync(invoiceId);
|
||||
|
||||
if (invoice == null)
|
||||
{
|
||||
return NotFound(new { error = "Invoice not found" });
|
||||
}
|
||||
|
||||
// BTCPay Server v2 manages addresses internally
|
||||
// Customers use the CheckoutLink for payments
|
||||
var paymentInfo = new
|
||||
{
|
||||
checkoutMethod = "BTCPay Checkout",
|
||||
info = "Use the checkout link to complete payment"
|
||||
};
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
invoiceId = invoice.Id,
|
||||
status = invoice.Status,
|
||||
amount = invoice.Amount,
|
||||
currency = invoice.Currency,
|
||||
checkoutLink = invoice.CheckoutLink,
|
||||
expiresAt = invoice.ExpirationTime,
|
||||
paymentMethods = paymentInfo,
|
||||
metadata = invoice.Metadata,
|
||||
message = "Invoice details retrieved successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to get invoice {InvoiceId}", invoiceId);
|
||||
return StatusCode(500, new
|
||||
{
|
||||
error = ex.Message,
|
||||
type = ex.GetType().Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("test-invoice")]
|
||||
public async Task<IActionResult> CreateTestInvoice([FromBody] TestInvoiceRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseUrl = _configuration["BTCPayServer:BaseUrl"];
|
||||
var apiKey = _configuration["BTCPayServer:ApiKey"];
|
||||
var storeId = _configuration["BTCPayServer:StoreId"];
|
||||
|
||||
if (string.IsNullOrEmpty(storeId))
|
||||
{
|
||||
return BadRequest(new { error = "Store ID not configured" });
|
||||
}
|
||||
|
||||
// Create HttpClient with certificate bypass for internal networks
|
||||
var httpClient = new HttpClient(new HttpClientHandler()
|
||||
{
|
||||
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
|
||||
});
|
||||
|
||||
var client = new BTCPayServerClient(new Uri(baseUrl), apiKey, httpClient);
|
||||
|
||||
// Create test invoice
|
||||
var invoiceRequest = new CreateInvoiceRequest
|
||||
{
|
||||
Amount = request.Amount,
|
||||
Currency = request.Currency ?? "GBP",
|
||||
Metadata = JObject.FromObject(new
|
||||
{
|
||||
orderId = $"test-{Guid.NewGuid()}",
|
||||
source = "LittleShop-Test"
|
||||
})
|
||||
};
|
||||
|
||||
var invoice = await client.CreateInvoice(storeId, invoiceRequest);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
status = "Invoice Created",
|
||||
invoiceId = invoice.Id,
|
||||
amount = invoice.Amount,
|
||||
currency = invoice.Currency,
|
||||
checkoutLink = invoice.CheckoutLink,
|
||||
expiresAt = invoice.ExpirationTime,
|
||||
message = "Test invoice created successfully"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(500, new
|
||||
{
|
||||
error = ex.Message,
|
||||
type = ex.GetType().Name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("test-payment")]
|
||||
public async Task<IActionResult> CreateTestPayment([FromBody] TestPaymentRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a test order ID
|
||||
var testOrderId = $"test-order-{Guid.NewGuid():N}".Substring(0, 20);
|
||||
|
||||
_logger.LogInformation("Creating test payment for {Currency} with amount {Amount} GBP",
|
||||
request.CryptoCurrency, request.Amount);
|
||||
|
||||
// Use the actual service to create an invoice
|
||||
var invoiceId = await _btcPayService.CreateInvoiceAsync(
|
||||
request.Amount,
|
||||
request.CryptoCurrency,
|
||||
testOrderId,
|
||||
"Test payment from BTCPay diagnostic endpoint"
|
||||
);
|
||||
|
||||
// Get the invoice details
|
||||
var invoice = await _btcPayService.GetInvoiceAsync(invoiceId);
|
||||
|
||||
// BTCPay Server v2 uses checkout links instead of exposing raw addresses
|
||||
var checkoutUrl = invoice?.CheckoutLink;
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
status = "Success",
|
||||
invoiceId = invoiceId,
|
||||
orderId = testOrderId,
|
||||
amount = request.Amount,
|
||||
currency = "GBP",
|
||||
requestedCrypto = request.CryptoCurrency.ToString(),
|
||||
checkoutLink = checkoutUrl,
|
||||
paymentUrl = checkoutUrl ?? $"https://{_configuration["BTCPayServer:BaseUrl"]}/i/{invoiceId}",
|
||||
message = !string.IsNullOrEmpty(checkoutUrl)
|
||||
? "✅ Test payment created successfully - Use checkout link to complete payment"
|
||||
: "⚠️ Invoice created but checkout link not available - Check BTCPay configuration"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create test payment");
|
||||
return StatusCode(500, new
|
||||
{
|
||||
error = ex.Message,
|
||||
type = ex.GetType().Name,
|
||||
hint = "Check that BTCPay Server has wallets configured for the requested currency"
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class TestInvoiceRequest
|
||||
{
|
||||
public decimal Amount { get; set; } = 0.01m;
|
||||
public string? Currency { get; set; } = "GBP";
|
||||
}
|
||||
|
||||
public class TestPaymentRequest
|
||||
{
|
||||
public decimal Amount { get; set; } = 10.00m;
|
||||
public CryptoCurrency CryptoCurrency { get; set; } = CryptoCurrency.BTC;
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Services;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/btcpay")]
|
||||
public class BTCPayWebhookController : ControllerBase
|
||||
{
|
||||
private readonly ICryptoPaymentService _cryptoPaymentService;
|
||||
private readonly IBTCPayServerService _btcPayService;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<BTCPayWebhookController> _logger;
|
||||
|
||||
public BTCPayWebhookController(
|
||||
ICryptoPaymentService cryptoPaymentService,
|
||||
IBTCPayServerService btcPayService,
|
||||
IConfiguration configuration,
|
||||
ILogger<BTCPayWebhookController> logger)
|
||||
{
|
||||
_cryptoPaymentService = cryptoPaymentService;
|
||||
_btcPayService = btcPayService;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpPost("webhook")]
|
||||
public async Task<IActionResult> ProcessWebhook()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Read the raw request body
|
||||
using var reader = new StreamReader(Request.Body);
|
||||
var requestBody = await reader.ReadToEndAsync();
|
||||
|
||||
// Get webhook signature from headers
|
||||
var signature = Request.Headers["BTCPAY-SIG"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(signature))
|
||||
{
|
||||
_logger.LogWarning("Webhook received without signature");
|
||||
return BadRequest("Missing webhook signature");
|
||||
}
|
||||
|
||||
// Validate webhook signature
|
||||
var webhookSecret = _configuration["BTCPayServer:WebhookSecret"];
|
||||
if (string.IsNullOrEmpty(webhookSecret))
|
||||
{
|
||||
_logger.LogError("BTCPay webhook secret not configured");
|
||||
return StatusCode(500, "Webhook validation not configured");
|
||||
}
|
||||
|
||||
if (!ValidateWebhookSignature(requestBody, signature, webhookSecret))
|
||||
{
|
||||
_logger.LogWarning("Invalid webhook signature");
|
||||
return BadRequest("Invalid webhook signature");
|
||||
}
|
||||
|
||||
// Parse webhook data
|
||||
var webhookData = JsonSerializer.Deserialize<BTCPayWebhookDto>(requestBody, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (webhookData == null)
|
||||
{
|
||||
_logger.LogWarning("Unable to parse webhook data");
|
||||
return BadRequest("Invalid webhook data");
|
||||
}
|
||||
|
||||
_logger.LogInformation("Processing BTCPay webhook: Type={Type}, InvoiceId={InvoiceId}, StoreId={StoreId}",
|
||||
webhookData.Type, webhookData.InvoiceId, webhookData.StoreId);
|
||||
|
||||
// Process the webhook based on event type
|
||||
var success = await ProcessWebhookEvent(webhookData);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
return BadRequest("Failed to process webhook");
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing BTCPay webhook");
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ValidateWebhookSignature(string payload, string signature, string secret)
|
||||
{
|
||||
try
|
||||
{
|
||||
// BTCPay Server uses HMAC-SHA256 with format "sha256=<hex>"
|
||||
if (!signature.StartsWith("sha256="))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedHash = signature.Substring(7); // Remove "sha256=" prefix
|
||||
var secretBytes = Encoding.UTF8.GetBytes(secret);
|
||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
||||
|
||||
using var hmac = new HMACSHA256(secretBytes);
|
||||
var computedHash = hmac.ComputeHash(payloadBytes);
|
||||
var computedHashHex = Convert.ToHexString(computedHash).ToLowerInvariant();
|
||||
|
||||
return expectedHash.Equals(computedHashHex, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error validating webhook signature");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> ProcessWebhookEvent(BTCPayWebhookDto webhookData)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Map BTCPay webhook event types to our payment status
|
||||
var paymentStatus = MapWebhookEventToPaymentStatus(webhookData.Type);
|
||||
|
||||
if (!paymentStatus.HasValue)
|
||||
{
|
||||
_logger.LogInformation("Ignoring webhook event type: {Type}", webhookData.Type);
|
||||
return true; // Not an error, just not a status we care about
|
||||
}
|
||||
|
||||
// Extract payment details
|
||||
var amount = webhookData.Payment?.PaymentMethodPaid ?? 0;
|
||||
var transactionHash = webhookData.Payment?.TransactionData?.TransactionHash;
|
||||
|
||||
// Process the payment update
|
||||
var success = await _cryptoPaymentService.ProcessPaymentWebhookAsync(
|
||||
webhookData.InvoiceId,
|
||||
paymentStatus.Value,
|
||||
amount,
|
||||
transactionHash);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_logger.LogInformation("Successfully processed webhook for invoice {InvoiceId} with status {Status}",
|
||||
webhookData.InvoiceId, paymentStatus.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to process webhook for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing webhook event for invoice {InvoiceId}", webhookData.InvoiceId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static PaymentStatus? MapWebhookEventToPaymentStatus(string eventType)
|
||||
{
|
||||
return eventType switch
|
||||
{
|
||||
"InvoiceCreated" => PaymentStatus.Pending,
|
||||
"InvoiceReceivedPayment" => PaymentStatus.Processing,
|
||||
"InvoicePaymentSettled" => PaymentStatus.Completed,
|
||||
"InvoiceProcessing" => PaymentStatus.Processing,
|
||||
"InvoiceExpired" => PaymentStatus.Expired,
|
||||
"InvoiceSettled" => PaymentStatus.Completed,
|
||||
"InvoiceInvalid" => PaymentStatus.Failed,
|
||||
_ => null // Unknown event type
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -98,28 +98,28 @@ public class TestController : ControllerBase
|
||||
{
|
||||
// Get count before cleanup
|
||||
var totalBots = await _context.Bots.CountAsync();
|
||||
|
||||
|
||||
// Keep only the most recent active bot per platform
|
||||
var keepBots = await _context.Bots
|
||||
.Where(b => b.IsActive && b.Status == Enums.BotStatus.Active)
|
||||
.GroupBy(b => b.PlatformId)
|
||||
.Select(g => g.OrderByDescending(b => b.LastSeenAt ?? b.CreatedAt).First())
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
var keepBotIds = keepBots.Select(b => b.Id).ToList();
|
||||
|
||||
|
||||
// Delete old/inactive bots and related data
|
||||
var botsToDelete = await _context.Bots
|
||||
.Where(b => !keepBotIds.Contains(b.Id))
|
||||
.ToListAsync();
|
||||
|
||||
|
||||
_context.Bots.RemoveRange(botsToDelete);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
|
||||
var deletedCount = botsToDelete.Count;
|
||||
var remainingCount = keepBots.Count;
|
||||
|
||||
return Ok(new {
|
||||
|
||||
return Ok(new {
|
||||
message = "Bot cleanup completed",
|
||||
totalBots = totalBots,
|
||||
deletedBots = deletedCount,
|
||||
@@ -138,4 +138,117 @@ public class TestController : ControllerBase
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("cleanup-test-data")]
|
||||
public async Task<IActionResult> CleanupTestData()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get counts before cleanup
|
||||
var totalOrders = await _context.Orders.CountAsync();
|
||||
var totalCryptoPayments = await _context.CryptoPayments.CountAsync();
|
||||
var totalOrderItems = await _context.OrderItems.CountAsync();
|
||||
|
||||
// Find test orders (identity references starting with "test-")
|
||||
var testOrders = await _context.Orders
|
||||
.Where(o => o.IdentityReference != null && o.IdentityReference.StartsWith("test-"))
|
||||
.ToListAsync();
|
||||
|
||||
var testOrderIds = testOrders.Select(o => o.Id).ToList();
|
||||
|
||||
// Remove crypto payments for test orders
|
||||
var cryptoPaymentsToDelete = await _context.CryptoPayments
|
||||
.Where(cp => testOrderIds.Contains(cp.OrderId))
|
||||
.ToListAsync();
|
||||
|
||||
// Remove order items for test orders
|
||||
var orderItemsToDelete = await _context.OrderItems
|
||||
.Where(oi => testOrderIds.Contains(oi.OrderId))
|
||||
.ToListAsync();
|
||||
|
||||
// Delete all related data
|
||||
_context.CryptoPayments.RemoveRange(cryptoPaymentsToDelete);
|
||||
_context.OrderItems.RemoveRange(orderItemsToDelete);
|
||||
_context.Orders.RemoveRange(testOrders);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
// Get counts after cleanup
|
||||
var remainingOrders = await _context.Orders.CountAsync();
|
||||
var remainingCryptoPayments = await _context.CryptoPayments.CountAsync();
|
||||
var remainingOrderItems = await _context.OrderItems.CountAsync();
|
||||
|
||||
return Ok(new {
|
||||
message = "Test data cleanup completed",
|
||||
before = new {
|
||||
orders = totalOrders,
|
||||
cryptoPayments = totalCryptoPayments,
|
||||
orderItems = totalOrderItems
|
||||
},
|
||||
after = new {
|
||||
orders = remainingOrders,
|
||||
cryptoPayments = remainingCryptoPayments,
|
||||
orderItems = remainingOrderItems
|
||||
},
|
||||
deleted = new {
|
||||
orders = testOrders.Count,
|
||||
cryptoPayments = cryptoPaymentsToDelete.Count,
|
||||
orderItems = orderItemsToDelete.Count
|
||||
},
|
||||
testOrdersFound = testOrders.Select(o => new {
|
||||
id = o.Id,
|
||||
identityReference = o.IdentityReference,
|
||||
createdAt = o.CreatedAt,
|
||||
total = o.Total
|
||||
})
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return BadRequest(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("database")]
|
||||
public async Task<IActionResult> DatabaseHealthCheck()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Test database connectivity by executing a simple query
|
||||
var canConnect = await _context.Database.CanConnectAsync();
|
||||
|
||||
if (!canConnect)
|
||||
{
|
||||
return StatusCode(503, new {
|
||||
status = "unhealthy",
|
||||
message = "Cannot connect to database",
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
// Test actual query execution
|
||||
var categoryCount = await _context.Categories.CountAsync();
|
||||
var productCount = await _context.Products.CountAsync();
|
||||
var orderCount = await _context.Orders.CountAsync();
|
||||
|
||||
return Ok(new {
|
||||
status = "healthy",
|
||||
message = "Database connection successful",
|
||||
stats = new {
|
||||
categories = categoryCount,
|
||||
products = productCount,
|
||||
orders = orderCount
|
||||
},
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return StatusCode(503, new {
|
||||
status = "unhealthy",
|
||||
error = ex.Message,
|
||||
timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user