Add customer communication system

This commit is contained in:
sysadmin
2025-08-27 18:02:39 +01:00
parent 1f7c0af497
commit eae5be3e7c
136 changed files with 14552 additions and 97 deletions

View File

@@ -0,0 +1,30 @@
using Microsoft.AspNetCore.Mvc;
using LittleShop.DTOs;
using LittleShop.Services;
namespace LittleShop.Controllers;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IAuthService _authService;
public AuthController(IAuthService authService)
{
_authService = authService;
}
[HttpPost("login")]
public async Task<ActionResult<AuthResponseDto>> Login([FromBody] LoginDto loginDto)
{
var result = await _authService.LoginAsync(loginDto);
if (result != null)
{
return Ok(result);
}
return Unauthorized(new { message = "Invalid credentials" });
}
}

View File

@@ -0,0 +1,265 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using LittleShop.DTOs;
using LittleShop.Services;
namespace LittleShop.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BotsController : ControllerBase
{
private readonly IBotService _botService;
private readonly IBotMetricsService _metricsService;
private readonly ILogger<BotsController> _logger;
public BotsController(
IBotService botService,
IBotMetricsService metricsService,
ILogger<BotsController> logger)
{
_botService = botService;
_metricsService = metricsService;
_logger = logger;
}
// Bot Registration and Authentication
[HttpPost("register")]
[AllowAnonymous]
public async Task<ActionResult<BotRegistrationResponseDto>> RegisterBot([FromBody] BotRegistrationDto dto)
{
try
{
var result = await _botService.RegisterBotAsync(dto);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to register bot");
return BadRequest("Failed to register bot");
}
}
[HttpPost("authenticate")]
[AllowAnonymous]
public async Task<ActionResult<BotDto>> AuthenticateBot([FromBody] BotAuthenticateDto dto)
{
var bot = await _botService.AuthenticateBotAsync(dto.BotKey);
if (bot == null)
return Unauthorized("Invalid bot key");
return Ok(bot);
}
// Bot Settings
[HttpGet("settings")]
public async Task<ActionResult<Dictionary<string, object>>> GetBotSettings()
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var settings = await _botService.GetBotSettingsAsync(bot.Id);
return Ok(settings);
}
[HttpPut("settings")]
public async Task<IActionResult> UpdateBotSettings([FromBody] UpdateBotSettingsDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var success = await _botService.UpdateBotSettingsAsync(bot.Id, dto);
if (!success)
return NotFound("Bot not found");
return NoContent();
}
// Heartbeat
[HttpPost("heartbeat")]
public async Task<IActionResult> RecordHeartbeat([FromBody] BotHeartbeatDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
await _botService.RecordHeartbeatAsync(bot.Id, dto);
return Ok();
}
[HttpPut("platform-info")]
public async Task<IActionResult> UpdatePlatformInfo([FromBody] UpdatePlatformInfoDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var success = await _botService.UpdatePlatformInfoAsync(bot.Id, dto);
if (!success)
return NotFound("Bot not found");
return NoContent();
}
// Metrics
[HttpPost("metrics")]
public async Task<ActionResult<BotMetricDto>> RecordMetric([FromBody] CreateBotMetricDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var metric = await _metricsService.RecordMetricAsync(bot.Id, dto);
return Ok(metric);
}
[HttpPost("metrics/batch")]
public async Task<IActionResult> RecordMetricsBatch([FromBody] BotMetricsBatchDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var success = await _metricsService.RecordMetricsBatchAsync(bot.Id, dto);
if (!success)
return BadRequest("Failed to record metrics");
return Ok();
}
// Sessions
[HttpPost("sessions/start")]
public async Task<ActionResult<BotSessionDto>> StartSession([FromBody] CreateBotSessionDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var session = await _metricsService.StartSessionAsync(bot.Id, dto);
return Ok(session);
}
[HttpPut("sessions/{sessionId}")]
public async Task<IActionResult> UpdateSession(Guid sessionId, [FromBody] UpdateBotSessionDto dto)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var success = await _metricsService.UpdateSessionAsync(sessionId, dto);
if (!success)
return NotFound("Session not found");
return NoContent();
}
[HttpPost("sessions/{sessionId}/end")]
public async Task<IActionResult> EndSession(Guid sessionId)
{
var botKey = Request.Headers["X-Bot-Key"].ToString();
if (string.IsNullOrEmpty(botKey))
return Unauthorized("Bot key required");
var bot = await _botService.GetBotByKeyAsync(botKey);
if (bot == null)
return Unauthorized("Invalid bot key");
var success = await _metricsService.EndSessionAsync(sessionId);
if (!success)
return NotFound("Session not found");
return NoContent();
}
// Admin endpoints (require Bearer authentication)
[HttpGet]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<ActionResult> GetAllBots()
{
var bots = await _botService.GetAllBotsAsync();
return Ok(bots);
}
[HttpGet("{id}")]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<ActionResult<BotDto>> GetBot(Guid id)
{
var bot = await _botService.GetBotByIdAsync(id);
if (bot == null)
return NotFound();
return Ok(bot);
}
[HttpGet("{id}/metrics")]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<ActionResult> GetBotMetrics(Guid id, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
{
var metrics = await _metricsService.GetBotMetricsAsync(id, startDate, endDate);
return Ok(metrics);
}
[HttpGet("{id}/metrics/summary")]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<ActionResult<BotMetricsSummaryDto>> GetMetricsSummary(Guid id, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate)
{
var summary = await _metricsService.GetMetricsSummaryAsync(id, startDate, endDate);
return Ok(summary);
}
[HttpGet("{id}/sessions")]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<ActionResult> GetBotSessions(Guid id, [FromQuery] bool activeOnly = false)
{
var sessions = await _metricsService.GetBotSessionsAsync(id, activeOnly);
return Ok(sessions);
}
[HttpDelete("{id}")]
[Authorize(AuthenticationSchemes = "Bearer", Roles = "Admin")]
public async Task<IActionResult> DeleteBot(Guid id)
{
var success = await _botService.DeleteBotAsync(id);
if (!success)
return NotFound();
return NoContent();
}
}

View File

@@ -7,7 +7,6 @@ namespace LittleShop.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
public class CatalogController : ControllerBase
{
private readonly ICategoryService _categoryService;
@@ -39,13 +38,29 @@ public class CatalogController : ControllerBase
}
[HttpGet("products")]
public async Task<ActionResult<IEnumerable<ProductDto>>> GetProducts([FromQuery] Guid? categoryId = null)
public async Task<ActionResult<PagedResult<ProductDto>>> GetProducts(
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 20,
[FromQuery] Guid? categoryId = null)
{
var products = categoryId.HasValue
var allProducts = categoryId.HasValue
? await _productService.GetProductsByCategoryAsync(categoryId.Value)
: await _productService.GetAllProductsAsync();
return Ok(products);
var productList = allProducts.ToList();
var totalCount = productList.Count;
var skip = (pageNumber - 1) * pageSize;
var pagedProducts = productList.Skip(skip).Take(pageSize).ToList();
var result = new PagedResult<ProductDto>
{
Items = pagedProducts,
TotalCount = totalCount,
PageNumber = pageNumber,
PageSize = pageSize
};
return Ok(result);
}
[HttpGet("products/{id}")]

View File

@@ -0,0 +1,139 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using LittleShop.DTOs;
using LittleShop.Services;
namespace LittleShop.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(AuthenticationSchemes = "Bearer")]
public class CustomersController : ControllerBase
{
private readonly ICustomerService _customerService;
private readonly ILogger<CustomersController> _logger;
public CustomersController(ICustomerService customerService, ILogger<CustomersController> logger)
{
_customerService = customerService;
_logger = logger;
}
[HttpGet]
public async Task<ActionResult<IEnumerable<CustomerDto>>> GetCustomers([FromQuery] string? search = null)
{
if (!string.IsNullOrEmpty(search))
{
var searchResults = await _customerService.SearchCustomersAsync(search);
return Ok(searchResults);
}
var customers = await _customerService.GetAllCustomersAsync();
return Ok(customers);
}
[HttpGet("{id}")]
public async Task<ActionResult<CustomerDto>> GetCustomer(Guid id)
{
var customer = await _customerService.GetCustomerByIdAsync(id);
if (customer == null)
{
return NotFound("Customer not found");
}
return Ok(customer);
}
[HttpGet("by-telegram/{telegramUserId}")]
public async Task<ActionResult<CustomerDto>> GetCustomerByTelegramId(long telegramUserId)
{
var customer = await _customerService.GetCustomerByTelegramUserIdAsync(telegramUserId);
if (customer == null)
{
return NotFound("Customer not found");
}
return Ok(customer);
}
[HttpPost]
public async Task<ActionResult<CustomerDto>> CreateCustomer([FromBody] CreateCustomerDto createCustomerDto)
{
try
{
var customer = await _customerService.CreateCustomerAsync(createCustomerDto);
return CreatedAtAction(nameof(GetCustomer), new { id = customer.Id }, customer);
}
catch (InvalidOperationException ex)
{
return BadRequest(ex.Message);
}
}
[HttpPost("get-or-create")]
[AllowAnonymous] // Allow TeleBot to create customers
public async Task<ActionResult<CustomerDto>> GetOrCreateCustomer([FromBody] CreateCustomerDto createCustomerDto)
{
var customer = await _customerService.GetOrCreateCustomerAsync(
createCustomerDto.TelegramUserId,
createCustomerDto.TelegramDisplayName,
createCustomerDto.TelegramUsername,
createCustomerDto.TelegramFirstName,
createCustomerDto.TelegramLastName);
if (customer == null)
{
return BadRequest("Failed to create customer");
}
return Ok(customer);
}
[HttpPut("{id}")]
public async Task<ActionResult<CustomerDto>> UpdateCustomer(Guid id, [FromBody] UpdateCustomerDto updateCustomerDto)
{
var customer = await _customerService.UpdateCustomerAsync(id, updateCustomerDto);
if (customer == null)
{
return NotFound("Customer not found");
}
return Ok(customer);
}
[HttpPost("{id}/block")]
public async Task<ActionResult> BlockCustomer(Guid id, [FromBody] string reason)
{
var success = await _customerService.BlockCustomerAsync(id, reason);
if (!success)
{
return NotFound("Customer not found");
}
return Ok(new { message = "Customer blocked successfully" });
}
[HttpPost("{id}/unblock")]
public async Task<ActionResult> UnblockCustomer(Guid id)
{
var success = await _customerService.UnblockCustomerAsync(id);
if (!success)
{
return NotFound("Customer not found");
}
return Ok(new { message = "Customer unblocked successfully" });
}
[HttpDelete("{id}")]
public async Task<ActionResult> DeleteCustomer(Guid id)
{
var success = await _customerService.DeleteCustomerAsync(id);
if (!success)
{
return NotFound("Customer not found");
}
return Ok(new { message = "Customer marked for deletion" });
}
}

View File

@@ -0,0 +1,134 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using LittleShop.DTOs;
using LittleShop.Services;
using System.Security.Claims;
namespace LittleShop.Controllers;
[ApiController]
[Route("api/[controller]")]
[Authorize(Policy = "AdminOnly")]
public class MessagesController : ControllerBase
{
private readonly ICustomerMessageService _messageService;
private readonly ILogger<MessagesController> _logger;
public MessagesController(ICustomerMessageService messageService, ILogger<MessagesController> logger)
{
_messageService = messageService;
_logger = logger;
}
[HttpPost]
public async Task<ActionResult<CustomerMessageDto>> SendMessage([FromBody] CreateCustomerMessageDto createMessageDto)
{
try
{
// Always set AdminUserId to null to avoid FK constraint issues for now
createMessageDto.AdminUserId = null;
// Validate that CustomerId exists
var customerExists = await _messageService.ValidateCustomerExistsAsync(createMessageDto.CustomerId);
if (!customerExists)
{
return BadRequest($"Customer {createMessageDto.CustomerId} does not exist");
}
// If OrderId is provided, validate it belongs to the customer
if (createMessageDto.OrderId.HasValue)
{
var orderBelongsToCustomer = await _messageService.ValidateOrderBelongsToCustomerAsync(
createMessageDto.OrderId.Value,
createMessageDto.CustomerId);
if (!orderBelongsToCustomer)
{
return BadRequest("Order does not belong to the specified customer");
}
}
var message = await _messageService.CreateMessageAsync(createMessageDto);
if (message == null)
{
return BadRequest("Failed to create message");
}
return Ok(message); // Use Ok instead of CreatedAtAction to avoid routing issues
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating customer message");
return BadRequest($"Error creating message: {ex.Message}");
}
}
[HttpGet("{id}")]
public async Task<ActionResult<CustomerMessageDto>> GetMessage(Guid id)
{
var message = await _messageService.GetMessageByIdAsync(id);
if (message == null)
{
return NotFound("Message not found");
}
return Ok(message);
}
[HttpGet("customer/{customerId}")]
public async Task<ActionResult<IEnumerable<CustomerMessageDto>>> GetCustomerMessages(Guid customerId)
{
var messages = await _messageService.GetCustomerMessagesAsync(customerId);
return Ok(messages);
}
[HttpGet("order/{orderId}")]
public async Task<ActionResult<IEnumerable<CustomerMessageDto>>> GetOrderMessages(Guid orderId)
{
var messages = await _messageService.GetOrderMessagesAsync(orderId);
return Ok(messages);
}
[HttpGet("pending")]
public async Task<ActionResult<IEnumerable<CustomerMessageDto>>> GetPendingMessages([FromQuery] string platform = "Telegram")
{
var messages = await _messageService.GetPendingMessagesAsync(platform);
return Ok(messages);
}
[HttpPost("{id}/mark-sent")]
public async Task<ActionResult> MarkMessageAsSent(Guid id, [FromQuery] string? platformMessageId = null)
{
var success = await _messageService.MarkMessageAsSentAsync(id, platformMessageId);
if (!success)
{
return NotFound("Message not found");
}
return Ok();
}
[HttpPost("{id}/mark-delivered")]
public async Task<ActionResult> MarkMessageAsDelivered(Guid id)
{
var success = await _messageService.MarkMessageAsDeliveredAsync(id);
if (!success)
{
return NotFound("Message not found");
}
return Ok();
}
[HttpPost("{id}/mark-failed")]
public async Task<ActionResult> MarkMessageAsFailed(Guid id, [FromBody] string reason)
{
var success = await _messageService.MarkMessageAsFailedAsync(id, reason);
if (!success)
{
return NotFound("Message not found");
}
return Ok();
}
}

View File

@@ -57,6 +57,7 @@ public class OrdersController : ControllerBase
// Public endpoints for client identity
[HttpGet("by-identity/{identityReference}")]
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<OrderDto>>> GetOrdersByIdentity(string identityReference)
{
var orders = await _orderService.GetOrdersByIdentityAsync(identityReference);
@@ -64,6 +65,7 @@ public class OrdersController : ControllerBase
}
[HttpGet("by-identity/{identityReference}/{id}")]
[AllowAnonymous]
public async Task<ActionResult<OrderDto>> GetOrderByIdentity(string identityReference, Guid id)
{
var order = await _orderService.GetOrderByIdAsync(id);
@@ -76,6 +78,7 @@ public class OrdersController : ControllerBase
}
[HttpPost]
[AllowAnonymous]
public async Task<ActionResult<OrderDto>> CreateOrder([FromBody] CreateOrderDto createOrderDto)
{
try
@@ -91,6 +94,7 @@ public class OrdersController : ControllerBase
}
[HttpPost("{id}/payments")]
[AllowAnonymous]
public async Task<ActionResult<CryptoPaymentDto>> CreatePayment(Guid id, [FromBody] CreatePaymentDto createPaymentDto)
{
var order = await _orderService.GetOrderByIdAsync(id);