# Sistema de Agendamento para Filas de Espera

## Visão Geral

O sistema de agendamento permite controlar quando os trabalhos podem ser executados, definindo janelas de tempo específicas e dias da semana. Trabalhos fora do horário configurado **não serão despachados**, garantindo controlo total sobre quando as tarefas são executadas.

## Características Principais

- ✅ **Controlo de horário**: Define horários de início e fim (ex: 08:00 às 18:00)
- ✅ **Controlo de dias**: Especifica dias da semana permitidos
- ✅ **Consciência de fuso horário**: Suporte completo a fusos horários
- ✅ **Configuração global e por inquilino**: Configurações padrão com substituição por inquilino
- ✅ **Múltiplas formas de configuração**: Atributos, métodos fluent, configuração dinâmica, substituição de classe
- ✅ **Integração com sistema existente**: Funciona com JobTenantAware e sistema de bloqueios
- ✅ **Registos e notificações**: Regista violações de horário para monitorização
- ✅ **Context-aware**: Distingue entre dispatch inicial e retries
- ✅ **Gestão inteligente de retries**: Configuração flexível para retries vs schedule
- ✅ **Arquitetura extensível**: Sistema composicional preparado para futuras funcionalidades

## Como Utilizar

### 1. Usando Atributos (Recomendado)

```php
use Og\Modules\Common\Queue\Attributes\RequiresSchedule;
use Og\Modules\Common\Queue\Traits\Schedulable;

#[RequiresSchedule(
    from: '08:00',
    to: '18:00',
    days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
    timezone: 'Europe/Lisbon'
)]
class ProcessOrdersJob extends Job
{
    // Nota: Traits Schedulable e JobTenantAware já estão na classe base Job

    public function handle(): void
    {
        // Lógica do trabalho aqui
    }
}
```

### 2. Usando Métodos Fluent

```php
class FlexibleJob extends Job
{
    use Schedulable, JobTenantAware;

    public function __construct()
    {
        parent::__construct();

        $this->availableBetween('09:00', '17:00')
             ->availableDays(['monday', 'friday'])
             ->timezone('Europe/Lisbon');
    }
}
```

### 3. Configuração Dinâmica

```php
$job = new ProcessDataJob();
$job->schedule([
    'from' => '22:00',
    'to' => '06:00',
    'days' => ['saturday', 'sunday'],
    'timezone' => 'UTC'
]);
```

### 4. Substituição Directa na Classe (Override)

```php
class CustomScheduleJob extends Job
{
    use Schedulable, JobTenantAware;
    
    /**
     * Define o horário de início para este trabalho
     */
    public function getScheduleFromTime(): string
    {
        return '07:30'; // Horário específico para esta classe
    }
    
    /**
     * Define o horário de fim para este trabalho
     */
    public function getScheduleToTime(): string
    {
        return '19:30'; // Horário específico para esta classe
    }
    
    /**
     * Define os dias disponíveis para este trabalho
     */
    public function getScheduleDays(): array
    {
        return ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
    }
    
    /**
     * Define o fuso horário para este trabalho
     */
    public function getScheduleTimezone(): string
    {
        return 'Europe/Lisbon';
    }
}
```

### 5. Bypass para Casos Urgentes

```php
$urgentJob = new CriticalJob();
$urgentJob->withoutSchedule(); // Ignora restrições de horário
```

## Configuração Global

No ficheiro `config/queue.php`, secção `scheduling`:

```php
'scheduling' => [
    // Configurações padrão para todos os trabalhos
    'default_timezone' => 'Europe/Lisbon',
    'default_from' => '08:00',
    'default_to' => '18:00',
    'default_days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],

    // Configurações específicas por inquilino
    'tenant_schedules' => [
        1 => [
            'from' => '07:00',
            'to' => '20:00',
            'days' => ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'],
        ],
        2 => [
            'from' => null, // 24/7
            'to' => null,
            'days' => [],
        ],
    ],
]
```

## Prioridade de Configuração

O sistema segue esta ordem de prioridade:

1. **Configuração manual** (métodos `availableBetween()`, etc.)
2. **Substituição de classe** (métodos `getScheduleFromTime()`, `getScheduleToTime()`, etc. na classe)
3. **Configuração por inquilino** (se o trabalho usa `JobTenantAware`)
4. **Atributos da classe** (`#[RequiresSchedule]`)
5. **Configuração global** (ficheiro de configuração)

## Comportamento

- **Trabalhos fora do horário**: Não são despachados, lançam `JobScheduleException`
- **Registos**: Violações são registadas automaticamente
- **Notificações**: Sistema de notificação integrado informa sobre bloqueios
- **Integração**: Funciona em conjunto com sistema de bloqueios existente
- **Context-aware**: Sistema distingue entre dispatch inicial (`queued`) e retries (`retry`)

## 🔄 Gestão de Retries vs Schedule

Um dos principais desafios do sistema de agendamento é evitar que retries quebrem as cadeias de execução. O sistema oferece **3 estratégias flexíveis**:

### **Estratégia 1: Bypass Completo (DEFAULT - Recomendado)**

```php
#[RequiresSchedule(
    from: '15:10',
    to: '15:15',
    retryWithinSchedule: false  // Retries sempre passam (DEFAULT)
)]
```

**Comportamento:**
- ✅ **15:14** - Job dispara (dentro do horário)
- ❌ **15:14** - Job falha 
- ✅ **15:17** - Retry executa (fora do horário, mas bypass ativo)

**Quando usar:** A maioria dos casos. Garante que retries nunca quebram cadeias.

### **Estratégia 2: Retry com Grace Period**

```php
#[RequiresSchedule(
    from: '15:10',
    to: '15:15',
    retryWithinSchedule: true,
    retryGracePeriodMinutes: 30  // +30min para retries
)]
```

**Comportamento:**
- ✅ **15:14** - Job dispara (15:10-15:15)
- ❌ **15:14** - Job falha
- ✅ **15:17** - Retry executa (15:10-**15:45** para retries)
- ❌ **15:46** - Retry falharia (fora da janela estendida)

**Quando usar:** Jobs críticos que devem ter algum limite temporal, mas com flexibilidade.

### **Estratégia 3: Retry Estrito** 

```php
#[RequiresSchedule(
    from: '15:10',
    to: '15:15',
    retryWithinSchedule: true,
    retryGracePeriodMinutes: 0  // Sem grace period
)]
```

**Comportamento:**
- ✅ **15:14** - Job dispara (15:10-15:15)
- ❌ **15:14** - Job falha
- ❌ **15:17** - Retry bloqueado (fora do horário)

**Quando usar:** Jobs que absolutamente não podem executar fora do horário (ex: integrações com sistemas externos com janela rígida).

### **Configuração de Atributos Completa**

```php
#[RequiresSchedule(
    from: '08:00',                     // Horário início
    to: '18:00',                       // Horário fim  
    days: ['monday', 'friday'],        // Dias permitidos
    timezone: 'Europe/Lisbon',         // Fuso horário
    description: 'Business hours',     // Descrição
    retryWithinSchedule: false,        // Retries ignoram schedule (DEFAULT)
    retryGracePeriodMinutes: 60        // Grace period se retryWithinSchedule = true
)]
```

### **Context-Aware: Como Funciona Internamente**

O sistema distingue automaticamente entre diferentes contextos de dispatch:

```php
// Dispatch inicial - deve respeitar schedule
QueueManager::push($job);  // context = 'queued'

// Retry automático - usa configuração de retry  
QueueManager::pushRetry($job);  // context = 'retry'

// Dispatch imediato - deve respeitar schedule
QueueManager::dispatchNow($job);  // context = 'immediate'
```

**Logs de Debug:**
```
[INFO] Processing job schedule | context: "queued" | is_schedulable: true
[DEBUG] Schedule validation passed | job: ProcessOrdersJob
```

```  
[INFO] Processing job schedule | context: "retry" | is_schedulable: true
[DEBUG] Retry bypassing schedule validation | retry_within_schedule: false
```

## Métodos Disponíveis

### Configuração Fluent
- `availableBetween(string $from, string $to)`: Define janela de tempo
- `availableFrom(string $from)`: Define horário de início
- `availableTo(string $to)`: Define horário de fim
- `availableDays(array $days)`: Define dias da semana
- `scheduleTimezone(string $timezone)`: Define fuso horário
- `schedule(array $config)`: Configuração completa
- `withoutSchedule()`: Desactiva verificações

### Métodos de Override (para definir na classe)
- `getScheduleFromTime(): string`: Retorna horário de início
- `getScheduleToTime(): string`: Retorna horário de fim
- `getScheduleDays(): array`: Retorna dias permitidos
- `getScheduleTimezone(): string`: Retorna fuso horário

### Verificação
- `isSchedulable()`: Verifica se o trabalho tem restrições
- `canExecuteNow(?Carbon $now, string $context = 'queued')`: Verifica se pode executar agora
- `validateScheduleForDispatch(?Carbon $now, string $context = 'queued')`: Valida e lança excepção se necessário

### Informações
- `getScheduleContext()`: Obtém contexto completo para depuração
- `getScheduleSummary()`: Resumo legível do agendamento

## Exemplos de Uso Prático

### Trabalho de Processamento Nocturno
```php
#[RequiresSchedule(from: '22:00', to: '06:00')]
class HeavyProcessingJob extends Job
{
    use Schedulable, JobTenantAware;
}
```

### Trabalho de Manutenção de Fim de Semana
```php
#[RequiresSchedule(days: ['saturday', 'sunday'])]
class MaintenanceJob extends Job
{
    use Schedulable, JobTenantAware;
}
```

### Trabalho de Horário Comercial
```php
#[RequiresSchedule(
    from: '08:00',
    to: '18:00',
    days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
    timezone: 'Europe/Lisbon'
    // retryWithinSchedule: false (DEFAULT - retries ignoram schedule)
)]
class BusinessHoursJob extends Job
{
    // Traits já incluídas na classe base Job
}
```

### Trabalho com Grace Period para Retries
```php
#[RequiresSchedule(
    from: '15:00',
    to: '17:00',
    days: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'],
    timezone: 'Europe/Lisbon',
    retryWithinSchedule: true,       // Retries respeitam schedule
    retryGracePeriodMinutes: 120     // Mas têm +2h de grace period
)]
class CriticalIntegrationJob extends Job
{
    // Se disparar às 16:59 e falhar, retry pode executar até 19:00
}
```

### Trabalho com Schedule Rígido para Retries
```php
#[RequiresSchedule(
    from: '09:00',
    to: '09:15',
    days: ['monday'],
    timezone: 'Europe/Lisbon',
    retryWithinSchedule: true,       // Retries também respeitam schedule
    retryGracePeriodMinutes: 0,      // Sem grace period
    description: 'Weekly bank sync - strict window'
)]
class WeeklyBankSyncJob extends Job
{
    // Deve executar estritamente entre 09:00-09:15 segunda-feira
    // Retries fora dessa janela serão bloqueados
}
```

### Trabalho com Horário Personalizado (Override)
```php
class SpecialHoursJob extends Job
{
    use Schedulable, JobTenantAware;

    public function getScheduleFromTime(): string
    {
        // Lógica personalizada baseada em condições
        if ($this->isHoliday()) {
            return '10:00';
        }
        return '08:00';
    }

    public function getScheduleToTime(): string
    {
        // Horário de fim baseado em regras de negócio
        return $this->getCustomEndTime();
    }

    public function getScheduleDays(): array
    {
        // Dias específicos baseados em regras de negócio
        return $this->getCustomBusinessDays();
    }

    public function getScheduleTimezone(): string
    {
        // Fuso horário baseado na localização do inquilino
        return $this->getTenantTimezone() ?? 'Europe/Lisbon';
    }
}
```

## Monitorização

O sistema automaticamente:
- Regista tentativas de despacho fora do horário
- Envia notificações via `CliNotifier`
- Inclui contexto completo para depuração
- Integra com sistema de registos existente

## 🏗️ Arquitetura e Extensibilidade

O sistema foi projectado com arquitetura composicional e extensível:

### **Validation Chain Pattern**
- `ScheduleValidationChain`: Orquestra múltiplos validators  
- `DayOfWeekValidator`: Valida dias da semana
- `TimeWindowValidator`: Valida janelas de tempo
- **Extensível**: Novos validators podem ser facilmente adicionados

```php
// Futuro: Adicionar novos validators é trivial
$chain->addValidator(new HolidayValidator());
$chain->addValidator(new ResourceValidator()); 
$chain->addValidator(new CronPatternValidator());
```

### **Enum-based Block Reasons**
- `DispatchBlockReason`: Enum type-safe para motivos de bloqueio
- **Auto-mapping**: Cada reason sabe sua exception
- **Extensível**: Novos reasons são automaticamente suportados

```php
enum DispatchBlockReason: string {
    case SCHEDULE_RESTRICTION = 'blocked-by-schedule';
    case LOCK_CONFLICT = 'blocked-by-lock';
    case RATE_LIMIT = 'blocked-by-rate-limit';    // Futuro
    case CIRCUIT_BREAKER = 'blocked-by-circuit-breaker';  // Futuro
}
```

### **Context-Aware Processing**
- Sistema distingue contextos: `queued`, `retry`, `immediate`
- Comportamento diferenciado por contexto
- **Extensível**: Novos contextos podem ser adicionados

## 🔧 Como Estender o Sistema

### **1. Criando um Novo Validator**

Para adicionar uma nova regra de validação (ex: feriados):

**Passo 1: Criar o Validator**
```php
// Modules/Common/Queue/Validation/ScheduleValidators/HolidayValidator.php
<?php
namespace Og\Modules\Common\Queue\Validation\ScheduleValidators;

use Og\Modules\Common\Queue\Contracts\ScheduleValidatorInterface;
use Og\Modules\Common\Utils\Carbon;

class HolidayValidator implements ScheduleValidatorInterface
{
    public function validate(Carbon $now, array $context): bool
    {
        // Consultar base de dados de feriados
        $holidays = $this->getHolidays($now->year);
        $currentDate = $now->format('Y-m-d');
        
        return !in_array($currentDate, $holidays);
    }

    public function getRuleName(): string
    {
        return 'holiday_check';
    }

    public function getValidationContext(Carbon $now, array $context): array
    {
        return [
            'rule' => $this->getRuleName(),
            'current_date' => $now->format('Y-m-d'),
            'is_holiday' => $this->isHoliday($now),
            'holiday_name' => $this->getHolidayName($now),
        ];
    }
    
    private function getHolidays(int $year): array
    {
        // Implementar lógica de consulta de feriados
        return config("holidays.{$year}", []);
    }
}
```

**Passo 2: Registar no Chain**
```php
// Na trait Schedulable, método buildValidationChain()
protected function buildValidationChain(): ScheduleValidationChain
{
    $chain = new ScheduleValidationChain();

    // Validators existentes
    $chain->addValidator(new DayOfWeekValidator());
    $chain->addValidator(new TimeWindowValidator());
    
    // Novo validator
    $chain->addValidator(new HolidayValidator());

    return $chain;
}
```

### **2. Adicionando Novos Block Reasons**

Para adicionar um novo motivo de bloqueio (ex: rate limiting):

**Passo 1: Atualizar o Enum**
```php
// Modules/Common/Queue/Enums/DispatchBlockReason.php
enum DispatchBlockReason: string
{
    case SCHEDULE_RESTRICTION = 'blocked-by-schedule';
    case LOCK_CONFLICT = 'blocked-by-lock';
    case RATE_LIMIT = 'blocked-by-rate-limit';        // NOVO
    case RESOURCE_UNAVAILABLE = 'blocked-by-resource'; // NOVO

    public function getDescription(): string
    {
        return match($this) {
            // Casos existentes...
            self::RATE_LIMIT => 'Rate limit exceeded for this job type',
            self::RESOURCE_UNAVAILABLE => 'Required resources not available',
        };
    }

    public function getExceptionClass(): string
    {
        return match($this) {
            // Casos existentes...
            self::RATE_LIMIT => \Og\Modules\Common\Queue\Exceptions\RateLimitException::class,
            self::RESOURCE_UNAVAILABLE => \Og\Modules\Common\Queue\Exceptions\ResourceException::class,
        };
    }
}
```

**Passo 2: Implementar a Lógica de Block**
```php
// Nova trait para rate limiting
trait RateLimited
{
    protected static function processJobRateLimit(JobInterface $job, string $context, array $additionalContext): ?string
    {
        $rateLimit = $job->getRateLimit();
        if (!$rateLimit) {
            return null;
        }

        if ($this->isRateLimitExceeded($job, $rateLimit)) {
            return DispatchBlockReason::RATE_LIMIT->value;
        }

        return null;
    }
}
```

**Passo 3: Integrar no prepareJobForDispatch**
```php
// JobTenantAware::prepareJobForDispatch()
protected static function prepareJobForDispatch(JobInterface $job, string $context, array $additionalContext): ?string
{
    self::defineJobTenantProperties($job);

    // Verificações existentes
    $scheduleBlock = self::processJobSchedule($job, $context, $additionalContext);
    if ($scheduleBlock) return $scheduleBlock;

    // NOVA verificação
    $rateLimitBlock = self::processJobRateLimit($job, $context, $additionalContext);
    if ($rateLimitBlock) return $rateLimitBlock;

    $lockBlock = self::processJobLock($job, $context, $additionalContext);
    return $lockBlock;
}
```

### **3. Adicionando Novos Contextos**

Para suportar novos contextos de dispatch (ex: `urgent`):

**Passo 1: Definir o Contexto**
```php
// Constantes para contextos
class DispatchContext
{
    public const QUEUED = 'queued';
    public const RETRY = 'retry';
    public const IMMEDIATE = 'immediate';
    public const URGENT = 'urgent';     // NOVO
    public const SCHEDULED = 'scheduled'; // NOVO
}
```

**Passo 2: Atualizar a Lógica Context-Aware**
```php
// Na trait Schedulable
public function canExecuteNow(?Carbon $now = null, string $dispatchContext = 'queued'): bool
{
    if (!$this->isSchedulable()) {
        return true;
    }

    // Contextos que ignoram schedule
    if (in_array($dispatchContext, [
        DispatchContext::RETRY, 
        DispatchContext::URGENT  // NOVO - contexto urgente ignora schedule
    ])) {
        return true;
    }

    // Validação normal para outros contextos
    return $this->validateScheduleRules($now);
}
```

**Passo 3: Usar o Novo Contexto**
```php
// QueueManager - novo método para dispatch urgente
public function pushUrgent(JobInterface $job, ?string $queue = null): string
{
    $lockResult = self::prepareJobForDispatch($job, DispatchContext::URGENT, [
        'queue' => $queue,
        'urgent' => true
    ]);

    // Resto da lógica...
}
```

### **4. Criando Novos Atributos de Schedule**

Para funcionalidades avançadas:

```php
// Modules/Common/Queue/Attributes/RequiresResource.php
#[Attribute(Attribute::TARGET_CLASS)]
class RequiresResource
{
    public function __construct(
        public readonly string $resourceType,
        public readonly int $minAmount,
        public readonly bool $exclusive = false
    ) {}
}

// Usar no job
#[RequiresSchedule(from: '08:00', to: '18:00')]
#[RequiresResource(resourceType: 'memory', minAmount: 2048, exclusive: true)]
class HeavyProcessingJob extends Job
{
    // Job precisa de 2GB RAM exclusivo durante horário comercial
}
```

### **5. Criando Validators Personalizados**

```php
class CronPatternValidator implements ScheduleValidatorInterface
{
    public function validate(Carbon $now, array $context): bool
    {
        $cronPattern = $context['cron_pattern'] ?? null;
        if (!$cronPattern) return true;

        return $this->cronMatches($cronPattern, $now);
    }

    private function cronMatches(string $pattern, Carbon $now): bool
    {
        // Implementar parsing de cron pattern
        // Ex: "0 9 * * 1-5" = 9h, segunda a sexta
        return (new CronExpression($pattern))->isDue($now);
    }
}
```

### **Futuras Funcionalidades Preparadas**

1. **Holiday Calendar System**: Implementar `HolidayValidator`
2. **Cron Pattern Support**: Implementar `CronPatternValidator`  
3. **Resource-Aware Scheduling**: Usar `RequiresResource` attribute
4. **Rate Limiting Integration**: Adicionar `RATE_LIMIT` block reason
5. **Circuit Breaker Pattern**: Implementar `CircuitBreakerValidator`
6. **Schedule Analytics & Metrics**: Instrumentação automática

## 🧪 Testes

Execute os testes para verificar funcionamento:

```bash
./vendor/bin/phpunit Tests/Unit/Queue/Scheduling/SchedulableTraitTest.php
```

## 📊 Exemplo Real: SendToWmsJob

```php
#[RequiresSchedule(from: '15:10', to: '15:12')]
class SendToWmsJob extends Job
{
    // Comportamento:
    // ✅ 15:10 - Job dispara corretamente
    // ❌ 15:10 - Job falha (erro de rede)  
    // ✅ 15:13 - Retry executa (retryWithinSchedule=false por defeito)
    // ✅ 15:13 - Job completa com sucesso
}
```

**Logs esperados:**
```
[DEBUG] Processing job schedule | context: "queued" | is_schedulable: true
[DEBUG] Schedule validation passed | job: SendToWmsJob
[DEBUG] Processing job schedule | context: "retry" | is_schedulable: true  
[DEBUG] Retry bypassing schedule validation | retry_within_schedule: false
```
