---
description: Validation schemas and patterns for Evolution API
globs:
  - "src/validate/**/*.ts"
alwaysApply: false
---

# Evolution API Validation Rules

## Validation Schema Structure

### JSONSchema7 Pattern (Evolution API Standard)
```typescript
import { JSONSchema7 } from 'json-schema';
import { v4 } from 'uuid';

const isNotEmpty = (...fields: string[]) => {
  const properties = {};
  fields.forEach((field) => {
    properties[field] = {
      if: { properties: { [field]: { type: 'string' } } },
      then: { properties: { [field]: { minLength: 1 } } },
    };
  });

  return {
    allOf: Object.values(properties),
  };
};

export const exampleSchema: JSONSchema7 = {
  $id: v4(),
  type: 'object',
  properties: {
    name: { type: 'string' },
    description: { type: 'string' },
    enabled: { type: 'boolean' },
    settings: {
      type: 'object',
      properties: {
        timeout: { type: 'number', minimum: 1000, maximum: 60000 },
        retries: { type: 'number', minimum: 0, maximum: 5 },
      },
    },
    tags: {
      type: 'array',
      items: { type: 'string' },
    },
  },
  required: ['name', 'enabled'],
  ...isNotEmpty('name'),
};
```

## Message Validation Schemas

### Send Message Validation
```typescript
const numberDefinition = {
  type: 'string',
  pattern: '^\\d+[\\.@\\w-]+',
  description: 'Invalid number',
};

export const sendTextSchema: JSONSchema7 = {
  $id: v4(),
  type: 'object',
  properties: {
    number: numberDefinition,
    text: { type: 'string', minLength: 1, maxLength: 4096 },
    delay: { type: 'number', minimum: 0, maximum: 60000 },
    linkPreview: { type: 'boolean' },
    mentionsEveryOne: { type: 'boolean' },
    mentioned: {
      type: 'array',
      items: { type: 'string' },
    },
  },
  required: ['number', 'text'],
  ...isNotEmpty('number', 'text'),
};

export const sendMediaSchema = Joi.object({
  number: Joi.string().required().pattern(/^\d+$/),
  mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
  media: Joi.alternatives().try(
    Joi.string().uri(),
    Joi.string().base64(),
  ).required(),
  caption: Joi.string().optional().max(1024),
  fileName: Joi.string().optional().max(255),
  delay: Joi.number().optional().min(0).max(60000),
}).required();

export const sendButtonsSchema = Joi.object({
  number: Joi.string().required().pattern(/^\d+$/),
  title: Joi.string().required().max(1024),
  description: Joi.string().optional().max(1024),
  footer: Joi.string().optional().max(60),
  buttons: Joi.array().items(
    Joi.object({
      type: Joi.string().required().valid('replyButton', 'urlButton', 'callButton'),
      displayText: Joi.string().required().max(20),
      id: Joi.string().when('type', {
        is: 'replyButton',
        then: Joi.required().max(256),
        otherwise: Joi.optional(),
      }),
      url: Joi.string().when('type', {
        is: 'urlButton',
        then: Joi.required().uri(),
        otherwise: Joi.optional(),
      }),
      phoneNumber: Joi.string().when('type', {
        is: 'callButton',
        then: Joi.required().pattern(/^\+?\d+$/),
        otherwise: Joi.optional(),
      }),
    })
  ).min(1).max(3).required(),
}).required();
```

## Instance Validation Schemas

### Instance Creation Validation
```typescript
export const instanceSchema = Joi.object({
  instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
  integration: Joi.string().required().valid('WHATSAPP-BAILEYS', 'WHATSAPP-BUSINESS', 'EVOLUTION'),
  token: Joi.string().when('integration', {
    is: Joi.valid('WHATSAPP-BUSINESS', 'EVOLUTION'),
    then: Joi.required().min(10),
    otherwise: Joi.optional(),
  }),
  qrcode: Joi.boolean().optional().default(false),
  number: Joi.string().optional().pattern(/^\d+$/),
  businessId: Joi.string().when('integration', {
    is: 'WHATSAPP-BUSINESS',
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),
}).required();

export const settingsSchema = Joi.object({
  rejectCall: Joi.boolean().optional(),
  msgCall: Joi.string().optional().max(500),
  groupsIgnore: Joi.boolean().optional(),
  alwaysOnline: Joi.boolean().optional(),
  readMessages: Joi.boolean().optional(),
  readStatus: Joi.boolean().optional(),
  syncFullHistory: Joi.boolean().optional(),
  wavoipToken: Joi.string().optional(),
}).optional();

export const proxySchema = Joi.object({
  host: Joi.string().required().hostname(),
  port: Joi.string().required().pattern(/^\d+$/).custom((value) => {
    const port = parseInt(value);
    if (port < 1 || port > 65535) {
      throw new Error('Port must be between 1 and 65535');
    }
    return value;
  }),
  protocol: Joi.string().required().valid('http', 'https', 'socks4', 'socks5'),
  username: Joi.string().optional(),
  password: Joi.string().optional(),
}).optional();
```

## Webhook Validation Schemas

### Webhook Configuration Validation
```typescript
export const webhookSchema = Joi.object({
  enabled: Joi.boolean().required(),
  url: Joi.string().when('enabled', {
    is: true,
    then: Joi.required().uri({ scheme: ['http', 'https'] }),
    otherwise: Joi.optional(),
  }),
  events: Joi.array().items(
    Joi.string().valid(
      'APPLICATION_STARTUP',
      'INSTANCE_CREATE',
      'INSTANCE_DELETE',
      'QRCODE_UPDATED',
      'CONNECTION_UPDATE',
      'STATUS_INSTANCE',
      'MESSAGES_SET',
      'MESSAGES_UPSERT',
      'MESSAGES_UPDATE',
      'MESSAGES_DELETE',
      'CONTACTS_SET',
      'CONTACTS_UPSERT',
      'CONTACTS_UPDATE',
      'CHATS_SET',
      'CHATS_UPDATE',
      'CHATS_UPSERT',
      'CHATS_DELETE',
      'GROUPS_UPSERT',
      'GROUPS_UPDATE',
      'GROUP_PARTICIPANTS_UPDATE',
      'CALL'
    )
  ).min(1).when('enabled', {
    is: true,
    then: Joi.required(),
    otherwise: Joi.optional(),
  }),
  headers: Joi.object().pattern(
    Joi.string(),
    Joi.string()
  ).optional(),
  byEvents: Joi.boolean().optional().default(false),
  base64: Joi.boolean().optional().default(false),
}).required();
```

## Chatbot Validation Schemas

### Base Chatbot Validation
```typescript
export const baseChatbotSchema = Joi.object({
  enabled: Joi.boolean().required(),
  description: Joi.string().required().min(1).max(500),
  expire: Joi.number().optional().min(0).max(86400), // 24 hours in seconds
  keywordFinish: Joi.string().optional().max(100),
  delayMessage: Joi.number().optional().min(0).max(10000),
  unknownMessage: Joi.string().optional().max(1000),
  listeningFromMe: Joi.boolean().optional().default(false),
  stopBotFromMe: Joi.boolean().optional().default(false),
  keepOpen: Joi.boolean().optional().default(false),
  debounceTime: Joi.number().optional().min(0).max(60000),
  triggerType: Joi.string().required().valid('ALL', 'KEYWORD', 'REGEX'),
  triggerOperator: Joi.string().when('triggerType', {
    is: 'KEYWORD',
    then: Joi.required().valid('EQUALS', 'CONTAINS', 'STARTS_WITH', 'ENDS_WITH'),
    otherwise: Joi.optional(),
  }),
  triggerValue: Joi.string().when('triggerType', {
    is: Joi.valid('KEYWORD', 'REGEX'),
    then: Joi.required().min(1).max(500),
    otherwise: Joi.optional(),
  }),
  ignoreJids: Joi.array().items(Joi.string()).optional(),
  splitMessages: Joi.boolean().optional().default(false),
  timePerChar: Joi.number().optional().min(10).max(1000).default(100),
}).required();

export const typebotSchema = baseChatbotSchema.keys({
  url: Joi.string().required().uri({ scheme: ['http', 'https'] }),
  typebot: Joi.string().required().min(1).max(100),
  apiVersion: Joi.string().optional().valid('v1', 'v2').default('v1'),
}).required();

export const openaiSchema = baseChatbotSchema.keys({
  apiKey: Joi.string().required().min(10),
  model: Joi.string().optional().valid(
    'gpt-3.5-turbo',
    'gpt-3.5-turbo-16k',
    'gpt-4',
    'gpt-4-32k',
    'gpt-4-turbo-preview'
  ).default('gpt-3.5-turbo'),
  systemMessage: Joi.string().optional().max(2000),
  maxTokens: Joi.number().optional().min(1).max(4000).default(1000),
  temperature: Joi.number().optional().min(0).max(2).default(0.7),
}).required();
```

## Business API Validation Schemas

### Template Validation
```typescript
export const templateSchema = Joi.object({
  name: Joi.string().required().min(1).max(512).pattern(/^[a-z0-9_]+$/),
  category: Joi.string().required().valid('MARKETING', 'UTILITY', 'AUTHENTICATION'),
  allowCategoryChange: Joi.boolean().required(),
  language: Joi.string().required().pattern(/^[a-z]{2}_[A-Z]{2}$/), // e.g., pt_BR, en_US
  components: Joi.array().items(
    Joi.object({
      type: Joi.string().required().valid('HEADER', 'BODY', 'FOOTER', 'BUTTONS'),
      format: Joi.string().when('type', {
        is: 'HEADER',
        then: Joi.valid('TEXT', 'IMAGE', 'VIDEO', 'DOCUMENT'),
        otherwise: Joi.optional(),
      }),
      text: Joi.string().when('type', {
        is: Joi.valid('HEADER', 'BODY', 'FOOTER'),
        then: Joi.required().min(1).max(1024),
        otherwise: Joi.optional(),
      }),
      buttons: Joi.array().when('type', {
        is: 'BUTTONS',
        then: Joi.items(
          Joi.object({
            type: Joi.string().required().valid('QUICK_REPLY', 'URL', 'PHONE_NUMBER'),
            text: Joi.string().required().min(1).max(25),
            url: Joi.string().when('type', {
              is: 'URL',
              then: Joi.required().uri(),
              otherwise: Joi.optional(),
            }),
            phone_number: Joi.string().when('type', {
              is: 'PHONE_NUMBER',
              then: Joi.required().pattern(/^\+?\d+$/),
              otherwise: Joi.optional(),
            }),
          })
        ).min(1).max(10),
        otherwise: Joi.optional(),
      }),
    })
  ).min(1).required(),
  webhookUrl: Joi.string().optional().uri({ scheme: ['http', 'https'] }),
}).required();

export const catalogSchema = Joi.object({
  number: Joi.string().optional().pattern(/^\d+$/),
  limit: Joi.number().optional().min(1).max(1000).default(10),
  cursor: Joi.string().optional(),
}).optional();
```

## Group Validation Schemas

### Group Management Validation
```typescript
export const createGroupSchema = Joi.object({
  subject: Joi.string().required().min(1).max(100),
  description: Joi.string().optional().max(500),
  participants: Joi.array().items(
    Joi.string().pattern(/^\d+$/)
  ).min(1).max(256).required(),
  promoteParticipants: Joi.boolean().optional().default(false),
}).required();

export const updateGroupSchema = Joi.object({
  subject: Joi.string().optional().min(1).max(100),
  description: Joi.string().optional().max(500),
}).min(1).required();

export const groupParticipantsSchema = Joi.object({
  participants: Joi.array().items(
    Joi.string().pattern(/^\d+$/)
  ).min(1).max(50).required(),
  action: Joi.string().required().valid('add', 'remove', 'promote', 'demote'),
}).required();
```

## Label Validation Schemas

### Label Management Validation
```typescript
export const labelSchema = Joi.object({
  name: Joi.string().required().min(1).max(100),
  color: Joi.string().required().pattern(/^#[0-9A-Fa-f]{6}$/), // Hex color
  predefinedId: Joi.string().optional(),
}).required();

export const handleLabelSchema = Joi.object({
  number: Joi.string().required().pattern(/^\d+$/),
  labelId: Joi.string().required(),
  action: Joi.string().required().valid('add', 'remove'),
}).required();
```

## Custom Validation Functions

### Phone Number Validation
```typescript
export function validatePhoneNumber(number: string): boolean {
  // Remove any non-digit characters
  const cleaned = number.replace(/\D/g, '');
  
  // Check minimum and maximum length
  if (cleaned.length < 10 || cleaned.length > 15) {
    return false;
  }
  
  // Check for valid country codes (basic validation)
  const validCountryCodes = ['1', '7', '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '51', '52', '53', '54', '55', '56', '57', '58', '60', '61', '62', '63', '64', '65', '66', '81', '82', '84', '86', '90', '91', '92', '93', '94', '95', '98'];
  
  // Check if starts with valid country code
  const startsWithValidCode = validCountryCodes.some(code => cleaned.startsWith(code));
  
  return startsWithValidCode;
}

export const phoneNumberValidator = Joi.string().custom((value, helpers) => {
  if (!validatePhoneNumber(value)) {
    return helpers.error('any.invalid');
  }
  return value;
}, 'Phone number validation');
```

### Base64 Validation
```typescript
export function validateBase64(base64: string): boolean {
  try {
    // Check if it's a valid base64 string
    const decoded = Buffer.from(base64, 'base64').toString('base64');
    return decoded === base64;
  } catch {
    return false;
  }
}

export const base64Validator = Joi.string().custom((value, helpers) => {
  if (!validateBase64(value)) {
    return helpers.error('any.invalid');
  }
  return value;
}, 'Base64 validation');
```

### URL Validation with Protocol Check
```typescript
export function validateWebhookUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

export const webhookUrlValidator = Joi.string().custom((value, helpers) => {
  if (!validateWebhookUrl(value)) {
    return helpers.error('any.invalid');
  }
  return value;
}, 'Webhook URL validation');
```

## Validation Error Handling

### Error Message Customization
```typescript
export const validationMessages = {
  'any.required': 'O campo {#label} é obrigatório',
  'string.empty': 'O campo {#label} não pode estar vazio',
  'string.min': 'O campo {#label} deve ter pelo menos {#limit} caracteres',
  'string.max': 'O campo {#label} deve ter no máximo {#limit} caracteres',
  'string.pattern.base': 'O campo {#label} possui formato inválido',
  'number.min': 'O campo {#label} deve ser maior ou igual a {#limit}',
  'number.max': 'O campo {#label} deve ser menor ou igual a {#limit}',
  'array.min': 'O campo {#label} deve ter pelo menos {#limit} itens',
  'array.max': 'O campo {#label} deve ter no máximo {#limit} itens',
  'any.only': 'O campo {#label} deve ser um dos valores: {#valids}',
};

export function formatValidationError(error: Joi.ValidationError): any {
  return {
    message: 'Dados de entrada inválidos',
    details: error.details.map(detail => ({
      field: detail.path.join('.'),
      message: detail.message,
      value: detail.context?.value,
    })),
  };
}
```

## Schema Composition

### Reusable Schema Components
```typescript
export const commonFields = {
  instanceName: Joi.string().required().min(1).max(100).pattern(/^[a-zA-Z0-9_-]+$/),
  number: phoneNumberValidator.required(),
  delay: Joi.number().optional().min(0).max(60000),
  enabled: Joi.boolean().optional().default(true),
};

export const mediaFields = {
  mediatype: Joi.string().required().valid('image', 'video', 'audio', 'document'),
  media: Joi.alternatives().try(
    Joi.string().uri(),
    base64Validator,
  ).required(),
  caption: Joi.string().optional().max(1024),
  fileName: Joi.string().optional().max(255),
};

// Compose schemas using common fields
export const quickMessageSchema = Joi.object({
  ...commonFields,
  text: Joi.string().required().min(1).max(4096),
}).required();

export const quickMediaSchema = Joi.object({
  ...commonFields,
  ...mediaFields,
}).required();
```