Skip to content

Estándares de Código — Plataforma Imagy

Backend (.NET 10)

Estructura de Solución

Cada servicio sigue Clean Architecture con 5 proyectos:

Imagy.{Domain}.Api/              → Controllers, middleware, DI
Imagy.{Domain}.Application/      → Commands, queries, handlers, validators, DTOs
Imagy.{Domain}.Domain/           → Entidades, value objects, interfaces, domain services
Imagy.{Domain}.Infrastructure/   → EF Core, Dapper, MassTransit, HTTP clients
Imagy.{Domain}.Migrations/       → DbUp scripts SQL
Imagy.{Domain}.Tests/            → Unit + integration tests

Entidades Base

csharp
// Entidad base — todos los agregados heredan de aquí
public abstract class BaseEntity
{
    public Guid Id { get; protected set; } = Guid.NewGuid();
    public DateTime CreatedAt { get; protected set; } = DateTime.UtcNow;
    public DateTime? UpdatedAt { get; protected set; }
}

// Entidad multi-tenant — la mayoría de entidades de negocio
public abstract class TenantEntity : BaseEntity
{
    public Guid TenantId { get; protected set; }
}

// Value Objects — inmutables, comparación por valor
public record Money(decimal Amount, string Currency);
public record DateRange(DateTime Start, DateTime End);

Commands y Queries (CQRS con MediatR)

csharp
// Command → escritura → EF Core → Primary DB
public record CreateCreditProductCommand(
    string Code,
    string Name,
    decimal MinAmount,
    decimal MaxAmount,
    decimal InterestRate,
    int MinTermDays,
    int MaxTermDays
) : IRequest<Result<CreditProductCreatedResponse>>;

// Query → lectura → Dapper → Read Replica
public record GetCreditProductsQuery(
    Guid TenantId,
    bool? IsActive,
    int Page = 1,
    int PageSize = 20
) : IRequest<PagedResult<CreditProductDto>>;

Handlers

csharp
public class CreateCreditProductHandler 
    : IRequestHandler<CreateCreditProductCommand, Result<CreditProductCreatedResponse>>
{
    private readonly ICreditProductRepository _repository;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IPublishEndpoint _publishEndpoint;
    private readonly IIdentityContext _identity;

    public async Task<Result<CreditProductCreatedResponse>> Handle(
        CreateCreditProductCommand cmd, CancellationToken ct)
    {
        // 1. Validar reglas de dominio
        var product = CreditProduct.Create(
            cmd.Code, cmd.Name, cmd.MinAmount, cmd.MaxAmount,
            cmd.InterestRate, cmd.MinTermDays, cmd.MaxTermDays,
            _identity.TenantId);

        // 2. Persistir
        await _repository.AddAsync(product, ct);
        await _unitOfWork.SaveChangesAsync(ct);

        // 3. Publicar evento DESPUÉS de persistir
        await _publishEndpoint.Publish(new CreditProductCreated
        {
            EventId = Guid.NewGuid(),
            EventType = "lending.product.created",
            TenantId = _identity.TenantId,
            Source = "imagy-lending",
            ActorId = _identity.UserId,
            Data = new { product.Id, product.Code, product.Name }
        }, ct);

        return Result<CreditProductCreatedResponse>.Success(new(product.Id));
    }
}

Validación (FluentValidation)

csharp
public class CreateCreditProductValidator : AbstractValidator<CreateCreditProductCommand>
{
    public CreateCreditProductValidator()
    {
        RuleFor(x => x.Code)
            .NotEmpty()
            .MaximumLength(50)
            .Matches(@"^[a-z0-9\-]+$").WithMessage("Solo lowercase, números y guiones");

        RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
        RuleFor(x => x.MinAmount).GreaterThan(0);
        RuleFor(x => x.MaxAmount).GreaterThan(x => x.MinAmount);
        RuleFor(x => x.InterestRate).InclusiveBetween(0, 1);
        RuleFor(x => x.MinTermDays).GreaterThan(0);
        RuleFor(x => x.MaxTermDays).GreaterThan(x => x.MinTermDays);
    }
}

Controllers (delgados — solo mapeo)

csharp
[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class CreditProductsController : ControllerBase
{
    private readonly IMediator _mediator;

    [HttpPost]
    [Authorize(Roles = "tenant_admin,platform_admin")]
    public async Task<IActionResult> Create([FromBody] CreateCreditProductDto dto)
    {
        var command = dto.ToCommand();
        var result = await _mediator.Send(command);
        return result.IsSuccess
            ? CreatedAtAction(nameof(GetById), new { id = result.Value.Id }, result.Value)
            : BadRequest(result.ToErrorResponse());
    }

    [HttpGet]
    [Authorize]
    public async Task<IActionResult> GetAll([FromQuery] GetCreditProductsQueryParams queryParams)
    {
        var query = queryParams.ToQuery();
        var result = await _mediator.Send(query);
        return Ok(result.ToApiResponse());
    }
}

Result Pattern

csharp
public class Result<T>
{
    public bool IsSuccess { get; }
    public T? Value { get; }
    public List<ErrorDetail> Errors { get; }

    public static Result<T> Success(T value) => new(true, value, []);
    public static Result<T> Failure(string code, string message) => 
        new(false, default, [new ErrorDetail(code, message)]);
    public static Result<T> Failure(params ErrorDetail[] errors) => 
        new(false, default, errors.ToList());
}

public record ErrorDetail(string Code, string Message, string? Field = null);

Repository Pattern

csharp
// Interface en Application layer
public interface ICreditProductRepository
{
    Task<CreditProduct?> GetByIdAsync(Guid id, CancellationToken ct = default);
    Task<CreditProduct?> GetByCodeAsync(string code, CancellationToken ct = default);
    Task AddAsync(CreditProduct product, CancellationToken ct = default);
}

// Implementación en Infrastructure (EF Core → Primary)
public class CreditProductRepository : ICreditProductRepository
{
    private readonly LendingDbContext _db;

    public async Task<CreditProduct?> GetByIdAsync(Guid id, CancellationToken ct) =>
        await _db.CreditProducts.FirstOrDefaultAsync(p => p.Id == id, ct);
}

// Read Repository (Dapper → Read Replica)
public class CreditProductReadRepository : ICreditProductReadRepository
{
    private readonly IDbConnection _readDb;
    private readonly IIdentityContext _identity;

    public async Task<PagedResult<CreditProductDto>> GetAllAsync(
        bool? isActive, int page, int pageSize)
    {
        await SetTenantContextAsync();
        
        var sql = @"SELECT id, code, name, min_amount, max_amount, interest_rate, is_active
                    FROM credit_products
                    WHERE (@IsActive IS NULL OR is_active = @IsActive)
                    ORDER BY created_at DESC
                    LIMIT @Limit OFFSET @Offset";

        var results = await _readDb.QueryAsync<CreditProductDto>(sql, new
        {
            IsActive = isActive,
            Limit = pageSize,
            Offset = (page - 1) * pageSize
        });

        return new PagedResult<CreditProductDto>(results, total, page, pageSize);
    }

    private async Task SetTenantContextAsync()
    {
        await _readDb.ExecuteAsync(
            "SET app.current_tenant_id = @TenantId",
            new { _identity.TenantId });
    }
}

Naming Conventions (.NET)

ElementoConvenciónEjemplo
NamespacesPascalCase, prefijo ImagyImagy.Lending.Application
ClasesPascalCaseCreditProduct, CreateCreditProductHandler
InterfacesI + PascalCaseICreditProductRepository
MétodosPascalCaseGetByIdAsync, CalculateInterest
PropertiesPascalCaseInterestRate, TenantId
Private fields_camelCase_repository, _identity
ParameterscamelCasetenantId, creditProduct
ConstantsPascalCaseMaxRetryAttempts
EnumsPascalCase (singular)CreditStatus.Active
Records (DTOs)PascalCase + sufijoCreditProductDto, CreateCreditProductCommand
Async methodsSufijo AsyncGetByIdAsync, SaveChangesAsync

Frontend (React 19 + TypeScript 6)

Estructura de Proyecto (Panel Admin)

src/
├── app/                         # Configuración global
│   ├── App.tsx
│   ├── router.tsx
│   └── providers.tsx
├── components/                  # Componentes compartidos
│   ├── ui/                      # Primitivos (Button, Input, Table, Modal)
│   ├── form/                    # FormField, FormSection
│   ├── layout/                  # Shell, Sidebar, Header
│   └── feedback/                # Toast, Alert, Loading
├── features/                    # Módulos por dominio
│   ├── lending/                 # ImagLend
│   │   ├── products/
│   │   ├── applications/
│   │   └── portfolio/
│   ├── flows/                   # ImagFlow
│   │   ├── designer/
│   │   ├── executions/
│   │   └── providers/
│   ├── sign/                    # ImagSign
│   ├── subjects/                # ImagID
│   └── settings/                # Configuración del tenant
├── hooks/                       # Hooks compartidos
├── stores/                      # Zustand stores
├── types/                       # Tipos globales
├── lib/                         # Utilidades (api client, formatters)
└── config/                      # Configuración de la app

Componentes

typescript
// Componente funcional con tipos explícitos
interface CreditProductCardProps {
  product: CreditProductDto;
  onEdit: (id: string) => void;
  onToggleActive: (id: string, active: boolean) => void;
}

const CreditProductCard: React.FC<CreditProductCardProps> = ({
  product,
  onEdit,
  onToggleActive,
}) => {
  return (
    <div className="rounded-lg border p-4">
      <h3 className="text-lg font-medium">{product.name}</h3>
      <p className="text-sm text-muted">{product.code}</p>
      {/* ... */}
    </div>
  );
};

Hooks y Data Fetching (TanStack Query)

typescript
// Hook para queries
export function useCreditProducts(filters?: CreditProductFilters) {
  return useQuery({
    queryKey: ['credit-products', filters],
    queryFn: () => api.get<PagedResponse<CreditProductDto>>('/api/v1/credit-products', {
      params: filters,
    }),
  });
}

// Hook para mutations
export function useCreateCreditProduct() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (data: CreateCreditProductDto) =>
      api.post<CreditProductCreatedResponse>('/api/v1/credit-products', data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['credit-products'] });
      toast.success('Producto creado exitosamente');
    },
    onError: (error) => {
      toast.error(error.message);
    },
  });
}

Formularios (React Hook Form + Zod)

typescript
const creditProductSchema = z.object({
  code: z.string().min(1).max(50).regex(/^[a-z0-9-]+$/),
  name: z.string().min(1).max(200),
  minAmount: z.number().positive(),
  maxAmount: z.number().positive(),
  interestRate: z.number().min(0).max(1),
  minTermDays: z.number().int().positive(),
  maxTermDays: z.number().int().positive(),
});

type CreditProductFormData = z.infer<typeof creditProductSchema>;

const CreditProductForm: React.FC<{ onSubmit: (data: CreditProductFormData) => void }> = ({
  onSubmit,
}) => {
  const { register, handleSubmit, formState: { errors } } = useForm<CreditProductFormData>({
    resolver: zodResolver(creditProductSchema),
    mode: 'onBlur',
  });

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <FormField label="Código" error={errors.code?.message} required>
        <Input {...register('code')} placeholder="microcredito-30d" />
      </FormField>
      {/* ... */}
    </form>
  );
};

State Management (Zustand)

typescript
// Store por feature — no un store global gigante
interface LendingStore {
  selectedProductId: string | null;
  filters: CreditProductFilters;
  setSelectedProduct: (id: string | null) => void;
  setFilters: (filters: Partial<CreditProductFilters>) => void;
  resetFilters: () => void;
}

export const useLendingStore = create<LendingStore>((set) => ({
  selectedProductId: null,
  filters: { page: 1, pageSize: 20 },
  setSelectedProduct: (id) => set({ selectedProductId: id }),
  setFilters: (filters) => set((state) => ({
    filters: { ...state.filters, ...filters },
  })),
  resetFilters: () => set({ filters: { page: 1, pageSize: 20 } }),
}));

Naming Conventions (Frontend)

ElementoConvenciónEjemplo
ComponentesPascalCaseCreditProductCard.tsx
HookscamelCase con useuseCreditProducts.ts
StorescamelCase con use + StoreuseLendingStore.ts
Types/InterfacesPascalCaseCreditProductDto
Schemas (Zod)camelCase + SchemacreditProductSchema
UtilidadescamelCaseformatCurrency.ts
ConstantesUPPER_SNAKE_CASEMAX_FILE_SIZE
CSS classeskebab-case (Tailwind)text-sm font-medium
Archivos de componentePascalCaseCreditProductForm.tsx
Archivos de hook/utilcamelCaseuseCreditProducts.ts
Carpetaskebab-casecredit-products/, data-model/

API Client

typescript
// lib/api.ts — instancia Axios configurada
import axios from 'axios';

export const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  withCredentials: true, // envía cookie de sesión
});

// Interceptor: refresh automático si 401
api.interceptors.response.use(
  (response) => response.data,
  async (error) => {
    if (error.response?.status === 401) {
      // Redirigir a login
      window.location.href = `/api/v1/auth/login?cid=${getTenantCode()}`;
    }
    return Promise.reject(error.response?.data?.error ?? error);
  }
);

Reglas Generales (Backend + Frontend)

Patrones Obligatorios

PatrónDóndePor qué
Result patternBackend Application layerNo excepciones para flujo de negocio
CQRS (MediatR)Backend Application layerSeparación commands/queries
Repository patternBackend InfrastructureAbstracción de acceso a datos
Dependency InjectionBackend (todo)Testabilidad
Guard clausesBackend DomainValidar invariantes
Hooks + TanStack QueryFrontendData fetching declarativo
Zod schemasFrontendValidación type-safe
Feature-slicedFrontendOrganización por dominio

Patrones Prohibidos

Anti-patrónPor qué
Service LocatorOculta dependencias
God class / God componentDividir responsabilidades
Anemic domain modelEntidades con comportamiento
Magic strings/numbersUsar constantes o enums
any en TypeScriptSiempre tipar explícitamente
catch (Exception) sin loggingSiempre loggear
Task.Result / .Wait()Siempre await
Lógica en controllersSolo mapeo y delegación
useEffect para data fetchingUsar TanStack Query
Props drilling (>3 niveles)Usar Zustand o Context

Seguridad en Código

  • Input validation en AMBOS lados (frontend con Zod, backend con FluentValidation)
  • Queries parametrizadas (nunca string interpolation en SQL)
  • No loggear datos sensibles (tokens, passwords, PII)
  • Pinned versions en dependencias (no rangos abiertos)
  • Headers de seguridad en todas las respuestas
  • No exponer stack traces al cliente
  • HTTPS obligatorio en producción

.editorconfig (obligatorio en cada repo)

ini
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.cs]
dotnet_sort_system_directives_first = true
csharp_new_line_before_open_brace = all
csharp_prefer_braces = true
csharp_style_var_for_built_in_types = false
csharp_style_var_when_type_is_apparent = true
csharp_style_var_elsewhere = false

[*.{ts,tsx}]
indent_size = 2

[*.{json,yml,yaml}]
indent_size = 2

[*.md]
trim_trailing_whitespace = false

Code Review Checklist

Antes de aprobar un PR:

  • [ ] Input validation presente (frontend Y backend)
  • [ ] Queries parametrizadas (no string interpolation)
  • [ ] No se loggean datos sensibles
  • [ ] Errores manejados correctamente
  • [ ] Tests cubren happy path + al menos 1 error path
  • [ ] No hay TODOs sin issue asociado
  • [ ] Eventos publicados DESPUÉS de persistir
  • [ ] Tenant context establecido correctamente
  • [ ] No hay dependencias nuevas sin justificación
  • [ ] Tipos explícitos (no any en TS, no dynamic en C#)
  • [ ] Código legible sin comentarios explicativos

Reimagine Tech LLC — Documentacion Interna