feat(ideas): add idea creator, grant calls and applications

Implement the idea creator and application generator: idea status workflow, social canvas card, grant call windows with 409 when closed, and an LLM-backed idea assistant with offline fallback.
This commit is contained in:
Rafał Tyński
2026-10-04 03:13:45 +02:00
parent fd408b102b
commit 127ca92f63
15 changed files with 1089 additions and 0 deletions
@@ -0,0 +1,104 @@
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/applications")]
[Authorize]
public class ApplicationsController : ControllerBase
{
private readonly IApplicationService _applicationService;
private readonly IIdeaService _ideaService;
public ApplicationsController(IApplicationService applicationService, IIdeaService ideaService)
{
_applicationService = applicationService;
_ideaService = ideaService;
}
[HttpPost]
public async Task<ActionResult<ApplicationResponse>> Create(
CreateApplicationRequest request,
CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(userId))
{
return Unauthorized();
}
var result = await _applicationService.CreateAsync(userId, request, cancellationToken);
return result.Outcome switch
{
CreateApplicationOutcome.Created when result.Application is not null =>
CreatedAtAction(nameof(GetById), new { applicationId = result.Application.Id }, result.Application),
CreateApplicationOutcome.CallNotFound => NotFound(new ProblemDetails
{
Title = "Grant call not found",
Status = StatusCodes.Status404NotFound,
}),
CreateApplicationOutcome.IdeaNotFound => NotFound(new ProblemDetails
{
Title = "Idea not found",
Status = StatusCodes.Status404NotFound,
}),
_ => Conflict(new ProblemDetails
{
Title = "Grant call is not open",
Detail = "Applications are only accepted while the call is open and within its window.",
Status = StatusCodes.Status409Conflict,
}),
};
}
[HttpGet]
public async Task<ActionResult<IReadOnlyList<ApplicationResponse>>> ListMine(CancellationToken cancellationToken)
{
var userId = User.GetUserId();
if (string.IsNullOrWhiteSpace(userId))
{
return Unauthorized();
}
var applications = await _applicationService.ListForAuthorAsync(userId, cancellationToken);
return Ok(applications);
}
[HttpGet("{applicationId}")]
public async Task<ActionResult<ApplicationResponse>> GetById(
string applicationId,
CancellationToken cancellationToken)
{
var application = await _applicationService.GetAsync(applicationId, cancellationToken);
if (application is null)
{
return NotFound();
}
if (!IsStaff())
{
var idea = await _ideaService.GetAsync(application.IdeaId, cancellationToken);
if (idea is null || !string.Equals(idea.AuthorId, User.GetUserId(), StringComparison.Ordinal))
{
return Forbid();
}
}
return Ok(application);
}
private bool IsStaff() =>
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
}