# Templates Controller SmartStack

> **Note:** Ces templates sont utilisés par le skill `controller` et la commande `/controller:create`.
> Adapter les variables `{Area}`, `{Module}`, `{Entity}`, `{PermissionPath}` selon le contexte.

---

## Template CRUD Controller (Standard)

```csharp
// src/SmartStack.Api/Controllers/{Area}/{Module}Controller.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using SmartStack.Application.Common.Authorization;
using SmartStack.Application.Common.Interfaces;
using SmartStack.Api.Authorization;
using SmartStack.Domain.{DomainNamespace};

namespace SmartStack.Api.Controllers.{Area};

[ApiController]
[Route("api/{area-kebab}/{module-kebab}")]
[Authorize]
[Tags("{Module}")]
public class {Module}Controller : ControllerBase
{
    private readonly IApplicationDbContext _context;
    private readonly ICurrentUserService _currentUser;
    private readonly ILogger<{Module}Controller> _logger;

    public {Module}Controller(
        IApplicationDbContext context,
        ICurrentUserService currentUser,
        ILogger<{Module}Controller> logger)
    {
        _context = context;
        _currentUser = currentUser;
        _logger = logger;
    }

    #region GET - List with Pagination

    [HttpGet]
    [RequirePermission(Permissions.{PermissionClass}.View)]
    [ProducesResponseType(typeof(PagedResult<{Entity}ListDto>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResult<{Entity}ListDto>>> Get{Module}(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? search = null,
        CancellationToken cancellationToken = default)
    {
        var query = _context.{DbSet}.AsQueryable();

        // Search filter
        if (!string.IsNullOrWhiteSpace(search))
        {
            var searchLower = search.ToLower();
            query = query.Where(x =>
                x.Name.ToLower().Contains(searchLower) ||
                x.Description != null && x.Description.ToLower().Contains(searchLower));
        }

        var totalCount = await query.CountAsync(cancellationToken);

        var items = await query
            .OrderBy(x => x.Name)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new {Entity}ListDto(
                x.Id,
                x.Name,
                x.Description,
                x.IsActive,
                x.CreatedAt
            ))
            .ToListAsync(cancellationToken);

        _logger.LogInformation("User {User} retrieved {Count} {Module}",
            _currentUser.Email, items.Count, "{Module}");

        return Ok(new PagedResult<{Entity}ListDto>(items, totalCount, page, pageSize));
    }

    #endregion

    #region GET - Single by ID

    [HttpGet("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.View)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        return Ok(new {Entity}DetailDto(
            entity.Id,
            entity.Name,
            entity.Description,
            entity.IsActive,
            entity.CreatedAt,
            entity.UpdatedAt
        ));
    }

    #endregion

    #region POST - Create

    [HttpPost]
    [RequirePermission(Permissions.{PermissionClass}.Create)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<{Entity}DetailDto>> Create{Entity}(
        [FromBody] Create{Entity}Request request,
        CancellationToken cancellationToken)
    {
        // Check for duplicates
        var exists = await _context.{DbSet}
            .AnyAsync(x => x.Name == request.Name, cancellationToken);

        if (exists)
            return Conflict(new { message = "{Entity} with this name already exists" });

        var entity = {Entity}.Create(
            request.Name,
            request.Description
        );

        _context.{DbSet}.Add(entity);
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} created {Entity} {EntityId} ({Name})",
            _currentUser.Email, entity.Id, entity.Name);

        return CreatedAtAction(
            nameof(Get{Entity}),
            new { id = entity.Id },
            new {Entity}DetailDto(
                entity.Id,
                entity.Name,
                entity.Description,
                entity.IsActive,
                entity.CreatedAt,
                entity.UpdatedAt
            ));
    }

    #endregion

    #region PUT - Update

    [HttpPut("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<ActionResult<{Entity}DetailDto>> Update{Entity}(
        Guid id,
        [FromBody] Update{Entity}Request request,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        // Check for duplicate name (excluding current)
        if (!string.IsNullOrEmpty(request.Name))
        {
            var duplicate = await _context.{DbSet}
                .AnyAsync(x => x.Name == request.Name && x.Id != id, cancellationToken);

            if (duplicate)
                return Conflict(new { message = "{Entity} with this name already exists" });
        }

        entity.Update(
            request.Name ?? entity.Name,
            request.Description ?? entity.Description
        );

        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} updated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return Ok(new {Entity}DetailDto(
            entity.Id,
            entity.Name,
            entity.Description,
            entity.IsActive,
            entity.CreatedAt,
            entity.UpdatedAt
        ));
    }

    #endregion

    #region PATCH - Activate/Deactivate

    [HttpPatch("{id:guid}/activate")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Activate{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        entity.Activate();
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogInformation("User {User} activated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return NoContent();
    }

    [HttpPatch("{id:guid}/deactivate")]
    [RequirePermission(Permissions.{PermissionClass}.Update)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Deactivate{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        entity.Deactivate();
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogWarning("User {User} deactivated {Entity} {EntityId}",
            _currentUser.Email, entity.Id);

        return NoContent();
    }

    #endregion

    #region DELETE

    [HttpDelete("{id:guid}")]
    [RequirePermission(Permissions.{PermissionClass}.Delete)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Delete{Entity}(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await _context.{DbSet}
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

        if (entity == null)
            return NotFound(new { message = "{Entity} not found" });

        // Check for dependencies before deletion
        // var hasReferences = await _context.ChildEntities.AnyAsync(x => x.{Entity}Id == id, ct);
        // if (hasReferences)
        //     return BadRequest(new { message = "Cannot delete: has dependent records" });

        _context.{DbSet}.Remove(entity);
        await _context.SaveChangesAsync(cancellationToken);

        _logger.LogWarning("User {User} deleted {Entity} {EntityId} ({Name})",
            _currentUser.Email, id, entity.Name);

        return NoContent();
    }

    #endregion
}

#region DTOs

public record {Entity}ListDto(
    Guid Id,
    string Name,
    string? Description,
    bool IsActive,
    DateTime CreatedAt
);

public record {Entity}DetailDto(
    Guid Id,
    string Name,
    string? Description,
    bool IsActive,
    DateTime CreatedAt,
    DateTime? UpdatedAt
);

public record Create{Entity}Request(
    string Name,
    string? Description
);

public record Update{Entity}Request(
    string? Name,
    string? Description
);

public record PagedResult<T>(
    List<T> Items,
    int TotalCount,
    int Page,
    int PageSize
)
{
    public int TotalPages => (int)Math.Ceiling((double)TotalCount / PageSize);
    public bool HasPrevious => Page > 1;
    public bool HasNext => Page < TotalPages;
}

#endregion
```

---

## Template Permissions Constants

```csharp
// src/SmartStack.Application/Common/Authorization/Permissions.cs
// AJOUTER dans la classe existante

public static class Permissions
{
    // ... existing permissions ...

    public static class {PermissionClass}
    {
        public const string Access = "{permission.path}";
        public const string View = "{permission.path}.read";
        public const string Create = "{permission.path}.create";
        public const string Update = "{permission.path}.update";
        public const string Delete = "{permission.path}.delete";
        // Optionnel selon module
        public const string Assign = "{permission.path}.assign";
        public const string Execute = "{permission.path}.execute";
        public const string Export = "{permission.path}.export";
    }
}
```

---

## Template PermissionConfiguration Seed

> **CRITIQUE:** Ce template est OBLIGATOIRE. Sans ces entrées, tous les appels API retourneront 403 Forbidden.

```csharp
// src/SmartStack.Infrastructure/Persistence/Configurations/Authorization/PermissionConfiguration.cs
// AJOUTER dans la méthode Configure(), section HasData

// ============================================
// ÉTAPE 1: Déclarer le ModuleId
// ============================================
// Vérifier dans ModuleConfiguration.cs si le module existe déjà
// Sinon, créer le module d'abord via /application skill

var {module}ModuleId = Guid.Parse("{GUID-DU-MODULE}");  // Récupérer depuis ModuleConfiguration.cs

// ============================================
// ÉTAPE 2: Ajouter les permissions (HasData)
// ============================================

// Pattern: {context}.{application}.{module}.{action}
// Exemple: business.crm.leads.read

var seedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);

builder.HasData(
    // Wildcard permission (accès complet au module)
    new
    {
        Id = Guid.Parse("{RANDOM-GUID-1}"),  // Générer avec [guid]::NewGuid()
        Path = "{context}.{application}.{module}.*",
        Level = PermissionLevel.Module,
        Action = (PermissionAction?)null,
        IsWildcard = true,
        ModuleId = {module}ModuleId,
        Description = "Full {module} management",
        CreatedAt = seedDate
    },

    // Read permission
    new
    {
        Id = Guid.Parse("{RANDOM-GUID-2}"),
        Path = "{context}.{application}.{module}.read",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Read,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "View {module}",
        CreatedAt = seedDate
    },

    // Create permission
    new
    {
        Id = Guid.Parse("{RANDOM-GUID-3}"),
        Path = "{context}.{application}.{module}.create",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Create,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Create {module}",
        CreatedAt = seedDate
    },

    // Update permission
    new
    {
        Id = Guid.Parse("{RANDOM-GUID-4}"),
        Path = "{context}.{application}.{module}.update",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Update,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Update {module}",
        CreatedAt = seedDate
    },

    // Delete permission
    new
    {
        Id = Guid.Parse("{RANDOM-GUID-5}"),
        Path = "{context}.{application}.{module}.delete",
        Level = PermissionLevel.Module,
        Action = PermissionAction.Delete,
        IsWildcard = false,
        ModuleId = {module}ModuleId,
        Description = "Delete {module}",
        CreatedAt = seedDate
    }
);
```

### Génération de GUIDs

```bash
# PowerShell (Windows)
[guid]::NewGuid().ToString()

# Bash (Linux/Mac)
uuidgen | tr '[:upper:]' '[:lower:]'
```

---

## Template Controller avec Relations

```csharp
// Pour les controllers avec entités liées (ex: Tickets avec Comments)

#region GET with Includes

[HttpGet("{id:guid}")]
[RequirePermission(Permissions.{PermissionClass}.View)]
[ProducesResponseType(typeof({Entity}DetailDto), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<{Entity}DetailDto>> Get{Entity}(
    Guid id,
    CancellationToken cancellationToken)
{
    var entity = await _context.{DbSet}
        .Include(x => x.CreatedByUser)
        .Include(x => x.AssignedToUser)
        .Include(x => x.Comments)
            .ThenInclude(c => c.Author)
        .Include(x => x.Attachments)
        .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);

    if (entity == null)
        return NotFound(new { message = "{Entity} not found" });

    return Ok(MapToDetailDto(entity));
}

#endregion

#region Nested Resources

[HttpGet("{parentId:guid}/children")]
[RequirePermission(Permissions.{PermissionClass}.View)]
[ProducesResponseType(typeof(List<ChildDto>), StatusCodes.Status200OK)]
public async Task<ActionResult<List<ChildDto>>> GetChildren(
    Guid parentId,
    CancellationToken cancellationToken)
{
    var children = await _context.Children
        .Where(x => x.ParentId == parentId)
        .OrderByDescending(x => x.CreatedAt)
        .Select(x => new ChildDto(x.Id, x.Name, x.CreatedAt))
        .ToListAsync(cancellationToken);

    return Ok(children);
}

[HttpPost("{parentId:guid}/children")]
[RequirePermission(Permissions.{PermissionClass}.Create)]
[ProducesResponseType(typeof(ChildDto), StatusCodes.Status201Created)]
public async Task<ActionResult<ChildDto>> AddChild(
    Guid parentId,
    [FromBody] CreateChildRequest request,
    CancellationToken cancellationToken)
{
    var parent = await _context.{DbSet}
        .FirstOrDefaultAsync(x => x.Id == parentId, cancellationToken);

    if (parent == null)
        return NotFound(new { message = "Parent not found" });

    var child = Child.Create(parentId, request.Name, _currentUser.UserId!.Value);

    _context.Children.Add(child);
    await _context.SaveChangesAsync(cancellationToken);

    _logger.LogInformation("User {User} added child to {Entity} {ParentId}",
        _currentUser.Email, parentId);

    return CreatedAtAction(
        nameof(GetChildren),
        new { parentId },
        new ChildDto(child.Id, child.Name, child.CreatedAt));
}

#endregion
```

---

## Patterns Réutilisables

### Error Response Standard

```csharp
public record ErrorResponse(string Message, string? Code = null);

// Usage:
return BadRequest(new ErrorResponse("Validation failed", "VALIDATION_ERROR"));
return Conflict(new ErrorResponse("Already exists", "DUPLICATE"));
return NotFound(new { message = "Resource not found" });
```

### Pagination Query Extension

```csharp
public static class QueryableExtensions
{
    public static async Task<PagedResult<T>> ToPagedResultAsync<T>(
        this IQueryable<T> query,
        int page,
        int pageSize,
        CancellationToken ct = default)
    {
        var totalCount = await query.CountAsync(ct);
        var items = await query
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync(ct);

        return new PagedResult<T>(items, totalCount, page, pageSize);
    }
}
```

### Log Context Pattern

```csharp
// Toujours inclure le contexte utilisateur dans les logs
_logger.LogInformation(
    "User {User} ({UserId}) performed {Action} on {Entity} {EntityId}",
    _currentUser.Email,
    _currentUser.UserId,
    "Create",
    "{Entity}",
    entity.Id);
```
