# Research: Backup Scheduling MVP (032) **Date**: 2026-01-05 This document resolves technical decisions and clarifies implementation approach for Feature 032. ## Decisions ### 1) Reuse existing sync + backup services - **Decision**: Use `App\Services\Intune\PolicySyncService::syncPoliciesWithReport(Tenant $tenant, ?array $supportedTypes = null): array` and `App\Services\Intune\BackupService::createBackupSet(...)`. - **Rationale**: These are already tenant-aware, use `GraphClientInterface` behind the scenes (via `PolicySyncService`), and `BackupService` already writes a `backup.created` audit log entry. - **Alternatives considered**: - Implement new Graph calls directly in the scheduler job → rejected (violates Graph abstraction gate; duplicates logic). ### 2) Policy type source of truth + validation - **Decision**: - Persist `backup_schedules.policy_types` as `array` of **type keys** present in `config('tenantpilot.supported_policy_types')`. - **Hard validation at save-time**: unknown keys are rejected. - **Runtime defensive check** (legacy/DB): unknown keys are skipped. - If ≥1 valid type remains → run becomes `partial` and `error_code=UNKNOWN_POLICY_TYPE`. - If 0 valid types remain → run becomes `skipped` and `error_code=UNKNOWN_POLICY_TYPE` (no `BackupSet` created). - **Rationale**: Prevent silent misconfiguration and enforce fail-safe behavior at entry points, while still handling legacy data safely. - **Alternatives considered**: - Save unknown keys and ignore silently → rejected (silent misconfiguration). - Fail the run for any unknown type → rejected (too brittle for legacy). ### 3) Graph calls and contracts - **Decision**: Do not hardcode Graph endpoints. All Graph access happens via `GraphClientInterface` (through `PolicySyncService` and `BackupService`). - **Rationale**: Matches constitution requirements and existing code paths. - **Alternatives considered**: - Calling `deviceManagement/{type}` directly → rejected (explicitly forbidden by constitution; also unsafe for unknown types). ### 4) Scheduling mechanism - **Decision**: Add an Artisan command `tenantpilot:schedules:dispatch` and register it with Laravel scheduler to run every minute. - **Rationale**: Fits Laravel 12 structure (no Kernel), supports Dokploy operation models (`schedule:run` cron or `schedule:work`). - **Alternatives considered**: - Long-running daemon polling DB directly → rejected (less idiomatic; harder ops). ### 5) Due calculation + time semantics - **Decision**: - `scheduled_for` is minute-slot based and stored in UTC. - Due calculation uses the schedule timezone. - DST (MVP): invalid local time → skip; ambiguous local time → first occurrence. - **Rationale**: Predictable and testable; avoids “surprise catch-up”. - **Alternatives considered**: - Catch-up missed slots → rejected by spec (MVP explicitly “no catch-up”). ### 6) Idempotency + concurrency - **Decision**: - DB unique constraint: `(backup_schedule_id, scheduled_for)`. - Cache lock per schedule (`lock:backup_schedule:{id}`) to prevent parallel execution. - If lock held, do not run in parallel: mark run `skipped` with a clear error_code. - **Rationale**: Prevents double runs and provides deterministic behavior. - **Alternatives considered**: - Only cache lock (no DB constraint) → rejected (less robust under crashes/restarts). ### 7) Retry/backoff policy - **Decision**: - Transient/throttling failures (e.g. 429/503) → retries with backoff. - Auth/permission failures (401/403) → no retry. - Unknown failures → limited retries, then fail. - **Rationale**: Avoid noisy retry loops for non-recoverable errors. ### 8) Audit logging - **Decision**: Use `App\Services\Intune\AuditLogger` for: - dispatch cycle (optional aggregated) - run start + completion - retention applied (count deletions) - **Rationale**: Constitution requires audit log for every operation; existing `BackupService` already writes `backup.created`. ### 9) Notifications - **Decision**: Only interactive actions (Run now / Retry) notify the acting user (database notifications). Scheduled runs rely on Run history. - **Rationale**: Avoid undefined “who gets notified” without adding new ownership fields. ## Open Items None blocking Phase 1 design.