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:
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/grant-calls")]
|
||||
[Authorize]
|
||||
public class GrantCallsController : ControllerBase
|
||||
{
|
||||
private readonly IGrantCallService _grantCalls;
|
||||
|
||||
public GrantCallsController(IGrantCallService grantCalls)
|
||||
{
|
||||
_grantCalls = grantCalls;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<GrantCallResponse>>> List(
|
||||
[FromQuery] string? status,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var calls = await _grantCalls.ListAsync(status, cancellationToken);
|
||||
|
||||
return Ok(calls);
|
||||
}
|
||||
|
||||
[HttpGet("{grantCallId}")]
|
||||
public async Task<ActionResult<GrantCallResponse>> GetById(
|
||||
string grantCallId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var call = await _grantCalls.GetAsync(grantCallId, cancellationToken);
|
||||
|
||||
return call is null ? NotFound() : Ok(call);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using TarninaHub.Application.Abstractions;
|
||||
using TarninaHub.Application.Dtos;
|
||||
using TarninaHub.Application.Mappings;
|
||||
using TarninaHub.Application.Security;
|
||||
using TarninaHub.Domain.Rules;
|
||||
|
||||
namespace TarninaHub.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/ideas")]
|
||||
[Authorize]
|
||||
public class IdeasController : ControllerBase
|
||||
{
|
||||
private readonly IIdeaService _ideaService;
|
||||
private readonly IIdeaAssistant _ideaAssistant;
|
||||
|
||||
public IdeasController(IIdeaService ideaService, IIdeaAssistant ideaAssistant)
|
||||
{
|
||||
_ideaService = ideaService;
|
||||
_ideaAssistant = ideaAssistant;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<IdeaResponse>> Create(
|
||||
CreateIdeaRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var created = await _ideaService.CreateAsync(userId, request, cancellationToken);
|
||||
|
||||
return CreatedAtAction(nameof(GetById), new { ideaId = created.Id }, created);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<IReadOnlyList<IdeaResponse>>> ListMine(CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = User.GetUserId();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userId))
|
||||
{
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
var ideas = await _ideaService.ListForAuthorAsync(userId, cancellationToken);
|
||||
|
||||
return Ok(ideas);
|
||||
}
|
||||
|
||||
[HttpGet("{ideaId}")]
|
||||
public async Task<ActionResult<IdeaResponse>> GetById(string ideaId, CancellationToken cancellationToken)
|
||||
{
|
||||
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
||||
|
||||
if (idea is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsOwner(idea) && !IsStaff())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
return Ok(idea);
|
||||
}
|
||||
|
||||
[HttpPut("{ideaId}")]
|
||||
public async Task<ActionResult<IdeaResponse>> Update(
|
||||
string ideaId,
|
||||
UpdateIdeaRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var current = await _ideaService.GetAsync(ideaId, cancellationToken);
|
||||
|
||||
if (current is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsOwner(current))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(request.Status))
|
||||
{
|
||||
var from = IdeaStatusWire.Parse(current.Status);
|
||||
var to = IdeaStatusWire.Parse(request.Status);
|
||||
|
||||
if (to is null || from is null || !IdeaWorkflow.CanTransition(from.Value, to.Value))
|
||||
{
|
||||
ModelState.AddModelError(
|
||||
nameof(request.Status),
|
||||
$"Transition from '{current.Status}' to '{request.Status}' is not allowed.");
|
||||
|
||||
return ValidationProblem(ModelState);
|
||||
}
|
||||
}
|
||||
|
||||
var updated = await _ideaService.UpdateAsync(ideaId, request, cancellationToken);
|
||||
|
||||
return updated is null ? NotFound() : Ok(updated);
|
||||
}
|
||||
|
||||
[HttpGet("{ideaId}/card")]
|
||||
public async Task<ActionResult<IdeaCardResponse>> GetCard(string ideaId, CancellationToken cancellationToken)
|
||||
{
|
||||
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
||||
|
||||
if (idea is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsOwner(idea) && !IsStaff())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var card = await _ideaService.GetCardAsync(ideaId, cancellationToken);
|
||||
|
||||
return card is null ? NotFound() : Ok(card);
|
||||
}
|
||||
|
||||
[HttpPut("{ideaId}/card")]
|
||||
public async Task<ActionResult<IdeaCardResponse>> UpdateCard(
|
||||
string ideaId,
|
||||
IdeaCardRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
||||
|
||||
if (idea is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsOwner(idea))
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var card = await _ideaService.UpdateCardAsync(ideaId, request, cancellationToken);
|
||||
|
||||
return card is null ? NotFound() : Ok(card);
|
||||
}
|
||||
|
||||
[HttpPost("{ideaId}/assist")]
|
||||
public async Task<ActionResult<IdeaAssistResponse>> Assist(
|
||||
string ideaId,
|
||||
IdeaAssistRequest request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var idea = await _ideaService.GetAsync(ideaId, cancellationToken);
|
||||
|
||||
if (idea is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (!IsOwner(idea) && !IsStaff())
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var assist = await _ideaAssistant.AssistAsync(ideaId, request, cancellationToken);
|
||||
|
||||
return assist is null ? NotFound() : Ok(assist);
|
||||
}
|
||||
|
||||
private bool IsOwner(IdeaResponse idea) =>
|
||||
string.Equals(idea.AuthorId, User.GetUserId(), StringComparison.Ordinal);
|
||||
|
||||
private bool IsStaff() =>
|
||||
User.IsInRole(RoleNames.Operator) || User.IsInRole(RoleNames.Manager) || User.IsInRole(RoleNames.Admin);
|
||||
}
|
||||
Reference in New Issue
Block a user