🚀 Oferta especial: 60% OFF no CrazyStack - Últimas vagas!Garantir vaga →
Módulo 3 - Aula 2

Mutations Complexas

Domine mutations complexas no tRPC: transações atômicas, validações avançadas, upload de arquivos e operações em lote para SaaS profissionais.

80 min
Avançado
Mutations

🎯 Por que mutations complexas são essenciais em SaaS?

Integridade dos Dados: Transações garantem que operações complexas sejam atômicas e consistentes.

Experiência do Usuário: Operações em lote e validações robustas melhoram a UX significativamente.

🔄 Transações Atômicas

src/server/trpc/routers/order.ts
// 📁 src/server/trpc/routers/order.ts
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { router, protectedProcedure } from '../trpc';

export const orderRouter = router({
  // 🛒 Criar pedido com transação complexa
  createOrder: protectedProcedure
    .input(z.object({
      items: z.array(z.object({
        productId: z.string(),
        quantity: z.number().min(1),
        price: z.number().min(0),
      })),
      shippingAddress: z.object({
        street: z.string(),
        city: z.string(),
        state: z.string(),
        zipCode: z.string(),
        country: z.string(),
      }),
      paymentMethod: z.enum(['CREDIT_CARD', 'PIX', 'BOLETO']),
      couponCode: z.string().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { items, shippingAddress, paymentMethod, couponCode } = input;
      const userId = ctx.session.user.id;

      // 🔄 Executar tudo em uma transação atômica
      const result = await ctx.prisma.$transaction(async (tx) => {
        // 1️⃣ Validar estoque de todos os produtos
        const productIds = items.map(item => item.productId);
        const products = await tx.product.findMany({
          where: { id: { in: productIds } },
          select: { id: true, stockQuantity: true, price: true, name: true },
        });

        // 🔍 Verificar se todos os produtos existem
        if (products.length !== productIds.length) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Um ou mais produtos não foram encontrados',
          });
        }

        // 📊 Validar estoque e preços
        const stockIssues: string[] = [];
        const priceIssues: string[] = [];

        for (const item of items) {
          const product = products.find(p => p.id === item.productId);
          if (!product) continue;

          // 📦 Verificar estoque
          if (product.stockQuantity < item.quantity) {
            stockIssues.push(`${product.name}: estoque insuficiente`);
          }

          // 💰 Verificar preço (proteção contra race conditions)
          if (Math.abs(product.price - item.price) > 0.01) {
            priceIssues.push(`${product.name}: preço alterado`);
          }
        }

        if (stockIssues.length > 0) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Estoque insuficiente: ' + stockIssues.join(', '),
          });
        }

        if (priceIssues.length > 0) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Preços desatualizados: ' + priceIssues.join(', '),
          });
        }

        // 2️⃣ Aplicar cupom de desconto se fornecido
        let discount = 0;
        let appliedCoupon = null;

        if (couponCode) {
          const coupon = await tx.coupon.findUnique({
            where: { code: couponCode },
            include: { usedBy: true },
          });

          if (!coupon) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: 'Cupom inválido',
            });
          }

          // 🗓️ Verificar validade
          if (coupon.expiresAt && coupon.expiresAt < new Date()) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: 'Cupom expirado',
            });
          }

          // 📊 Verificar limite de uso
          if (coupon.maxUses && coupon.usedBy.length >= coupon.maxUses) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: 'Cupom esgotado',
            });
          }

          // 👤 Verificar se usuário já usou (se for cupom único)
          if (coupon.onePerUser && coupon.usedBy.some(u => u.userId === userId)) {
            throw new TRPCError({
              code: 'BAD_REQUEST',
              message: 'Cupom já utilizado',
            });
          }

          discount = coupon.discountPercent || 0;
          appliedCoupon = coupon;
        }

        // 3️⃣ Calcular totais
        const subtotal = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
        const discountAmount = subtotal * (discount / 100);
        const shippingCost = 15.99; // Poderia ser calculado dinamicamente
        const total = subtotal - discountAmount + shippingCost;

        // 4️⃣ Criar pedido
        const order = await tx.order.create({
          data: {
            userId,
            status: 'PENDING',
            subtotal,
            discountAmount,
            shippingCost,
            total,
            paymentMethod,
            shippingAddress: {
              create: shippingAddress,
            },
            items: {
              create: items.map(item => ({
                productId: item.productId,
                quantity: item.quantity,
                price: item.price,
                total: item.price * item.quantity,
              })),
            },
            ...(appliedCoupon && {
              coupon: {
                connect: { id: appliedCoupon.id },
              },
            }),
          },
          include: {
            items: {
              include: {
                product: {
                  select: { name: true, image: true },
                },
              },
            },
            shippingAddress: true,
            coupon: true,
          },
        });

        // 5️⃣ Atualizar estoque dos produtos
        await Promise.all(
          items.map(item =>
            tx.product.update({
              where: { id: item.productId },
              data: {
                stockQuantity: {
                  decrement: item.quantity,
                },
              },
            })
          )
        );

        // 6️⃣ Registrar uso do cupom
        if (appliedCoupon) {
          await tx.couponUsage.create({
            data: {
              couponId: appliedCoupon.id,
              userId,
              orderId: order.id,
            },
          });
        }

        // 7️⃣ Criar registro de auditoria
        await tx.auditLog.create({
          data: {
            userId,
            action: 'ORDER_CREATED',
            resource: 'ORDER',
            resourceId: order.id,
            metadata: {
              orderTotal: total,
              itemCount: items.length,
              couponUsed: couponCode || null,
            },
          },
        });

        return order;
      });

      // 🔔 Dispara eventos pós-transação
      await Promise.all([
        // 📧 Enviar email de confirmação
        ctx.emailService.sendOrderConfirmation(result.id),
        
        // 📊 Atualizar analytics
        ctx.analytics.trackEvent('order_created', {
          userId,
          orderId: result.id,
          total: result.total,
        }),
        
        // 📱 Notificar vendedor
        ctx.notificationService.notifyNewOrder(result.id),
      ]);

      return result;
    }),

  // 🔄 Cancelar pedido com rollback
  cancelOrder: protectedProcedure
    .input(z.object({
      orderId: z.string(),
      reason: z.string().min(10),
    }))
    .mutation(async ({ input, ctx }) => {
      const { orderId, reason } = input;
      const userId = ctx.session.user.id;

      const result = await ctx.prisma.$transaction(async (tx) => {
        // 1️⃣ Buscar pedido
        const order = await tx.order.findUnique({
          where: { id: orderId },
          include: { items: true, coupon: true },
        });

        if (!order) {
          throw new TRPCError({
            code: 'NOT_FOUND',
            message: 'Pedido não encontrado',
          });
        }

        // 🔒 Verificar permissão
        if (order.userId !== userId) {
          throw new TRPCError({
            code: 'FORBIDDEN',
            message: 'Você não tem permissão para cancelar este pedido',
          });
        }

        // 📊 Verificar se pode cancelar
        if (order.status === 'CANCELLED') {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Pedido já cancelado',
          });
        }

        if (order.status === 'DELIVERED') {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Não é possível cancelar pedido já entregue',
          });
        }

        // 2️⃣ Restaurar estoque
        await Promise.all(
          order.items.map(item =>
            tx.product.update({
              where: { id: item.productId },
              data: {
                stockQuantity: {
                  increment: item.quantity,
                },
              },
            })
          )
        );

        // 3️⃣ Reverter uso do cupom
        if (order.coupon) {
          await tx.couponUsage.deleteMany({
            where: {
              couponId: order.coupon.id,
              userId,
              orderId,
            },
          });
        }

        // 4️⃣ Atualizar pedido
        const updatedOrder = await tx.order.update({
          where: { id: orderId },
          data: {
            status: 'CANCELLED',
            cancelledAt: new Date(),
            cancellationReason: reason,
          },
          include: {
            items: {
              include: {
                product: {
                  select: { name: true },
                },
              },
            },
          },
        });

        // 5️⃣ Auditoria
        await tx.auditLog.create({
          data: {
            userId,
            action: 'ORDER_CANCELLED',
            resource: 'ORDER',
            resourceId: orderId,
            metadata: {
              reason,
              refundAmount: order.total,
            },
          },
        });

        return updatedOrder;
      });

      return result;
    }),
});

🔍 Validações Avançadas com Zod

src/server/trpc/validations/advanced.ts
// 📁 src/server/trpc/validations/advanced.ts
import { z } from 'zod';

// 🎯 Validação de CPF/CNPJ
const cpfRegex = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/;
const cnpjRegex = /^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/;

export const cpfSchema = z.string()
  .regex(cpfRegex, 'CPF deve ter formato: 000.000.000-00')
  .refine(validateCPF, 'CPF inválido');

export const cnpjSchema = z.string()
  .regex(cnpjRegex, 'CNPJ deve ter formato: 00.000.000/0000-00')
  .refine(validateCNPJ, 'CNPJ inválido');

// 📧 Validação de email com domínios permitidos
export const businessEmailSchema = z.string()
  .email('Email inválido')
  .refine(
    (email) => {
      const blockedDomains = ['tempmail.com', '10minutemail.com', 'guerrillamail.com'];
      const domain = email.split('@')[1];
      return !blockedDomains.includes(domain);
    },
    'Email temporário não é permitido'
  );

// 🔐 Validação de senha forte
export const strongPasswordSchema = z.string()
  .min(8, 'Senha deve ter pelo menos 8 caracteres')
  .regex(/[a-z]/, 'Senha deve conter pelo menos uma letra minúscula')
  .regex(/[A-Z]/, 'Senha deve conter pelo menos uma letra maiúscula')
  .regex(/[0-9]/, 'Senha deve conter pelo menos um número')
  .regex(/[^A-Za-z0-9]/, 'Senha deve conter pelo menos um caractere especial');

// 📱 Validação de telefone brasileiro
export const phoneSchema = z.string()
  .regex(/^\(\d{2}\) \d{4,5}-\d{4}$/, 'Formato: (11) 99999-9999')
  .refine(validateBrazilianPhone, 'Telefone inválido');

// 💳 Validação de cartão de crédito
export const creditCardSchema = z.object({
  number: z.string()
    .regex(/^\d{4} \d{4} \d{4} \d{4}$/, 'Formato: 0000 0000 0000 0000')
    .refine(validateCreditCard, 'Número do cartão inválido'),
  
  expiryDate: z.string()
    .regex(/^\d{2}\/\d{2}$/, 'Formato: MM/AA')
    .refine(validateExpiryDate, 'Data de expiração inválida'),
  
  cvv: z.string()
    .regex(/^\d{3,4}$/, 'CVV deve ter 3 ou 4 dígitos'),
  
  holderName: z.string()
    .min(2, 'Nome deve ter pelo menos 2 caracteres')
    .max(50, 'Nome deve ter no máximo 50 caracteres')
    .regex(/^[a-zA-ZÀ-ÿ\s]+$/, 'Nome deve conter apenas letras e espaços'),
});

// 🏢 Validação de endereço brasileiro
export const addressSchema = z.object({
  zipCode: z.string()
    .regex(/^\d{5}-\d{3}$/, 'CEP deve ter formato: 00000-000')
    .refine(async (cep) => {
      // 🌐 Validar CEP via API dos Correios
      try {
        const response = await fetch(`https://viacep.com.br/ws/${cep.replace('-', '')}/json/`);
        const data = await response.json();
        return !data.erro;
      } catch {
        return false;
      }
    }, 'CEP não encontrado'),
  
  street: z.string().min(5, 'Rua deve ter pelo menos 5 caracteres'),
  number: z.string().min(1, 'Número é obrigatório'),
  complement: z.string().optional(),
  neighborhood: z.string().min(2, 'Bairro é obrigatório'),
  city: z.string().min(2, 'Cidade é obrigatória'),
  state: z.string().length(2, 'Estado deve ter 2 caracteres'),
});

// 📊 Validação de dados de produto
export const productSchema = z.object({
  name: z.string()
    .min(3, 'Nome deve ter pelo menos 3 caracteres')
    .max(100, 'Nome deve ter no máximo 100 caracteres'),
  
  description: z.string()
    .min(10, 'Descrição deve ter pelo menos 10 caracteres')
    .max(5000, 'Descrição deve ter no máximo 5000 caracteres'),
  
  price: z.number()
    .positive('Preço deve ser positivo')
    .max(999999.99, 'Preço máximo é R$ 999.999,99')
    .multipleOf(0.01, 'Preço deve ter no máximo 2 casas decimais'),
  
  weight: z.number()
    .positive('Peso deve ser positivo')
    .max(50, 'Peso máximo é 50kg'),
  
  dimensions: z.object({
    length: z.number().positive().max(200, 'Comprimento máximo é 200cm'),
    width: z.number().positive().max(200, 'Largura máxima é 200cm'),
    height: z.number().positive().max(200, 'Altura máxima é 200cm'),
  }),
  
  category: z.string().min(1, 'Categoria é obrigatória'),
  
  images: z.array(z.string().url('URL de imagem inválida'))
    .min(1, 'Pelo menos uma imagem é obrigatória')
    .max(10, 'Máximo 10 imagens por produto'),
  
  attributes: z.record(z.string(), z.any()).optional(),
  
  seoTitle: z.string()
    .min(10, 'Título SEO deve ter pelo menos 10 caracteres')
    .max(60, 'Título SEO deve ter no máximo 60 caracteres'),
  
  seoDescription: z.string()
    .min(50, 'Descrição SEO deve ter pelo menos 50 caracteres')
    .max(160, 'Descrição SEO deve ter no máximo 160 caracteres'),
});

// 🔄 Validação de operação em lote
export const batchOperationSchema = z.object({
  operation: z.enum(['CREATE', 'UPDATE', 'DELETE']),
  
  items: z.array(z.object({
    id: z.string().optional(),
    data: z.record(z.string(), z.any()),
  }))
    .min(1, 'Pelo menos um item é obrigatório')
    .max(100, 'Máximo 100 itens por operação'),
  
  options: z.object({
    skipValidation: z.boolean().default(false),
    continueOnError: z.boolean().default(false),
    dryRun: z.boolean().default(false),
  }).optional(),
});

// 🎨 Validação de upload de arquivo
export const fileUploadSchema = z.object({
  filename: z.string()
    .min(1, 'Nome do arquivo é obrigatório')
    .regex(/^[a-zA-Z0-9._-]+$/, 'Nome de arquivo inválido'),
  
  mimeType: z.string()
    .refine(
      (type) => {
        const allowedTypes = [
          'image/jpeg',
          'image/png',
          'image/webp',
          'image/gif',
          'application/pdf',
          'text/csv',
          'application/vnd.ms-excel',
          'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        ];
        return allowedTypes.includes(type);
      },
      'Tipo de arquivo não permitido'
    ),
  
  size: z.number()
    .positive('Tamanho do arquivo deve ser positivo')
    .max(10 * 1024 * 1024, 'Arquivo deve ter no máximo 10MB'), // 10MB
  
  base64: z.string()
    .refine(
      (data) => {
        try {
          return Buffer.from(data, 'base64').toString('base64') === data;
        } catch {
          return false;
        }
      },
      'Dados base64 inválidos'
    ),
});

// 🧮 Funções de validação personalizadas
function validateCPF(cpf: string): boolean {
  const numbers = cpf.replace(/[^\d]/g, '');
  
  if (numbers.length !== 11) return false;
  if (/^(\d)\1{10}$/.test(numbers)) return false;
  
  // Algoritmo de validação do CPF
  let sum = 0;
  for (let i = 0; i < 9; i++) {
    sum += parseInt(numbers[i]) * (10 - i);
  }
  let remainder = (sum * 10) % 11;
  if (remainder === 10 || remainder === 11) remainder = 0;
  if (remainder !== parseInt(numbers[9])) return false;
  
  sum = 0;
  for (let i = 0; i < 10; i++) {
    sum += parseInt(numbers[i]) * (11 - i);
  }
  remainder = (sum * 10) % 11;
  if (remainder === 10 || remainder === 11) remainder = 0;
  if (remainder !== parseInt(numbers[10])) return false;
  
  return true;
}

function validateCNPJ(cnpj: string): boolean {
  const numbers = cnpj.replace(/[^\d]/g, '');
  
  if (numbers.length !== 14) return false;
  if (/^(\d)\1{13}$/.test(numbers)) return false;
  
  // Algoritmo de validação do CNPJ
  const weights1 = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
  const weights2 = [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2];
  
  let sum = 0;
  for (let i = 0; i < 12; i++) {
    sum += parseInt(numbers[i]) * weights1[i];
  }
  let remainder = sum % 11;
  const digit1 = remainder < 2 ? 0 : 11 - remainder;
  
  if (digit1 !== parseInt(numbers[12])) return false;
  
  sum = 0;
  for (let i = 0; i < 13; i++) {
    sum += parseInt(numbers[i]) * weights2[i];
  }
  remainder = sum % 11;
  const digit2 = remainder < 2 ? 0 : 11 - remainder;
  
  return digit2 === parseInt(numbers[13]);
}

function validateBrazilianPhone(phone: string): boolean {
  const numbers = phone.replace(/[^\d]/g, '');
  
  // Celular: 11 dígitos (DDD + 9 + 8 dígitos)
  // Fixo: 10 dígitos (DDD + 8 dígitos)
  if (numbers.length !== 10 && numbers.length !== 11) return false;
  
  const ddd = parseInt(numbers.substring(0, 2));
  const validDDDs = [11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 22, 24, 27, 28, 31, 32, 33, 34, 35, 37, 38, 41, 42, 43, 44, 45, 46, 47, 48, 49, 51, 53, 54, 55, 61, 62, 63, 64, 65, 66, 67, 68, 69, 71, 73, 74, 75, 77, 79, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 93, 94, 95, 96, 97, 98, 99];
  
  return validDDDs.includes(ddd);
}

function validateCreditCard(number: string): boolean {
  const numbers = number.replace(/[^\d]/g, '');
  
  // Algoritmo de Luhn
  let sum = 0;
  let isEven = false;
  
  for (let i = numbers.length - 1; i >= 0; i--) {
    let digit = parseInt(numbers[i]);
    
    if (isEven) {
      digit *= 2;
      if (digit > 9) {
        digit -= 9;
      }
    }
    
    sum += digit;
    isEven = !isEven;
  }
  
  return sum % 10 === 0;
}

function validateExpiryDate(date: string): boolean {
  const [month, year] = date.split('/');
  const currentDate = new Date();
  const currentYear = currentDate.getFullYear() % 100;
  const currentMonth = currentDate.getMonth() + 1;
  
  const monthNum = parseInt(month);
  const yearNum = parseInt(year);
  
  if (monthNum < 1 || monthNum > 12) return false;
  if (yearNum < currentYear) return false;
  if (yearNum === currentYear && monthNum < currentMonth) return false;
  
  return true;
}

📁 Upload de Arquivos

src/server/trpc/routers/upload.ts
// 📁 src/server/trpc/routers/upload.ts
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { router, protectedProcedure } from '../trpc';
import { fileUploadSchema } from '../validations/advanced';
import { uploadToS3, deleteFromS3 } from '@/lib/aws-s3';
import { scanForVirus } from '@/lib/virus-scanner';

export const uploadRouter = router({
  // 📤 Upload de arquivo único
  uploadFile: protectedProcedure
    .input(fileUploadSchema.extend({
      folder: z.enum(['products', 'avatars', 'documents', 'imports']),
      isPublic: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      const { filename, mimeType, size, base64, folder, isPublic } = input;
      const userId = ctx.session.user.id;

      try {
        // 🔍 Verificar quota do usuário
        const userQuota = await ctx.prisma.userQuota.findUnique({
          where: { userId },
        });

        if (userQuota && userQuota.storageUsed + size > userQuota.storageLimit) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Quota de armazenamento excedida',
          });
        }

        // 🦠 Verificar vírus no arquivo
        const buffer = Buffer.from(base64, 'base64');
        const isClean = await scanForVirus(buffer);
        
        if (!isClean) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Arquivo contém vírus ou malware',
          });
        }

        // 📊 Gerar nome único para o arquivo
        const timestamp = Date.now();
        const randomString = Math.random().toString(36).substring(2, 15);
        const extension = filename.split('.').pop();
        const uniqueFilename = `${timestamp}-${randomString}.${extension}`;

        // 🗂️ Definir caminho no S3
        const s3Key = `${folder}/${userId}/${uniqueFilename}`;

        // ☁️ Upload para S3
        const uploadResult = await uploadToS3({
          key: s3Key,
          buffer,
          contentType: mimeType,
          isPublic,
        });

        // 💾 Salvar no banco de dados
        const fileRecord = await ctx.prisma.file.create({
          data: {
            userId,
            originalName: filename,
            filename: uniqueFilename,
            mimeType,
            size,
            s3Key,
            s3Url: uploadResult.url,
            folder,
            isPublic,
            metadata: {
              uploadedAt: new Date(),
              userAgent: ctx.req.headers['user-agent'],
              ip: ctx.ip,
            },
          },
        });

        // 📊 Atualizar quota do usuário
        if (userQuota) {
          await ctx.prisma.userQuota.update({
            where: { userId },
            data: {
              storageUsed: {
                increment: size,
              },
            },
          });
        }

        // 📝 Log de auditoria
        await ctx.prisma.auditLog.create({
          data: {
            userId,
            action: 'FILE_UPLOADED',
            resource: 'FILE',
            resourceId: fileRecord.id,
            metadata: {
              filename: filename,
              size: size,
              mimeType: mimeType,
              folder: folder,
            },
          },
        });

        return {
          id: fileRecord.id,
          url: uploadResult.url,
          filename: uniqueFilename,
          originalName: filename,
          size,
          mimeType,
        };
      } catch (error) {
        if (error instanceof TRPCError) {
          throw error;
        }

        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Erro ao fazer upload do arquivo',
        });
      }
    }),

  // 📤 Upload múltiplo de arquivos
  uploadMultiple: protectedProcedure
    .input(z.object({
      files: z.array(fileUploadSchema).min(1).max(10),
      folder: z.enum(['products', 'avatars', 'documents', 'imports']),
      isPublic: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      const { files, folder, isPublic } = input;
      const userId = ctx.session.user.id;

      // 📊 Verificar quota total
      const totalSize = files.reduce((sum, file) => sum + file.size, 0);
      const userQuota = await ctx.prisma.userQuota.findUnique({
        where: { userId },
      });

      if (userQuota && userQuota.storageUsed + totalSize > userQuota.storageLimit) {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Quota de armazenamento excedida',
        });
      }

      // 🔄 Processar uploads em paralelo (máximo 3 simultâneos)
      const results = [];
      const chunkSize = 3;
      
      for (let i = 0; i < files.length; i += chunkSize) {
        const chunk = files.slice(i, i + chunkSize);
        
        const chunkResults = await Promise.all(
          chunk.map(async (file) => {
            // 🦠 Verificar vírus
            const buffer = Buffer.from(file.base64, 'base64');
            const isClean = await scanForVirus(buffer);
            
            if (!isClean) {
              return {
                filename: file.filename,
                error: 'Arquivo contém vírus ou malware',
              };
            }

            try {
              // 📊 Gerar nome único
              const timestamp = Date.now();
              const randomString = Math.random().toString(36).substring(2, 15);
              const extension = file.filename.split('.').pop();
              const uniqueFilename = `${timestamp}-${randomString}.${extension}`;
              const s3Key = `${folder}/${userId}/${uniqueFilename}`;

              // ☁️ Upload para S3
              const uploadResult = await uploadToS3({
                key: s3Key,
                buffer,
                contentType: file.mimeType,
                isPublic,
              });

              // 💾 Salvar no banco
              const fileRecord = await ctx.prisma.file.create({
                data: {
                  userId,
                  originalName: file.filename,
                  filename: uniqueFilename,
                  mimeType: file.mimeType,
                  size: file.size,
                  s3Key,
                  s3Url: uploadResult.url,
                  folder,
                  isPublic,
                  metadata: {
                    uploadedAt: new Date(),
                    batchUpload: true,
                  },
                },
              });

              return {
                id: fileRecord.id,
                url: uploadResult.url,
                filename: uniqueFilename,
                originalName: file.filename,
                size: file.size,
                mimeType: file.mimeType,
              };
            } catch (error) {
              return {
                filename: file.filename,
                error: 'Erro ao fazer upload',
              };
            }
          })
        );

        results.push(...chunkResults);
      }

      // 📊 Atualizar quota apenas para uploads bem-sucedidos
      const successfulUploads = results.filter(r => !r.error);
      const successfulSize = successfulUploads.reduce((sum, r) => sum + (r.size || 0), 0);

      if (successfulSize > 0 && userQuota) {
        await ctx.prisma.userQuota.update({
          where: { userId },
          data: {
            storageUsed: {
              increment: successfulSize,
            },
          },
        });
      }

      return {
        successful: successfulUploads.length,
        failed: results.filter(r => r.error).length,
        results,
      };
    }),

  // 🗑️ Deletar arquivo
  deleteFile: protectedProcedure
    .input(z.object({
      fileId: z.string(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { fileId } = input;
      const userId = ctx.session.user.id;

      const file = await ctx.prisma.file.findUnique({
        where: { id: fileId },
      });

      if (!file) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Arquivo não encontrado',
        });
      }

      // 🔒 Verificar permissão
      if (file.userId !== userId) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Você não tem permissão para deletar este arquivo',
        });
      }

      try {
        // ☁️ Deletar do S3
        await deleteFromS3(file.s3Key);

        // 💾 Deletar do banco
        await ctx.prisma.file.delete({
          where: { id: fileId },
        });

        // 📊 Atualizar quota do usuário
        await ctx.prisma.userQuota.update({
          where: { userId },
          data: {
            storageUsed: {
              decrement: file.size,
            },
          },
        });

        // 📝 Log de auditoria
        await ctx.prisma.auditLog.create({
          data: {
            userId,
            action: 'FILE_DELETED',
            resource: 'FILE',
            resourceId: fileId,
            metadata: {
              filename: file.originalName,
              size: file.size,
            },
          },
        });

        return { success: true };
      } catch (error) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Erro ao deletar arquivo',
        });
      }
    }),

  // 📋 Listar arquivos do usuário
  getFiles: protectedProcedure
    .input(z.object({
      folder: z.enum(['products', 'avatars', 'documents', 'imports']).optional(),
      limit: z.number().min(1).max(100).default(20),
      cursor: z.string().optional(),
    }))
    .query(async ({ input, ctx }) => {
      const { folder, limit, cursor } = input;
      const userId = ctx.session.user.id;

      const files = await ctx.prisma.file.findMany({
        where: {
          userId,
          ...(folder && { folder }),
        },
        take: limit + 1,
        cursor: cursor ? { id: cursor } : undefined,
        orderBy: { createdAt: 'desc' },
        select: {
          id: true,
          originalName: true,
          filename: true,
          mimeType: true,
          size: true,
          s3Url: true,
          folder: true,
          isPublic: true,
          createdAt: true,
        },
      });

      let nextCursor: string | undefined;
      if (files.length > limit) {
        const nextItem = files.pop();
        nextCursor = nextItem!.id;
      }

      return {
        files,
        nextCursor,
        hasMore: !!nextCursor,
      };
    }),
});

📦 Operações em Lote

src/server/trpc/routers/batch.ts
// 📁 src/server/trpc/routers/batch.ts
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { router, protectedProcedure } from '../trpc';
import { batchOperationSchema } from '../validations/advanced';

export const batchRouter = router({
  // 📦 Operação em lote para produtos
  batchProducts: protectedProcedure
    .input(batchOperationSchema.extend({
      resourceType: z.literal('product'),
    }))
    .mutation(async ({ input, ctx }) => {
      const { operation, items, options = {} } = input;
      const userId = ctx.session.user.id;

      // 🔒 Verificar permissões
      const user = await ctx.prisma.user.findUnique({
        where: { id: userId },
        select: { role: true },
      });

      if (!user || user.role !== 'ADMIN') {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: 'Apenas administradores podem executar operações em lote',
        });
      }

      const results = {
        successful: 0,
        failed: 0,
        errors: [] as Array<{ index: number; error: string }>,
        items: [] as Array<{ index: number; id?: string; data?: any }>,
      };

      // 🎯 Validar dados antes de processar
      if (!options.skipValidation) {
        const validationErrors = await validateBatchItems(items, operation);
        if (validationErrors.length > 0) {
          throw new TRPCError({
            code: 'BAD_REQUEST',
            message: 'Erros de validação encontrados',
            cause: validationErrors,
          });
        }
      }

      // 🔄 Modo dry run (apenas simular)
      if (options.dryRun) {
        return {
          ...results,
          message: 'Simulação executada com sucesso',
          wouldProcess: items.length,
        };
      }

      // 📊 Processar em chunks para evitar timeout
      const chunkSize = 10;
      for (let i = 0; i < items.length; i += chunkSize) {
        const chunk = items.slice(i, i + chunkSize);
        
        await Promise.all(
          chunk.map(async (item, chunkIndex) => {
            const globalIndex = i + chunkIndex;
            
            try {
              let result;
              
              switch (operation) {
                case 'CREATE':
                  result = await ctx.prisma.product.create({
                    data: {
                      ...item.data,
                      userId,
                      createdAt: new Date(),
                    },
                  });
                  break;
                
                case 'UPDATE':
                  if (!item.id) {
                    throw new Error('ID é obrigatório para operação UPDATE');
                  }
                  result = await ctx.prisma.product.update({
                    where: { id: item.id },
                    data: {
                      ...item.data,
                      updatedAt: new Date(),
                    },
                  });
                  break;
                
                case 'DELETE':
                  if (!item.id) {
                    throw new Error('ID é obrigatório para operação DELETE');
                  }
                  await ctx.prisma.product.delete({
                    where: { id: item.id },
                  });
                  result = { id: item.id, deleted: true };
                  break;
                
                default:
                  throw new Error(`Operação ${operation} não suportada`);
              }

              results.successful++;
              results.items.push({
                index: globalIndex,
                id: result.id,
                data: result,
              });
            } catch (error) {
              results.failed++;
              results.errors.push({
                index: globalIndex,
                error: error instanceof Error ? error.message : 'Erro desconhecido',
              });

              // 🚫 Parar se não deve continuar em erro
              if (!options.continueOnError) {
                throw new TRPCError({
                  code: 'INTERNAL_SERVER_ERROR',
                  message: `Erro no item ${globalIndex}: ${error instanceof Error ? error.message : 'Erro desconhecido'}`,
                });
              }
            }
          })
        );
      }

      // 📝 Log de auditoria
      await ctx.prisma.auditLog.create({
        data: {
          userId,
          action: `BATCH_${operation}`,
          resource: 'PRODUCT',
          resourceId: 'batch-operation',
          metadata: {
            itemsProcessed: items.length,
            successful: results.successful,
            failed: results.failed,
            operation,
          },
        },
      });

      return results;
    }),

  // 📊 Importar dados via CSV
  importFromCSV: protectedProcedure
    .input(z.object({
      fileId: z.string(),
      resourceType: z.enum(['product', 'customer', 'order']),
      mapping: z.record(z.string(), z.string()), // Campo CSV -> Campo DB
      options: z.object({
        skipFirstRow: z.boolean().default(true),
        validateOnly: z.boolean().default(false),
        chunkSize: z.number().min(1).max(100).default(50),
      }).optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      const { fileId, resourceType, mapping, options = {} } = input;
      const userId = ctx.session.user.id;

      // 📁 Buscar arquivo
      const file = await ctx.prisma.file.findUnique({
        where: { id: fileId },
      });

      if (!file || file.userId !== userId) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Arquivo não encontrado',
        });
      }

      if (file.mimeType !== 'text/csv') {
        throw new TRPCError({
          code: 'BAD_REQUEST',
          message: 'Arquivo deve ser CSV',
        });
      }

      try {
        // 📥 Baixar e processar CSV
        const csvData = await downloadAndParseCSV(file.s3Url);
        
        // 🔄 Aplicar mapeamento
        const mappedData = csvData.map((row, index) => {
          const mappedRow: any = {};
          
          Object.entries(mapping).forEach(([csvColumn, dbField]) => {
            if (row[csvColumn] !== undefined) {
              mappedRow[dbField] = row[csvColumn];
            }
          });
          
          return {
            index: index + (options.skipFirstRow ? 2 : 1), // +1 para linha de cabeçalho
            data: mappedRow,
          };
        });

        // ✅ Apenas validar se solicitado
        if (options.validateOnly) {
          const validationErrors = await validateImportData(mappedData, resourceType);
          
          return {
            valid: validationErrors.length === 0,
            totalRows: mappedData.length,
            errors: validationErrors,
            preview: mappedData.slice(0, 5), // Mostrar 5 primeiros
          };
        }

        // 📊 Processar importação
        const results = {
          successful: 0,
          failed: 0,
          errors: [] as Array<{ row: number; error: string }>,
        };

        const chunkSize = options.chunkSize || 50;
        
        for (let i = 0; i < mappedData.length; i += chunkSize) {
          const chunk = mappedData.slice(i, i + chunkSize);
          
          await Promise.all(
            chunk.map(async (item) => {
              try {
                switch (resourceType) {
                  case 'product':
                    await ctx.prisma.product.create({
                      data: {
                        ...item.data,
                        userId,
                      },
                    });
                    break;
                  
                  case 'customer':
                    await ctx.prisma.customer.create({
                      data: {
                        ...item.data,
                        userId,
                      },
                    });
                    break;
                  
                  case 'order':
                    await ctx.prisma.order.create({
                      data: {
                        ...item.data,
                        userId,
                      },
                    });
                    break;
                }
                
                results.successful++;
              } catch (error) {
                results.failed++;
                results.errors.push({
                  row: item.index,
                  error: error instanceof Error ? error.message : 'Erro desconhecido',
                });
              }
            })
          );
        }

        // 📝 Log de auditoria
        await ctx.prisma.auditLog.create({
          data: {
            userId,
            action: 'CSV_IMPORT',
            resource: resourceType.toUpperCase(),
            resourceId: fileId,
            metadata: {
              totalRows: mappedData.length,
              successful: results.successful,
              failed: results.failed,
              filename: file.originalName,
            },
          },
        });

        return results;
      } catch (error) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Erro ao processar importação',
        });
      }
    }),

  // 📤 Exportar dados para CSV
  exportToCSV: protectedProcedure
    .input(z.object({
      resourceType: z.enum(['product', 'customer', 'order']),
      filters: z.record(z.string(), z.any()).optional(),
      columns: z.array(z.string()).optional(),
      limit: z.number().min(1).max(10000).default(1000),
    }))
    .mutation(async ({ input, ctx }) => {
      const { resourceType, filters = {}, columns, limit } = input;
      const userId = ctx.session.user.id;

      try {
        let data;
        
        switch (resourceType) {
          case 'product':
            data = await ctx.prisma.product.findMany({
              where: {
                userId,
                ...filters,
              },
              select: columns ? 
                Object.fromEntries(columns.map(col => [col, true])) : 
                undefined,
              take: limit,
              orderBy: { createdAt: 'desc' },
            });
            break;
          
          case 'customer':
            data = await ctx.prisma.customer.findMany({
              where: {
                userId,
                ...filters,
              },
              select: columns ? 
                Object.fromEntries(columns.map(col => [col, true])) : 
                undefined,
              take: limit,
              orderBy: { createdAt: 'desc' },
            });
            break;
          
          case 'order':
            data = await ctx.prisma.order.findMany({
              where: {
                userId,
                ...filters,
              },
              select: columns ? 
                Object.fromEntries(columns.map(col => [col, true])) : 
                undefined,
              take: limit,
              orderBy: { createdAt: 'desc' },
            });
            break;
        }

        // 📊 Converter para CSV
        const csvContent = convertToCSV(data);
        
        // 📁 Salvar arquivo CSV
        const filename = `export-${resourceType}-${Date.now()}.csv`;
        const s3Key = `exports/${userId}/${filename}`;
        
        const uploadResult = await uploadToS3({
          key: s3Key,
          buffer: Buffer.from(csvContent, 'utf8'),
          contentType: 'text/csv',
          isPublic: false,
        });

        // 💾 Salvar referência do arquivo
        const fileRecord = await ctx.prisma.file.create({
          data: {
            userId,
            originalName: filename,
            filename,
            mimeType: 'text/csv',
            size: Buffer.from(csvContent, 'utf8').length,
            s3Key,
            s3Url: uploadResult.url,
            folder: 'exports',
            isPublic: false,
            metadata: {
              exportType: resourceType,
              recordCount: data.length,
              filters,
            },
          },
        });

        return {
          fileId: fileRecord.id,
          filename,
          url: uploadResult.url,
          recordCount: data.length,
        };
      } catch (error) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: 'Erro ao exportar dados',
        });
      }
    }),
});

// 🔧 Funções auxiliares
async function validateBatchItems(items: any[], operation: string): Promise<string[]> {
  const errors: string[] = [];
  
  items.forEach((item, index) => {
    if (operation !== 'CREATE' && !item.id) {
      errors.push(`Item ${index}: ID é obrigatório para operação ${operation}`);
    }
    
    if (!item.data || Object.keys(item.data).length === 0) {
      errors.push(`Item ${index}: dados são obrigatórios`);
    }
  });
  
  return errors;
}

async function downloadAndParseCSV(url: string): Promise<any[]> {
  // Implementar download e parsing do CSV
  // Usar biblioteca como papaparse ou csv-parser
  return [];
}

async function validateImportData(data: any[], resourceType: string): Promise<any[]> {
  // Implementar validação específica por tipo de recurso
  return [];
}

function convertToCSV(data: any[]): string {
  if (data.length === 0) return '';
  
  const headers = Object.keys(data[0]);
  const csvRows = [headers.join(',')];
  
  data.forEach(row => {
    const values = headers.map(header => {
      const value = row[header];
      return typeof value === 'string' ? `"${value.replace(/"/g, '""')}"` : value;
    });
    csvRows.push(values.join(','));
  });
  
  return csvRows.join('\n');
}

💡 Melhores Práticas para Mutations

Transações Atômicas:Use $transaction para operações que afetam múltiplas tabelas.

Validação Robusta:Sempre valide dados no servidor, nunca confie apenas no cliente.

Tratamento de Erros:Forneça mensagens claras e específicas para cada tipo de erro.

Auditoria:Registre todas as operações importantes para compliance.