Skip to Content
UNIMAST
DocumentaciónDesarrollarEstándares de Código

Estándares de Código

UNIMAST ERP sigue estándares de código rigurosos para mantener la calidad, consistencia y mantenibilidad del código. Estos estándares se aplican a través de herramientas automatizadas, configuraciones de linting, y convenciones de equipo.

🔧 Herramientas de Calidad de Código

ESLint - Linting Automatizado

// app/eslint.config.mjs import { FlatCompat } from '@eslint/eslintrc' const compat = new FlatCompat({ baseDirectory: import.meta.dirname, }) const eslintConfig = [ ...compat.config({ plugins: ['simple-import-sort'], // Plugin de ordenamiento de imports extends: ['next'], // Configuración base de Next.js rules: { 'react/no-unescaped-entities': 'off', // Permitir entidades HTML '@next/next/no-page-custom-font': 'off', // Permitir fuentes personalizadas 'react-hooks/exhaustive-deps': 0, // Deshabilitar regla de dependencias 'simple-import-sort/imports': 'error', // Error en imports desordenados 'simple-import-sort/exports': 'error', // Error en exports desordenados 'no-unused-vars': 'warn', // Warning para variables no usadas 'eol-last': ['error', 'always'] // Nueva línea al final obligatoria }, ignorePatterns: [ 'next.config.ts', 'tailwind.config.js', '.next', 'node_modules' ], }), ] export default eslintConfig

Prettier - Formateo Automatizado

// app/package.json { "prettier": { "arrowParens": "always", // Paréntesis siempre en arrow functions "singleQuote": true, // Comillas simples "tabWidth": 2, // Ancho de tabulación de 2 espacios "trailingComma": "none", // Sin coma final "semi": true // Punto y coma al final } }

TypeScript - Configuración Estricta

// app/tsconfig.json { "compilerOptions": { "strict": true, // Modo estricto habilitado "forceConsistentCasingInFileNames": true, // Consistencia en nombres de archivos "noEmit": true, // No generar archivos de salida "isolatedModules": true, // Módulos aislados "baseUrl": ".", // URL base para imports "paths": { "@/*": ["./*"] // Alias de paths } } }

📝 Estándares de TypeScript

1. Tipado Estricto y Explícito

// ✅ CORRECTO: Tipado explícito y estricto interface UserFormData { name: string; email: string; password?: string; roleId: string; } export async function createUser(data: UserFormData): Promise<User> { const user = await prisma.user.create({ data: { name: data.name, email: data.email, password: data.password ? await hashPassword(data.password) : null, roleId: data.roleId } }); return user; } // ❌ INCORRECTO: Uso de any o tipado implícito export async function createUser(data: any) { const user = await prisma.user.create({ data }); return user; }

2. Interfaces y Tipos

// ✅ CORRECTO: Interfaces descriptivas y específicas export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link'; size?: 'default' | 'sm' | 'lg' | 'icon'; } // ✅ CORRECTO: Tipos union para valores específicos export type TaxDocumentType = | 'E31' | 'E32' | 'E33' | 'E34' | 'E41' | 'E43' | 'E44' | 'E45' | 'E46' | 'E47'; // ✅ CORRECTO: Tipos utilitarios de Prisma export type EcfIssued = Prisma.TaxDocumentGetPayload<{ include: { creditNote: { include: { invoice: { include: { customer: true } } } }; invoice: { include: { customer: true } }; }; }>;

3. Generics y Tipos Reutilizables

// ✅ CORRECTO: Generics para funciones reutilizables export function useDebounceValue<T>(value: T, delay = 300): T { const [debouncedValue, setDebouncedValue] = React.useState<T>(value); React.useEffect(() => { const timeout = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(timeout); }, [value, delay]); return debouncedValue; } // ✅ CORRECTO: Tipos condicionales export type ConditionalUser = User & { password: string | null; role: Role; }; // ✅ CORRECTO: Tipos de mapeo export const STATUS_MAP: Record<string, TaxDocumentStatus> = { Aceptado: TaxDocumentStatus.APPROVED, Rechazado: TaxDocumentStatus.REJECTED, ['Aceptado Condicional']: TaxDocumentStatus.CONDITIONALLY_APPROVED, ['En Proceso']: TaxDocumentStatus.IN_PROCESS };

🎨 Estándares de Estilo y Formato

1. Nomenclatura de Archivos y Directorios

# ✅ CORRECTO: Nomenclatura kebab-case para archivos app/ ├── (dashboard)/ # Grupos de rutas ├── invoices/ # Módulos en plural ├── page.tsx # Páginas principales ├── create-invoice-dialog.tsx # Componentes descriptivos ├── invoice-row-actions.tsx # Acciones específicas └── invoices-filter.tsx # Filtros del módulo └── settings/ # Configuraciones ├── general/ # Configuración general └── users/ # Gestión de usuarios ├── components/ ├── ui/ # Componentes base (shadcn/ui) ├── button.tsx # Componentes simples ├── form.tsx # Componentes de formulario └── table.tsx # Componentes de datos └── submit-button.tsx # Componentes especializados └── lib/ ├── auth/ # Autenticación ├── ecf/ # Facturación electrónica ├── queue/ # Colas de trabajo └── utils/ # Utilidades generales

2. Nomenclatura de Variables y Funciones

// ✅ CORRECTO: Nombres descriptivos y específicos export async function getTaxDocuments(searchParams: SearchParams) { const { page = 1, perPage = 10, status, type, startDate, endDate } = searchParams; const whereClause = buildWhereClause({ status, type, startDate, endDate }); const skip = (page - 1) * perPage; const [documents, totalCount] = await Promise.all([ prisma.taxDocument.findMany({ where: whereClause, skip, take: perPage, include: { invoice: true, creditNote: true } }), prisma.taxDocument.count({ where: whereClause }) ]); return { documents, totalCount, page, perPage }; } // ✅ CORRECTO: Constantes en UPPER_SNAKE_CASE export const TAX_DOCUMENT_STATUS_LABEL = { [TaxDocumentStatus.APPROVED]: 'Aceptado', [TaxDocumentStatus.REJECTED]: 'Rechazado', [TaxDocumentStatus.CONDITIONALLY_APPROVED]: 'Aceptado Condicional', [TaxDocumentStatus.IN_PROCESS]: 'En Proceso' } as const; // ✅ CORRECTO: Funciones con verbos descriptivos export function validateInvoiceForECF(invoice: Invoice): ValidationResult export function processDocumentSubmission(inputData: ElectronicInvoice): Promise<TaxDocument> export function queueSendTaxDocument(encf: string): Promise<Job>

3. Estructura de Imports

// ✅ CORRECTO: Orden de imports (automático con simple-import-sort) // 1. Imports de React y Next.js import { useEffect, useState } from 'react'; import { usePathname, useRouter, useSearchParams } from 'next/navigation'; // 2. Imports de librerías externas import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; // 3. Imports de Prisma y tipos import { Role, User } from '@prisma/client'; // 4. Imports de componentes UI import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog'; import { Form, FormControl, FormField } from '@/components/ui/form'; // 5. Imports locales del módulo import { createUser, updateUser } from './actions'; // ❌ INCORRECTO: Imports desordenados import { Button } from '@/components/ui/button'; import { z } from 'zod'; import { useEffect, useState } from 'react'; import { createUser, updateUser } from './actions';

🏗️ Estándares de Arquitectura

1. Separación de Responsabilidades

// ✅ CORRECTO: Separación clara de responsabilidades // app/(dashboard)/invoices/page.tsx - Página principal (UI) export default async function InvoicesPage({ searchParams }: Props) { const { invoices, totalCount, page, perPage } = await getInvoices(searchParams); return ( <div> <InvoicesHeader /> <InvoicesFilter /> <InvoicesTable invoices={invoices} /> <Pagination totalCount={totalCount} currentPage={page} perPage={perPage} /> </div> ); } // app/(dashboard)/invoices/actions.ts - Lógica de negocio export async function getInvoices(searchParams: SearchParams) { const { page = 1, perPage = 10, status, type } = searchParams; const whereClause = buildWhereClause({ status, type }); const [invoices, totalCount] = await Promise.all([ prisma.invoice.findMany({ where: whereClause, skip: (page - 1) * perPage, take: perPage, include: { customer: true } }), prisma.invoice.count({ where: whereClause }) ]); return { invoices, totalCount, page, perPage }; } // app/(dashboard)/invoices/invoices-filter.tsx - Componente de filtros export default function InvoicesFilter() { const [filters, setFilters] = useState(initialFilters); const applyFilters = () => { // Lógica de aplicación de filtros }; return ( <Card> <CardContent> {/* UI de filtros */} </CardContent> </Card> ); }

2. Patrones de Componentes

// ✅ CORRECTO: Componentes funcionales con forwardRef const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; return ( <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} /> ); } ); Button.displayName = "Button"; // ✅ CORRECTO: Hooks personalizados para lógica reutilizable export function useCustomSearchParams() { const searchParams = useSearchParams(); const pathname = usePathname(); const { replace } = useRouter(); const updateSearchParams = useCallback((updates: Record<string, string>) => { const params = new URLSearchParams(searchParams); Object.entries(updates).forEach(([key, value]) => { if (value) { params.set(key, value); } else { params.delete(key); } }); replace(`${pathname}?${params.toString()}`); }, [searchParams, pathname, replace]); return { searchParams, updateSearchParams }; }

3. Manejo de Estado

// ✅ CORRECTO: Estado local para componentes específicos export function CreateInvoiceDialog() { const [isOpen, setIsOpen] = useState(false); const [items, setItems] = useState<InvoiceItem[]>([]); const addItem = useCallback(() => { setItems(prev => [...prev, { description: '', quantity: 1, price: 0 }]); }, []); const removeItem = useCallback((index: number) => { setItems(prev => prev.filter((_, i) => i !== index)); }, []); return ( <Dialog open={isOpen} onOpenChange={setIsOpen}> {/* Contenido del diálogo */} </Dialog> ); } // ✅ CORRECTO: Estado global en contextos export const SessionProvider = ({ children, session }: SessionProviderProps) => { return ( <SessionContext.Provider value={{ session }}> {children} </SessionContext.Provider> ); }; export const useSession = () => { const context = useContext(SessionContext); if (!context) { throw new Error('useSession must be used within SessionProvider'); } return context; };

🔄 Control de Flujo y Manejo de Errores

1. Early Returns y Validaciones

// ✅ CORRECTO: Early returns para validaciones export async function sendInvoiceECF(invoiceId: string) { const session = await getSession(); if (!session) { throw new Error('No tienes permiso para realizar esta acción.'); } const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new Error('Certificado eCF no encontrado. Configure el certificado en Configuración > General.'); } const ecfResult = await getNextECF(type); if (!ecfResult) { throw new Error(`No existen secuencias disponibles para el tipo de documento ${type}. Contacte al administrador.`); } // Procesar envío si todas las validaciones pasan const result = await processDocumentSubmission(inputData, ecfResult); return result; } // ❌ INCORRECTO: Anidación profunda export async function sendInvoiceECF(invoiceId: string) { const session = await getSession(); if (session) { const cert = await getCertificate(); if (cert && cert.key && cert.cert) { const ecfResult = await getNextECF(type); if (ecfResult) { const result = await processDocumentSubmission(inputData, ecfResult); return result; } else { throw new Error('No existen secuencias...'); } } else { throw new Error('Certificado no encontrado...'); } } else { throw new Error('No tienes permiso...'); } }

2. Manejo Explícito de Errores

// ✅ CORRECTO: Manejo explícito y descriptivo de errores export async function processDocumentSubmission(inputData: ElectronicInvoice) { try { const cert = await getCertificate(); if (!cert || !cert.key || !cert.cert) { throw new Error('No ECF certificate found'); } const ecf = new ECF(cert, ECF_ENV); await ecf.authenticate(); const transformer = new Transformer(); const jsonData = await ECFBuilder(inputData); const xmlDoc = transformer.json2xml(jsonData); const signature = new Signature(cert.key, cert.cert); const signedXml = signature.signXml(xmlDoc, 'ECF'); const securityCode = getCodeSixDigitfromSignature(signedXml); if (!securityCode) { throw new Error('Unable to get the first 6 digits of the SignatureValue'); } // Continuar con el procesamiento... return await saveAndSendDocument(signedXml, securityCode); } catch (error) { console.error('Error processing document submission:', { error: error.message, inputData: { encf: inputData.encf, type: inputData.type }, stack: error.stack }); // Re-lanzar error con contexto adicional throw new Error(`Failed to process document ${inputData.encf}: ${error.message}`); } } // ✅ CORRECTO: Errores tipados y específicos export class ECFError extends Error { constructor( message: string, public code: 'CERTIFICATE_NOT_FOUND' | 'NO_SEQUENCES' | 'AUTHENTICATION_FAILED', public details?: Record<string, any> ) { super(message); this.name = 'ECFError'; } } // Uso if (!cert) { throw new ECFError( 'Certificado eCF no encontrado', 'CERTIFICATE_NOT_FOUND', { path: settings.ecfCertificatePath } ); }

3. Evitar Anidación Profunda

// ✅ CORRECTO: Extraer lógica compleja a funciones separadas export async function handleInvoiceCreation(formData: FormData) { try { const invoiceData = await validateAndParseInvoiceData(formData); const customer = await findOrCreateCustomer(invoiceData.customer); const invoice = await createInvoiceWithItems(invoiceData, customer.id); if (invoiceData.shouldSendECF) { await queueECFSending(invoice.id); } return { success: true, invoice }; } catch (error) { return { success: false, error: error.message }; } } async function validateAndParseInvoiceData(formData: FormData) { const rawData = Object.fromEntries(formData.entries()); const validatedData = invoiceSchema.parse(rawData); return validatedData; } async function findOrCreateCustomer(customerData: CustomerData) { const existingCustomer = await prisma.customer.findUnique({ where: { taxId: customerData.taxId } }); if (existingCustomer) return existingCustomer; return await prisma.customer.create({ data: customerData }); } // ❌ INCORRECTO: Lógica anidada en una sola función export async function handleInvoiceCreation(formData: FormData) { try { const rawData = Object.fromEntries(formData.entries()); const validatedData = invoiceSchema.parse(rawData); const existingCustomer = await prisma.customer.findUnique({ where: { taxId: validatedData.customer.taxId } }); let customer; if (existingCustomer) { customer = existingCustomer; } else { customer = await prisma.customer.create({ data: validatedData.customer }); } const invoice = await prisma.invoice.create({ data: { ...validatedData, customerId: customer.id, items: { create: validatedData.items } } }); if (validatedData.shouldSendECF) { await queueECFSending(invoice.id); } return { success: true, invoice }; } catch (error) { return { success: false, error: error.message }; } }

♿ Estándares de Accesibilidad (UI/UX)

1. Labels y Aria Attributes

// ✅ CORRECTO: Labels explícitos y aria attributes export function UserForm() { return ( <Form {...form}> <form onSubmit={form.handleSubmit(handleSubmit)}> <FormField control={form.control} name="name" render={({ field }) => ( <FormItem> <FormLabel htmlFor="user-name">Nombre Completo</FormLabel> <FormControl> <Input id="user-name" placeholder="Ingrese su nombre completo" aria-describedby="name-help" {...field} /> </FormControl> <FormDescription id="name-help"> Ingrese su nombre completo como aparece en documentos oficiales </FormDescription> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel htmlFor="user-email">Correo Electrónico</FormLabel> <FormControl> <Input id="user-email" type="email" placeholder="usuario@ejemplo.com" aria-describedby="email-help" {...field} /> </FormControl> <FormDescription id="email-help"> Este correo se utilizará para notificaciones del sistema </FormDescription> <FormMessage /> </FormItem> )} /> </form> </Form> ); }

2. Focus Management y Keyboard Navigation

// ✅ CORRECTO: Manejo de focus y navegación por teclado export function ConfirmationDialog({ isOpen, onConfirm, onCancel }: Props) { const confirmButtonRef = useRef<HTMLButtonElement>(null); useEffect(() => { if (isOpen && confirmButtonRef.current) { confirmButtonRef.current.focus(); } }, [isOpen]); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { onCancel(); } }; return ( <Dialog open={isOpen} onOpenChange={onCancel}> <DialogContent onKeyDown={handleKeyDown}> <DialogHeader> <DialogTitle>Confirmar Acción</DialogTitle> <DialogDescription> Esta acción no se puede deshacer. ¿Está seguro? </DialogDescription> </DialogHeader> <DialogFooter> <Button variant="outline" onClick={onCancel}> Cancelar </Button> <Button ref={confirmButtonRef} onClick={onConfirm} variant="destructive" > Confirmar </Button> </DialogFooter> </DialogContent> </Dialog> ); }

3. Estados de Carga y Feedback

// ✅ CORRECTO: Estados de carga y feedback no bloqueantes export function SubmitButton({ children, loading, ...props }: SubmitButtonProps) { return ( <Button type="submit" disabled={loading} {...props} > {loading ? ( <> <Loader2 className="mr-2 h-4 w-4 animate-spin" /> Procesando... </> ) : ( children )} </Button> ); } // ✅ CORRECTO: Toasts para feedback no bloqueante export function UserModal({ user, roles }: UserModalProps) { const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (values: UserFormData) => { setIsSubmitting(true); try { if (user) { await updateUser(user.id, values); toast.success('Usuario actualizado exitosamente'); } else { await createUser(values); toast.success('Usuario creado exitosamente'); } onClose(); } catch (error) { toast.error(`Error: ${error.message}`); } finally { setIsSubmitting(false); } }; return ( <Dialog> <DialogContent> <Form {...form}> <form onSubmit={form.handleSubmit(handleSubmit)}> {/* Campos del formulario */} <DialogFooter> <Button type="button" variant="outline" onClick={onClose}> Cancelar </Button> <SubmitButton loading={isSubmitting}> {user ? 'Actualizar' : 'Crear'} </SubmitButton> </DialogFooter> </form> </Form> </DialogContent> </Dialog> ); }

📁 Organización y Estructura

1. Nombres Descriptivos por Dominio/Acción

// ✅ CORRECTO: Nombres que reflejan el dominio y la acción // Archivos de acciones export async function createUser(userData: CreateUserData): Promise<User> export async function updateUser(userId: string, userData: UpdateUserData): Promise<User> export async function deleteUser(userId: string): Promise<void> export async function getUserById(userId: string): Promise<User | null> export async function getUsersWithPagination(params: PaginationParams): Promise<UsersResult> // Archivos de componentes export function UserList({ users, onUserSelect }: UserListProps) export function UserForm({ user, onSubmit, onCancel }: UserFormProps) export function UserModal({ user, roles, isOpen, onClose }: UserModalProps) export function UserRowActions({ user, onEdit, onDelete }: UserRowActionsProps) // Archivos de utilidades export function validateUserData(data: unknown): UserData export function formatUserName(user: User): string export function getUserInitials(user: User): string export function isUserActive(user: User): boolean

2. Extracción de Utilidades y Hooks

// ✅ CORRECTO: Extraer lógica reutilizable // app/lib/hooks/use-debounce.ts export function useDebounceValue<T>(value: T, delay = 300): T { const [debouncedValue, setDebouncedValue] = useState<T>(value); useEffect(() => { const timeout = setTimeout(() => { setDebouncedValue(value); }, delay); return () => clearTimeout(timeout); }, [value, delay]); return debouncedValue; } // app/lib/utils/validation.ts export function validateEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } export function validateTaxId(taxId: string): boolean { // Validación específica para RNC dominicano const taxIdRegex = /^\d{9}$/; return taxIdRegex.test(taxId); } // app/lib/utils/formatting.ts export function formatCurrency(amount: number, currency = 'DOP'): string { return new Intl.NumberFormat('es-DO', { style: 'currency', currency, minimumFractionDigits: 2 }).format(amount); } export function formatDate(date: Date, locale = 'es-DO'): string { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'long', day: 'numeric' }).format(date); }

3. Estructura de Directorios por Módulo

# ✅ CORRECTO: Estructura modular y organizada app/ ├── (dashboard)/ ├── invoices/ # Módulo de facturas ├── page.tsx # Página principal ├── actions.ts # Acciones del módulo ├── constants.ts # Constantes específicas ├── types.ts # Tipos del módulo ├── components/ # Componentes específicos ├── create-invoice-dialog.tsx ├── invoice-row-actions.tsx └── invoices-filter.tsx └── hooks/ # Hooks específicos └── use-invoice-filters.ts ├── customers/ # Módulo de clientes ├── page.tsx ├── actions.ts ├── components/ └── hooks/ └── settings/ # Módulo de configuraciones ├── general/ ├── users/ └── roles/ ├── components/ ├── ui/ # Componentes base ├── forms/ # Componentes de formularios └── shared/ # Componentes compartidos └── lib/ ├── auth/ # Autenticación ├── ecf/ # Facturación electrónica ├── queue/ # Colas de trabajo ├── storage/ # Almacenamiento ├── utils/ # Utilidades generales └── hooks/ # Hooks compartidos

🚀 Mejores Prácticas

1. Performance y Optimización

// ✅ CORRECTO: Memoización inteligente export function AdvancedFilters({ columns, onFilter }: AdvancedFiltersProps) { const [filterState, setFilterState] = useState<FilterRule[]>([]); // Memoizar agrupación de columnas para evitar recálculos const groupedColumns = useMemo(() => { return columns.reduce((acc, column) => { acc[column.key] = column; return acc; }, {} as Record<string, Column>); }, [columns]); // Memoizar validación de filtros const isValidFilter = useMemo(() => { return filterState.every(rule => rule.field && rule.operator && rule.value ); }, [filterState]); // Callback estable para evitar re-renders const handleFilterChange = useCallback((index: number, field: keyof FilterRule, value: any) => { setFilterState(prev => prev.map((rule, i) => i === index ? { ...rule, [field]: value } : rule )); }, []); return ( <div> {/* Renderizado de filtros */} <Button disabled={!isValidFilter} onClick={() => onFilter(filterState)} > Aplicar Filtros </Button> </div> ); } // ❌ INCORRECTO: Sin memoización export function BadAdvancedFilters({ columns, onFilter }: AdvancedFiltersProps) { const [filterState, setFilterState] = useState<FilterRule[]>([]); // Se recalcula en cada render const groupedColumns = columns.reduce((acc, column) => { acc[column.key] = column; return acc; }, {} as Record<string, Column>); // Se valida en cada render const isValidFilter = filterState.every(rule => rule.field && rule.operator && rule.value ); // Nueva función en cada render const handleFilterChange = (index: number, field: keyof FilterRule, value: any) => { setFilterState(prev => prev.map((rule, i) => i === index ? { ...rule, [field]: value } : rule )); }; return ( <div> {/* Renderizado de filtros */} <Button disabled={!isValidFilter} onClick={() => onFilter(filterState)} > Aplicar Filtros </Button> </div> ); }

2. Manejo de Errores y Logging

// ✅ CORRECTO: Logging estructurado y manejo de errores export async function processECFDocument(encf: string) { const startTime = Date.now(); const logContext = { encf, startTime }; console.log('[ECF] Starting document processing', logContext); try { const document = await getDocument(encf); if (!document) { throw new Error(`Document ${encf} not found`); } const result = await sendToDGII(document); const duration = Date.now() - startTime; console.log('[ECF] Document processed successfully', { ...logContext, duration, result: { trackId: result.trackId, status: result.status } }); return result; } catch (error) { const duration = Date.now() - startTime; console.error('[ECF] Document processing failed', { ...logContext, duration, error: { message: error.message, stack: error.stack, name: error.name } }); // Re-lanzar error con contexto adicional throw new Error(`Failed to process ECF document ${encf}: ${error.message}`); } } // ❌ INCORRECTO: Logging básico y manejo de errores pobre export async function processECFDocument(encf: string) { try { const document = await getDocument(encf); const result = await sendToDGII(document); return result; } catch (error) { console.error('Error:', error); throw error; } }

3. Testing y Validación

// ✅ CORRECTO: Validación con Zod y manejo de tipos const userSchema = z.object({ name: z.string() .min(1, 'El nombre es obligatorio') .min(2, 'El nombre debe tener al menos 2 caracteres') .max(50, 'El nombre no puede exceder 50 caracteres'), email: z.string() .email('Correo electrónico no válido') .min(1, 'El correo es obligatorio'), password: z.string() .min(6, 'La contraseña debe tener al menos 6 caracteres') .regex(/[A-Z]/, 'Debe contener al menos una mayúscula') .regex(/[0-9]/, 'Debe contener al menos un número') .optional(), roleId: z.string() .nonempty('El rol es obligatorio') .cuid('ID de rol inválido') }); export async function createUser(userData: unknown) { // Validar datos de entrada const validatedData = userSchema.parse(userData); // Verificar que el rol existe const role = await prisma.role.findUnique({ where: { id: validatedData.roleId } }); if (!role) { throw new Error('Rol especificado no existe'); } // Crear usuario con datos validados const user = await prisma.user.create({ data: { name: validatedData.name, email: validatedData.email, password: validatedData.password ? await hashPassword(validatedData.password) : null, roleId: validatedData.roleId } }); return user; }

🔧 Scripts y Comandos

Scripts de Desarrollo

# Linting y formateo pnpm lint # Ejecutar ESLint pnpm lint:fix # Ejecutar ESLint con auto-fix # TypeScript pnpm tsc --noEmit # Verificar tipos sin generar archivos pnpm tsc --build # Build de TypeScript # Base de datos pnpm prisma generate # Generar cliente de Prisma pnpm prisma migrate dev # Ejecutar migraciones pnpm prisma db seed # Ejecutar seed de base de datos pnpm prisma studio # Abrir Prisma Studio # Desarrollo pnpm dev # Servidor de desarrollo con Turbopack pnpm build # Build de producción pnpm start # Servidor de producción

Configuración de Pre-commit Hooks

// .husky/pre-commit #!/bin/sh . "$(dirname "$0")/_/husky.sh" # Linting automático pnpm lint:fix # Verificación de tipos pnpm tsc --noEmit # Formateo automático pnpm format

🔍 Troubleshooting

Problemas Comunes

1. Errores de ESLint

# Verificar configuración npx eslint --print-config app/components/ui/button.tsx # Verificar reglas específicas npx eslint --rule 'simple-import-sort/imports: error' app/components/ui/button.tsx # Auto-fix de problemas pnpm lint:fix

2. Errores de TypeScript

# Verificar tipos sin generar archivos pnpm tsc --noEmit # Verificar archivo específico npx tsc --noEmit app/components/ui/button.tsx # Verificar configuración npx tsc --showConfig

3. Problemas de Formateo

# Verificar configuración de Prettier npx prettier --config app/package.json --check . # Formatear archivos npx prettier --write . # Verificar archivo específico npx prettier --check app/components/ui/button.tsx

Debug de Estándares

// Función de debug para estándares de código export function debugCodingStandards() { console.log('=== UNIMAST ERP Coding Standards Debug ==='); // Configuración de ESLint console.log('ESLint Configuration:'); console.log('- Next.js config:', 'enabled'); console.log('- Simple import sort:', 'enabled'); console.log('- Strict rules:', 'enabled'); // Configuración de TypeScript console.log('\nTypeScript Configuration:'); console.log('- Strict mode:', 'enabled'); console.log('- Target:', 'ES5'); console.log('- Module resolution:', 'node'); // Configuración de Prettier console.log('\nPrettier Configuration:'); console.log('- Single quotes:', 'enabled'); console.log('- Tab width:', '2 spaces'); console.log('- Trailing comma:', 'disabled'); console.log('- Semicolons:', 'enabled'); // Estándares del proyecto console.log('\nProject Standards:'); console.log('- File naming:', 'kebab-case'); console.log('- Component naming:', 'PascalCase'); console.log('- Function naming:', 'camelCase'); console.log('- Constant naming:', 'UPPER_SNAKE_CASE'); }

¿Necesitas más detalles sobre algún aspecto específico? Revisa la documentación de environment-and-config para entender cómo se configuran las herramientas de calidad de código, o la documentación de UI y formularios para ver ejemplos de implementación de estándares de accesibilidad.