Add customer communication system
This commit is contained in:
26
LittleShop/.dockerignore
Normal file
26
LittleShop/.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
||||
**/.classpath
|
||||
**/.dockerignore
|
||||
**/.env
|
||||
**/.git
|
||||
**/.gitignore
|
||||
**/.project
|
||||
**/.settings
|
||||
**/.toolstarget
|
||||
**/.vs
|
||||
**/.vscode
|
||||
**/*.*proj.user
|
||||
**/*.dbmdl
|
||||
**/*.jfm
|
||||
**/bin
|
||||
**/charts
|
||||
**/docker-compose*
|
||||
**/compose*
|
||||
**/Dockerfile*
|
||||
**/node_modules
|
||||
**/npm-debug.log
|
||||
**/obj
|
||||
**/secrets.dev.yaml
|
||||
**/values.dev.yaml
|
||||
README.md
|
||||
*.db
|
||||
logs/
|
||||
332
LittleShop/Areas/Admin/Controllers/BotsController.cs
Normal file
332
LittleShop/Areas/Admin/Controllers/BotsController.cs
Normal file
@@ -0,0 +1,332 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Enums;
|
||||
using LittleShop.Services;
|
||||
|
||||
namespace LittleShop.Areas.Admin.Controllers;
|
||||
|
||||
[Area("Admin")]
|
||||
[Authorize(Policy = "AdminOnly")]
|
||||
public class BotsController : Controller
|
||||
{
|
||||
private readonly IBotService _botService;
|
||||
private readonly IBotMetricsService _metricsService;
|
||||
private readonly ITelegramBotManagerService _telegramManager;
|
||||
private readonly ILogger<BotsController> _logger;
|
||||
|
||||
public BotsController(
|
||||
IBotService botService,
|
||||
IBotMetricsService metricsService,
|
||||
ITelegramBotManagerService telegramManager,
|
||||
ILogger<BotsController> logger)
|
||||
{
|
||||
_botService = botService;
|
||||
_metricsService = metricsService;
|
||||
_telegramManager = telegramManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// GET: Admin/Bots
|
||||
public async Task<IActionResult> Index()
|
||||
{
|
||||
var bots = await _botService.GetAllBotsAsync();
|
||||
return View(bots);
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/Details/5
|
||||
public async Task<IActionResult> Details(Guid id)
|
||||
{
|
||||
var bot = await _botService.GetBotByIdAsync(id);
|
||||
if (bot == null)
|
||||
return NotFound();
|
||||
|
||||
// Get metrics summary for the last 30 days
|
||||
var metricsSummary = await _metricsService.GetMetricsSummaryAsync(id, DateTime.UtcNow.AddDays(-30), DateTime.UtcNow);
|
||||
ViewData["MetricsSummary"] = metricsSummary;
|
||||
|
||||
// Get active sessions
|
||||
var activeSessions = await _metricsService.GetBotSessionsAsync(id, activeOnly: true);
|
||||
ViewData["ActiveSessions"] = activeSessions;
|
||||
|
||||
return View(bot);
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/Create
|
||||
public IActionResult Create()
|
||||
{
|
||||
return View(new BotRegistrationDto());
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/Wizard
|
||||
public IActionResult Wizard()
|
||||
{
|
||||
return View(new BotWizardDto());
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Wizard
|
||||
[HttpPost]
|
||||
// [ValidateAntiForgeryToken] // Temporarily disabled for testing
|
||||
public async Task<IActionResult> Wizard(BotWizardDto dto)
|
||||
{
|
||||
_logger.LogInformation("Wizard POST received - BotName: '{BotName}', BotUsername: '{BotUsername}'", dto.BotName, dto.BotUsername);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Validation failed");
|
||||
foreach (var error in ModelState)
|
||||
{
|
||||
_logger.LogWarning("Field {Field}: {Errors}", error.Key, string.Join(", ", error.Value.Errors.Select(e => e.ErrorMessage)));
|
||||
}
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
// Generate BotFather commands
|
||||
var commands = GenerateBotFatherCommands(dto);
|
||||
ViewData["BotFatherCommands"] = commands;
|
||||
ViewData["ShowCommands"] = true;
|
||||
|
||||
_logger.LogInformation("Generated BotFather commands successfully for bot '{BotName}'", dto.BotName);
|
||||
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/CompleteWizard
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> CompleteWizard(BotWizardDto dto)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dto.BotToken))
|
||||
{
|
||||
ModelState.AddModelError("BotToken", "Bot token is required");
|
||||
ViewData["BotFatherCommands"] = GenerateBotFatherCommands(dto);
|
||||
ViewData["ShowCommands"] = true;
|
||||
return View("Wizard", dto);
|
||||
}
|
||||
|
||||
// Validate token first
|
||||
if (!await ValidateTelegramToken(dto.BotToken))
|
||||
{
|
||||
ModelState.AddModelError("BotToken", "Invalid bot token");
|
||||
ViewData["BotFatherCommands"] = GenerateBotFatherCommands(dto);
|
||||
ViewData["ShowCommands"] = true;
|
||||
return View("Wizard", dto);
|
||||
}
|
||||
|
||||
// Create the bot
|
||||
var registrationDto = new BotRegistrationDto
|
||||
{
|
||||
Name = dto.BotName,
|
||||
Description = dto.Description,
|
||||
Type = BotType.Telegram,
|
||||
Version = "1.0.0",
|
||||
PersonalityName = dto.PersonalityName,
|
||||
InitialSettings = new Dictionary<string, object>
|
||||
{
|
||||
["telegram"] = new { botToken = dto.BotToken },
|
||||
["personality"] = new { name = dto.PersonalityName }
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _botService.RegisterBotAsync(registrationDto);
|
||||
|
||||
// Add bot to Telegram manager
|
||||
var telegramAdded = await _telegramManager.AddBotAsync(result.BotId, dto.BotToken);
|
||||
|
||||
if (telegramAdded)
|
||||
{
|
||||
TempData["Success"] = $"Bot '{result.Name}' created successfully and is now running on Telegram!";
|
||||
}
|
||||
else
|
||||
{
|
||||
TempData["Warning"] = $"Bot '{result.Name}' created but failed to connect to Telegram. Check token.";
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Details), new { id = result.BotId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create bot");
|
||||
ModelState.AddModelError("", $"Failed to create bot: {ex.Message}");
|
||||
return View("Wizard", dto);
|
||||
}
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Create(BotRegistrationDto dto)
|
||||
{
|
||||
_logger.LogInformation("Received bot registration: Name={Name}, Type={Type}, Version={Version}",
|
||||
dto?.Name, dto?.Type, dto?.Version);
|
||||
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
_logger.LogWarning("Model validation failed for bot registration");
|
||||
foreach (var error in ModelState.Values.SelectMany(v => v.Errors))
|
||||
{
|
||||
_logger.LogWarning("Validation error: {Error}", error.ErrorMessage);
|
||||
}
|
||||
return View(dto);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var result = await _botService.RegisterBotAsync(dto);
|
||||
_logger.LogInformation("Bot registered successfully: {BotId}, Key: {KeyPrefix}...",
|
||||
result.BotId, result.BotKey.Substring(0, 8));
|
||||
|
||||
TempData["BotKey"] = result.BotKey;
|
||||
TempData["Success"] = $"Bot '{result.Name}' created successfully. Save the API key securely!";
|
||||
return RedirectToAction(nameof(Details), new { id = result.BotId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to create bot");
|
||||
ModelState.AddModelError("", $"Failed to create bot: {ex.Message}");
|
||||
return View(dto);
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/Edit/5
|
||||
public async Task<IActionResult> Edit(Guid id)
|
||||
{
|
||||
var bot = await _botService.GetBotByIdAsync(id);
|
||||
if (bot == null)
|
||||
return NotFound();
|
||||
|
||||
ViewData["BotSettings"] = JsonSerializer.Serialize(bot.Settings, new JsonSerializerOptions { WriteIndented = true });
|
||||
return View(bot);
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Edit/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Edit(Guid id, string settingsJson, BotStatus status)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse and update settings
|
||||
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(settingsJson) ?? new Dictionary<string, object>();
|
||||
var updateDto = new UpdateBotSettingsDto { Settings = settings };
|
||||
|
||||
await _botService.UpdateBotSettingsAsync(id, updateDto);
|
||||
await _botService.UpdateBotStatusAsync(id, status);
|
||||
|
||||
TempData["Success"] = "Bot updated successfully";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to update bot");
|
||||
TempData["Error"] = "Failed to update bot";
|
||||
return RedirectToAction(nameof(Edit), new { id });
|
||||
}
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/Metrics/5
|
||||
public async Task<IActionResult> Metrics(Guid id, DateTime? startDate, DateTime? endDate)
|
||||
{
|
||||
var bot = await _botService.GetBotByIdAsync(id);
|
||||
if (bot == null)
|
||||
return NotFound();
|
||||
|
||||
var start = startDate ?? DateTime.UtcNow.AddDays(-7);
|
||||
var end = endDate ?? DateTime.UtcNow;
|
||||
|
||||
var metricsSummary = await _metricsService.GetMetricsSummaryAsync(id, start, end);
|
||||
var sessionSummary = await _metricsService.GetSessionSummaryAsync(id, start, end);
|
||||
|
||||
ViewData["Bot"] = bot;
|
||||
ViewData["SessionSummary"] = sessionSummary;
|
||||
ViewData["StartDate"] = start;
|
||||
ViewData["EndDate"] = end;
|
||||
|
||||
return View(metricsSummary);
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Delete/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Delete(Guid id)
|
||||
{
|
||||
var success = await _botService.DeleteBotAsync(id);
|
||||
if (!success)
|
||||
{
|
||||
TempData["Error"] = "Failed to delete bot";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
TempData["Success"] = "Bot deleted successfully";
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Suspend/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Suspend(Guid id)
|
||||
{
|
||||
await _botService.UpdateBotStatusAsync(id, BotStatus.Suspended);
|
||||
TempData["Success"] = "Bot suspended";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
// POST: Admin/Bots/Activate/5
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> Activate(Guid id)
|
||||
{
|
||||
await _botService.UpdateBotStatusAsync(id, BotStatus.Active);
|
||||
TempData["Success"] = "Bot activated";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
// GET: Admin/Bots/RegenerateKey/5
|
||||
public async Task<IActionResult> RegenerateKey(Guid id)
|
||||
{
|
||||
// This would require updating the bot model to support key regeneration
|
||||
TempData["Error"] = "Key regeneration not yet implemented";
|
||||
return RedirectToAction(nameof(Details), new { id });
|
||||
}
|
||||
|
||||
private string GenerateBotFatherCommands(BotWizardDto dto)
|
||||
{
|
||||
var commands = new List<string>
|
||||
{
|
||||
"1. Open Telegram and find @BotFather",
|
||||
"2. Send: /newbot",
|
||||
$"3. Send bot name: {dto.BotName}",
|
||||
$"4. Send bot username: {dto.BotUsername}",
|
||||
"5. Copy the token from BotFather's response",
|
||||
"6. Paste the token in the field below"
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(dto.Description))
|
||||
{
|
||||
commands.Add($"7. Optional: Send /setdescription and then: {dto.Description}");
|
||||
}
|
||||
|
||||
return string.Join("\n", commands);
|
||||
}
|
||||
|
||||
private async Task<bool> ValidateTelegramToken(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.GetAsync($"https://api.telegram.org/bot{token}/getMe");
|
||||
return response.IsSuccessStatusCode;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
109
LittleShop/Areas/Admin/Views/Bots/Create.cshtml
Normal file
109
LittleShop/Areas/Admin/Views/Bots/Create.cshtml
Normal file
@@ -0,0 +1,109 @@
|
||||
@model LittleShop.DTOs.BotRegistrationDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Register New Bot";
|
||||
}
|
||||
|
||||
<h1>Register New Bot</h1>
|
||||
|
||||
<hr />
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<form action="/Admin/Bots/Create" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
|
||||
|
||||
@if (ViewData.ModelState.IsValid == false)
|
||||
{
|
||||
<div class="alert alert-danger">
|
||||
<ul>
|
||||
@foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors))
|
||||
{
|
||||
<li>@error.ErrorMessage</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Name" class="form-label">Bot Name</label>
|
||||
<input name="Name" id="Name" class="form-control" placeholder="e.g., Customer Service Bot" required />
|
||||
<span class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Description" class="form-label">Description</label>
|
||||
<textarea name="Description" id="Description" class="form-control" rows="3"
|
||||
placeholder="Brief description of what this bot does"></textarea>
|
||||
<span class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Type" class="form-label">Bot Type</label>
|
||||
<select name="Type" id="Type" class="form-select">
|
||||
<option value="0">Telegram</option>
|
||||
<option value="1">Discord</option>
|
||||
<option value="2">WhatsApp</option>
|
||||
<option value="3">Signal</option>
|
||||
<option value="4">Matrix</option>
|
||||
<option value="5">IRC</option>
|
||||
<option value="99">Custom</option>
|
||||
</select>
|
||||
<span class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Version" class="form-label">Bot Version</label>
|
||||
<input name="Version" id="Version" class="form-control" placeholder="e.g., 1.0.0" />
|
||||
<span class="text-danger"></span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Initial Settings (Optional)</label>
|
||||
<div class="alert alert-info">
|
||||
<small>You can configure detailed settings after registration.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<button type="submit" class="btn btn-primary">Register Bot</button>
|
||||
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Registration Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>After registering your bot, you will receive:</p>
|
||||
<ul>
|
||||
<li><strong>Bot ID:</strong> Unique identifier for your bot</li>
|
||||
<li><strong>API Key:</strong> Secret key for authentication (save this securely!)</li>
|
||||
<li><strong>Configuration:</strong> Access to manage bot settings</li>
|
||||
</ul>
|
||||
|
||||
<hr />
|
||||
|
||||
<h6>Bot Types:</h6>
|
||||
<ul class="small">
|
||||
<li><strong>Telegram:</strong> Telegram messenger bot</li>
|
||||
<li><strong>Discord:</strong> Discord server bot</li>
|
||||
<li><strong>WhatsApp:</strong> WhatsApp Business API</li>
|
||||
<li><strong>Signal:</strong> Signal messenger bot</li>
|
||||
<li><strong>Matrix:</strong> Matrix protocol bot</li>
|
||||
<li><strong>IRC:</strong> Internet Relay Chat bot</li>
|
||||
<li><strong>Custom:</strong> Custom implementation</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script>
|
||||
}
|
||||
295
LittleShop/Areas/Admin/Views/Bots/Details.cshtml
Normal file
295
LittleShop/Areas/Admin/Views/Bots/Details.cshtml
Normal file
@@ -0,0 +1,295 @@
|
||||
@model LittleShop.DTOs.BotDto
|
||||
@{
|
||||
ViewData["Title"] = $"Bot Details - {Model.Name}";
|
||||
var metricsSummary = ViewData["MetricsSummary"] as LittleShop.DTOs.BotMetricsSummaryDto;
|
||||
var activeSessions = ViewData["ActiveSessions"] as IEnumerable<LittleShop.DTOs.BotSessionDto>;
|
||||
}
|
||||
|
||||
<h1>Bot Details</h1>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["BotKey"] != null)
|
||||
{
|
||||
<div class="alert alert-warning">
|
||||
<strong>Important!</strong> Save this API key securely. It will not be shown again:
|
||||
<div class="mt-2">
|
||||
<code class="bg-dark text-white p-2 d-block">@TempData["BotKey"]</code>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Bot Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Name</dt>
|
||||
<dd class="col-sm-9">@Model.Name</dd>
|
||||
|
||||
<dt class="col-sm-3">Description</dt>
|
||||
<dd class="col-sm-9">@(string.IsNullOrEmpty(Model.Description) ? "N/A" : Model.Description)</dd>
|
||||
|
||||
<dt class="col-sm-3">Type</dt>
|
||||
<dd class="col-sm-9"><span class="badge bg-info">@Model.Type</span></dd>
|
||||
|
||||
<dt class="col-sm-3">Platform Info</dt>
|
||||
<dd class="col-sm-9">
|
||||
@if (!string.IsNullOrEmpty(Model.PlatformUsername))
|
||||
{
|
||||
<div>
|
||||
<strong>@@@Model.PlatformUsername</strong>
|
||||
@if (!string.IsNullOrEmpty(Model.PlatformDisplayName))
|
||||
{
|
||||
<br />
|
||||
<span class="text-muted">@Model.PlatformDisplayName</span>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.PlatformId))
|
||||
{
|
||||
<br />
|
||||
<small><code>ID: @Model.PlatformId</code></small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not configured - platform info will be auto-detected on first connection</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Status</dt>
|
||||
<dd class="col-sm-9">
|
||||
@switch (Model.Status)
|
||||
{
|
||||
case LittleShop.Enums.BotStatus.Active:
|
||||
<span class="badge bg-success">Active</span>
|
||||
break;
|
||||
case LittleShop.Enums.BotStatus.Inactive:
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
break;
|
||||
case LittleShop.Enums.BotStatus.Suspended:
|
||||
<span class="badge bg-warning">Suspended</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge bg-dark">@Model.Status</span>
|
||||
break;
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Bot ID</dt>
|
||||
<dd class="col-sm-9"><code>@Model.Id</code></dd>
|
||||
|
||||
<dt class="col-sm-3">Version</dt>
|
||||
<dd class="col-sm-9">@(string.IsNullOrEmpty(Model.Version) ? "N/A" : Model.Version)</dd>
|
||||
|
||||
<dt class="col-sm-3">IP Address</dt>
|
||||
<dd class="col-sm-9">@(string.IsNullOrEmpty(Model.IpAddress) ? "N/A" : Model.IpAddress)</dd>
|
||||
|
||||
<dt class="col-sm-3">Created</dt>
|
||||
<dd class="col-sm-9">@Model.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")</dd>
|
||||
|
||||
<dt class="col-sm-3">Last Seen</dt>
|
||||
<dd class="col-sm-9">
|
||||
@if (Model.LastSeenAt.HasValue)
|
||||
{
|
||||
@Model.LastSeenAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
@if ((DateTime.UtcNow - Model.LastSeenAt.Value).TotalMinutes < 5)
|
||||
{
|
||||
<span class="badge bg-success ms-2">Online</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Never</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">Config Synced</dt>
|
||||
<dd class="col-sm-9">
|
||||
@if (Model.LastConfigSyncAt.HasValue)
|
||||
{
|
||||
@Model.LastConfigSyncAt.Value.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Never</span>
|
||||
}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">30-Day Metrics Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (metricsSummary != null)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3>@metricsSummary.TotalSessions</h3>
|
||||
<p class="text-muted">Total Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3>@metricsSummary.TotalOrders</h3>
|
||||
<p class="text-muted">Total Orders</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3>$@metricsSummary.TotalRevenue.ToString("F2")</h3>
|
||||
<p class="text-muted">Total Revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="text-center">
|
||||
<h3>@metricsSummary.TotalErrors</h3>
|
||||
<p class="text-muted">Total Errors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (metricsSummary.UptimePercentage > 0)
|
||||
{
|
||||
<div class="progress mt-3">
|
||||
<div class="progress-bar bg-success" role="progressbar" style="width: @metricsSummary.UptimePercentage%"
|
||||
aria-valuenow="@metricsSummary.UptimePercentage" aria-valuemin="0" aria-valuemax="100">
|
||||
Uptime: @metricsSummary.UptimePercentage.ToString("F1")%
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted">No metrics available</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Active Sessions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (activeSessions != null && activeSessions.Any())
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Session ID</th>
|
||||
<th>Platform</th>
|
||||
<th>Started</th>
|
||||
<th>Messages</th>
|
||||
<th>Orders</th>
|
||||
<th>Spent</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var session in activeSessions.Take(10))
|
||||
{
|
||||
<tr>
|
||||
<td><small>@session.Id.ToString().Substring(0, 8)...</small></td>
|
||||
<td>@session.Platform</td>
|
||||
<td>@session.StartedAt.ToString("HH:mm:ss")</td>
|
||||
<td>@session.MessageCount</td>
|
||||
<td>@session.OrderCount</td>
|
||||
<td>$@session.TotalSpent.ToString("F2")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted">No active sessions</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="/Admin/Bots/Edit/@Model.Id" class="btn btn-primary">
|
||||
<i class="bi bi-pencil"></i> Edit Settings
|
||||
</a>
|
||||
<a href="/Admin/Bots/Metrics/@Model.Id" class="btn btn-success">
|
||||
<i class="bi bi-graph-up"></i> View Detailed Metrics
|
||||
</a>
|
||||
|
||||
@if (Model.Status == LittleShop.Enums.BotStatus.Active)
|
||||
{
|
||||
<form action="/Admin/Bots/Suspend/@Model.Id" method="post">
|
||||
<button type="submit" class="btn btn-warning w-100"
|
||||
onclick="return confirm('Are you sure you want to suspend this bot?')">
|
||||
<i class="bi bi-pause-circle"></i> Suspend Bot
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
else if (Model.Status == LittleShop.Enums.BotStatus.Suspended)
|
||||
{
|
||||
<form action="/Admin/Bots/Activate/@Model.Id" method="post">
|
||||
<button type="submit" class="btn btn-success w-100">
|
||||
<i class="bi bi-play-circle"></i> Activate Bot
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
<hr />
|
||||
|
||||
<form action="/Admin/Bots/Delete/@Model.Id" method="post">
|
||||
<button type="submit" class="btn btn-danger w-100"
|
||||
onclick="return confirm('Are you sure you want to delete this bot? This action cannot be undone.')">
|
||||
<i class="bi bi-trash"></i> Delete Bot
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Quick Stats</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li>
|
||||
<strong>Total Sessions:</strong> @Model.TotalSessions
|
||||
</li>
|
||||
<li>
|
||||
<strong>Active Sessions:</strong> @Model.ActiveSessions
|
||||
</li>
|
||||
<li>
|
||||
<strong>Total Orders:</strong> @Model.TotalOrders
|
||||
</li>
|
||||
<li>
|
||||
<strong>Total Revenue:</strong> $@Model.TotalRevenue.ToString("F2")
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="/Admin/Bots" class="btn btn-secondary">Back to List</a>
|
||||
</div>
|
||||
135
LittleShop/Areas/Admin/Views/Bots/Edit.cshtml
Normal file
135
LittleShop/Areas/Admin/Views/Bots/Edit.cshtml
Normal file
@@ -0,0 +1,135 @@
|
||||
@model LittleShop.DTOs.BotDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = $"Edit Bot - {Model.Name}";
|
||||
var settingsJson = ViewData["BotSettings"] as string ?? "{}";
|
||||
}
|
||||
|
||||
<h1>Edit Bot Settings</h1>
|
||||
|
||||
<hr />
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@TempData["Error"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<form asp-action="Edit" method="post">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Bot Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">Name</dt>
|
||||
<dd class="col-sm-9">@Model.Name</dd>
|
||||
|
||||
<dt class="col-sm-3">Type</dt>
|
||||
<dd class="col-sm-9">@Model.Type</dd>
|
||||
|
||||
<dt class="col-sm-3">Bot ID</dt>
|
||||
<dd class="col-sm-9"><code>@Model.Id</code></dd>
|
||||
</dl>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select name="status" id="status" class="form-select">
|
||||
<option value="1" selected="@(Model.Status == LittleShop.Enums.BotStatus.Active)">Active</option>
|
||||
<option value="2" selected="@(Model.Status == LittleShop.Enums.BotStatus.Inactive)">Inactive</option>
|
||||
<option value="3" selected="@(Model.Status == LittleShop.Enums.BotStatus.Suspended)">Suspended</option>
|
||||
<option value="4" selected="@(Model.Status == LittleShop.Enums.BotStatus.Maintenance)">Maintenance</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Bot Configuration (JSON)</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label for="settingsJson" class="form-label">Settings</label>
|
||||
<textarea name="settingsJson" id="settingsJson" class="form-control font-monospace" rows="20">@settingsJson</textarea>
|
||||
<small class="text-muted">Edit the JSON configuration for this bot. Be careful to maintain valid JSON format.</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Configuration Template</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">Example configuration structure:</p>
|
||||
<pre class="bg-light p-2 small"><code>{
|
||||
"Telegram": {
|
||||
"BotToken": "YOUR_BOT_TOKEN",
|
||||
"WebhookUrl": "",
|
||||
"AdminChatId": ""
|
||||
},
|
||||
"Privacy": {
|
||||
"Mode": "strict",
|
||||
"RequirePGP": false,
|
||||
"EnableTor": false
|
||||
},
|
||||
"Features": {
|
||||
"EnableQRCodes": true,
|
||||
"EnableVoiceSearch": false
|
||||
},
|
||||
"Cryptocurrencies": [
|
||||
"BTC", "XMR", "USDT"
|
||||
]
|
||||
}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button type="submit" class="btn btn-primary w-100 mb-2">
|
||||
<i class="bi bi-save"></i> Save Changes
|
||||
</button>
|
||||
<a asp-action="Details" asp-route-id="@Model.Id" class="btn btn-secondary w-100">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
// Pretty print JSON on load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var textarea = document.getElementById('settingsJson');
|
||||
try {
|
||||
var json = JSON.parse(textarea.value);
|
||||
textarea.value = JSON.stringify(json, null, 2);
|
||||
} catch (e) {
|
||||
console.error('Invalid JSON:', e);
|
||||
}
|
||||
});
|
||||
|
||||
// Validate JSON before submit
|
||||
document.querySelector('form').addEventListener('submit', function(e) {
|
||||
var textarea = document.getElementById('settingsJson');
|
||||
try {
|
||||
JSON.parse(textarea.value);
|
||||
} catch (error) {
|
||||
e.preventDefault();
|
||||
alert('Invalid JSON format: ' + error.message);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
150
LittleShop/Areas/Admin/Views/Bots/Index.cshtml
Normal file
150
LittleShop/Areas/Admin/Views/Bots/Index.cshtml
Normal file
@@ -0,0 +1,150 @@
|
||||
@model IEnumerable<LittleShop.DTOs.BotDto>
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Bot Management";
|
||||
}
|
||||
|
||||
<h1>Bot Management</h1>
|
||||
|
||||
<p>
|
||||
<a href="/Admin/Bots/Wizard" class="btn btn-primary">
|
||||
<i class="fas fa-magic"></i> Create Telegram Bot (Wizard)
|
||||
</a>
|
||||
<a href="/Admin/Bots/Create" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-plus"></i> Manual Registration
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@if (TempData["Success"] != null)
|
||||
{
|
||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
@TempData["Success"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (TempData["Error"] != null)
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
@TempData["Error"]
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Platform Info</th>
|
||||
<th>Status</th>
|
||||
<th>Active Sessions</th>
|
||||
<th>Total Revenue</th>
|
||||
<th>Last Seen</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var bot in Model)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@bot.Name</strong>
|
||||
@if (!string.IsNullOrEmpty(bot.PersonalityName))
|
||||
{
|
||||
<span class="badge bg-secondary ms-2">@bot.PersonalityName</span>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(bot.Description))
|
||||
{
|
||||
<br />
|
||||
<small class="text-muted">@bot.Description</small>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-info">@bot.Type</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(bot.PlatformUsername))
|
||||
{
|
||||
<div>
|
||||
<strong>@@@bot.PlatformUsername</strong>
|
||||
@if (!string.IsNullOrEmpty(bot.PlatformDisplayName))
|
||||
{
|
||||
<br />
|
||||
<small class="text-muted">@bot.PlatformDisplayName</small>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Not configured</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@switch (bot.Status)
|
||||
{
|
||||
case LittleShop.Enums.BotStatus.Active:
|
||||
<span class="badge bg-success">Active</span>
|
||||
break;
|
||||
case LittleShop.Enums.BotStatus.Inactive:
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
break;
|
||||
case LittleShop.Enums.BotStatus.Suspended:
|
||||
<span class="badge bg-warning">Suspended</span>
|
||||
break;
|
||||
case LittleShop.Enums.BotStatus.Maintenance:
|
||||
<span class="badge bg-info">Maintenance</span>
|
||||
break;
|
||||
default:
|
||||
<span class="badge bg-dark">@bot.Status</span>
|
||||
break;
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">@bot.ActiveSessions</span>
|
||||
</td>
|
||||
<td>$@bot.TotalRevenue.ToString("F2")</td>
|
||||
<td>
|
||||
@if (bot.LastSeenAt.HasValue)
|
||||
{
|
||||
<span title="@bot.LastSeenAt.Value.ToString("yyyy-MM-dd HH:mm:ss")">
|
||||
@((DateTime.UtcNow - bot.LastSeenAt.Value).TotalMinutes < 5 ? "Online" : bot.LastSeenAt.Value.ToString("yyyy-MM-dd HH:mm"))
|
||||
</span>
|
||||
@if ((DateTime.UtcNow - bot.LastSeenAt.Value).TotalMinutes < 5)
|
||||
{
|
||||
<span class="text-success">●</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">Never</span>
|
||||
}
|
||||
</td>
|
||||
<td>@bot.CreatedAt.ToString("yyyy-MM-dd")</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="/Admin/Bots/Details/@bot.Id" class="btn btn-outline-info" title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="/Admin/Bots/Edit/@bot.Id" class="btn btn-outline-primary" title="Edit">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<a href="/Admin/Bots/Metrics/@bot.Id" class="btn btn-outline-success" title="View Metrics">
|
||||
<i class="bi bi-graph-up"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="alert alert-info">
|
||||
No bots have been registered yet. <a asp-action="Create">Register your first bot</a>.
|
||||
</div>
|
||||
}
|
||||
284
LittleShop/Areas/Admin/Views/Bots/Metrics.cshtml
Normal file
284
LittleShop/Areas/Admin/Views/Bots/Metrics.cshtml
Normal file
@@ -0,0 +1,284 @@
|
||||
@model LittleShop.DTOs.BotMetricsSummaryDto
|
||||
@{
|
||||
ViewData["Title"] = $"Bot Metrics - {Model.BotName}";
|
||||
var bot = ViewData["Bot"] as LittleShop.DTOs.BotDto;
|
||||
var sessionSummary = ViewData["SessionSummary"] as LittleShop.DTOs.BotSessionSummaryDto;
|
||||
var startDate = (DateTime)ViewData["StartDate"]!;
|
||||
var endDate = (DateTime)ViewData["EndDate"]!;
|
||||
}
|
||||
|
||||
<h1>Bot Metrics</h1>
|
||||
<h3>@Model.BotName</h3>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-12">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-auto">
|
||||
<label for="startDate" class="col-form-label">Start Date:</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="date" id="startDate" name="startDate" class="form-control" value="@startDate.ToString("yyyy-MM-dd")" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<label for="endDate" class="col-form-label">End Date:</label>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<input type="date" id="endDate" name="endDate" class="form-control" value="@endDate.ToString("yyyy-MM-dd")" />
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h2 class="text-primary">@Model.TotalSessions</h2>
|
||||
<p class="text-muted">Total Sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h2 class="text-success">@Model.TotalOrders</h2>
|
||||
<p class="text-muted">Total Orders</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h2 class="text-info">$@Model.TotalRevenue.ToString("F2")</h2>
|
||||
<p class="text-muted">Total Revenue</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-center mb-3">
|
||||
<div class="card-body">
|
||||
<h2 class="text-warning">@Model.TotalMessages</h2>
|
||||
<p class="text-muted">Total Messages</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Performance Metrics</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Average Response Time:</dt>
|
||||
<dd class="col-sm-6">@Model.AverageResponseTime.ToString("F2") ms</dd>
|
||||
|
||||
<dt class="col-sm-6">Uptime Percentage:</dt>
|
||||
<dd class="col-sm-6">
|
||||
@if (Model.UptimePercentage > 0)
|
||||
{
|
||||
<span class="badge bg-success">@Model.UptimePercentage.ToString("F1")%</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">N/A</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6">Total Errors:</dt>
|
||||
<dd class="col-sm-6">
|
||||
@if (Model.TotalErrors > 0)
|
||||
{
|
||||
<span class="text-danger">@Model.TotalErrors</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-success">0</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6">Unique Sessions:</dt>
|
||||
<dd class="col-sm-6">@Model.UniqueSessions</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Session Statistics</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (sessionSummary != null)
|
||||
{
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6">Active Sessions:</dt>
|
||||
<dd class="col-sm-6">@sessionSummary.ActiveSessions</dd>
|
||||
|
||||
<dt class="col-sm-6">Completed Sessions:</dt>
|
||||
<dd class="col-sm-6">@sessionSummary.CompletedSessions</dd>
|
||||
|
||||
<dt class="col-sm-6">Avg Session Duration:</dt>
|
||||
<dd class="col-sm-6">@sessionSummary.AverageSessionDuration.ToString("F1") min</dd>
|
||||
|
||||
<dt class="col-sm-6">Avg Orders/Session:</dt>
|
||||
<dd class="col-sm-6">@sessionSummary.AverageOrdersPerSession.ToString("F2")</dd>
|
||||
|
||||
<dt class="col-sm-6">Avg Spend/Session:</dt>
|
||||
<dd class="col-sm-6">$@sessionSummary.AverageSpendPerSession.ToString("F2")</dd>
|
||||
</dl>
|
||||
}
|
||||
else
|
||||
{
|
||||
<p class="text-muted">No session data available</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.MetricsByType.Any())
|
||||
{
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Metrics by Type</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Metric Type</th>
|
||||
<th>Total Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var metric in Model.MetricsByType.OrderByDescending(m => m.Value))
|
||||
{
|
||||
<tr>
|
||||
<td>@metric.Key</td>
|
||||
<td>@metric.Value.ToString("F0")</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (sessionSummary != null)
|
||||
{
|
||||
<div class="row">
|
||||
@if (sessionSummary.SessionsByPlatform.Any())
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Sessions by Platform</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
@foreach (var platform in sessionSummary.SessionsByPlatform)
|
||||
{
|
||||
<li>@platform.Key: <strong>@platform.Value</strong></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (sessionSummary.SessionsByCountry.Any())
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Sessions by Country</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
@foreach (var country in sessionSummary.SessionsByCountry.Take(5))
|
||||
{
|
||||
<li>@country.Key: <strong>@country.Value</strong></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (sessionSummary.SessionsByLanguage.Any())
|
||||
{
|
||||
<div class="col-md-4">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Sessions by Language</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
@foreach (var language in sessionSummary.SessionsByLanguage)
|
||||
{
|
||||
<li>@language.Key: <strong>@language.Value</strong></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (Model.TimeSeries.Any())
|
||||
{
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Activity Timeline</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="metricsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-3">
|
||||
<a asp-action="Details" asp-route-id="@Model.BotId" class="btn btn-secondary">Back to Details</a>
|
||||
<a asp-action="Index" class="btn btn-secondary">Back to List</a>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@if (Model.TimeSeries.Any())
|
||||
{
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
const ctx = document.getElementById('metricsChart').getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [@Html.Raw(string.Join(",", Model.TimeSeries.Select(t => $"'{t.Label}'")))],
|
||||
datasets: [{
|
||||
label: 'Activity',
|
||||
data: [@string.Join(",", Model.TimeSeries.Select(t => t.Value))],
|
||||
borderColor: 'rgb(75, 192, 192)',
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
tension: 0.1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
||||
230
LittleShop/Areas/Admin/Views/Bots/Wizard.cshtml
Normal file
230
LittleShop/Areas/Admin/Views/Bots/Wizard.cshtml
Normal file
@@ -0,0 +1,230 @@
|
||||
@model LittleShop.DTOs.BotWizardDto
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Telegram Bot Creation Wizard";
|
||||
var showCommands = ViewData["ShowCommands"] as bool? ?? false;
|
||||
var commands = ViewData["BotFatherCommands"] as string ?? "";
|
||||
}
|
||||
|
||||
<h1>Telegram Bot Creation Wizard</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
@if (!showCommands)
|
||||
{
|
||||
<!-- Step 1: Basic Info -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Step 1: Bot Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-area="Admin" asp-controller="Bots" asp-action="Wizard" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotName" class="form-label">Bot Display Name</label>
|
||||
<input asp-for="BotName" class="form-control"
|
||||
placeholder="e.g., LittleShop Electronics Bot" required />
|
||||
<span asp-validation-for="BotName" class="text-danger"></span>
|
||||
<small class="text-muted">This is the name users will see</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotUsername" class="form-label">Bot Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">@@</span>
|
||||
<input asp-for="BotUsername" class="form-control"
|
||||
placeholder="littleshop_bot" required />
|
||||
</div>
|
||||
<span asp-validation-for="BotUsername" class="text-danger"></span>
|
||||
<small class="text-muted">Must end with 'bot' and be unique on Telegram</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="PersonalityName" class="form-label">Personality</label>
|
||||
<select asp-for="PersonalityName" class="form-select">
|
||||
<option value="">Auto-assign (recommended)</option>
|
||||
<option value="Alan" @(Model.PersonalityName == "Alan" ? "selected" : "")>Alan (Professional)</option>
|
||||
<option value="Dave" @(Model.PersonalityName == "Dave" ? "selected" : "")>Dave (Casual)</option>
|
||||
<option value="Sarah" @(Model.PersonalityName == "Sarah" ? "selected" : "")>Sarah (Helpful)</option>
|
||||
<option value="Mike" @(Model.PersonalityName == "Mike" ? "selected" : "")>Mike (Direct)</option>
|
||||
<option value="Emma" @(Model.PersonalityName == "Emma" ? "selected" : "")>Emma (Friendly)</option>
|
||||
<option value="Tom" @(Model.PersonalityName == "Tom" ? "selected" : "")>Tom (Efficient)</option>
|
||||
</select>
|
||||
<small class="text-muted">Bot conversation style (can be changed later)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Description" class="form-label">Description (Optional)</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="2"
|
||||
placeholder="Brief description of what this bot does"></textarea>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Generate BotFather Commands" class="btn btn-primary" />
|
||||
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<!-- Step 2: BotFather Commands & Token -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
<h5 class="mb-0">Step 2: Create Bot with BotFather</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>Follow these steps in Telegram:</strong>
|
||||
</div>
|
||||
|
||||
<div class="bg-light p-3 rounded">
|
||||
<pre class="mb-0">@commands</pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-sm btn-outline-secondary" onclick="copyCommands()">
|
||||
<i class="fas fa-copy"></i> Copy Instructions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Step 3: Complete Bot Setup</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/Admin/Bots/CompleteWizard" method="post">
|
||||
@Html.AntiForgeryToken()
|
||||
|
||||
<!-- Hidden fields to preserve data -->
|
||||
<input type="hidden" name="BotName" value="@Model.BotName" />
|
||||
<input type="hidden" name="BotUsername" value="@Model.BotUsername" />
|
||||
<input type="hidden" name="Description" value="@Model.Description" />
|
||||
<input type="hidden" name="PersonalityName" value="@Model.PersonalityName" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="BotToken" class="form-label">Bot Token from BotFather</label>
|
||||
<input name="BotToken" id="BotToken" class="form-control font-monospace"
|
||||
value="@Model.BotToken" placeholder="123456789:ABCdefGHIjklMNOpqrsTUVwxyz" required />
|
||||
<span asp-validation-for="BotToken" class="text-danger"></span>
|
||||
<small class="text-muted">Paste the token you received from @@BotFather</small>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex">
|
||||
<button type="submit" class="btn btn-success me-md-2">
|
||||
<i class="fas fa-check"></i> Create Bot & Validate Token
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" onclick="history.back()">
|
||||
<i class="fas fa-arrow-left"></i> Back to Edit Info
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Wizard Progress</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="@(!showCommands ? "text-primary fw-bold" : "text-success")">
|
||||
<i class="fas fa-@(!showCommands ? "edit" : "check")"></i>
|
||||
1. Bot Information
|
||||
</li>
|
||||
<li class="@(showCommands && string.IsNullOrEmpty(Model.BotToken) ? "text-primary fw-bold" : showCommands ? "text-success" : "text-muted")">
|
||||
<i class="fas fa-@(showCommands && string.IsNullOrEmpty(Model.BotToken) ? "robot" : showCommands ? "check" : "circle")"></i>
|
||||
2. Create with BotFather
|
||||
</li>
|
||||
<li class="@(!string.IsNullOrEmpty(Model.BotToken) ? "text-primary fw-bold" : "text-muted")">
|
||||
<i class="fas fa-@(!string.IsNullOrEmpty(Model.BotToken) ? "key" : "circle")"></i>
|
||||
3. Complete Setup
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (showCommands)
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Quick Tips</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="small">
|
||||
<li>BotFather responds instantly</li>
|
||||
<li>Username must end with 'bot'</li>
|
||||
<li>Keep your token secure</li>
|
||||
<li>Token starts with numbers followed by colon</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Personality Preview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
@if (!string.IsNullOrEmpty(Model.PersonalityName))
|
||||
{
|
||||
<strong>@Model.PersonalityName</strong><text> personality selected</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>Auto-assigned personality based on bot name</text>
|
||||
}
|
||||
</p>
|
||||
|
||||
<p class="small text-muted">
|
||||
Personalities affect how your bot communicates with customers.
|
||||
This can be customized later in bot settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@section Scripts {
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
// Test if JavaScript is working
|
||||
console.log('Wizard page scripts loaded');
|
||||
|
||||
function copyCommands() {
|
||||
const commands = `@Html.Raw(commands)`;
|
||||
navigator.clipboard.writeText(commands).then(() => {
|
||||
alert('Commands copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-generate username from bot name
|
||||
$(document).ready(function() {
|
||||
console.log('Document ready, setting up auto-generation');
|
||||
|
||||
$('#BotName').on('input', function() {
|
||||
try {
|
||||
const name = $(this).val().toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
|
||||
if (name && !name.endsWith('_bot')) {
|
||||
$('#BotUsername').val(name + '_bot');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in auto-generation:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
}
|
||||
@@ -10,6 +10,12 @@
|
||||
<p class="text-muted">Order ID: @Model.Id</p>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
@if (Model.Customer != null)
|
||||
{
|
||||
<button class="btn btn-success" onclick="showMessageModal('@Model.Id', '@Model.Customer.DisplayName')">
|
||||
<i class="fas fa-comment"></i> Message Customer
|
||||
</button>
|
||||
}
|
||||
<a href="@Url.Action("Edit", new { id = Model.Id })" class="btn btn-primary">
|
||||
<i class="fas fa-edit"></i> Edit Order
|
||||
</a>
|
||||
@@ -28,7 +34,28 @@
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Identity Reference:</strong> @Model.IdentityReference</p>
|
||||
@if (Model.Customer != null)
|
||||
{
|
||||
<p><strong>Customer:</strong> @Model.Customer.DisplayName
|
||||
@if (!string.IsNullOrEmpty(Model.Customer.TelegramUsername))
|
||||
{
|
||||
<span class="text-muted">(@@@Model.Customer.TelegramUsername)</span>
|
||||
}
|
||||
</p>
|
||||
<p><strong>Customer Type:</strong> <span class="badge bg-info">@Model.Customer.CustomerType</span></p>
|
||||
@if (Model.Customer.RiskScore > 0)
|
||||
{
|
||||
<p><strong>Risk Score:</strong>
|
||||
<span class="badge @(Model.Customer.RiskScore > 50 ? "bg-danger" : Model.Customer.RiskScore > 25 ? "bg-warning" : "bg-success")">
|
||||
@Model.Customer.RiskScore/100
|
||||
</span>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(Model.IdentityReference))
|
||||
{
|
||||
<p><strong>Identity Reference:</strong> @Model.IdentityReference</p>
|
||||
}
|
||||
<p><strong>Status:</strong>
|
||||
@{
|
||||
var badgeClass = Model.Status switch
|
||||
@@ -192,4 +219,126 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Model.Customer != null)
|
||||
{
|
||||
<!-- Customer Messaging Modal -->
|
||||
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="messageModalLabel">
|
||||
<i class="fas fa-comment"></i> Message Customer: <span id="customerName"></span>
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="messageForm">
|
||||
<input type="hidden" id="orderId" name="orderId" />
|
||||
<input type="hidden" id="customerId" name="customerId" value="@Model.Customer.Id" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="messageType" class="form-label">Message Type</label>
|
||||
<select class="form-select" id="messageType" name="messageType" required>
|
||||
<option value="0">Order Update</option>
|
||||
<option value="1">Payment Reminder</option>
|
||||
<option value="2">Shipping Information</option>
|
||||
<option value="3">Customer Service</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="messageSubject" class="form-label">Subject</label>
|
||||
<input type="text" class="form-control" id="messageSubject" name="subject" required
|
||||
placeholder="Brief subject line..." maxlength="100">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="messageContent" class="form-label">Message</label>
|
||||
<textarea class="form-control" id="messageContent" name="content" rows="4" required
|
||||
placeholder="Type your message to the customer..." maxlength="1000"></textarea>
|
||||
<div class="form-text">Message will be delivered via Telegram</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="isUrgent" name="isUrgent">
|
||||
<label class="form-check-label" for="isUrgent">
|
||||
Mark as urgent (higher priority delivery)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="sendMessage()">
|
||||
<i class="fas fa-paper-plane"></i> Send Message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<script>
|
||||
function showMessageModal(orderId, customerName) {
|
||||
document.getElementById('orderId').value = orderId;
|
||||
document.getElementById('customerName').textContent = customerName;
|
||||
|
||||
// Clear previous form data
|
||||
document.getElementById('messageForm').reset();
|
||||
document.getElementById('orderId').value = orderId; // Reset cleared this
|
||||
|
||||
// Show modal
|
||||
var modal = new bootstrap.Modal(document.getElementById('messageModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
const form = document.getElementById('messageForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
const messageData = {
|
||||
customerId: formData.get('customerId'),
|
||||
orderId: formData.get('orderId'),
|
||||
type: parseInt(formData.get('messageType')),
|
||||
subject: formData.get('subject'),
|
||||
content: formData.get('content'),
|
||||
isUrgent: formData.get('isUrgent') === 'on',
|
||||
priority: formData.get('isUrgent') === 'on' ? 1 : 5
|
||||
};
|
||||
|
||||
// Send message via API
|
||||
fetch('/api/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + sessionStorage.getItem('authToken')
|
||||
},
|
||||
body: JSON.stringify(messageData)
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
// Close modal and show success
|
||||
var modal = bootstrap.Modal.getInstance(document.getElementById('messageModal'));
|
||||
modal.hide();
|
||||
|
||||
// Show success message
|
||||
alert('Message sent successfully! The customer will receive it via Telegram.');
|
||||
|
||||
// Optionally refresh the page to show updated communication history
|
||||
window.location.reload();
|
||||
} else {
|
||||
response.text().then(error => {
|
||||
alert('Failed to send message: ' + error);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error sending message:', error);
|
||||
alert('Error sending message. Please try again.');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -37,7 +37,27 @@
|
||||
{
|
||||
<tr>
|
||||
<td><code>@order.Id.ToString().Substring(0, 8)...</code></td>
|
||||
<td>@order.ShippingName</td>
|
||||
<td>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<div>
|
||||
<strong>@order.Customer.DisplayName</strong>
|
||||
@if (!string.IsNullOrEmpty(order.Customer.TelegramUsername))
|
||||
{
|
||||
<br><small class="text-muted">@@@order.Customer.TelegramUsername</small>
|
||||
}
|
||||
<br><small class="badge bg-info">@order.Customer.CustomerType</small>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">@order.ShippingName</span>
|
||||
@if (!string.IsNullOrEmpty(order.IdentityReference))
|
||||
{
|
||||
<br><small class="text-muted">(@order.IdentityReference)</small>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
<td>@order.ShippingCity, @order.ShippingCountry</td>
|
||||
<td>
|
||||
@{
|
||||
@@ -60,6 +80,12 @@
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
@if (order.Customer != null)
|
||||
{
|
||||
<a href="@Url.Action("Details", new { id = order.Id })" class="btn btn-sm btn-success ms-1" title="Message Customer">
|
||||
<i class="fas fa-comment"></i>
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>@ViewData["Title"] - LittleShop Admin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
@@ -49,6 +50,11 @@
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="@Url.Action("Index", "Bots", new { area = "Admin" })">
|
||||
<i class="fas fa-robot"></i> Bots
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
@@ -76,6 +82,7 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
@await RenderSectionAsync("Scripts", required: false)
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
@* Client-side validation scripts *@
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.5/jquery.validate.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.12/jquery.validate.unobtrusive.min.js"></script>
|
||||
30
LittleShop/Controllers/AuthController.cs
Normal file
30
LittleShop/Controllers/AuthController.cs
Normal 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" });
|
||||
}
|
||||
}
|
||||
265
LittleShop/Controllers/BotsController.cs
Normal file
265
LittleShop/Controllers/BotsController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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}")]
|
||||
|
||||
139
LittleShop/Controllers/CustomersController.cs
Normal file
139
LittleShop/Controllers/CustomersController.cs
Normal 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" });
|
||||
}
|
||||
}
|
||||
134
LittleShop/Controllers/MessagesController.cs
Normal file
134
LittleShop/Controllers/MessagesController.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
113
LittleShop/DTOs/BotDto.cs
Normal file
113
LittleShop/DTOs/BotDto.cs
Normal file
@@ -0,0 +1,113 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class BotDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public BotType Type { get; set; }
|
||||
public BotStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? LastSeenAt { get; set; }
|
||||
public DateTime? LastConfigSyncAt { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
public string PlatformUsername { get; set; } = string.Empty;
|
||||
public string PlatformDisplayName { get; set; } = string.Empty;
|
||||
public string PlatformId { get; set; } = string.Empty;
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Settings { get; set; } = new();
|
||||
|
||||
// Metrics summary
|
||||
public int TotalSessions { get; set; }
|
||||
public int ActiveSessions { get; set; }
|
||||
public decimal TotalRevenue { get; set; }
|
||||
public int TotalOrders { get; set; }
|
||||
}
|
||||
|
||||
public class BotRegistrationDto
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public BotType Type { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
|
||||
public Dictionary<string, object> InitialSettings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotRegistrationResponseDto
|
||||
{
|
||||
public Guid BotId { get; set; }
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Settings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotAuthenticateDto
|
||||
{
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BotSettingsDto
|
||||
{
|
||||
public Dictionary<string, object> Telegram { get; set; } = new();
|
||||
public Dictionary<string, object> Privacy { get; set; } = new();
|
||||
public Dictionary<string, object> Features { get; set; } = new();
|
||||
public Dictionary<string, object> Redis { get; set; } = new();
|
||||
public List<string> Cryptocurrencies { get; set; } = new();
|
||||
}
|
||||
|
||||
public class UpdateBotSettingsDto
|
||||
{
|
||||
public Dictionary<string, object> Settings { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotHeartbeatDto
|
||||
{
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
public int ActiveSessions { get; set; }
|
||||
public Dictionary<string, object> Status { get; set; } = new();
|
||||
}
|
||||
|
||||
public class UpdatePlatformInfoDto
|
||||
{
|
||||
public string PlatformUsername { get; set; } = string.Empty;
|
||||
public string PlatformDisplayName { get; set; } = string.Empty;
|
||||
public string PlatformId { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class BotWizardDto
|
||||
{
|
||||
[Required(ErrorMessage = "Bot name is required")]
|
||||
[StringLength(50)]
|
||||
public string BotName { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Bot username is required")]
|
||||
[StringLength(100)]
|
||||
public string BotUsername { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
|
||||
public string BotToken { get; set; } = string.Empty;
|
||||
}
|
||||
62
LittleShop/DTOs/BotMetricDto.cs
Normal file
62
LittleShop/DTOs/BotMetricDto.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class BotMetricDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid BotId { get; set; }
|
||||
public MetricType MetricType { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public DateTime RecordedAt { get; set; }
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreateBotMetricDto
|
||||
{
|
||||
public MetricType MetricType { get; set; }
|
||||
public decimal Value { get; set; }
|
||||
public string Category { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotMetricsBatchDto
|
||||
{
|
||||
public List<CreateBotMetricDto> Metrics { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BotMetricsSummaryDto
|
||||
{
|
||||
public Guid BotId { get; set; }
|
||||
public string BotName { get; set; } = string.Empty;
|
||||
public DateTime PeriodStart { get; set; }
|
||||
public DateTime PeriodEnd { get; set; }
|
||||
|
||||
// Key metrics
|
||||
public int TotalSessions { get; set; }
|
||||
public int UniqueSessions { get; set; }
|
||||
public int TotalOrders { get; set; }
|
||||
public decimal TotalRevenue { get; set; }
|
||||
public int TotalMessages { get; set; }
|
||||
public int TotalErrors { get; set; }
|
||||
public decimal AverageResponseTime { get; set; }
|
||||
public decimal UptimePercentage { get; set; }
|
||||
|
||||
// Breakdown by type
|
||||
public Dictionary<string, decimal> MetricsByType { get; set; } = new();
|
||||
|
||||
// Time series data (for charts)
|
||||
public List<TimeSeriesDataPoint> TimeSeries { get; set; } = new();
|
||||
}
|
||||
|
||||
public class TimeSeriesDataPoint
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public decimal Value { get; set; }
|
||||
}
|
||||
54
LittleShop/DTOs/BotSessionDto.cs
Normal file
54
LittleShop/DTOs/BotSessionDto.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class BotSessionDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid BotId { get; set; }
|
||||
public string SessionIdentifier { get; set; } = string.Empty;
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
public DateTime StartedAt { get; set; }
|
||||
public DateTime LastActivityAt { get; set; }
|
||||
public DateTime? EndedAt { get; set; }
|
||||
public int OrderCount { get; set; }
|
||||
public int MessageCount { get; set; }
|
||||
public decimal TotalSpent { get; set; }
|
||||
public string Language { get; set; } = string.Empty;
|
||||
public string Country { get; set; } = string.Empty;
|
||||
public bool IsAnonymous { get; set; }
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
public class CreateBotSessionDto
|
||||
{
|
||||
public string SessionIdentifier { get; set; } = string.Empty;
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
public string Language { get; set; } = "en";
|
||||
public string Country { get; set; } = string.Empty;
|
||||
public bool IsAnonymous { get; set; } = true;
|
||||
public Dictionary<string, object> Metadata { get; set; } = new();
|
||||
}
|
||||
|
||||
public class UpdateBotSessionDto
|
||||
{
|
||||
public int? OrderCount { get; set; }
|
||||
public int? MessageCount { get; set; }
|
||||
public decimal? TotalSpent { get; set; }
|
||||
public bool? EndSession { get; set; }
|
||||
public Dictionary<string, object>? Metadata { get; set; }
|
||||
}
|
||||
|
||||
public class BotSessionSummaryDto
|
||||
{
|
||||
public int TotalSessions { get; set; }
|
||||
public int ActiveSessions { get; set; }
|
||||
public int CompletedSessions { get; set; }
|
||||
public decimal AverageSessionDuration { get; set; } // in minutes
|
||||
public decimal AverageOrdersPerSession { get; set; }
|
||||
public decimal AverageSpendPerSession { get; set; }
|
||||
public Dictionary<string, int> SessionsByPlatform { get; set; } = new();
|
||||
public Dictionary<string, int> SessionsByCountry { get; set; } = new();
|
||||
public Dictionary<string, int> SessionsByLanguage { get; set; } = new();
|
||||
}
|
||||
90
LittleShop/DTOs/CustomerDto.cs
Normal file
90
LittleShop/DTOs/CustomerDto.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class CustomerDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public long TelegramUserId { get; set; }
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
public string TelegramDisplayName { get; set; } = string.Empty;
|
||||
public string TelegramFirstName { get; set; } = string.Empty;
|
||||
public string TelegramLastName { get; set; } = string.Empty;
|
||||
public string? Email { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public bool AllowMarketing { get; set; }
|
||||
public bool AllowOrderUpdates { get; set; }
|
||||
public string Language { get; set; } = "en";
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
public int TotalOrders { get; set; }
|
||||
public decimal TotalSpent { get; set; }
|
||||
public decimal AverageOrderValue { get; set; }
|
||||
public DateTime FirstOrderDate { get; set; }
|
||||
public DateTime LastOrderDate { get; set; }
|
||||
public string? CustomerNotes { get; set; }
|
||||
public bool IsBlocked { get; set; }
|
||||
public string? BlockReason { get; set; }
|
||||
public int RiskScore { get; set; }
|
||||
public int SuccessfulOrders { get; set; }
|
||||
public int CancelledOrders { get; set; }
|
||||
public int DisputedOrders { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime UpdatedAt { get; set; }
|
||||
public DateTime LastActiveAt { get; set; }
|
||||
public DateTime? DataRetentionDate { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
// Calculated Properties
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string CustomerType { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CustomerSummaryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
public int TotalOrders { get; set; }
|
||||
public decimal TotalSpent { get; set; }
|
||||
public string CustomerType { get; set; } = string.Empty;
|
||||
public int RiskScore { get; set; }
|
||||
public DateTime LastActiveAt { get; set; }
|
||||
public bool IsBlocked { get; set; }
|
||||
}
|
||||
|
||||
public class CreateCustomerDto
|
||||
{
|
||||
[Required]
|
||||
public long TelegramUserId { get; set; }
|
||||
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string TelegramDisplayName { get; set; } = string.Empty;
|
||||
|
||||
public string TelegramFirstName { get; set; } = string.Empty;
|
||||
public string TelegramLastName { get; set; } = string.Empty;
|
||||
public string? Email { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public bool AllowMarketing { get; set; } = false;
|
||||
public bool AllowOrderUpdates { get; set; } = true;
|
||||
public string Language { get; set; } = "en";
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
}
|
||||
|
||||
public class UpdateCustomerDto
|
||||
{
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
public string TelegramDisplayName { get; set; } = string.Empty;
|
||||
public string TelegramFirstName { get; set; } = string.Empty;
|
||||
public string TelegramLastName { get; set; } = string.Empty;
|
||||
public string? Email { get; set; }
|
||||
public string? PhoneNumber { get; set; }
|
||||
public bool AllowMarketing { get; set; }
|
||||
public bool AllowOrderUpdates { get; set; }
|
||||
public string Language { get; set; } = "en";
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
public string? CustomerNotes { get; set; }
|
||||
public bool IsBlocked { get; set; }
|
||||
public string? BlockReason { get; set; }
|
||||
}
|
||||
118
LittleShop/DTOs/CustomerMessageDto.cs
Normal file
118
LittleShop/DTOs/CustomerMessageDto.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class CustomerMessageDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid CustomerId { get; set; }
|
||||
public Guid? OrderId { get; set; }
|
||||
public Guid? AdminUserId { get; set; }
|
||||
public MessageDirection Direction { get; set; }
|
||||
public MessageType Type { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public string Content { get; set; } = string.Empty;
|
||||
public MessageStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? SentAt { get; set; }
|
||||
public DateTime? DeliveredAt { get; set; }
|
||||
public DateTime? ReadAt { get; set; }
|
||||
public DateTime? FailedAt { get; set; }
|
||||
public string? FailureReason { get; set; }
|
||||
public int RetryCount { get; set; }
|
||||
public DateTime? NextRetryAt { get; set; }
|
||||
public Guid? ParentMessageId { get; set; }
|
||||
public Guid? ThreadId { get; set; }
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
public string? PlatformMessageId { get; set; }
|
||||
public int Priority { get; set; }
|
||||
public DateTime? ScheduledFor { get; set; }
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
public bool RequiresResponse { get; set; }
|
||||
public bool IsUrgent { get; set; }
|
||||
public bool IsMarketing { get; set; }
|
||||
public bool IsAutoGenerated { get; set; }
|
||||
public string? AutoGenerationTrigger { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public CustomerSummaryDto? Customer { get; set; }
|
||||
public string? AdminUsername { get; set; }
|
||||
public string? OrderReference { get; set; }
|
||||
|
||||
// For message delivery
|
||||
public long TelegramUserId { get; set; }
|
||||
|
||||
// Helper properties
|
||||
public string DisplayTitle { get; set; } = string.Empty;
|
||||
public string StatusDisplay { get; set; } = string.Empty;
|
||||
public string DirectionDisplay { get; set; } = string.Empty;
|
||||
public string TypeDisplay { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class CreateCustomerMessageDto
|
||||
{
|
||||
[Required]
|
||||
public Guid CustomerId { get; set; }
|
||||
|
||||
public Guid? OrderId { get; set; }
|
||||
|
||||
public Guid? AdminUserId { get; set; } // Set by controller from claims
|
||||
|
||||
[Required]
|
||||
public MessageType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(4000)]
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public int Priority { get; set; } = 5;
|
||||
|
||||
public DateTime? ScheduledFor { get; set; }
|
||||
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
|
||||
public bool RequiresResponse { get; set; } = false;
|
||||
|
||||
public bool IsUrgent { get; set; } = false;
|
||||
|
||||
public bool IsMarketing { get; set; } = false;
|
||||
}
|
||||
|
||||
public class CustomerMessageSummaryDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid CustomerId { get; set; }
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public Guid? OrderId { get; set; }
|
||||
public string? OrderReference { get; set; }
|
||||
public MessageDirection Direction { get; set; }
|
||||
public MessageType Type { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public MessageStatus Status { get; set; }
|
||||
public DateTime CreatedAt { get; set; }
|
||||
public DateTime? SentAt { get; set; }
|
||||
public bool IsUrgent { get; set; }
|
||||
public bool RequiresResponse { get; set; }
|
||||
public string DisplayTitle { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class MessageThreadDto
|
||||
{
|
||||
public Guid ThreadId { get; set; }
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
public Guid CustomerId { get; set; }
|
||||
public string CustomerName { get; set; } = string.Empty;
|
||||
public Guid? OrderId { get; set; }
|
||||
public string? OrderReference { get; set; }
|
||||
public DateTime StartedAt { get; set; }
|
||||
public DateTime LastMessageAt { get; set; }
|
||||
public int MessageCount { get; set; }
|
||||
public bool HasUnreadMessages { get; set; }
|
||||
public bool RequiresResponse { get; set; }
|
||||
public List<CustomerMessageDto> Messages { get; set; } = new();
|
||||
}
|
||||
@@ -6,8 +6,12 @@ namespace LittleShop.DTOs;
|
||||
public class OrderDto
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
public Guid? CustomerId { get; set; }
|
||||
public string? IdentityReference { get; set; }
|
||||
public OrderStatus Status { get; set; }
|
||||
|
||||
// Customer Information (embedded for convenience)
|
||||
public CustomerSummaryDto? Customer { get; set; }
|
||||
public decimal TotalAmount { get; set; }
|
||||
public string Currency { get; set; } = "GBP";
|
||||
public string ShippingName { get; set; } = string.Empty;
|
||||
@@ -37,8 +41,13 @@ public class OrderItemDto
|
||||
|
||||
public class CreateOrderDto
|
||||
{
|
||||
[Required]
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
// Either Customer ID (for registered customers) OR Identity Reference (for anonymous)
|
||||
public Guid? CustomerId { get; set; }
|
||||
|
||||
public string? IdentityReference { get; set; }
|
||||
|
||||
// Customer Information (collected at checkout for anonymous orders)
|
||||
public CreateCustomerDto? CustomerInfo { get; set; }
|
||||
|
||||
[Required]
|
||||
public string ShippingName { get; set; } = string.Empty;
|
||||
|
||||
12
LittleShop/DTOs/PagedResultDto.cs
Normal file
12
LittleShop/DTOs/PagedResultDto.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace LittleShop.DTOs;
|
||||
|
||||
public class PagedResult<T>
|
||||
{
|
||||
public List<T> Items { get; set; } = new();
|
||||
public int TotalCount { get; set; }
|
||||
public int PageNumber { get; set; }
|
||||
public int PageSize { get; set; }
|
||||
public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
|
||||
public bool HasPrevious => PageNumber > 1;
|
||||
public bool HasNext => PageNumber < TotalPages;
|
||||
}
|
||||
@@ -17,6 +17,11 @@ public class LittleShopContext : DbContext
|
||||
public DbSet<OrderItem> OrderItems { get; set; }
|
||||
public DbSet<CryptoPayment> CryptoPayments { get; set; }
|
||||
public DbSet<ShippingRate> ShippingRates { get; set; }
|
||||
public DbSet<Bot> Bots { get; set; }
|
||||
public DbSet<BotMetric> BotMetrics { get; set; }
|
||||
public DbSet<BotSession> BotSessions { get; set; }
|
||||
public DbSet<Customer> Customers { get; set; }
|
||||
public DbSet<CustomerMessage> CustomerMessages { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -54,6 +59,12 @@ public class LittleShopContext : DbContext
|
||||
// Order entity
|
||||
modelBuilder.Entity<Order>(entity =>
|
||||
{
|
||||
entity.HasOne(o => o.Customer)
|
||||
.WithMany(c => c.Orders)
|
||||
.HasForeignKey(o => o.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired(false); // Make customer optional for transition
|
||||
|
||||
entity.HasMany(o => o.Items)
|
||||
.WithOne(oi => oi.Order)
|
||||
.HasForeignKey(oi => oi.OrderId)
|
||||
@@ -64,7 +75,9 @@ public class LittleShopContext : DbContext
|
||||
.HasForeignKey(cp => cp.OrderId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasIndex(e => e.CustomerId);
|
||||
entity.HasIndex(e => e.IdentityReference);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
});
|
||||
|
||||
// OrderItem entity
|
||||
@@ -79,5 +92,92 @@ public class LittleShopContext : DbContext
|
||||
entity.HasIndex(e => e.BTCPayInvoiceId);
|
||||
entity.HasIndex(e => e.WalletAddress);
|
||||
});
|
||||
|
||||
// Bot entity
|
||||
modelBuilder.Entity<Bot>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.BotKey).IsUnique();
|
||||
entity.HasIndex(e => e.Name);
|
||||
entity.HasIndex(e => e.Status);
|
||||
|
||||
entity.HasMany(b => b.Metrics)
|
||||
.WithOne(m => m.Bot)
|
||||
.HasForeignKey(m => m.BotId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasMany(b => b.Sessions)
|
||||
.WithOne(s => s.Bot)
|
||||
.HasForeignKey(s => s.BotId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// BotMetric entity
|
||||
modelBuilder.Entity<BotMetric>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => new { e.BotId, e.RecordedAt });
|
||||
entity.HasIndex(e => e.MetricType);
|
||||
});
|
||||
|
||||
// BotSession entity
|
||||
modelBuilder.Entity<BotSession>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => new { e.BotId, e.SessionIdentifier });
|
||||
entity.HasIndex(e => e.StartedAt);
|
||||
entity.HasIndex(e => e.LastActivityAt);
|
||||
});
|
||||
|
||||
// Customer entity
|
||||
modelBuilder.Entity<Customer>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.TelegramUserId).IsUnique();
|
||||
entity.HasIndex(e => e.TelegramUsername);
|
||||
entity.HasIndex(e => e.Email);
|
||||
entity.HasIndex(e => e.CreatedAt);
|
||||
entity.HasIndex(e => e.LastActiveAt);
|
||||
entity.HasIndex(e => e.DataRetentionDate);
|
||||
|
||||
entity.HasMany(c => c.Orders)
|
||||
.WithOne(o => o.Customer)
|
||||
.HasForeignKey(o => o.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasMany(c => c.Messages)
|
||||
.WithOne(m => m.Customer)
|
||||
.HasForeignKey(m => m.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
});
|
||||
|
||||
// CustomerMessage entity
|
||||
modelBuilder.Entity<CustomerMessage>(entity =>
|
||||
{
|
||||
entity.HasOne(m => m.Customer)
|
||||
.WithMany(c => c.Messages)
|
||||
.HasForeignKey(m => m.CustomerId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
entity.HasOne(m => m.Order)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.OrderId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(m => m.AdminUser)
|
||||
.WithMany()
|
||||
.HasForeignKey(m => m.AdminUserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
entity.HasOne(m => m.ParentMessage)
|
||||
.WithMany(m => m.Replies)
|
||||
.HasForeignKey(m => m.ParentMessageId)
|
||||
.OnDelete(DeleteBehavior.Restrict);
|
||||
|
||||
entity.HasIndex(e => new { e.CustomerId, e.CreatedAt });
|
||||
entity.HasIndex(e => e.OrderId);
|
||||
entity.HasIndex(e => e.Status);
|
||||
entity.HasIndex(e => e.Direction);
|
||||
entity.HasIndex(e => e.Type);
|
||||
entity.HasIndex(e => e.ScheduledFor);
|
||||
entity.HasIndex(e => e.ThreadId);
|
||||
entity.HasIndex(e => e.ParentMessageId);
|
||||
});
|
||||
}
|
||||
}
|
||||
38
LittleShop/Dockerfile
Normal file
38
LittleShop/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# Build stage
|
||||
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Copy project file and restore dependencies
|
||||
COPY LittleShop.csproj .
|
||||
RUN dotnet restore
|
||||
|
||||
# Copy all source files and build
|
||||
COPY . .
|
||||
RUN dotnet publish -c Release -o /app/publish
|
||||
|
||||
# Runtime stage
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0
|
||||
WORKDIR /app
|
||||
|
||||
# Install curl for health checks
|
||||
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy published app
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
# Create directories for uploads and database
|
||||
RUN mkdir -p /app/data /app/wwwroot/uploads/products /app/logs
|
||||
|
||||
# Set environment variables
|
||||
ENV ASPNETCORE_URLS=http://+:5000
|
||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:5000/api/test || exit 1
|
||||
|
||||
# Run the application
|
||||
ENTRYPOINT ["dotnet", "LittleShop.dll"]
|
||||
11
LittleShop/Enums/BotStatus.cs
Normal file
11
LittleShop/Enums/BotStatus.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace LittleShop.Enums;
|
||||
|
||||
public enum BotStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Active = 1,
|
||||
Inactive = 2,
|
||||
Suspended = 3,
|
||||
Maintenance = 4,
|
||||
Deleted = 5
|
||||
}
|
||||
12
LittleShop/Enums/BotType.cs
Normal file
12
LittleShop/Enums/BotType.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace LittleShop.Enums;
|
||||
|
||||
public enum BotType
|
||||
{
|
||||
Telegram = 0,
|
||||
Discord = 1,
|
||||
WhatsApp = 2,
|
||||
Signal = 3,
|
||||
Matrix = 4,
|
||||
IRC = 5,
|
||||
Custom = 99
|
||||
}
|
||||
21
LittleShop/Enums/MetricType.cs
Normal file
21
LittleShop/Enums/MetricType.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace LittleShop.Enums;
|
||||
|
||||
public enum MetricType
|
||||
{
|
||||
UserContact = 0,
|
||||
NewSession = 1,
|
||||
Order = 2,
|
||||
Payment = 3,
|
||||
Message = 4,
|
||||
Command = 5,
|
||||
Error = 6,
|
||||
ApiCall = 7,
|
||||
CacheHit = 8,
|
||||
CacheMiss = 9,
|
||||
ResponseTime = 10,
|
||||
Uptime = 11,
|
||||
Revenue = 12,
|
||||
CartAbandoned = 13,
|
||||
ProductView = 14,
|
||||
CategoryBrowse = 15
|
||||
}
|
||||
@@ -43,5 +43,35 @@ public class MappingProfile : Profile
|
||||
.ForMember(dest => dest.ProductName, opt => opt.MapFrom(src => src.Product.Name));
|
||||
|
||||
CreateMap<CryptoPayment, CryptoPaymentDto>();
|
||||
|
||||
// Customer mappings
|
||||
CreateMap<Customer, CustomerDto>()
|
||||
.ForMember(dest => dest.DisplayName, opt => opt.Ignore())
|
||||
.ForMember(dest => dest.CustomerType, opt => opt.Ignore());
|
||||
CreateMap<CreateCustomerDto, Customer>();
|
||||
CreateMap<UpdateCustomerDto, Customer>()
|
||||
.ForAllMembers(opt => opt.Condition((src, dest, srcMember) => srcMember != null));
|
||||
|
||||
CreateMap<Customer, CustomerSummaryDto>()
|
||||
.ForMember(dest => dest.DisplayName, opt => opt.MapFrom(src => src.DisplayName))
|
||||
.ForMember(dest => dest.CustomerType, opt => opt.MapFrom(src => src.CustomerType));
|
||||
|
||||
// CustomerMessage mappings
|
||||
CreateMap<CustomerMessage, CustomerMessageDto>()
|
||||
.ForMember(dest => dest.Customer, opt => opt.MapFrom(src => src.Customer))
|
||||
.ForMember(dest => dest.AdminUsername, opt => opt.MapFrom(src => src.AdminUser != null ? src.AdminUser.Username : null))
|
||||
.ForMember(dest => dest.OrderReference, opt => opt.MapFrom(src => src.Order != null ? src.Order.Id.ToString().Substring(0, 8) : null))
|
||||
.ForMember(dest => dest.TelegramUserId, opt => opt.MapFrom(src => src.Customer != null ? src.Customer.TelegramUserId : 0))
|
||||
.ForMember(dest => dest.DisplayTitle, opt => opt.MapFrom(src => src.GetDisplayTitle()))
|
||||
.ForMember(dest => dest.StatusDisplay, opt => opt.MapFrom(src => src.Status.ToString()))
|
||||
.ForMember(dest => dest.DirectionDisplay, opt => opt.MapFrom(src => src.Direction == MessageDirection.AdminToCustomer ? "Outbound" : "Inbound"))
|
||||
.ForMember(dest => dest.TypeDisplay, opt => opt.MapFrom(src => src.Type.ToString()));
|
||||
|
||||
CreateMap<CreateCustomerMessageDto, CustomerMessage>()
|
||||
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid()))
|
||||
.ForMember(dest => dest.Direction, opt => opt.MapFrom(src => MessageDirection.AdminToCustomer))
|
||||
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
|
||||
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => MessageStatus.Pending))
|
||||
.ForMember(dest => dest.Platform, opt => opt.MapFrom(src => "Telegram"));
|
||||
}
|
||||
}
|
||||
58
LittleShop/Models/Bot.cs
Normal file
58
LittleShop/Models/Bot.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class Bot
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string BotKey { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public BotType Type { get; set; }
|
||||
|
||||
public BotStatus Status { get; set; }
|
||||
|
||||
public string Settings { get; set; } = "{}"; // JSON storage
|
||||
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
public DateTime? LastSeenAt { get; set; }
|
||||
|
||||
public DateTime? LastConfigSyncAt { get; set; }
|
||||
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string IpAddress { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100)]
|
||||
public string PlatformUsername { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(200)]
|
||||
public string PlatformDisplayName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(100)]
|
||||
public string PlatformId { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string PersonalityName { get; set; } = string.Empty;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<BotMetric> Metrics { get; set; } = new List<BotMetric>();
|
||||
public virtual ICollection<BotSession> Sessions { get; set; } = new List<BotSession>();
|
||||
}
|
||||
30
LittleShop/Models/BotMetric.cs
Normal file
30
LittleShop/Models/BotMetric.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class BotMetric
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid BotId { get; set; }
|
||||
|
||||
public MetricType MetricType { get; set; }
|
||||
|
||||
public decimal Value { get; set; }
|
||||
|
||||
public string Metadata { get; set; } = "{}"; // JSON storage for additional data
|
||||
|
||||
public DateTime RecordedAt { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string Category { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(500)]
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
// Navigation property
|
||||
public virtual Bot Bot { get; set; } = null!;
|
||||
}
|
||||
44
LittleShop/Models/BotSession.cs
Normal file
44
LittleShop/Models/BotSession.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class BotSession
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public Guid BotId { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(256)]
|
||||
public string SessionIdentifier { get; set; } = string.Empty; // Hashed user ID
|
||||
|
||||
[StringLength(100)]
|
||||
public string Platform { get; set; } = string.Empty; // Telegram, Discord, etc.
|
||||
|
||||
public DateTime StartedAt { get; set; }
|
||||
|
||||
public DateTime LastActivityAt { get; set; }
|
||||
|
||||
public DateTime? EndedAt { get; set; }
|
||||
|
||||
public int OrderCount { get; set; }
|
||||
|
||||
public int MessageCount { get; set; }
|
||||
|
||||
public decimal TotalSpent { get; set; }
|
||||
|
||||
[StringLength(50)]
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
[StringLength(100)]
|
||||
public string Country { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAnonymous { get; set; }
|
||||
|
||||
public string Metadata { get; set; } = "{}"; // JSON for additional session data
|
||||
|
||||
// Navigation property
|
||||
public virtual Bot Bot { get; set; } = null!;
|
||||
}
|
||||
148
LittleShop/Models/Customer.cs
Normal file
148
LittleShop/Models/Customer.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public class Customer
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Telegram Information
|
||||
[Required]
|
||||
public long TelegramUserId { get; set; }
|
||||
|
||||
[StringLength(100)]
|
||||
public string TelegramUsername { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(200)]
|
||||
public string TelegramDisplayName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string TelegramFirstName { get; set; } = string.Empty;
|
||||
|
||||
[StringLength(50)]
|
||||
public string TelegramLastName { get; set; } = string.Empty;
|
||||
|
||||
// Optional Contact Information (if customer provides)
|
||||
[StringLength(100)]
|
||||
public string? Email { get; set; }
|
||||
|
||||
[StringLength(20)]
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
// Customer Preferences
|
||||
public bool AllowMarketing { get; set; } = false;
|
||||
|
||||
public bool AllowOrderUpdates { get; set; } = true;
|
||||
|
||||
[StringLength(10)]
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
[StringLength(10)]
|
||||
public string Timezone { get; set; } = "UTC";
|
||||
|
||||
// Customer Metrics
|
||||
public int TotalOrders { get; set; } = 0;
|
||||
|
||||
[Column(TypeName = "decimal(18,2)")]
|
||||
public decimal TotalSpent { get; set; } = 0;
|
||||
|
||||
[Column(TypeName = "decimal(18,2)")]
|
||||
public decimal AverageOrderValue { get; set; } = 0;
|
||||
|
||||
public DateTime FirstOrderDate { get; set; }
|
||||
|
||||
public DateTime LastOrderDate { get; set; }
|
||||
|
||||
// Customer Service Notes
|
||||
[StringLength(2000)]
|
||||
public string? CustomerNotes { get; set; }
|
||||
|
||||
public bool IsBlocked { get; set; } = false;
|
||||
|
||||
[StringLength(500)]
|
||||
public string? BlockReason { get; set; }
|
||||
|
||||
// Risk Assessment
|
||||
public int RiskScore { get; set; } = 0; // 0-100, 0 = trusted, 100 = high risk
|
||||
|
||||
public int SuccessfulOrders { get; set; } = 0;
|
||||
|
||||
public int CancelledOrders { get; set; } = 0;
|
||||
|
||||
public int DisputedOrders { get; set; } = 0;
|
||||
|
||||
// Data Management
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime LastActiveAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? DataRetentionDate { get; set; } // When to delete this customer's data
|
||||
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
// Navigation properties
|
||||
public virtual ICollection<Order> Orders { get; set; } = new List<Order>();
|
||||
public virtual ICollection<CustomerMessage> Messages { get; set; } = new List<CustomerMessage>();
|
||||
|
||||
// Calculated Properties
|
||||
public string DisplayName =>
|
||||
!string.IsNullOrEmpty(TelegramDisplayName) ? TelegramDisplayName :
|
||||
!string.IsNullOrEmpty(TelegramUsername) ? $"@{TelegramUsername}" :
|
||||
$"{TelegramFirstName} {TelegramLastName}".Trim();
|
||||
|
||||
public string CustomerType =>
|
||||
TotalOrders == 0 ? "New" :
|
||||
TotalOrders == 1 ? "First-time" :
|
||||
TotalOrders < 5 ? "Regular" :
|
||||
TotalOrders < 20 ? "Loyal" : "VIP";
|
||||
|
||||
public void UpdateMetrics()
|
||||
{
|
||||
if (Orders?.Any() == true)
|
||||
{
|
||||
TotalOrders = Orders.Count();
|
||||
TotalSpent = Orders.Sum(o => o.TotalAmount);
|
||||
AverageOrderValue = TotalOrders > 0 ? TotalSpent / TotalOrders : 0;
|
||||
FirstOrderDate = Orders.Min(o => o.CreatedAt);
|
||||
LastOrderDate = Orders.Max(o => o.CreatedAt);
|
||||
SuccessfulOrders = Orders.Count(o => o.Status == Enums.OrderStatus.Delivered);
|
||||
CancelledOrders = Orders.Count(o => o.Status == Enums.OrderStatus.Cancelled);
|
||||
}
|
||||
|
||||
UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
// Update risk score based on order history
|
||||
CalculateRiskScore();
|
||||
}
|
||||
|
||||
private void CalculateRiskScore()
|
||||
{
|
||||
int score = 0;
|
||||
|
||||
// New customers have moderate risk
|
||||
if (TotalOrders == 0) score += 30;
|
||||
|
||||
// High cancellation rate increases risk
|
||||
if (TotalOrders > 0)
|
||||
{
|
||||
var cancellationRate = (double)CancelledOrders / TotalOrders;
|
||||
if (cancellationRate > 0.5) score += 40;
|
||||
else if (cancellationRate > 0.3) score += 20;
|
||||
}
|
||||
|
||||
// Disputes increase risk significantly
|
||||
score += DisputedOrders * 25;
|
||||
|
||||
// Long-term customers with good history reduce risk
|
||||
if (TotalOrders > 10 && SuccessfulOrders > 8) score -= 20;
|
||||
if (TotalSpent > 500) score -= 10;
|
||||
|
||||
// Clamp between 0-100
|
||||
RiskScore = Math.Max(0, Math.Min(100, score));
|
||||
}
|
||||
}
|
||||
185
LittleShop/Models/CustomerMessage.cs
Normal file
185
LittleShop/Models/CustomerMessage.cs
Normal file
@@ -0,0 +1,185 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace LittleShop.Models;
|
||||
|
||||
public enum MessageDirection
|
||||
{
|
||||
AdminToCustomer = 0,
|
||||
CustomerToAdmin = 1
|
||||
}
|
||||
|
||||
public enum MessageStatus
|
||||
{
|
||||
Pending = 0, // Message created, waiting to be sent
|
||||
Sent = 1, // Message delivered to customer
|
||||
Delivered = 2, // Message acknowledged by bot/platform
|
||||
Read = 3, // Customer has read the message
|
||||
Failed = 4 // Delivery failed
|
||||
}
|
||||
|
||||
public enum MessageType
|
||||
{
|
||||
OrderUpdate = 0, // Status update about an order
|
||||
PaymentReminder = 1, // Payment reminder
|
||||
ShippingInfo = 2, // Tracking/shipping information
|
||||
CustomerService = 3, // General customer service
|
||||
Marketing = 4, // Marketing/promotional (requires consent)
|
||||
SystemAlert = 5 // System-generated alerts
|
||||
}
|
||||
|
||||
public class CustomerMessage
|
||||
{
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
// Relationships
|
||||
[Required]
|
||||
public Guid CustomerId { get; set; }
|
||||
|
||||
public Guid? OrderId { get; set; } // Optional - message may not be about specific order
|
||||
|
||||
public Guid? AdminUserId { get; set; } // Which admin sent the message (null for system messages or unidentified admins)
|
||||
|
||||
// Message Details
|
||||
[Required]
|
||||
public MessageDirection Direction { get; set; }
|
||||
|
||||
[Required]
|
||||
public MessageType Type { get; set; }
|
||||
|
||||
[Required]
|
||||
[StringLength(100)]
|
||||
public string Subject { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[StringLength(4000)] // Telegram message limit is ~4096 characters
|
||||
public string Content { get; set; } = string.Empty;
|
||||
|
||||
public MessageStatus Status { get; set; } = MessageStatus.Pending;
|
||||
|
||||
// Delivery Information
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public DateTime? SentAt { get; set; }
|
||||
|
||||
public DateTime? DeliveredAt { get; set; }
|
||||
|
||||
public DateTime? ReadAt { get; set; }
|
||||
|
||||
public DateTime? FailedAt { get; set; }
|
||||
|
||||
[StringLength(500)]
|
||||
public string? FailureReason { get; set; }
|
||||
|
||||
public int RetryCount { get; set; } = 0;
|
||||
|
||||
public DateTime? NextRetryAt { get; set; }
|
||||
|
||||
// Message Threading (for conversations)
|
||||
public Guid? ParentMessageId { get; set; } // Reply to another message
|
||||
|
||||
public Guid? ThreadId { get; set; } // Conversation thread identifier
|
||||
|
||||
// Platform-specific Information
|
||||
[StringLength(100)]
|
||||
public string Platform { get; set; } = "Telegram"; // Telegram, Email, SMS, etc.
|
||||
|
||||
[StringLength(200)]
|
||||
public string? PlatformMessageId { get; set; } // Telegram message ID, email message ID, etc.
|
||||
|
||||
// Priority and Scheduling
|
||||
public int Priority { get; set; } = 5; // 1-10, 1 = highest priority
|
||||
|
||||
public DateTime? ScheduledFor { get; set; } // For scheduled messages
|
||||
|
||||
public DateTime? ExpiresAt { get; set; } // Message expires if not delivered
|
||||
|
||||
// Customer Preferences
|
||||
public bool RequiresResponse { get; set; } = false;
|
||||
|
||||
public bool IsUrgent { get; set; } = false;
|
||||
|
||||
public bool IsMarketing { get; set; } = false;
|
||||
|
||||
// Auto-generated flags
|
||||
public bool IsAutoGenerated { get; set; } = false;
|
||||
|
||||
[StringLength(100)]
|
||||
public string? AutoGenerationTrigger { get; set; } // e.g., "OrderStatusChanged", "PaymentOverdue"
|
||||
|
||||
// Data retention
|
||||
public bool IsArchived { get; set; } = false;
|
||||
|
||||
public DateTime? ArchivedAt { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer Customer { get; set; } = null!;
|
||||
public virtual Order? Order { get; set; }
|
||||
public virtual User? AdminUser { get; set; }
|
||||
public virtual CustomerMessage? ParentMessage { get; set; }
|
||||
public virtual ICollection<CustomerMessage> Replies { get; set; } = new List<CustomerMessage>();
|
||||
|
||||
// Helper methods
|
||||
public bool CanRetry()
|
||||
{
|
||||
return Status == MessageStatus.Failed &&
|
||||
RetryCount < 3 &&
|
||||
(NextRetryAt == null || DateTime.UtcNow >= NextRetryAt) &&
|
||||
(ExpiresAt == null || DateTime.UtcNow < ExpiresAt);
|
||||
}
|
||||
|
||||
public void MarkAsSent()
|
||||
{
|
||||
Status = MessageStatus.Sent;
|
||||
SentAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MarkAsDelivered(string? platformMessageId = null)
|
||||
{
|
||||
Status = MessageStatus.Delivered;
|
||||
DeliveredAt = DateTime.UtcNow;
|
||||
if (!string.IsNullOrEmpty(platformMessageId))
|
||||
{
|
||||
PlatformMessageId = platformMessageId;
|
||||
}
|
||||
}
|
||||
|
||||
public void MarkAsRead()
|
||||
{
|
||||
Status = MessageStatus.Read;
|
||||
ReadAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void MarkAsFailed(string reason)
|
||||
{
|
||||
Status = MessageStatus.Failed;
|
||||
FailedAt = DateTime.UtcNow;
|
||||
FailureReason = reason;
|
||||
RetryCount++;
|
||||
|
||||
// Exponential backoff for retries
|
||||
if (RetryCount <= 3)
|
||||
{
|
||||
var delayMinutes = Math.Pow(2, RetryCount) * 5; // 10, 20, 40 minutes
|
||||
NextRetryAt = DateTime.UtcNow.AddMinutes(delayMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDisplayTitle()
|
||||
{
|
||||
var prefix = Direction == MessageDirection.AdminToCustomer ? "→" : "←";
|
||||
var typeStr = Type switch
|
||||
{
|
||||
MessageType.OrderUpdate => "Order Update",
|
||||
MessageType.PaymentReminder => "Payment",
|
||||
MessageType.ShippingInfo => "Shipping",
|
||||
MessageType.CustomerService => "Support",
|
||||
MessageType.Marketing => "Marketing",
|
||||
MessageType.SystemAlert => "System",
|
||||
_ => "Message"
|
||||
};
|
||||
|
||||
return $"{prefix} {typeStr}: {Subject}";
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,12 @@ public class Order
|
||||
[Key]
|
||||
public Guid Id { get; set; }
|
||||
|
||||
[Required]
|
||||
// Customer Information (nullable for transition period)
|
||||
public Guid? CustomerId { get; set; }
|
||||
|
||||
// Legacy identity reference (still used for anonymous orders)
|
||||
[StringLength(100)]
|
||||
public string IdentityReference { get; set; } = string.Empty;
|
||||
public string? IdentityReference { get; set; }
|
||||
|
||||
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
|
||||
|
||||
@@ -57,6 +60,7 @@ public class Order
|
||||
public DateTime? ShippedAt { get; set; }
|
||||
|
||||
// Navigation properties
|
||||
public virtual Customer? Customer { get; set; }
|
||||
public virtual ICollection<OrderItem> Items { get; set; } = new List<OrderItem>();
|
||||
public virtual ICollection<CryptoPayment> Payments { get; set; } = new List<CryptoPayment>();
|
||||
}
|
||||
@@ -66,6 +66,12 @@ builder.Services.AddScoped<ICryptoPaymentService, CryptoPaymentService>();
|
||||
builder.Services.AddScoped<IBTCPayServerService, BTCPayServerService>();
|
||||
builder.Services.AddScoped<IShippingRateService, ShippingRateService>();
|
||||
builder.Services.AddScoped<IDataSeederService, DataSeederService>();
|
||||
builder.Services.AddScoped<IBotService, BotService>();
|
||||
builder.Services.AddScoped<IBotMetricsService, BotMetricsService>();
|
||||
builder.Services.AddScoped<ICustomerService, CustomerService>();
|
||||
builder.Services.AddScoped<ICustomerMessageService, CustomerMessageService>();
|
||||
builder.Services.AddSingleton<ITelegramBotManagerService, TelegramBotManagerService>();
|
||||
builder.Services.AddHostedService<TelegramBotManagerService>();
|
||||
|
||||
// AutoMapper
|
||||
builder.Services.AddAutoMapper(typeof(Program));
|
||||
|
||||
382
LittleShop/Services/BotMetricsService.cs
Normal file
382
LittleShop/Services/BotMetricsService.cs
Normal file
@@ -0,0 +1,382 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class BotMetricsService : IBotMetricsService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly ILogger<BotMetricsService> _logger;
|
||||
|
||||
public BotMetricsService(LittleShopContext context, ILogger<BotMetricsService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Metrics Methods
|
||||
public async Task<BotMetricDto> RecordMetricAsync(Guid botId, CreateBotMetricDto dto)
|
||||
{
|
||||
var metric = new BotMetric
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
MetricType = dto.MetricType,
|
||||
Value = dto.Value,
|
||||
Category = dto.Category,
|
||||
Description = dto.Description,
|
||||
Metadata = JsonSerializer.Serialize(dto.Metadata),
|
||||
RecordedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.BotMetrics.Add(metric);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return MapMetricToDto(metric);
|
||||
}
|
||||
|
||||
public async Task<bool> RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metrics = dto.Metrics.Select(m => new BotMetric
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
MetricType = m.MetricType,
|
||||
Value = m.Value,
|
||||
Category = m.Category,
|
||||
Description = m.Description,
|
||||
Metadata = JsonSerializer.Serialize(m.Metadata),
|
||||
RecordedAt = DateTime.UtcNow
|
||||
}).ToList();
|
||||
|
||||
_context.BotMetrics.AddRange(metrics);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Recorded {Count} metrics for bot {BotId}", metrics.Count, botId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to record metrics batch for bot {BotId}", botId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotMetricDto>> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var query = _context.BotMetrics.Where(m => m.BotId == botId);
|
||||
|
||||
if (startDate.HasValue)
|
||||
query = query.Where(m => m.RecordedAt >= startDate.Value);
|
||||
|
||||
if (endDate.HasValue)
|
||||
query = query.Where(m => m.RecordedAt <= endDate.Value);
|
||||
|
||||
var metrics = await query
|
||||
.OrderByDescending(m => m.RecordedAt)
|
||||
.Take(1000) // Limit to prevent large results
|
||||
.ToListAsync();
|
||||
|
||||
return metrics.Select(MapMetricToDto);
|
||||
}
|
||||
|
||||
public async Task<BotMetricsSummaryDto> GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return new BotMetricsSummaryDto { BotId = botId };
|
||||
|
||||
var start = startDate ?? DateTime.UtcNow.AddDays(-30);
|
||||
var end = endDate ?? DateTime.UtcNow;
|
||||
|
||||
var metrics = await _context.BotMetrics
|
||||
.Where(m => m.BotId == botId && m.RecordedAt >= start && m.RecordedAt <= end)
|
||||
.ToListAsync();
|
||||
|
||||
var sessions = await _context.BotSessions
|
||||
.Where(s => s.BotId == botId && s.StartedAt >= start && s.StartedAt <= end)
|
||||
.ToListAsync();
|
||||
|
||||
var summary = new BotMetricsSummaryDto
|
||||
{
|
||||
BotId = botId,
|
||||
BotName = bot.Name,
|
||||
PeriodStart = start,
|
||||
PeriodEnd = end,
|
||||
TotalSessions = sessions.Count,
|
||||
UniqueSessions = sessions.Select(s => s.SessionIdentifier).Distinct().Count(),
|
||||
TotalOrders = sessions.Sum(s => s.OrderCount),
|
||||
TotalRevenue = sessions.Sum(s => s.TotalSpent),
|
||||
TotalMessages = sessions.Sum(s => s.MessageCount),
|
||||
TotalErrors = (int)metrics.Where(m => m.MetricType == Enums.MetricType.Error).Sum(m => m.Value)
|
||||
};
|
||||
|
||||
// Calculate average response time
|
||||
var responseTimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.ResponseTime).ToList();
|
||||
if (responseTimeMetrics.Any())
|
||||
summary.AverageResponseTime = responseTimeMetrics.Average(m => m.Value);
|
||||
|
||||
// Calculate uptime percentage
|
||||
var uptimeMetrics = metrics.Where(m => m.MetricType == Enums.MetricType.Uptime).ToList();
|
||||
if (uptimeMetrics.Any())
|
||||
{
|
||||
var totalPossibleUptime = (end - start).TotalMinutes;
|
||||
var actualUptime = uptimeMetrics.Sum(m => m.Value);
|
||||
summary.UptimePercentage = (actualUptime / (decimal)totalPossibleUptime) * 100;
|
||||
}
|
||||
|
||||
// Group metrics by type
|
||||
summary.MetricsByType = metrics
|
||||
.GroupBy(m => m.MetricType.ToString())
|
||||
.ToDictionary(g => g.Key, g => g.Sum(m => m.Value));
|
||||
|
||||
// Generate time series data (daily aggregation)
|
||||
summary.TimeSeries = GenerateTimeSeries(metrics, start, end);
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
// Session Methods
|
||||
public async Task<BotSessionDto> StartSessionAsync(Guid botId, CreateBotSessionDto dto)
|
||||
{
|
||||
var session = new BotSession
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
SessionIdentifier = dto.SessionIdentifier,
|
||||
Platform = dto.Platform,
|
||||
StartedAt = DateTime.UtcNow,
|
||||
LastActivityAt = DateTime.UtcNow,
|
||||
Language = dto.Language,
|
||||
Country = dto.Country,
|
||||
IsAnonymous = dto.IsAnonymous,
|
||||
Metadata = JsonSerializer.Serialize(dto.Metadata),
|
||||
OrderCount = 0,
|
||||
MessageCount = 0,
|
||||
TotalSpent = 0
|
||||
};
|
||||
|
||||
_context.BotSessions.Add(session);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Started session {SessionId} for bot {BotId}", session.Id, botId);
|
||||
|
||||
return MapSessionToDto(session);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto)
|
||||
{
|
||||
var session = await _context.BotSessions.FindAsync(sessionId);
|
||||
if (session == null)
|
||||
return false;
|
||||
|
||||
session.LastActivityAt = DateTime.UtcNow;
|
||||
|
||||
if (dto.OrderCount.HasValue)
|
||||
session.OrderCount = dto.OrderCount.Value;
|
||||
|
||||
if (dto.MessageCount.HasValue)
|
||||
session.MessageCount = dto.MessageCount.Value;
|
||||
|
||||
if (dto.TotalSpent.HasValue)
|
||||
session.TotalSpent = dto.TotalSpent.Value;
|
||||
|
||||
if (dto.EndSession.HasValue && dto.EndSession.Value)
|
||||
session.EndedAt = DateTime.UtcNow;
|
||||
|
||||
if (dto.Metadata != null)
|
||||
session.Metadata = JsonSerializer.Serialize(dto.Metadata);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<BotSessionDto?> GetSessionAsync(Guid sessionId)
|
||||
{
|
||||
var session = await _context.BotSessions.FindAsync(sessionId);
|
||||
return session != null ? MapSessionToDto(session) : null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotSessionDto>> GetBotSessionsAsync(Guid botId, bool activeOnly = false)
|
||||
{
|
||||
var query = _context.BotSessions.Where(s => s.BotId == botId);
|
||||
|
||||
if (activeOnly)
|
||||
query = query.Where(s => !s.EndedAt.HasValue);
|
||||
|
||||
var sessions = await query
|
||||
.OrderByDescending(s => s.StartedAt)
|
||||
.Take(100)
|
||||
.ToListAsync();
|
||||
|
||||
return sessions.Select(MapSessionToDto);
|
||||
}
|
||||
|
||||
public async Task<BotSessionSummaryDto> GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null)
|
||||
{
|
||||
var query = _context.BotSessions.Where(s => s.BotId == botId);
|
||||
|
||||
if (startDate.HasValue)
|
||||
query = query.Where(s => s.StartedAt >= startDate.Value);
|
||||
|
||||
if (endDate.HasValue)
|
||||
query = query.Where(s => s.StartedAt <= endDate.Value);
|
||||
|
||||
var sessions = await query.ToListAsync();
|
||||
|
||||
var summary = new BotSessionSummaryDto
|
||||
{
|
||||
TotalSessions = sessions.Count,
|
||||
ActiveSessions = sessions.Count(s => !s.EndedAt.HasValue),
|
||||
CompletedSessions = sessions.Count(s => s.EndedAt.HasValue)
|
||||
};
|
||||
|
||||
if (sessions.Any())
|
||||
{
|
||||
var completedSessions = sessions.Where(s => s.EndedAt.HasValue).ToList();
|
||||
if (completedSessions.Any())
|
||||
{
|
||||
summary.AverageSessionDuration = (decimal)completedSessions
|
||||
.Average(s => (s.EndedAt!.Value - s.StartedAt).TotalMinutes);
|
||||
}
|
||||
|
||||
summary.AverageOrdersPerSession = (decimal)sessions.Average(s => s.OrderCount);
|
||||
summary.AverageSpendPerSession = sessions.Average(s => s.TotalSpent);
|
||||
|
||||
summary.SessionsByPlatform = sessions
|
||||
.GroupBy(s => s.Platform)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
summary.SessionsByCountry = sessions
|
||||
.Where(s => !string.IsNullOrEmpty(s.Country))
|
||||
.GroupBy(s => s.Country)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
|
||||
summary.SessionsByLanguage = sessions
|
||||
.GroupBy(s => s.Language)
|
||||
.ToDictionary(g => g.Key, g => g.Count());
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
public async Task<bool> EndSessionAsync(Guid sessionId)
|
||||
{
|
||||
var session = await _context.BotSessions.FindAsync(sessionId);
|
||||
if (session == null || session.EndedAt.HasValue)
|
||||
return false;
|
||||
|
||||
session.EndedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Ended session {SessionId}", sessionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<int> CleanupInactiveSessionsAsync(int inactiveMinutes = 30)
|
||||
{
|
||||
var cutoffTime = DateTime.UtcNow.AddMinutes(-inactiveMinutes);
|
||||
|
||||
var inactiveSessions = await _context.BotSessions
|
||||
.Where(s => !s.EndedAt.HasValue && s.LastActivityAt < cutoffTime)
|
||||
.ToListAsync();
|
||||
|
||||
foreach (var session in inactiveSessions)
|
||||
{
|
||||
session.EndedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Cleaned up {Count} inactive sessions", inactiveSessions.Count);
|
||||
return inactiveSessions.Count;
|
||||
}
|
||||
|
||||
// Helper Methods
|
||||
private BotMetricDto MapMetricToDto(BotMetric metric)
|
||||
{
|
||||
var metadata = new Dictionary<string, object>();
|
||||
try
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(metric.Metadata)
|
||||
?? new Dictionary<string, object>();
|
||||
}
|
||||
catch { }
|
||||
|
||||
return new BotMetricDto
|
||||
{
|
||||
Id = metric.Id,
|
||||
BotId = metric.BotId,
|
||||
MetricType = metric.MetricType,
|
||||
Value = metric.Value,
|
||||
Category = metric.Category,
|
||||
Description = metric.Description,
|
||||
RecordedAt = metric.RecordedAt,
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
private BotSessionDto MapSessionToDto(BotSession session)
|
||||
{
|
||||
var metadata = new Dictionary<string, object>();
|
||||
try
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, object>>(session.Metadata)
|
||||
?? new Dictionary<string, object>();
|
||||
}
|
||||
catch { }
|
||||
|
||||
return new BotSessionDto
|
||||
{
|
||||
Id = session.Id,
|
||||
BotId = session.BotId,
|
||||
SessionIdentifier = session.SessionIdentifier,
|
||||
Platform = session.Platform,
|
||||
StartedAt = session.StartedAt,
|
||||
LastActivityAt = session.LastActivityAt,
|
||||
EndedAt = session.EndedAt,
|
||||
OrderCount = session.OrderCount,
|
||||
MessageCount = session.MessageCount,
|
||||
TotalSpent = session.TotalSpent,
|
||||
Language = session.Language,
|
||||
Country = session.Country,
|
||||
IsAnonymous = session.IsAnonymous,
|
||||
Metadata = metadata
|
||||
};
|
||||
}
|
||||
|
||||
private List<TimeSeriesDataPoint> GenerateTimeSeries(List<BotMetric> metrics, DateTime start, DateTime end)
|
||||
{
|
||||
var dataPoints = new List<TimeSeriesDataPoint>();
|
||||
var currentDate = start.Date;
|
||||
|
||||
while (currentDate <= end.Date)
|
||||
{
|
||||
var dayMetrics = metrics.Where(m => m.RecordedAt.Date == currentDate).ToList();
|
||||
|
||||
if (dayMetrics.Any())
|
||||
{
|
||||
dataPoints.Add(new TimeSeriesDataPoint
|
||||
{
|
||||
Timestamp = currentDate,
|
||||
Label = currentDate.ToString("yyyy-MM-dd"),
|
||||
Value = dayMetrics.Sum(m => m.Value)
|
||||
});
|
||||
}
|
||||
|
||||
currentDate = currentDate.AddDays(1);
|
||||
}
|
||||
|
||||
return dataPoints;
|
||||
}
|
||||
}
|
||||
330
LittleShop/Services/BotService.cs
Normal file
330
LittleShop/Services/BotService.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Enums;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class BotService : IBotService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly ILogger<BotService> _logger;
|
||||
|
||||
public BotService(LittleShopContext context, ILogger<BotService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<BotRegistrationResponseDto> RegisterBotAsync(BotRegistrationDto dto)
|
||||
{
|
||||
_logger.LogInformation("Registering new bot: {BotName}", dto.Name);
|
||||
|
||||
var botKey = await GenerateBotKeyAsync();
|
||||
|
||||
var bot = new Bot
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = dto.Name,
|
||||
Description = dto.Description,
|
||||
Type = dto.Type,
|
||||
BotKey = botKey,
|
||||
Status = BotStatus.Active,
|
||||
Settings = JsonSerializer.Serialize(dto.InitialSettings),
|
||||
Version = dto.Version,
|
||||
PersonalityName = string.IsNullOrEmpty(dto.PersonalityName) ? AssignDefaultPersonality(dto.Name) : dto.PersonalityName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
_context.Bots.Add(bot);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Bot registered successfully: {BotId}", bot.Id);
|
||||
|
||||
return new BotRegistrationResponseDto
|
||||
{
|
||||
BotId = bot.Id,
|
||||
BotKey = botKey,
|
||||
Name = bot.Name,
|
||||
Settings = dto.InitialSettings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BotDto?> AuthenticateBotAsync(string botKey)
|
||||
{
|
||||
var bot = await _context.Bots
|
||||
.Include(b => b.Sessions)
|
||||
.Include(b => b.Metrics)
|
||||
.FirstOrDefaultAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
|
||||
|
||||
if (bot == null)
|
||||
{
|
||||
_logger.LogWarning("Authentication failed for bot key: {BotKey}", botKey.Substring(0, 8) + "...");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last seen
|
||||
bot.LastSeenAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return MapToDto(bot);
|
||||
}
|
||||
|
||||
public async Task<BotDto?> GetBotByIdAsync(Guid id)
|
||||
{
|
||||
var bot = await _context.Bots
|
||||
.Include(b => b.Sessions)
|
||||
.Include(b => b.Metrics)
|
||||
.FirstOrDefaultAsync(b => b.Id == id);
|
||||
|
||||
return bot != null ? MapToDto(bot) : null;
|
||||
}
|
||||
|
||||
public async Task<BotDto?> GetBotByKeyAsync(string botKey)
|
||||
{
|
||||
var bot = await _context.Bots
|
||||
.Include(b => b.Sessions)
|
||||
.Include(b => b.Metrics)
|
||||
.FirstOrDefaultAsync(b => b.BotKey == botKey);
|
||||
|
||||
return bot != null ? MapToDto(bot) : null;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotDto>> GetAllBotsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var bots = await _context.Bots
|
||||
.Include(b => b.Sessions)
|
||||
.Include(b => b.Metrics)
|
||||
.OrderByDescending(b => b.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return bots.Select(MapToDto);
|
||||
}
|
||||
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
|
||||
{
|
||||
// Tables don't exist yet - return empty list
|
||||
return new List<BotDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<BotDto>> GetActiveBots()
|
||||
{
|
||||
try
|
||||
{
|
||||
var bots = await _context.Bots
|
||||
.Include(b => b.Sessions)
|
||||
.Include(b => b.Metrics)
|
||||
.Where(b => b.Status == BotStatus.Active && b.IsActive)
|
||||
.OrderByDescending(b => b.LastSeenAt)
|
||||
.ToListAsync();
|
||||
|
||||
return bots.Select(MapToDto);
|
||||
}
|
||||
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
|
||||
{
|
||||
// Tables don't exist yet - return empty list
|
||||
return new List<BotDto>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return false;
|
||||
|
||||
bot.Settings = JsonSerializer.Serialize(dto.Settings);
|
||||
bot.LastConfigSyncAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated settings for bot {BotId}", botId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateBotStatusAsync(Guid botId, BotStatus status)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return false;
|
||||
|
||||
var oldStatus = bot.Status;
|
||||
bot.Status = status;
|
||||
bot.IsActive = status == BotStatus.Active;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Bot {BotId} status changed from {OldStatus} to {NewStatus}",
|
||||
botId, oldStatus, status);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> RecordHeartbeatAsync(Guid botId, BotHeartbeatDto dto)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return false;
|
||||
|
||||
bot.LastSeenAt = DateTime.UtcNow;
|
||||
bot.Version = dto.Version;
|
||||
bot.IpAddress = dto.IpAddress;
|
||||
|
||||
// Record uptime metric
|
||||
var uptimeMetric = new BotMetric
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
BotId = botId,
|
||||
MetricType = MetricType.Uptime,
|
||||
Value = 1,
|
||||
Metadata = JsonSerializer.Serialize(dto.Status),
|
||||
RecordedAt = DateTime.UtcNow,
|
||||
Category = "System",
|
||||
Description = "Heartbeat"
|
||||
};
|
||||
|
||||
_context.BotMetrics.Add(uptimeMetric);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteBotAsync(Guid botId)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return false;
|
||||
|
||||
bot.Status = BotStatus.Deleted;
|
||||
bot.IsActive = false;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Bot {BotId} marked as deleted", botId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, object>> GetBotSettingsAsync(Guid botId)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return new Dictionary<string, object>();
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
|
||||
?? new Dictionary<string, object>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Dictionary<string, object>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateBotKeyAsync(string botKey)
|
||||
{
|
||||
return await _context.Bots.AnyAsync(b => b.BotKey == botKey && b.Status == BotStatus.Active);
|
||||
}
|
||||
|
||||
public Task<string> GenerateBotKeyAsync()
|
||||
{
|
||||
const string prefix = "bot_";
|
||||
const int keyLength = 32;
|
||||
|
||||
using var rng = RandomNumberGenerator.Create();
|
||||
var bytes = new byte[keyLength];
|
||||
rng.GetBytes(bytes);
|
||||
|
||||
var key = prefix + Convert.ToBase64String(bytes)
|
||||
.Replace("+", "")
|
||||
.Replace("/", "")
|
||||
.Replace("=", "")
|
||||
.Substring(0, keyLength);
|
||||
|
||||
return Task.FromResult(key);
|
||||
}
|
||||
|
||||
public async Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto)
|
||||
{
|
||||
var bot = await _context.Bots.FindAsync(botId);
|
||||
if (bot == null)
|
||||
return false;
|
||||
|
||||
bot.PlatformUsername = dto.PlatformUsername;
|
||||
bot.PlatformDisplayName = dto.PlatformDisplayName;
|
||||
bot.PlatformId = dto.PlatformId;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated platform info for bot {BotId}: @{Username} ({DisplayName})",
|
||||
botId, dto.PlatformUsername, dto.PlatformDisplayName);
|
||||
return true;
|
||||
}
|
||||
|
||||
private BotDto MapToDto(Bot bot)
|
||||
{
|
||||
var settings = new Dictionary<string, object>();
|
||||
try
|
||||
{
|
||||
settings = JsonSerializer.Deserialize<Dictionary<string, object>>(bot.Settings)
|
||||
?? new Dictionary<string, object>();
|
||||
}
|
||||
catch { }
|
||||
|
||||
var activeSessions = bot.Sessions.Count(s => !s.EndedAt.HasValue);
|
||||
var totalRevenue = bot.Sessions.Sum(s => s.TotalSpent);
|
||||
var totalOrders = bot.Sessions.Sum(s => s.OrderCount);
|
||||
|
||||
return new BotDto
|
||||
{
|
||||
Id = bot.Id,
|
||||
Name = bot.Name,
|
||||
Description = bot.Description,
|
||||
Type = bot.Type,
|
||||
Status = bot.Status,
|
||||
CreatedAt = bot.CreatedAt,
|
||||
LastSeenAt = bot.LastSeenAt,
|
||||
LastConfigSyncAt = bot.LastConfigSyncAt,
|
||||
IsActive = bot.IsActive,
|
||||
Version = bot.Version,
|
||||
IpAddress = bot.IpAddress,
|
||||
PlatformUsername = bot.PlatformUsername,
|
||||
PlatformDisplayName = bot.PlatformDisplayName,
|
||||
PlatformId = bot.PlatformId,
|
||||
PersonalityName = bot.PersonalityName,
|
||||
Settings = settings,
|
||||
TotalSessions = bot.Sessions.Count,
|
||||
ActiveSessions = activeSessions,
|
||||
TotalRevenue = totalRevenue,
|
||||
TotalOrders = totalOrders
|
||||
};
|
||||
}
|
||||
|
||||
private string AssignDefaultPersonality(string botName)
|
||||
{
|
||||
var personalities = new[] { "Alan", "Dave", "Sarah", "Mike", "Emma", "Tom" };
|
||||
|
||||
// Smart assignment based on name
|
||||
var lowerName = botName.ToLower();
|
||||
if (lowerName.Contains("alan")) return "Alan";
|
||||
if (lowerName.Contains("dave")) return "Dave";
|
||||
if (lowerName.Contains("sarah")) return "Sarah";
|
||||
if (lowerName.Contains("mike")) return "Mike";
|
||||
if (lowerName.Contains("emma")) return "Emma";
|
||||
if (lowerName.Contains("tom")) return "Tom";
|
||||
|
||||
// Random assignment if no match
|
||||
var random = new Random();
|
||||
return personalities[random.Next(personalities.Length)];
|
||||
}
|
||||
}
|
||||
@@ -163,14 +163,18 @@ public class CryptoPaymentService : ICryptoPaymentService
|
||||
private static string GenerateWalletAddress(CryptoCurrency currency)
|
||||
{
|
||||
// Placeholder wallet addresses - in production these would come from BTCPay Server
|
||||
var guid = Guid.NewGuid().ToString("N"); // 32 characters
|
||||
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]
|
||||
CryptoCurrency.BTC => "bc1q" + guid[..26],
|
||||
CryptoCurrency.XMR => "4" + guid + guid[..32], // XMR needs ~95 chars, use double GUID
|
||||
CryptoCurrency.USDT => "0x" + guid[..32], // ERC-20 address
|
||||
CryptoCurrency.LTC => "ltc1q" + guid[..26],
|
||||
CryptoCurrency.ETH => "0x" + guid[..32],
|
||||
CryptoCurrency.ZEC => "zs1" + guid + guid[..29], // Shielded address
|
||||
CryptoCurrency.DASH => "X" + guid[..30],
|
||||
CryptoCurrency.DOGE => "D" + guid[..30],
|
||||
_ => "placeholder_" + guid[..20]
|
||||
};
|
||||
}
|
||||
}
|
||||
233
LittleShop/Services/CustomerMessageService.cs
Normal file
233
LittleShop/Services/CustomerMessageService.cs
Normal file
@@ -0,0 +1,233 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class CustomerMessageService : ICustomerMessageService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<CustomerMessageService> _logger;
|
||||
|
||||
public CustomerMessageService(LittleShopContext context, IMapper mapper, ILogger<CustomerMessageService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<CustomerMessageDto?> CreateMessageAsync(CreateCustomerMessageDto createMessageDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = _mapper.Map<CustomerMessage>(createMessageDto);
|
||||
message.Id = Guid.NewGuid();
|
||||
message.Direction = MessageDirection.AdminToCustomer;
|
||||
message.CreatedAt = DateTime.UtcNow;
|
||||
message.Status = MessageStatus.Pending;
|
||||
message.Platform = "Telegram";
|
||||
|
||||
// Generate thread ID if this is a new conversation
|
||||
if (message.ParentMessageId == null)
|
||||
{
|
||||
message.ThreadId = Guid.NewGuid();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get parent message's thread ID
|
||||
var parentMessage = await _context.CustomerMessages
|
||||
.FirstOrDefaultAsync(m => m.Id == message.ParentMessageId);
|
||||
message.ThreadId = parentMessage?.ThreadId ?? Guid.NewGuid();
|
||||
}
|
||||
|
||||
_context.CustomerMessages.Add(message);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Created message {MessageId} for customer {CustomerId}",
|
||||
message.Id, message.CustomerId);
|
||||
|
||||
// Return the created message with includes
|
||||
var createdMessage = await GetMessageByIdAsync(message.Id);
|
||||
return createdMessage;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating message for customer {CustomerId}", createMessageDto.CustomerId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CustomerMessageDto?> GetMessageByIdAsync(Guid id)
|
||||
{
|
||||
var message = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Include(m => m.AdminUser)
|
||||
.Include(m => m.ParentMessage)
|
||||
.Include(m => m.Replies)
|
||||
.FirstOrDefaultAsync(m => m.Id == id);
|
||||
|
||||
if (message == null) return null;
|
||||
|
||||
return _mapper.Map<CustomerMessageDto>(message);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CustomerMessageDto>> GetCustomerMessagesAsync(Guid customerId)
|
||||
{
|
||||
var messages = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Include(m => m.AdminUser)
|
||||
.Where(m => m.CustomerId == customerId && !m.IsArchived)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CustomerMessageDto>> GetOrderMessagesAsync(Guid orderId)
|
||||
{
|
||||
var messages = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Include(m => m.AdminUser)
|
||||
.Where(m => m.OrderId == orderId && !m.IsArchived)
|
||||
.OrderByDescending(m => m.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CustomerMessageDto>> GetPendingMessagesAsync(string platform = "Telegram")
|
||||
{
|
||||
var messages = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Include(m => m.AdminUser)
|
||||
.Where(m => m.Status == MessageStatus.Pending &&
|
||||
m.Platform == platform &&
|
||||
m.Direction == MessageDirection.AdminToCustomer &&
|
||||
(m.ScheduledFor == null || m.ScheduledFor <= DateTime.UtcNow) &&
|
||||
(m.ExpiresAt == null || m.ExpiresAt > DateTime.UtcNow))
|
||||
.OrderBy(m => m.Priority)
|
||||
.ThenBy(m => m.CreatedAt)
|
||||
.Take(50) // Limit for performance
|
||||
.ToListAsync();
|
||||
|
||||
return messages.Select(m => _mapper.Map<CustomerMessageDto>(m));
|
||||
}
|
||||
|
||||
public async Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null)
|
||||
{
|
||||
var message = await _context.CustomerMessages.FindAsync(messageId);
|
||||
if (message == null) return false;
|
||||
|
||||
message.MarkAsSent();
|
||||
if (!string.IsNullOrEmpty(platformMessageId))
|
||||
{
|
||||
message.PlatformMessageId = platformMessageId;
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Marked message {MessageId} as sent", messageId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkMessageAsDeliveredAsync(Guid messageId)
|
||||
{
|
||||
var message = await _context.CustomerMessages.FindAsync(messageId);
|
||||
if (message == null) return false;
|
||||
|
||||
message.MarkAsDelivered();
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Marked message {MessageId} as delivered", messageId);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason)
|
||||
{
|
||||
var message = await _context.CustomerMessages.FindAsync(messageId);
|
||||
if (message == null) return false;
|
||||
|
||||
message.MarkAsFailed(reason);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogWarning("Marked message {MessageId} as failed: {Reason}", messageId, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<MessageThreadDto?> GetMessageThreadAsync(Guid threadId)
|
||||
{
|
||||
var messages = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Include(m => m.AdminUser)
|
||||
.Where(m => m.ThreadId == threadId)
|
||||
.OrderBy(m => m.CreatedAt)
|
||||
.ToListAsync();
|
||||
|
||||
if (!messages.Any()) return null;
|
||||
|
||||
var firstMessage = messages.First();
|
||||
var thread = new MessageThreadDto
|
||||
{
|
||||
ThreadId = threadId,
|
||||
Subject = firstMessage.Subject,
|
||||
CustomerId = firstMessage.CustomerId,
|
||||
CustomerName = firstMessage.Customer?.DisplayName ?? "Unknown",
|
||||
OrderId = firstMessage.OrderId,
|
||||
OrderReference = firstMessage.Order?.Id.ToString().Substring(0, 8),
|
||||
StartedAt = firstMessage.CreatedAt,
|
||||
LastMessageAt = messages.Max(m => m.CreatedAt),
|
||||
MessageCount = messages.Count,
|
||||
HasUnreadMessages = messages.Any(m => m.Direction == MessageDirection.CustomerToAdmin && m.Status != MessageStatus.Read),
|
||||
RequiresResponse = messages.Any(m => m.RequiresResponse && m.Status != MessageStatus.Read),
|
||||
Messages = messages.Select(m => _mapper.Map<CustomerMessageDto>(m)).ToList()
|
||||
};
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<MessageThreadDto>> GetActiveThreadsAsync()
|
||||
{
|
||||
var threads = await _context.CustomerMessages
|
||||
.Include(m => m.Customer)
|
||||
.Include(m => m.Order)
|
||||
.Where(m => !m.IsArchived)
|
||||
.GroupBy(m => m.ThreadId)
|
||||
.Select(g => new MessageThreadDto
|
||||
{
|
||||
ThreadId = g.Key ?? Guid.Empty,
|
||||
Subject = g.First().Subject,
|
||||
CustomerId = g.First().CustomerId,
|
||||
CustomerName = g.First().Customer != null ? g.First().Customer.DisplayName : "Unknown",
|
||||
OrderId = g.First().OrderId,
|
||||
OrderReference = g.First().Order != null ? g.First().Order.Id.ToString().Substring(0, 8) : null,
|
||||
StartedAt = g.Min(m => m.CreatedAt),
|
||||
LastMessageAt = g.Max(m => m.CreatedAt),
|
||||
MessageCount = g.Count(),
|
||||
HasUnreadMessages = g.Any(m => m.Direction == MessageDirection.CustomerToAdmin && m.Status != MessageStatus.Read),
|
||||
RequiresResponse = g.Any(m => m.RequiresResponse && m.Status != MessageStatus.Read)
|
||||
})
|
||||
.OrderByDescending(t => t.LastMessageAt)
|
||||
.Take(100) // Limit for performance
|
||||
.ToListAsync();
|
||||
|
||||
return threads;
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateCustomerExistsAsync(Guid customerId)
|
||||
{
|
||||
return await _context.Customers.AnyAsync(c => c.Id == customerId);
|
||||
}
|
||||
|
||||
public async Task<bool> ValidateOrderBelongsToCustomerAsync(Guid orderId, Guid customerId)
|
||||
{
|
||||
return await _context.Orders.AnyAsync(o => o.Id == orderId && o.CustomerId == customerId);
|
||||
}
|
||||
}
|
||||
296
LittleShop/Services/CustomerService.cs
Normal file
296
LittleShop/Services/CustomerService.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using LittleShop.Data;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class CustomerService : ICustomerService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly IMapper _mapper;
|
||||
private readonly ILogger<CustomerService> _logger;
|
||||
|
||||
public CustomerService(LittleShopContext context, IMapper mapper, ILogger<CustomerService> logger)
|
||||
{
|
||||
_context = context;
|
||||
_mapper = mapper;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> GetCustomerByIdAsync(Guid id)
|
||||
{
|
||||
var customer = await _context.Customers
|
||||
.Include(c => c.Orders)
|
||||
.FirstOrDefaultAsync(c => c.Id == id);
|
||||
|
||||
if (customer == null) return null;
|
||||
|
||||
var dto = _mapper.Map<CustomerDto>(customer);
|
||||
dto.DisplayName = customer.DisplayName;
|
||||
dto.CustomerType = customer.CustomerType;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> GetCustomerByTelegramUserIdAsync(long telegramUserId)
|
||||
{
|
||||
var customer = await _context.Customers
|
||||
.Include(c => c.Orders)
|
||||
.FirstOrDefaultAsync(c => c.TelegramUserId == telegramUserId);
|
||||
|
||||
if (customer == null) return null;
|
||||
|
||||
var dto = _mapper.Map<CustomerDto>(customer);
|
||||
dto.DisplayName = customer.DisplayName;
|
||||
dto.CustomerType = customer.CustomerType;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<CustomerDto> CreateCustomerAsync(CreateCustomerDto createCustomerDto)
|
||||
{
|
||||
// Check if customer already exists
|
||||
var existingCustomer = await _context.Customers
|
||||
.FirstOrDefaultAsync(c => c.TelegramUserId == createCustomerDto.TelegramUserId);
|
||||
|
||||
if (existingCustomer != null)
|
||||
{
|
||||
throw new InvalidOperationException($"Customer with Telegram ID {createCustomerDto.TelegramUserId} already exists");
|
||||
}
|
||||
|
||||
var customer = _mapper.Map<Customer>(createCustomerDto);
|
||||
customer.Id = Guid.NewGuid();
|
||||
customer.CreatedAt = DateTime.UtcNow;
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
customer.LastActiveAt = DateTime.UtcNow;
|
||||
customer.IsActive = true;
|
||||
|
||||
// Set data retention date (default: 2 years after creation)
|
||||
customer.DataRetentionDate = DateTime.UtcNow.AddYears(2);
|
||||
|
||||
_context.Customers.Add(customer);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Created new customer {CustomerId} for Telegram user {TelegramUserId}",
|
||||
customer.Id, customer.TelegramUserId);
|
||||
|
||||
var dto = _mapper.Map<CustomerDto>(customer);
|
||||
dto.DisplayName = customer.DisplayName;
|
||||
dto.CustomerType = customer.CustomerType;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> UpdateCustomerAsync(Guid id, UpdateCustomerDto updateCustomerDto)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(id);
|
||||
if (customer == null) return null;
|
||||
|
||||
_mapper.Map(updateCustomerDto, customer);
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated customer {CustomerId}", id);
|
||||
|
||||
var dto = _mapper.Map<CustomerDto>(customer);
|
||||
dto.DisplayName = customer.DisplayName;
|
||||
dto.CustomerType = customer.CustomerType;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteCustomerAsync(Guid id)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(id);
|
||||
if (customer == null) return false;
|
||||
|
||||
// Instead of hard delete, mark as inactive for data retention compliance
|
||||
customer.IsActive = false;
|
||||
customer.DataRetentionDate = DateTime.UtcNow.AddDays(30); // Delete in 30 days
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Marked customer {CustomerId} for deletion", id);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CustomerDto>> GetAllCustomersAsync()
|
||||
{
|
||||
var customers = await _context.Customers
|
||||
.Where(c => c.IsActive)
|
||||
.Include(c => c.Orders)
|
||||
.OrderByDescending(c => c.LastActiveAt)
|
||||
.ToListAsync();
|
||||
|
||||
return customers.Select(c =>
|
||||
{
|
||||
var dto = _mapper.Map<CustomerDto>(c);
|
||||
dto.DisplayName = c.DisplayName;
|
||||
dto.CustomerType = c.CustomerType;
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<CustomerDto>> SearchCustomersAsync(string searchTerm)
|
||||
{
|
||||
var query = _context.Customers
|
||||
.Where(c => c.IsActive)
|
||||
.Include(c => c.Orders)
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(searchTerm))
|
||||
{
|
||||
searchTerm = searchTerm.ToLower();
|
||||
query = query.Where(c =>
|
||||
c.TelegramUsername.ToLower().Contains(searchTerm) ||
|
||||
c.TelegramDisplayName.ToLower().Contains(searchTerm) ||
|
||||
c.TelegramFirstName.ToLower().Contains(searchTerm) ||
|
||||
c.TelegramLastName.ToLower().Contains(searchTerm) ||
|
||||
(c.Email != null && c.Email.ToLower().Contains(searchTerm)));
|
||||
}
|
||||
|
||||
var customers = await query
|
||||
.OrderByDescending(c => c.LastActiveAt)
|
||||
.Take(50) // Limit search results
|
||||
.ToListAsync();
|
||||
|
||||
return customers.Select(c =>
|
||||
{
|
||||
var dto = _mapper.Map<CustomerDto>(c);
|
||||
dto.DisplayName = c.DisplayName;
|
||||
dto.CustomerType = c.CustomerType;
|
||||
return dto;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<CustomerDto?> GetOrCreateCustomerAsync(long telegramUserId, string displayName, string username = "", string firstName = "", string lastName = "")
|
||||
{
|
||||
// Try to find existing customer
|
||||
var customer = await _context.Customers
|
||||
.Include(c => c.Orders)
|
||||
.FirstOrDefaultAsync(c => c.TelegramUserId == telegramUserId);
|
||||
|
||||
if (customer != null)
|
||||
{
|
||||
// Update customer information if provided
|
||||
bool updated = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(displayName) && customer.TelegramDisplayName != displayName)
|
||||
{
|
||||
customer.TelegramDisplayName = displayName;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(username) && customer.TelegramUsername != username)
|
||||
{
|
||||
customer.TelegramUsername = username;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(firstName) && customer.TelegramFirstName != firstName)
|
||||
{
|
||||
customer.TelegramFirstName = firstName;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(lastName) && customer.TelegramLastName != lastName)
|
||||
{
|
||||
customer.TelegramLastName = lastName;
|
||||
updated = true;
|
||||
}
|
||||
|
||||
customer.LastActiveAt = DateTime.UtcNow;
|
||||
|
||||
if (updated)
|
||||
{
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
await _context.SaveChangesAsync();
|
||||
_logger.LogInformation("Updated existing customer {CustomerId} information", customer.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _context.SaveChangesAsync(); // Just update LastActiveAt
|
||||
}
|
||||
|
||||
var existingDto = _mapper.Map<CustomerDto>(customer);
|
||||
existingDto.DisplayName = customer.DisplayName;
|
||||
existingDto.CustomerType = customer.CustomerType;
|
||||
return existingDto;
|
||||
}
|
||||
|
||||
// Create new customer
|
||||
customer = new Customer
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TelegramUserId = telegramUserId,
|
||||
TelegramUsername = username,
|
||||
TelegramDisplayName = displayName,
|
||||
TelegramFirstName = firstName,
|
||||
TelegramLastName = lastName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow,
|
||||
LastActiveAt = DateTime.UtcNow,
|
||||
DataRetentionDate = DateTime.UtcNow.AddYears(2),
|
||||
IsActive = true,
|
||||
AllowOrderUpdates = true,
|
||||
AllowMarketing = false,
|
||||
Language = "en",
|
||||
Timezone = "UTC"
|
||||
};
|
||||
|
||||
_context.Customers.Add(customer);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Created new customer {CustomerId} for Telegram user {TelegramUserId} ({DisplayName})",
|
||||
customer.Id, telegramUserId, displayName);
|
||||
|
||||
var dto = _mapper.Map<CustomerDto>(customer);
|
||||
dto.DisplayName = customer.DisplayName;
|
||||
dto.CustomerType = customer.CustomerType;
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task UpdateCustomerMetricsAsync(Guid customerId)
|
||||
{
|
||||
var customer = await _context.Customers
|
||||
.Include(c => c.Orders)
|
||||
.FirstOrDefaultAsync(c => c.Id == customerId);
|
||||
|
||||
if (customer == null) return;
|
||||
|
||||
customer.UpdateMetrics();
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Updated metrics for customer {CustomerId}", customerId);
|
||||
}
|
||||
|
||||
public async Task<bool> BlockCustomerAsync(Guid customerId, string reason)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(customerId);
|
||||
if (customer == null) return false;
|
||||
|
||||
customer.IsBlocked = true;
|
||||
customer.BlockReason = reason;
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogWarning("Blocked customer {CustomerId} - Reason: {Reason}", customerId, reason);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> UnblockCustomerAsync(Guid customerId)
|
||||
{
|
||||
var customer = await _context.Customers.FindAsync(customerId);
|
||||
if (customer == null) return false;
|
||||
|
||||
customer.IsBlocked = false;
|
||||
customer.BlockReason = null;
|
||||
customer.UpdatedAt = DateTime.UtcNow;
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
_logger.LogInformation("Unblocked customer {CustomerId}", customerId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
24
LittleShop/Services/IBotMetricsService.cs
Normal file
24
LittleShop/Services/IBotMetricsService.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.DTOs;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface IBotMetricsService
|
||||
{
|
||||
// Metrics
|
||||
Task<BotMetricDto> RecordMetricAsync(Guid botId, CreateBotMetricDto dto);
|
||||
Task<bool> RecordMetricsBatchAsync(Guid botId, BotMetricsBatchDto dto);
|
||||
Task<IEnumerable<BotMetricDto>> GetBotMetricsAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<BotMetricsSummaryDto> GetMetricsSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
|
||||
// Sessions
|
||||
Task<BotSessionDto> StartSessionAsync(Guid botId, CreateBotSessionDto dto);
|
||||
Task<bool> UpdateSessionAsync(Guid sessionId, UpdateBotSessionDto dto);
|
||||
Task<BotSessionDto?> GetSessionAsync(Guid sessionId);
|
||||
Task<IEnumerable<BotSessionDto>> GetBotSessionsAsync(Guid botId, bool activeOnly = false);
|
||||
Task<BotSessionSummaryDto> GetSessionSummaryAsync(Guid botId, DateTime? startDate = null, DateTime? endDate = null);
|
||||
Task<bool> EndSessionAsync(Guid sessionId);
|
||||
Task<int> CleanupInactiveSessionsAsync(int inactiveMinutes = 30);
|
||||
}
|
||||
25
LittleShop/Services/IBotService.cs
Normal file
25
LittleShop/Services/IBotService.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Enums;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface IBotService
|
||||
{
|
||||
Task<BotRegistrationResponseDto> RegisterBotAsync(BotRegistrationDto dto);
|
||||
Task<BotDto?> AuthenticateBotAsync(string botKey);
|
||||
Task<BotDto?> GetBotByIdAsync(Guid id);
|
||||
Task<BotDto?> GetBotByKeyAsync(string botKey);
|
||||
Task<IEnumerable<BotDto>> GetAllBotsAsync();
|
||||
Task<IEnumerable<BotDto>> GetActiveBots();
|
||||
Task<bool> UpdateBotSettingsAsync(Guid botId, UpdateBotSettingsDto dto);
|
||||
Task<bool> UpdateBotStatusAsync(Guid botId, BotStatus status);
|
||||
Task<bool> RecordHeartbeatAsync(Guid botId, BotHeartbeatDto dto);
|
||||
Task<bool> DeleteBotAsync(Guid botId);
|
||||
Task<Dictionary<string, object>> GetBotSettingsAsync(Guid botId);
|
||||
Task<bool> ValidateBotKeyAsync(string botKey);
|
||||
Task<string> GenerateBotKeyAsync();
|
||||
Task<bool> UpdatePlatformInfoAsync(Guid botId, UpdatePlatformInfoDto dto);
|
||||
}
|
||||
20
LittleShop/Services/ICustomerMessageService.cs
Normal file
20
LittleShop/Services/ICustomerMessageService.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface ICustomerMessageService
|
||||
{
|
||||
Task<CustomerMessageDto?> CreateMessageAsync(CreateCustomerMessageDto createMessageDto);
|
||||
Task<CustomerMessageDto?> GetMessageByIdAsync(Guid id);
|
||||
Task<IEnumerable<CustomerMessageDto>> GetCustomerMessagesAsync(Guid customerId);
|
||||
Task<IEnumerable<CustomerMessageDto>> GetOrderMessagesAsync(Guid orderId);
|
||||
Task<IEnumerable<CustomerMessageDto>> GetPendingMessagesAsync(string platform = "Telegram");
|
||||
Task<bool> MarkMessageAsSentAsync(Guid messageId, string? platformMessageId = null);
|
||||
Task<bool> MarkMessageAsDeliveredAsync(Guid messageId);
|
||||
Task<bool> MarkMessageAsFailedAsync(Guid messageId, string reason);
|
||||
Task<MessageThreadDto?> GetMessageThreadAsync(Guid threadId);
|
||||
Task<IEnumerable<MessageThreadDto>> GetActiveThreadsAsync();
|
||||
Task<bool> ValidateCustomerExistsAsync(Guid customerId);
|
||||
Task<bool> ValidateOrderBelongsToCustomerAsync(Guid orderId, Guid customerId);
|
||||
}
|
||||
19
LittleShop/Services/ICustomerService.cs
Normal file
19
LittleShop/Services/ICustomerService.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using LittleShop.DTOs;
|
||||
using LittleShop.Models;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface ICustomerService
|
||||
{
|
||||
Task<CustomerDto?> GetCustomerByIdAsync(Guid id);
|
||||
Task<CustomerDto?> GetCustomerByTelegramUserIdAsync(long telegramUserId);
|
||||
Task<CustomerDto> CreateCustomerAsync(CreateCustomerDto createCustomerDto);
|
||||
Task<CustomerDto?> UpdateCustomerAsync(Guid id, UpdateCustomerDto updateCustomerDto);
|
||||
Task<bool> DeleteCustomerAsync(Guid id);
|
||||
Task<IEnumerable<CustomerDto>> GetAllCustomersAsync();
|
||||
Task<IEnumerable<CustomerDto>> SearchCustomersAsync(string searchTerm);
|
||||
Task<CustomerDto?> GetOrCreateCustomerAsync(long telegramUserId, string displayName, string username = "", string firstName = "", string lastName = "");
|
||||
Task UpdateCustomerMetricsAsync(Guid customerId);
|
||||
Task<bool> BlockCustomerAsync(Guid customerId, string reason);
|
||||
Task<bool> UnblockCustomerAsync(Guid customerId);
|
||||
}
|
||||
15
LittleShop/Services/ITelegramBotManagerService.cs
Normal file
15
LittleShop/Services/ITelegramBotManagerService.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public interface ITelegramBotManagerService
|
||||
{
|
||||
Task StartAsync(CancellationToken cancellationToken);
|
||||
Task StopAsync(CancellationToken cancellationToken);
|
||||
Task<bool> AddBotAsync(Guid botId, string botToken);
|
||||
Task<bool> RemoveBotAsync(Guid botId);
|
||||
Task<bool> UpdateBotSettingsAsync(Guid botId);
|
||||
Task<int> GetActiveBotCount();
|
||||
}
|
||||
@@ -10,16 +10,19 @@ public class OrderService : IOrderService
|
||||
{
|
||||
private readonly LittleShopContext _context;
|
||||
private readonly ILogger<OrderService> _logger;
|
||||
private readonly ICustomerService _customerService;
|
||||
|
||||
public OrderService(LittleShopContext context, ILogger<OrderService> logger)
|
||||
public OrderService(LittleShopContext context, ILogger<OrderService> logger, ICustomerService customerService)
|
||||
{
|
||||
_context = context;
|
||||
_logger = logger;
|
||||
_customerService = customerService;
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<OrderDto>> GetAllOrdersAsync()
|
||||
{
|
||||
var orders = await _context.Orders
|
||||
.Include(o => o.Customer)
|
||||
.Include(o => o.Items)
|
||||
.ThenInclude(oi => oi.Product)
|
||||
.Include(o => o.Payments)
|
||||
@@ -32,6 +35,7 @@ public class OrderService : IOrderService
|
||||
public async Task<IEnumerable<OrderDto>> GetOrdersByIdentityAsync(string identityReference)
|
||||
{
|
||||
var orders = await _context.Orders
|
||||
.Include(o => o.Customer)
|
||||
.Include(o => o.Items)
|
||||
.ThenInclude(oi => oi.Product)
|
||||
.Include(o => o.Payments)
|
||||
@@ -45,6 +49,7 @@ public class OrderService : IOrderService
|
||||
public async Task<OrderDto?> GetOrderByIdAsync(Guid id)
|
||||
{
|
||||
var order = await _context.Orders
|
||||
.Include(o => o.Customer)
|
||||
.Include(o => o.Items)
|
||||
.ThenInclude(oi => oi.Product)
|
||||
.Include(o => o.Payments)
|
||||
@@ -59,10 +64,38 @@ public class OrderService : IOrderService
|
||||
|
||||
try
|
||||
{
|
||||
// Handle customer creation/linking during checkout
|
||||
Guid? customerId = null;
|
||||
string? identityReference = null;
|
||||
|
||||
if (createOrderDto.CustomerInfo != null)
|
||||
{
|
||||
// Create customer during checkout process
|
||||
var customer = await _customerService.GetOrCreateCustomerAsync(
|
||||
createOrderDto.CustomerInfo.TelegramUserId,
|
||||
createOrderDto.CustomerInfo.TelegramDisplayName,
|
||||
createOrderDto.CustomerInfo.TelegramUsername,
|
||||
createOrderDto.CustomerInfo.TelegramFirstName,
|
||||
createOrderDto.CustomerInfo.TelegramLastName);
|
||||
|
||||
customerId = customer?.Id;
|
||||
}
|
||||
else if (createOrderDto.CustomerId.HasValue)
|
||||
{
|
||||
// Order for existing customer
|
||||
customerId = createOrderDto.CustomerId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Anonymous order (legacy support)
|
||||
identityReference = createOrderDto.IdentityReference;
|
||||
}
|
||||
|
||||
var order = new Order
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
IdentityReference = createOrderDto.IdentityReference,
|
||||
CustomerId = customerId,
|
||||
IdentityReference = identityReference,
|
||||
Status = OrderStatus.PendingPayment,
|
||||
TotalAmount = 0,
|
||||
Currency = "GBP",
|
||||
@@ -105,8 +138,16 @@ public class OrderService : IOrderService
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
|
||||
order.Id, createOrderDto.IdentityReference, totalAmount);
|
||||
if (customerId.HasValue)
|
||||
{
|
||||
_logger.LogInformation("Created order {OrderId} for customer {CustomerId} with total {Total}",
|
||||
order.Id, customerId.Value, totalAmount);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation("Created order {OrderId} for identity {Identity} with total {Total}",
|
||||
order.Id, identityReference, totalAmount);
|
||||
}
|
||||
|
||||
// Reload order with includes
|
||||
var createdOrder = await GetOrderByIdAsync(order.Id);
|
||||
@@ -175,8 +216,26 @@ public class OrderService : IOrderService
|
||||
return new OrderDto
|
||||
{
|
||||
Id = order.Id,
|
||||
CustomerId = order.CustomerId,
|
||||
IdentityReference = order.IdentityReference,
|
||||
Status = order.Status,
|
||||
Customer = order.Customer != null ? new CustomerSummaryDto
|
||||
{
|
||||
Id = order.Customer.Id,
|
||||
DisplayName = !string.IsNullOrEmpty(order.Customer.TelegramDisplayName) ? order.Customer.TelegramDisplayName :
|
||||
!string.IsNullOrEmpty(order.Customer.TelegramUsername) ? $"@{order.Customer.TelegramUsername}" :
|
||||
$"{order.Customer.TelegramFirstName} {order.Customer.TelegramLastName}".Trim(),
|
||||
TelegramUsername = order.Customer.TelegramUsername,
|
||||
TotalOrders = order.Customer.TotalOrders,
|
||||
TotalSpent = order.Customer.TotalSpent,
|
||||
CustomerType = order.Customer.TotalOrders == 0 ? "New" :
|
||||
order.Customer.TotalOrders == 1 ? "First-time" :
|
||||
order.Customer.TotalOrders < 5 ? "Regular" :
|
||||
order.Customer.TotalOrders < 20 ? "Loyal" : "VIP",
|
||||
RiskScore = order.Customer.RiskScore,
|
||||
LastActiveAt = order.Customer.LastActiveAt,
|
||||
IsBlocked = order.Customer.IsBlocked
|
||||
} : null,
|
||||
TotalAmount = order.TotalAmount,
|
||||
Currency = order.Currency,
|
||||
ShippingName = order.ShippingName,
|
||||
|
||||
207
LittleShop/Services/TelegramBotManagerService.cs
Normal file
207
LittleShop/Services/TelegramBotManagerService.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using LittleShop.Data;
|
||||
|
||||
namespace LittleShop.Services;
|
||||
|
||||
public class TelegramBotManagerService : BackgroundService, ITelegramBotManagerService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<TelegramBotManagerService> _logger;
|
||||
private readonly ConcurrentDictionary<Guid, BotInstance> _activeBots = new();
|
||||
|
||||
public TelegramBotManagerService(IServiceProvider serviceProvider, ILogger<TelegramBotManagerService> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("🤖 Telegram Bot Manager Service starting...");
|
||||
|
||||
try
|
||||
{
|
||||
// Load all active bots from database
|
||||
await LoadActiveBotsAsync();
|
||||
|
||||
// Keep service running
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
// Periodic health checks and cleanup
|
||||
await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
|
||||
await PerformHealthChecksAsync();
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Telegram Bot Manager Service is stopping.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in Telegram Bot Manager Service");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Telegram Bot Manager Service started");
|
||||
await base.StartAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("Stopping all Telegram bots...");
|
||||
|
||||
foreach (var bot in _activeBots.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
await bot.StopAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error stopping bot {BotId}", bot.BotId);
|
||||
}
|
||||
}
|
||||
|
||||
_activeBots.Clear();
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<bool> AddBotAsync(Guid botId, string botToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Adding bot {BotId} to Telegram manager", botId);
|
||||
|
||||
// Validate token first
|
||||
using var httpClient = new HttpClient();
|
||||
var response = await httpClient.GetAsync($"https://api.telegram.org/bot{botToken}/getMe");
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.LogWarning("Invalid bot token for bot {BotId}", botId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create bot instance (placeholder for now - will implement Telegram.Bot later)
|
||||
var botInstance = new BotInstance
|
||||
{
|
||||
BotId = botId,
|
||||
BotToken = botToken,
|
||||
IsRunning = false
|
||||
};
|
||||
|
||||
_activeBots.TryAdd(botId, botInstance);
|
||||
|
||||
_logger.LogInformation("✅ Bot {BotId} added successfully", botId);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to add bot {BotId}", botId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveBotAsync(Guid botId)
|
||||
{
|
||||
if (_activeBots.TryRemove(botId, out var botInstance))
|
||||
{
|
||||
await botInstance.StopAsync();
|
||||
_logger.LogInformation("Bot {BotId} removed from Telegram manager", botId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<bool> UpdateBotSettingsAsync(Guid botId)
|
||||
{
|
||||
if (_activeBots.TryGetValue(botId, out var botInstance))
|
||||
{
|
||||
// Reload settings from database
|
||||
_logger.LogInformation("Updating settings for bot {BotId}", botId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<int> GetActiveBotCount()
|
||||
{
|
||||
return Task.FromResult(_activeBots.Count(x => x.Value.IsRunning));
|
||||
}
|
||||
|
||||
private async Task LoadActiveBotsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var botService = scope.ServiceProvider.GetRequiredService<IBotService>();
|
||||
|
||||
var activeBots = await botService.GetActiveBots();
|
||||
|
||||
_logger.LogInformation("Loading {Count} active bots", activeBots.Count());
|
||||
|
||||
foreach (var bot in activeBots)
|
||||
{
|
||||
// Look for telegram token in settings
|
||||
if (bot.Settings.TryGetValue("telegram", out var telegramSettings) &&
|
||||
telegramSettings is System.Text.Json.JsonElement telegramElement &&
|
||||
telegramElement.TryGetProperty("botToken", out var tokenElement))
|
||||
{
|
||||
var token = tokenElement.GetString();
|
||||
if (!string.IsNullOrEmpty(token) && token != "YOUR_BOT_TOKEN_HERE")
|
||||
{
|
||||
await AddBotAsync(bot.Id, token);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Microsoft.Data.Sqlite.SqliteException ex) when (ex.Message.Contains("no such table"))
|
||||
{
|
||||
_logger.LogWarning("Bot tables don't exist yet. Skipping bot loading until database is fully initialized.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading active bots");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PerformHealthChecksAsync()
|
||||
{
|
||||
foreach (var kvp in _activeBots)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Placeholder for health check logic
|
||||
// In real implementation, would ping Telegram API
|
||||
_logger.LogDebug("Health check for bot {BotId}", kvp.Key);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Health check failed for bot {BotId}", kvp.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BotInstance
|
||||
{
|
||||
public Guid BotId { get; set; }
|
||||
public string BotToken { get; set; } = string.Empty;
|
||||
public bool IsRunning { get; set; }
|
||||
public DateTime StartedAt { get; set; }
|
||||
|
||||
public Task StopAsync()
|
||||
{
|
||||
IsRunning = false;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
30
LittleShop/appsettings.Production.json
Normal file
30
LittleShop/appsettings.Production.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Data Source=/app/data/littleshop.db"
|
||||
},
|
||||
"Jwt": {
|
||||
"Key": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!",
|
||||
"Issuer": "LittleShop",
|
||||
"Audience": "LittleShop"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"BTCPayServer": {
|
||||
"Url": "",
|
||||
"ApiKey": "",
|
||||
"StoreId": ""
|
||||
},
|
||||
"Kestrel": {
|
||||
"EndPoints": {
|
||||
"Http": {
|
||||
"Url": "http://0.0.0.0:5000"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8DxJ63S0c9FElmGFRyriOyxQ2VI2WE5MN0TByz7kNSHwr3VBa5sqlpt_YLV50eF1Yb04QBtu5uPrfXC_UzF1eF81n6XNYBeETJ9wfUrJwVFWThtFhEfOEzcYTtrr7ZB-yIMaV6A6QeOcIDnuiwquaWzkyno94PNydw2-lLD4jlBamWa32DiYNwI17zYglyaSEgS1ITdN4BQpfGSUAH2Mma6aw4MWZKK3xIj6Q8ps-x42Q-XWXgiKQhHvoSg09GpfFKoHBRIMWfxF5-6CkgVOGo7gGeXFhIEKrS6UcvyxfeQ2J79pR02IUfWgvGAStD5V2CBqoRphOnZMRj_Sgwhkon1JV-BRAkzmoG8UhGJe7l-xNnK8soPjER70h1ajZ-FNS-Zu7n5yupuCV50aRpvf1aroKryotLv9cDWgMVTlRzClrGqBwp2oTK6a1o9pkfHfQg
|
||||
|
||||
BIN
LittleShop/littleshop-wizard-fixed.tar.gz
Normal file
BIN
LittleShop/littleshop-wizard-fixed.tar.gz
Normal file
Binary file not shown.
5
LittleShop/runtime-cookies.txt
Normal file
5
LittleShop/runtime-cookies.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8DxJ63S0c9FElmGFRyriOywdY8scZrqW6HWcGVwU58xjlVJhpSDHLKxnS4BOGknzLfeD2kGcQpb80RJdM1i0aIg5RbvqYtjrjP7WPNau7I1yowNglh2p7yF8VjlgGJFaWNesPi9Kji_tXr8_WmOwJZtjgoAWgEgJ0cqbMj4Aep3boHP8Hb3WoJ9JEiB0s46ugKd2xFP08hwkMGDiAGgDsGebDlVDF-MZpTpaPn6Ebgk-OiOLKEq7FtsSx0Bm29LhT6V-9KRoE86P0_i0UZvOm2X9E71aACNQzsBpFg00yO0qaoOMrN4pn7-XxUBL1iJNqfn1bLHgD7su6nmJLCibCz3MF17PpJ_QFw6P3y3rSYOB63QjBaz8Euf0E9syBlbUZhbZyPYehmoYFYLTJcimq36TRyYYIQQcu34HJl8TueTRwubon4ONAkgvdoOctW-VXg
|
||||
5
LittleShop/test-cookies.txt
Normal file
5
LittleShop/test-cookies.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8DxJ63S0c9FElmGFRyriOyx1P83b6s01w91tvjVvvqS0MMT88nURas_5MEm7nd25COxbSbvcfZgDEsbsZZVYuvYoRGy0-pb9jezRvH8x6dIbX4Cj_IMadP_UxbKVVAPQetoxm4NztkBvfUJGwlvIU5BU3CA9fZcktohA-EWG3uP9e0Zn_SndMmAKMY_r2LdRWr7F5pLRN5AWcWW3P6-e1dNiNwWANcd3jhb4JidyQzKopv8PSUGshZ95W93rhBz9S4ATYYY4bx0cPHErPJY5bFgKSG4FR4f_z0lsdJDXo9VANmreWaHEMH7edvTNzIarQBJmEf7BlNdlMPA64uz-I53sSSxcWQnmkCjWjrM2v8tOCNRRBYUgCy7SPTIly7_kBTsfNvWJbK1w6QHhrYOssKrg1otJunolituIUjtByKNPP3CICPnvRdkizy6sY54z2Q
|
||||
6
LittleShop/test-session.txt
Normal file
6
LittleShop/test-session.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
# Netscape HTTP Cookie File
|
||||
# https://curl.se/docs/http-cookies.html
|
||||
# This file was generated by libcurl! Edit at your own risk.
|
||||
|
||||
#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Antiforgery.sPg1GqFvmmY CfDJ8DxJ63S0c9FElmGFRyriOyytVfaO36xemDWzgmgfcgLbYpXML8HPftA6ZSgZ1YJYEj_hiXOMdWjtWIsiccwTjMyl5Eb_8TQaqxTH3dZDDhpVXGqVN-EInaSXFRqwtzLeeZts4loGXgjbbQNk17vd_x8
|
||||
#HttpOnly_localhost FALSE / FALSE 0 .AspNetCore.Cookies CfDJ8DxJ63S0c9FElmGFRyriOywW_kaSzrXpYdygsfTY10CIFJrE7uu1rz4ucx999nQRJqNpzMdxLsVcvMI4WPArl-KN6KfehVxlAy4ipFPKHyFliJFgycGTRUCmijuHXIarfVur9ZqVpQOSoXsaARBxmXeCnJpYw3Y1X--Ftf7qt6qT2bDxrUFXN5HbJUi-BSDPMroe5YBi4qB5YTbmp7PtgMeSY-Fx33-I2yjC_QaHGqRdVcqegLHfhgNr-6GoTZcB7QeA8YER-nNXsV2mmbwuz9zHZy3c_e1VpBlhORAH8t8-hrp3efuhzAECgI13Ko-8j0Kc-Vv99KCnWoKw5LLPCou7ki33K0rcKs2Y5gRPkc062CPWCCBC0JWlL06xO65xWteYXU7lADUktpx213WuXKuHN6vniQkJvEQuVJj4SR2hO2aAE5J8D4SnV52mwkfqxQ
|
||||
224
LittleShop/test-wizard.html
Normal file
224
LittleShop/test-wizard.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Telegram Bot Creation Wizard - LittleShop Admin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/Admin">
|
||||
<i class="fas fa-store"></i> LittleShop Admin
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Categories">
|
||||
<i class="fas fa-tags"></i> Categories
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Products">
|
||||
<i class="fas fa-box"></i> Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Orders">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/ShippingRates">
|
||||
<i class="fas fa-truck"></i> Shipping
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Users">
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Bots">
|
||||
<i class="fas fa-robot"></i> Bots
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user"></i> admin
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<form method="post" action="/Admin/Account/Logout">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container-fluid">
|
||||
<main role="main" class="pb-3">
|
||||
|
||||
|
||||
<h1>Telegram Bot Creation Wizard</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Step 1: Basic Info -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Step 1: Bot Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-area="Admin" asp-controller="Bots" asp-action="Wizard" method="post">
|
||||
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8DxJ63S0c9FElmGFRyriOywAO8gOGGx2ME7YDgMh8HxARyoeEPbEYF_OtnMlFj1eMaUu2Qybw5OWx6N2a00408PZKQoGm7gFE6q72AVRSDLq791hMcHFCiALdhlW3VDCRE29HxPzkOi0m-j68V6TKV-q_KQ4_zk7LgfUmLCFgAfsm2kotjMBhfqs45wL9x8nhg" />
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotName" class="form-label">Bot Display Name</label>
|
||||
<input asp-for="BotName" class="form-control"
|
||||
placeholder="e.g., LittleShop Electronics Bot" required />
|
||||
<span asp-validation-for="BotName" class="text-danger"></span>
|
||||
<small class="text-muted">This is the name users will see</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotUsername" class="form-label">Bot Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">@</span>
|
||||
<input asp-for="BotUsername" class="form-control"
|
||||
placeholder="littleshop_bot" required />
|
||||
</div>
|
||||
<span asp-validation-for="BotUsername" class="text-danger"></span>
|
||||
<small class="text-muted">Must end with 'bot' and be unique on Telegram</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="PersonalityName" class="form-label">Personality</label>
|
||||
<select asp-for="PersonalityName" class="form-select">
|
||||
<option value="">Auto-assign (recommended)</option>
|
||||
<option value="Alan" >Alan (Professional)</option>
|
||||
<option value="Dave" >Dave (Casual)</option>
|
||||
<option value="Sarah" >Sarah (Helpful)</option>
|
||||
<option value="Mike" >Mike (Direct)</option>
|
||||
<option value="Emma" >Emma (Friendly)</option>
|
||||
<option value="Tom" >Tom (Efficient)</option>
|
||||
</select>
|
||||
<small class="text-muted">Bot conversation style (can be changed later)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Description" class="form-label">Description (Optional)</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="2"
|
||||
placeholder="Brief description of what this bot does"></textarea>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Generate BotFather Commands" class="btn btn-primary" />
|
||||
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Wizard Progress</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="text-primary fw-bold">
|
||||
<i class="fas fa-edit"></i>
|
||||
1. Bot Information
|
||||
</li>
|
||||
<li class="text-muted">
|
||||
<i class="fas fa-circle"></i>
|
||||
2. Create with BotFather
|
||||
</li>
|
||||
<li class="text-muted">
|
||||
<i class="fas fa-circle"></i>
|
||||
3. Complete Setup
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Personality Preview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
Auto-assigned personality based on bot name </p>
|
||||
|
||||
<p class="small text-muted">
|
||||
Personalities affect how your bot communicates with customers.
|
||||
This can be customized later in bot settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
// Test if JavaScript is working
|
||||
console.log('Wizard page scripts loaded');
|
||||
|
||||
function copyCommands() {
|
||||
const commands = ``;
|
||||
navigator.clipboard.writeText(commands).then(() => {
|
||||
alert('Commands copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-generate username from bot name
|
||||
$(document).ready(function() {
|
||||
console.log('Document ready, setting up auto-generation');
|
||||
|
||||
$('#BotName').on('input', function() {
|
||||
try {
|
||||
const name = $(this).val().toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
|
||||
if (name && !name.endsWith('_bot')) {
|
||||
$('#BotUsername').val(name + '_bot');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in auto-generation:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
0
LittleShop/wizard-result.html
Normal file
0
LittleShop/wizard-result.html
Normal file
224
LittleShop/wizard.html
Normal file
224
LittleShop/wizard.html
Normal file
@@ -0,0 +1,224 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Telegram Bot Creation Wizard - LittleShop Admin</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-sm navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/Admin">
|
||||
<i class="fas fa-store"></i> LittleShop Admin
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
|
||||
<ul class="navbar-nav flex-grow-1">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin">
|
||||
<i class="fas fa-tachometer-alt"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Categories">
|
||||
<i class="fas fa-tags"></i> Categories
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Products">
|
||||
<i class="fas fa-box"></i> Products
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Orders">
|
||||
<i class="fas fa-shopping-cart"></i> Orders
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/ShippingRates">
|
||||
<i class="fas fa-truck"></i> Shipping
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Users">
|
||||
<i class="fas fa-users"></i> Users
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/Admin/Bots">
|
||||
<i class="fas fa-robot"></i> Bots
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-user"></i> admin
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<form method="post" action="/Admin/Account/Logout">
|
||||
<button type="submit" class="dropdown-item">
|
||||
<i class="fas fa-sign-out-alt"></i> Logout
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container-fluid">
|
||||
<main role="main" class="pb-3">
|
||||
|
||||
|
||||
<h1>Telegram Bot Creation Wizard</h1>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Step 1: Basic Info -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Step 1: Bot Information</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form asp-area="Admin" asp-controller="Bots" asp-action="Wizard" method="post">
|
||||
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8DxJ63S0c9FElmGFRyriOyxtHuNw47houOiPBlthzQyPiKf9VWVnAx_pcF7wBhQLdU41vfn-d2-Km3hi22lPk3qY0n7ZnGz39DmlP8yczH-EJwXnnIDvrBp-gigGjKPJJwitTBbPiSg1GcarY6bbSfbU368kFk64Jnsd5VMc0tTukNDO9xXriuBWE6iEsQPb5A" />
|
||||
<div asp-validation-summary="All" class="text-danger"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotName" class="form-label">Bot Display Name</label>
|
||||
<input asp-for="BotName" class="form-control"
|
||||
placeholder="e.g., LittleShop Electronics Bot" required />
|
||||
<span asp-validation-for="BotName" class="text-danger"></span>
|
||||
<small class="text-muted">This is the name users will see</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label asp-for="BotUsername" class="form-label">Bot Username</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">@</span>
|
||||
<input asp-for="BotUsername" class="form-control"
|
||||
placeholder="littleshop_bot" required />
|
||||
</div>
|
||||
<span asp-validation-for="BotUsername" class="text-danger"></span>
|
||||
<small class="text-muted">Must end with 'bot' and be unique on Telegram</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="PersonalityName" class="form-label">Personality</label>
|
||||
<select asp-for="PersonalityName" class="form-select">
|
||||
<option value="">Auto-assign (recommended)</option>
|
||||
<option value="Alan" >Alan (Professional)</option>
|
||||
<option value="Dave" >Dave (Casual)</option>
|
||||
<option value="Sarah" >Sarah (Helpful)</option>
|
||||
<option value="Mike" >Mike (Direct)</option>
|
||||
<option value="Emma" >Emma (Friendly)</option>
|
||||
<option value="Tom" >Tom (Efficient)</option>
|
||||
</select>
|
||||
<small class="text-muted">Bot conversation style (can be changed later)</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="Description" class="form-label">Description (Optional)</label>
|
||||
<textarea asp-for="Description" class="form-control" rows="2"
|
||||
placeholder="Brief description of what this bot does"></textarea>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Generate BotFather Commands" class="btn btn-primary" />
|
||||
<a href="/Admin/Bots" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Wizard Progress</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul class="list-unstyled">
|
||||
<li class="text-primary fw-bold">
|
||||
<i class="fas fa-edit"></i>
|
||||
1. Bot Information
|
||||
</li>
|
||||
<li class="text-muted">
|
||||
<i class="fas fa-circle"></i>
|
||||
2. Create with BotFather
|
||||
</li>
|
||||
<li class="text-muted">
|
||||
<i class="fas fa-circle"></i>
|
||||
3. Complete Setup
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Personality Preview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="small">
|
||||
Auto-assigned personality based on bot name </p>
|
||||
|
||||
<p class="small text-muted">
|
||||
Personalities affect how your bot communicates with customers.
|
||||
This can be customized later in bot settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<partial name="_ValidationScriptsPartial" />
|
||||
<script>
|
||||
// Test if JavaScript is working
|
||||
console.log('Wizard page scripts loaded');
|
||||
|
||||
function copyCommands() {
|
||||
const commands = ``;
|
||||
navigator.clipboard.writeText(commands).then(() => {
|
||||
alert('Commands copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-generate username from bot name
|
||||
$(document).ready(function() {
|
||||
console.log('Document ready, setting up auto-generation');
|
||||
|
||||
$('#BotName').on('input', function() {
|
||||
try {
|
||||
const name = $(this).val().toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '_')
|
||||
.replace(/_+/g, '_')
|
||||
.replace(/^_|_$/g, '');
|
||||
|
||||
if (name && !name.endsWith('_bot')) {
|
||||
$('#BotUsername').val(name + '_bot');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error in auto-generation:', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user