.claude/skills/create-use-case/SKILL.md
Creates SBA (Story-Based Architecture) use cases in C# following the three-phase pattern (Load, Validate, Execute). Use when adding new business operations like creating invoices, issuing credit notes, transitioning statuses, or any domain action that combines data loading, validation, and execution into a single cohesive unit.
npx skillsauth add lazyoft/raymond-invoices create-use-caseInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are creating a use case following Story-Based Architecture (SBA) for the Fatturazione invoicing system. Every use case encapsulates a single business story with exactly three phases: Load, Validate, Execute.
A use case is a self-contained business story. It reads like a narrative:
"To issue an invoice, I need to load the invoice and its client, validate that the invoice can transition to Issued status and has valid data, then assign a number and mark it as issued."
The use case is the sole public entry point for a business operation. Endpoints become thin wrappers that delegate to use cases. Services remain stateless calculation/utility helpers.
Fatturazione.Domain.UseCasessrc/Fatturazione.Domain/UseCases/Fatturazione.Domain.Services, Repositories in Fatturazione.Infrastructure.RepositoriesWhen asked to create a use case, follow this process:
Ask yourself:
Place these inside the use case file, above the class definition.
// Request: everything the actor provides to trigger the story
public record IssueInvoiceRequest(Guid InvoiceId, Guid ActorId);
// Response: everything the actor needs to know after the story completes
public record IssueInvoiceResponse(Invoice Invoice, string InvoiceNumber);
Rules for Request/Response:
record types (immutable by default)ActorId (Guid) to identify who is performing the actionusing Fatturazione.Domain.Models;
using Fatturazione.Domain.Services;
using Fatturazione.Infrastructure.Repositories;
using Microsoft.Extensions.Logging;
namespace Fatturazione.Domain.UseCases;
// Request/Response records
public record IssueInvoiceRequest(Guid InvoiceId, Guid ActorId);
public record IssueInvoiceResponse(Invoice Invoice, string InvoiceNumber);
/// <summary>
/// Issues an invoice: assigns a progressive number and transitions status to Issued.
/// Art. 21 DPR 633/72 - Fatturazione delle operazioni.
/// </summary>
public class IssueInvoice
{
private readonly IInvoiceRepository _invoiceRepository;
private readonly IClientRepository _clientRepository;
private readonly IInvoiceNumberingService _numberingService;
private readonly IInvoiceCalculationService _calculationService;
private readonly ILogger<IssueInvoice> _logger;
public IssueInvoice(
IInvoiceRepository invoiceRepository,
IClientRepository clientRepository,
IInvoiceNumberingService numberingService,
IInvoiceCalculationService calculationService,
ILogger<IssueInvoice> logger)
{
_invoiceRepository = invoiceRepository;
_clientRepository = clientRepository;
_numberingService = numberingService;
_calculationService = calculationService;
_logger = logger;
}
// ── Sole public method ──────────────────────────────────────────
public async Task<IssueInvoiceResponse> Execute(IssueInvoiceRequest request)
{
// Phase 1: Load
var (invoice, client, lastNumber) = await GetInvoiceWithDependencies(request.InvoiceId);
// Phase 2: Validate
ValidateCanIssue(invoice, request.ActorId);
// Phase 3: Execute
var result = await PerformIssuance(invoice, client, lastNumber);
return result;
}
// ── Phase 1: Load ───────────────────────────────────────────────
// Method name: GetXxx — loads all data needed for the story
private async Task<(Invoice Invoice, Client Client, string? LastNumber)>
GetInvoiceWithDependencies(Guid invoiceId)
{
var invoice = await _invoiceRepository.GetByIdAsync(invoiceId)
?? throw new NotFoundException($"Fattura con ID {invoiceId} non trovata.");
var client = await _clientRepository.GetByIdAsync(invoice.ClientId)
?? throw new NotFoundException($"Cliente con ID {invoice.ClientId} non trovato.");
var lastNumber = await _invoiceRepository.GetLastInvoiceNumberAsync();
return (invoice, client, lastNumber);
}
// ── Phase 2: Validate ───────────────────────────────────────────
// Method name: ValidateXxx — checks all business rules, throws on failure
private void ValidateCanIssue(Invoice invoice, Guid actorId)
{
if (!Validators.InvoiceValidator.CanTransitionTo(invoice.Status, InvoiceStatus.Issued))
{
_logger.LogInformation(
"Actor {ActorId} attempted invalid transition from {CurrentStatus} to Issued for invoice {InvoiceId}",
actorId, invoice.Status, invoice.Id);
throw new ForbiddenOperationException(
$"Impossibile emettere la fattura: transizione da {invoice.Status} a Issued non consentita.");
}
var (isValid, errors) = Validators.InvoiceValidator.Validate(invoice);
if (!isValid)
{
_logger.LogInformation(
"Actor {ActorId} submitted invalid invoice {InvoiceId}: {Errors}",
actorId, invoice.Id, string.Join("; ", errors));
throw new InvalidInputException(errors);
}
}
// ── Phase 3: Execute ────────────────────────────────────────────
// Method name: PerformXxx — mutates state, persists, returns response
private async Task<IssueInvoiceResponse> PerformIssuance(
Invoice invoice, Client client, string? lastNumber)
{
invoice.Client = client;
_calculationService.CalculateInvoiceTotals(invoice);
invoice.InvoiceNumber = _numberingService.GenerateNextInvoiceNumber(lastNumber);
invoice.Status = InvoiceStatus.Issued;
var updated = await _invoiceRepository.UpdateAsync(invoice);
_logger.LogInformation(
"Invoice {InvoiceId} issued as {InvoiceNumber}",
invoice.Id, invoice.InvoiceNumber);
return new IssueInvoiceResponse(updated!, invoice.InvoiceNumber);
}
}
| Element | Convention | Example |
|---------|-----------|---------|
| File name | PascalCase business intent | IssueInvoice.cs, CreateCreditNote.cs |
| Class name | Same as file, no suffix | IssueInvoice, CreateCreditNote |
| Request record | {UseCaseName}Request | IssueInvoiceRequest |
| Response record | {UseCaseName}Response | IssueInvoiceResponse |
| Load method(s) | GetXxx | GetInvoiceWithDependencies |
| Validate method(s) | ValidateXxx | ValidateCanIssue |
| Execute method(s) | PerformXxx | PerformIssuance |
src/Fatturazione.Domain/UseCases/Execute(TRequest request) -- everything else is privatePhase 1 - Load (GetXxx):
NotFoundException if required entities are missingPhase 2 - Validate (ValidateXxx):
ForbiddenOperationException for authorization/state violationsInvalidInputException for data validation failuresInformation level with actor contextPhase 3 - Execute (PerformXxx):
Information levelRegister use cases as scoped services in Program.cs:
// Register use cases
builder.Services.AddScoped<IssueInvoice>();
builder.Services.AddScoped<CreateCreditNote>();
Endpoints become thin wrappers that map HTTP to use case execution:
private static async Task<IResult> IssueInvoice(
Guid id,
UseCases.IssueInvoice useCase)
{
try
{
var request = new IssueInvoiceRequest(id, ActorId: Guid.Empty); // TODO: from auth
var response = await useCase.Execute(request);
return Results.Ok(response.Invoice);
}
catch (NotFoundException ex)
{
return Results.NotFound(ex.Message);
}
catch (ForbiddenOperationException ex)
{
return Results.ValidationProblem(new Dictionary<string, string[]>
{
{ "Status", new[] { ex.Message } }
});
}
catch (InvalidInputException ex)
{
var errorDict = ex.Errors.Select((e, i) => new { Key = $"Error{i}", Value = e })
.ToDictionary(x => x.Key, x => new[] { x.Value });
return Results.ValidationProblem(errorDict);
}
}
A use case is warranted when an operation requires loading data + validating rules + persisting changes as a cohesive story.
src/Fatturazione.Domain/UseCases/{Name}.csActorIdGetXxx, ValidateXxx, PerformXxx namingExecute is publicILogger<T> injected and used for validation failures + successNotFoundException, ForbiddenOperationException, InvalidInputException)Program.cs as scopedwrite-tests skill)testing
Writes xUnit tests for SBA use cases in the Fatturazione invoicing system. Use when creating tests for use cases, services, or validators. Follows the three-phase testing pattern (load failures, validation failures, execution success/failure) with NSubstitute mocking and FluentAssertions. Preserves the project's existing test conventions.
development
Implementa feature nel sistema di fatturazione italiana validando contro normativa fiscale. Usa per aggiungere calcoli IVA, ritenuta d'acconto, split payment, imposta di bollo, gestione fatture PA, regime forfettario, numerazione progressiva, note di credito, o qualsiasi logica che deve rispettare DPR 633/72, DPR 600/73, DPR 642/72. NON usare per bug fix tecnici, refactoring, o modifiche UI senza impatto fiscale.
testing
Implements two-tier error handling for the Fatturazione invoicing system following SBA patterns. Use when adding error handling to use cases, defining new domain exceptions, or setting up structured logging with actor context. Covers expected errors (validation, authorization) vs unexpected errors (infrastructure), logging levels, and exception-to-HTTP-status mapping.
testing
Create, edit, improve, or audit AgentSkills. Use when creating a new skill from scratch or when asked to improve, review, audit, tidy up, or clean up an existing skill or SKILL.md file. Also use when editing or restructuring a skill directory (moving files to references/ or scripts/, removing stale content, validating against the AgentSkills spec). Triggers on phrases like "create a skill", "author a skill", "tidy up a skill", "improve this skill", "review the skill", "clean up the skill", "audit the skill".