feat(backend): add communication, tester, admin and middleman modules

Implement step 17 modules: threads/messages, notifications with automation (submission.created, idea.created, grant_call.opened), innovation test feedback, admin panel (submissions, verify, metrics, grant call status), Middleman adaptation and partnerships. Split multi-type files into one type per file and replace object-valued dictionaries with string-valued maps in data contracts (migrations 0006/0007).
This commit is contained in:
Rafał Tyński
2026-10-04 03:49:45 +02:00
parent f4f3b3896a
commit fa1199539b
136 changed files with 2338 additions and 574 deletions
+98
View File
@@ -0,0 +1,98 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TarninaHub.Application.Abstractions;
using TarninaHub.Application.Dtos;
using TarninaHub.Application.Security;
namespace TarninaHub.Controllers;
[ApiController]
[ApiVersion("1.0")]
[Route("api/admin")]
[Authorize(Policy = Policies.OperatorOrAdmin)]
public class AdminController : ControllerBase
{
private readonly IAdminService _admin;
private readonly IGrantCallService _grantCalls;
private readonly IIdeaService _ideas;
private readonly INotificationService _notifications;
public AdminController(
IAdminService admin,
IGrantCallService grantCalls,
IIdeaService ideas,
INotificationService notifications)
{
_admin = admin;
_grantCalls = grantCalls;
_ideas = ideas;
_notifications = notifications;
}
[HttpGet("submissions")]
public async Task<ActionResult<IReadOnlyList<SubmissionResponse>>> ListSubmissions(
[FromQuery] string? status,
[FromQuery] string? challengeId,
CancellationToken cancellationToken)
{
var submissions = await _admin.ListSubmissionsAsync(status, challengeId, cancellationToken);
return Ok(submissions);
}
[HttpPut("knowledge-items/{knowledgeItemId}/verify")]
public async Task<ActionResult<KnowledgeItemResponse>> VerifyKnowledgeItem(
string knowledgeItemId,
VerifyKnowledgeItemRequest request,
CancellationToken cancellationToken)
{
var item = await _admin.VerifyKnowledgeItemAsync(knowledgeItemId, request, cancellationToken);
return item is null ? NotFound() : Ok(item);
}
[HttpGet("metrics")]
public async Task<ActionResult<AdminMetricsResponse>> Metrics(CancellationToken cancellationToken)
{
var metrics = await _admin.GetMetricsAsync(cancellationToken);
return Ok(metrics);
}
[HttpPut("grant-calls/{grantCallId}/status")]
public async Task<ActionResult<GrantCallResponse>> UpdateGrantCallStatus(
string grantCallId,
UpdateGrantCallStatusRequest request,
CancellationToken cancellationToken)
{
var updated = await _grantCalls.UpdateStatusAsync(grantCallId, request.Status, cancellationToken);
if (updated is null)
{
return NotFound();
}
if (string.Equals(request.Status, "open", StringComparison.Ordinal))
{
await NotifyIdeaAuthorsAsync(updated, cancellationToken);
}
return Ok(updated);
}
private async Task NotifyIdeaAuthorsAsync(GrantCallResponse call, CancellationToken cancellationToken)
{
var authorIds = await _ideas.ListDistinctAuthorIdsAsync(cancellationToken);
var payload = new Dictionary<string, string?>
{
["grantCallId"] = call.Id,
["name"] = call.Name,
};
foreach (var authorId in authorIds)
{
await _notifications.NotifyUserAsync(authorId, "grant_call.opened", payload, cancellationToken);
}
}
}