# Cycle 2: Scheduled Jobs & Commands

## Goal
Create all 27 missing Artisan commands, register them in the scheduler, and wire them to services.

---

## Command Template

```php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class {CommandName} extends Command
{
    protected $signature = '{module}:{action} {--dry-run : Show what would happen without executing}';
    protected $description = '{Arabic + English description}';

    public function handle(): int
    {
        // Logic here
        return self::SUCCESS;
    }
}
```

---

## Financial Commands

### 1. MarkOverdueInvoices
**Signature:** `invoices:mark-overdue`
**Frequency:** Daily at 01:00
**Logic:**
```
1. Query invoices WHERE status IN ('sent', 'partially_paid') AND due_date < today
2. For each: update status → 'overdue'
3. Dispatch InvoiceOverdue event for each
4. Log count
```
**Service method needed:** `InvoiceService::markOverdue(Invoice $invoice)`

### 2. ApplyInstallmentLateFees
**Signature:** `installments:apply-late-fees`
**Frequency:** Daily at 02:00
**Logic:**
```
1. Query installments WHERE status = 'pending' AND due_date < today AND late_fee_applied = false
2. For each: calculate fee based on payment_plan.late_fee_type and late_fee_value
3. Update installment: late_fee_amount, total_due, status → 'overdue'
4. Dispatch InstallmentOverdue event
```
**Service method needed:** `PaymentPlanService::applyLateFee(Installment $installment)`
**Depends on:** Cycle 3 (payment_plans schema update with late_fee columns)

### 3. CheckPaymentPlanDefaults
**Signature:** `payment-plans:check-defaults`
**Frequency:** Daily at 03:00
**Logic:**
```
1. Query active payment_plans
2. For each: count consecutive missed installments (status = 'overdue')
3. If count >= plan.default_after_missed (default 3): change plan status → 'defaulted'
4. Dispatch PaymentPlanDefaulted event
```
**Service method needed:** `PaymentPlanService::checkAndDefault(PaymentPlan $plan)`
**Depends on:** Cycle 3

### 4. SendInstallmentReminders
**Signature:** `installments:send-reminders`
**Frequency:** Daily at 08:00
**Logic:**
```
1. Query installments WHERE status = 'pending' AND due_date BETWEEN today AND today+3 days
2. For each: dispatch InstallmentDueReminder notification via NotificationService
3. Mark reminder_sent_at
```
**Service method needed:** None (direct query + notification dispatch)
**Depends on:** Cycle 3 (reminder_sent_at column)

### 5. ExpireWalletCredits
**Signature:** `wallets:expire-credits`
**Frequency:** Daily at 04:00
**Logic:**
```
1. Query wallet_transactions WHERE type = 'deposit' AND expires_at IS NOT NULL AND expires_at < now
2. For expired credits not yet consumed: create withdrawal + log
3. Update wallet balance
```
**Service method needed:** `WalletService::expireCredit(WalletTransaction $tx)`
**Note:** Requires adding `expires_at` column to wallet_transactions (Cycle 3)

### 6. GenerateDailyClosing
**Signature:** `financial:daily-closing`
**Frequency:** Daily at 00:05 (just after midnight)
**Logic:**
```
1. Get yesterday's date
2. For each academy + branch combination:
   a. Sum all confirmed payments by method
   b. Sum all invoices created/paid
   c. Sum all refunds issued
   d. Get cash session opening/closing variance
   e. Create daily_closings record with all aggregates
3. Dispatch DailyClosingReady event
```
**Service method needed:** `DailyClosingService::generate(Carbon $date, int $academyId, ?int $branchId)`
**Depends on:** Cycle 3 (daily_closings table)

### 7. ReconcileAccountBalances
**Signature:** `financial:reconcile`
**Frequency:** Weekly (Sundays at 03:00)
**Logic:**
```
1. For each financial_account:
   a. Sum debit transactions
   b. Sum credit transactions
   c. Calculate expected balance based on account type (asset/expense = debits - credits)
   d. Compare with stored current_balance
   e. If mismatch: LOG WARNING, notify admin — DO NOT auto-fix
```
**Depends on:** Cycle 3 (`current_balance` column on financial_accounts)

---

## Training Commands

### 8. GenerateUpcomingSessions
**Signature:** `sessions:generate {--days=14 : Generate sessions for next N days}`
**Frequency:** Daily at 05:00
**Logic:**
```
1. Call SessionGeneratorService::generateUpcoming($days)
2. Service already handles: active groups → active schedules → check holidays → skip duplicates
3. Log count of sessions created per group
```
**Service method exists:** `SessionGeneratorService::generateUpcoming()`

### 9. SendSessionReminders
**Signature:** `sessions:send-reminders`
**Frequency:** Daily at 06:30 (before first sessions typically at 07:00-08:00)
**Logic:**
```
1. Query sessions WHERE session_date = today AND status = 'scheduled'
2. For each session:
   a. Get enrolled participants (active enrollments)
   b. Get assigned trainer(s)
   c. Notify each: "Your session {group.name_ar} starts at {start_time}"
```
**Via:** NotificationService

### 10. MarkNoShowSessions
**Signature:** `sessions:mark-no-show`
**Frequency:** Hourly
**Logic:**
```
1. Query sessions WHERE status = 'scheduled' AND start_time + 30min < now AND session_date = today
2. Check if ANY attendance was marked (at least one 'present' or 'late')
3. If zero attendance marked: flag session (status_notes += 'possible no-show')
4. Notify admin
```
**New field needed:** `training_sessions.status_notes` (text, nullable) or use metadata jsonb

### 11. AutoCancelLowEnrollment
**Signature:** `sessions:cancel-low-enrollment`
**Frequency:** Daily at 20:00 (evening, for next day's sessions)
**Logic:**
```
1. Query sessions WHERE session_date = tomorrow AND status = 'scheduled'
2. For each: count active enrollments in the group
3. If count < group.min_capacity (or academy setting 'min_session_enrollment', default 3):
   a. Cancel session (via SessionGeneratorService::cancelSession)
   b. Notify enrolled participants
```
**Setting needed:** `min_session_enrollment` in system_settings

### 12. ProcessWaitlists
**Signature:** `waitlists:process`
**Frequency:** Every 2 hours
**Logic:**
```
1. Query training_groups WHERE current_count < max_capacity AND status = 'active'
2. For each group with available spots:
   a. Get oldest un-responded waitlist entries
   b. For each (up to available spots): notify, set notified_at, set expires_at = now + 48h
3. Query waitlists WHERE notified_at IS NOT NULL AND expires_at < now AND response IS NULL:
   a. Set response = 'expired'
   b. Process next in queue
```
**Service method exists:** `EnrollmentService::processWaitlist()`

### 13. CheckMedicalClearance
**Signature:** `participants:check-medical`
**Frequency:** Daily at 07:00
**Logic:**
```
1. Query participants WHERE medical_clearance_expires IS NOT NULL
                       AND medical_clearance_expires <= today + 7 days
                       AND status = 'active'
2. For expired (past date): notify admin + guardian "Medical clearance expired"
3. For expiring soon (within 7 days): notify guardian "Medical clearance expiring on {date}"
```

---

## Facility Commands

### 14. FacilityMaintenanceCheck
**Signature:** `facilities:maintenance-check`
**Frequency:** Daily at 06:00
**Logic:**
```
1. Query facilities WHERE status = 'maintenance' AND status_until IS NOT NULL AND status_until < today
2. For each: notify admin "Facility {name_ar} maintenance period ended, still in maintenance status"
3. DO NOT auto-change status (requires human verification)
```
**Depends on:** Cycle 8 (`status_until` column on facilities)

---

## Attendance Commands

### 15. AttendanceAlerts (Enhanced)
**Signature:** `attendance:daily-alerts`
**Frequency:** Daily at 21:00
**Logic:**
```
1. Query participants with 3+ consecutive absences (not yet at 5 threshold):
   - Send guardian warning notification
2. Query participants with attendance_percentage < 80% (warning zone, above 75% suspend):
   - Send guardian + admin heads-up
3. Query trainers with any absence today:
   - Alert branch manager
```

### 16. TrainerAttendanceReminder
**Signature:** `attendance:trainer-reminder`
**Frequency:** Every 30 minutes (checks if session started 30 min ago)
**Logic:**
```
1. Query sessions WHERE start_time BETWEEN (now - 35min) AND (now - 25min) AND session_date = today
2. For each: check if trainer's attendance was marked
3. If not marked: notify admin "Trainer {name} has not been marked present for session at {start_time}"
```

### 17. ExcuseExpiryCheck
**Signature:** `attendance:expire-excuses`
**Frequency:** Daily at 08:00
**Logic:**
```
1. Query attendance_records WHERE status = 'excused'
                              AND excuse_approved IS NULL
                              AND marked_at < today - 7 days
2. For each: set excuse_approved = false (auto-rejected)
3. Recalculate attendance as 'absent' instead of 'excused'
4. Notify participant/guardian of rejection
```
**Depends on:** Cycle 7 (excuse columns on attendance_records)

---

## Inventory Commands

### 18. LowStockAlerts
**Signature:** `inventory:low-stock-alerts`
**Frequency:** Daily at 08:00
**Logic:**
```
1. Query inventory_levels with products:
   WHERE quantity_available <= product.min_stock_level
   AND product.track_inventory = true
   AND product.is_active = true
2. For each: dispatch ProductLowStock event (triggers notification)
3. Log summary
```

### 19. ReorderSuggestions
**Signature:** `inventory:reorder-suggestions`
**Frequency:** Weekly (Mondays at 09:00)
**Logic:**
```
1. Query products WHERE track_inventory = true AND is_active = true
2. For each: get average daily consumption (movements out / days in period)
3. Calculate days until stockout: quantity_available / avg_daily_consumption
4. If days_until_stockout <= 14: include in suggestion list
5. Generate suggestion report (product, current stock, avg consumption, suggested order qty)
6. Notify inventory manager
```

### 20. ExpiredStockCheck
**Signature:** `inventory:expired-stock`
**Frequency:** Daily at 07:00
**Logic:**
```
1. Query inventory_movements WHERE expiry_date IS NOT NULL AND expiry_date <= today + 30 days
2. Group by product + warehouse
3. For expired (past date): URGENT alert — "Expired stock must be removed"
4. For expiring (within 30 days): WARNING — "Stock expiring soon"
5. Notify inventory manager
```

### 21. StockCountReminders
**Signature:** `inventory:count-reminders`
**Frequency:** Weekly (Mondays at 08:00)
**Logic:**
```
1. Check system_setting 'stock_count_frequency' (default: monthly)
2. Query products WHERE last_counted_at < today - frequency OR last_counted_at IS NULL
3. If count > 0: create notification for inventory manager
4. Optionally auto-create stock_count in 'open' status for overdue products
```

### 22. RecalculateInventoryLevels
**Signature:** `inventory:reconcile`
**Frequency:** Weekly (Sundays at 02:00)
**Logic:**
```
1. For each inventory_level:
   a. Sum all 'in' movements for this product+warehouse
   b. Sum all 'out' movements for this product+warehouse
   c. Expected = sum(in) - sum(out)
   d. If expected != quantity_on_hand: LOG WARNING (do NOT auto-fix)
   e. Notify admin of discrepancies
```

---

## Notification Commands

### 23. RetryFailedNotifications
**Signature:** `notifications:retry`
**Frequency:** Every 5 minutes
**Logic:**
```
1. Query notification_logs WHERE status = 'failed' AND retry_count < max_retries AND next_retry_at <= now
2. For each: attempt re-send via NotificationService
3. On success: status = 'sent'
4. On failure: increment retry_count, set next_retry_at = now + (2^retry_count * 5 minutes)
```
**Depends on:** Cycle 6 (retry_count, max_retries, next_retry_at columns)

### 24. CleanupExpiredNotifications
**Signature:** `notifications:cleanup`
**Frequency:** Daily at 03:00
**Logic:**
```
1. Delete notification_logs WHERE created_at < today - 90 days AND channel != 'in_app'
2. Delete in_app WHERE read_at IS NOT NULL AND read_at < today - 30 days
3. Log count deleted
```

---

## Audit Commands

### 25. AuditLogArchive
**Signature:** `audit:archive {--days=730 : Archive logs older than N days}`
**Frequency:** Monthly (1st at 03:00)
**Logic:**
```
1. Query audit_logs WHERE is_financial = false AND created_at < today - days
2. Export to CSV/JSON file in storage/app/audit-archives/{year-month}.json
3. Delete archived records
4. Log: "Archived {count} records to {path}"
```
**Note:** Financial audit logs are NEVER archived or deleted.

---

## Document Commands

### 26. DocumentCleanup
**Signature:** `documents:cleanup`
**Frequency:** Weekly (Saturdays at 04:00)
**Logic:**
```
1. Query generated_documents WHERE created_at < today - 30 days AND is_permanent = false
2. Delete associated files from storage
3. Delete DB records
4. Log count
```
**Depends on:** Cycle 6 (generated_documents table)

---

## Registration in routes/console.php

```php
<?php

use Illuminate\Support\Facades\Schedule;

// ══════════════════════════════════════════════
// ATTENDANCE (existing — FIXED name)
// ══════════════════════════════════════════════
Schedule::command('attendance:mark-absent')->hourly();
Schedule::command('attendance:enforce-thresholds')->dailyAt('06:00');
Schedule::command('attendance:daily-alerts')->dailyAt('21:00');
Schedule::command('attendance:trainer-reminder')->everyThirtyMinutes();
Schedule::command('attendance:expire-excuses')->dailyAt('08:00');

// ══════════════════════════════════════════════
// FINANCIAL
// ══════════════════════════════════════════════
Schedule::command('invoices:mark-overdue')->dailyAt('01:00');
Schedule::command('installments:apply-late-fees')->dailyAt('02:00');
Schedule::command('payment-plans:check-defaults')->dailyAt('03:00');
Schedule::command('installments:send-reminders')->dailyAt('08:00');
Schedule::command('wallets:expire-credits')->dailyAt('04:00');
Schedule::command('financial:daily-closing')->dailyAt('00:05');
Schedule::command('financial:reconcile')->weeklyOn(0, '03:00'); // Sunday

// ══════════════════════════════════════════════
// TRAINING
// ══════════════════════════════════════════════
Schedule::command('sessions:generate --days=14')->dailyAt('05:00');
Schedule::command('sessions:send-reminders')->dailyAt('06:30');
Schedule::command('sessions:mark-no-show')->hourly();
Schedule::command('sessions:cancel-low-enrollment')->dailyAt('20:00');
Schedule::command('waitlists:process')->everyTwoHours();
Schedule::command('participants:check-medical')->dailyAt('07:00');

// ══════════════════════════════════════════════
// FACILITY
// ══════════════════════════════════════════════
Schedule::command('facilities:maintenance-check')->dailyAt('06:00');

// ══════════════════════════════════════════════
// INVENTORY
// ══════════════════════════════════════════════
Schedule::command('inventory:low-stock-alerts')->dailyAt('08:00');
Schedule::command('inventory:reorder-suggestions')->weeklyOn(1, '09:00'); // Monday
Schedule::command('inventory:expired-stock')->dailyAt('07:00');
Schedule::command('inventory:count-reminders')->weeklyOn(1, '08:00'); // Monday
Schedule::command('inventory:reconcile')->weeklyOn(0, '02:00'); // Sunday

// ══════════════════════════════════════════════
// NOTIFICATIONS & AUDIT
// ══════════════════════════════════════════════
Schedule::command('notifications:retry')->everyFiveMinutes();
Schedule::command('notifications:cleanup')->dailyAt('03:00');
Schedule::command('audit:cleanup --days=365')->weekly();
Schedule::command('audit:archive --days=730')->monthlyOn(1, '03:00');
Schedule::command('documents:cleanup')->weeklyOn(6, '04:00'); // Saturday
```

---

## Verification

```bash
php artisan schedule:list
# Should show 27+ scheduled commands with correct frequencies

php artisan invoices:mark-overdue --dry-run
php artisan sessions:generate --days=1
php artisan inventory:low-stock-alerts --dry-run
# Each should execute without error
```
