| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/client-portal-laravel/releases/20260626120707/routes/ |
Upload File : |
<?php
use App\Jobs\CacheDashboardStatsJob;
use App\Jobs\Calendar\RefreshCalendarWebhooksJob;
use App\Jobs\Calendar\SyncAllCalendarsJob;
use App\Jobs\CheckOverdueInvoicesJob;
use App\Jobs\CheckStaleAuditItemsJob;
use App\Jobs\CleanupExpiredBackupsJob;
use App\Jobs\CleanupOldActivityLogsJob;
use App\Jobs\CleanupOldDeliveriesJob;
use App\Jobs\PermanentlyDeleteOldRecordsJob;
use App\Jobs\ProcessRecurringInvoicesJob;
use App\Jobs\PublishScheduledBlogPostsJob;
use App\Jobs\RetryFailedWebhooksJob;
use App\Jobs\SendAuditDeadlineRemindersJob;
use App\Jobs\SendAuditWeeklyDigestJob;
use App\Jobs\SendInvoiceRemindersJob;
use App\Jobs\SendOverdueAuditDigestJob;
use App\Jobs\SendScheduledAuditReportsJob;
use App\Jobs\Social\AbandonOldPendingSharesJob;
use App\Jobs\Social\CleanupOrphanedSharesJob;
use App\Jobs\Social\CleanupSocialAuditLogJob;
use App\Jobs\Social\ProcessPendingSocialSharesJob;
use App\Services\SystemHealthService;
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
/*
|--------------------------------------------------------------------------
| Scheduled Tasks
|--------------------------------------------------------------------------
|
| Define the application's command schedule. These tasks are run
| automatically by the scheduler when `php artisan schedule:run` is
| executed (typically via cron: * * * * * php artisan schedule:run).
|
*/
// Check for overdue invoices daily at 8am
Schedule::job(new CheckOverdueInvoicesJob)
->dailyAt('08:00')
->name('check-overdue-invoices')
->withoutOverlapping()
->onOneServer();
// Send invoice reminders daily at 9am
Schedule::job(new SendInvoiceRemindersJob)
->dailyAt('09:00')
->name('send-invoice-reminders')
->withoutOverlapping()
->onOneServer();
// Send audit deadline reminders daily at 9:30am
Schedule::job(new SendAuditDeadlineRemindersJob)
->dailyAt('09:30')
->name('send-audit-deadline-reminders')
->withoutOverlapping()
->onOneServer();
// Send scheduled audit due reminders daily at 9:45am
Schedule::command('audits:send-reminders')
->dailyAt('09:45')
->name('send-scheduled-audit-reminders')
->withoutOverlapping()
->onOneServer();
// Check for stale audit items daily at 10am
Schedule::job(new CheckStaleAuditItemsJob)
->dailyAt('10:00')
->name('check-stale-audit-items')
->withoutOverlapping()
->onOneServer();
// Send audit weekly digest on Monday at 8am (for clients)
Schedule::job(new SendAuditWeeklyDigestJob)
->weeklyOn(1, '08:00')
->name('send-audit-weekly-digest')
->withoutOverlapping()
->onOneServer();
// Send overdue audit digest to admins on Monday at 8:30am
Schedule::job(new SendOverdueAuditDigestJob)
->weeklyOn(1, '08:30')
->name('send-overdue-audit-digest')
->withoutOverlapping()
->onOneServer();
// Send remediation overdue notifications daily at 10:15am
Schedule::command('audit:send-remediation-overdue')
->dailyAt('10:15')
->name('send-remediation-overdue')
->withoutOverlapping()
->onOneServer();
// Send scheduled audit reports every 15 minutes
Schedule::job(new SendScheduledAuditReportsJob)
->everyFifteenMinutes()
->name('send-scheduled-audit-reports')
->withoutOverlapping()
->onOneServer();
// Process recurring invoices daily at 7am
Schedule::job(new ProcessRecurringInvoicesJob)
->dailyAt('07:00')
->name('process-recurring-invoices')
->withoutOverlapping()
->onOneServer();
// Cache dashboard stats daily at 6am
Schedule::job(new CacheDashboardStatsJob)
->dailyAt('06:00')
->name('cache-dashboard-stats')
->withoutOverlapping()
->onOneServer();
// Cleanup old activity logs monthly on the 1st at 2am
Schedule::job(new CleanupOldActivityLogsJob)
->monthlyOn(1, '02:00')
->name('cleanup-activity-logs')
->withoutOverlapping()
->onOneServer();
// Prune Prunable models nightly (JobMetric >14d, ApiUsageLog >30d)
Schedule::command('model:prune')
->dailyAt('02:30')
->name('prune-models')
->withoutOverlapping()
->onOneServer();
// Permanently delete soft-deleted records quarterly at 3am
Schedule::job(new PermanentlyDeleteOldRecordsJob)
->quarterly()
->at('03:00')
->name('cleanup-soft-deletes')
->withoutOverlapping()
->onOneServer();
// Prune old notifications daily at 4am
Schedule::command('notifications:prune --days=90')
->dailyAt('04:00')
->name('prune-notifications')
->withoutOverlapping()
->onOneServer();
// Prune stale batches weekly on Sunday at 5am
Schedule::command('queue:prune-batches --hours=48')
->weeklyOn(0, '05:00')
->name('prune-batches')
->withoutOverlapping()
->onOneServer();
// Check state duration workflows hourly (questionnaire reminders, follow-ups, etc.)
Schedule::command('workflows:check-state-duration')
->hourly()
->name('check-state-duration-workflows')
->withoutOverlapping()
->onOneServer();
// Retry failed webhooks every 5 minutes
Schedule::job(new RetryFailedWebhooksJob)
->everyFiveMinutes()
->name('retry-failed-webhooks')
->withoutOverlapping()
->onOneServer();
// Send calendar event reminders every 15 minutes (Plan 224 unified command)
Schedule::command('calendar:send-reminders')
->everyFifteenMinutes()
->name('send-calendar-reminders')
->withoutOverlapping()
->onOneServer();
// Cleanup old webhook deliveries daily at 3am
Schedule::job(new CleanupOldDeliveriesJob)
->dailyAt('03:00')
->name('cleanup-webhook-deliveries')
->withoutOverlapping()
->onOneServer();
// Cleanup expired backups daily at 4:30am
Schedule::job(new CleanupExpiredBackupsJob)
->dailyAt('04:30')
->name('cleanup-expired-backups')
->withoutOverlapping()
->onOneServer();
// Publish scheduled blog posts every 5 minutes
Schedule::job(new PublishScheduledBlogPostsJob)
->everyFiveMinutes()
->name('publish-scheduled-blog-posts')
->withoutOverlapping()
->onOneServer();
// Record scheduler health check every minute
Schedule::call(fn () => SystemHealthService::recordSchedulerRun())
->everyMinute()
->name('record-scheduler-health');
// Sync Google Calendar connections every 15 minutes
Schedule::job(new SyncAllCalendarsJob)
->everyFifteenMinutes()
->name('sync-google-calendars')
->withoutOverlapping()
->onOneServer();
// Refresh Google Calendar webhooks daily at 3am
Schedule::job(new RefreshCalendarWebhooksJob)
->dailyAt('03:00')
->name('refresh-calendar-webhooks')
->withoutOverlapping()
->onOneServer();
/*
|--------------------------------------------------------------------------
| Social Media Sharing (Plan 227)
|--------------------------------------------------------------------------
*/
// Process pending social shares every minute
Schedule::job(new ProcessPendingSocialSharesJob)
->everyMinute()
->name('process-pending-social-shares')
->withoutOverlapping()
->onOneServer();
// Abandon old pending shares daily at 11pm
Schedule::job(new AbandonOldPendingSharesJob)
->dailyAt('23:00')
->name('abandon-old-social-shares')
->withoutOverlapping()
->onOneServer();
// Cleanup social audit logs weekly on Sunday at 4am
Schedule::job(new CleanupSocialAuditLogJob)
->weeklyOn(0, '04:00')
->name('cleanup-social-audit-logs')
->withoutOverlapping()
->onOneServer();
// Cleanup orphaned share records weekly on Sunday at 4:30am
Schedule::job(new CleanupOrphanedSharesJob)
->weeklyOn(0, '04:30')
->name('cleanup-orphaned-shares')
->withoutOverlapping()
->onOneServer();
// Cleanup orphaned records (null FKs) daily at 3am
Schedule::command('cleanup:orphaned-records')
->dailyAt('03:00')
->name('cleanup-orphaned-records')
->withoutOverlapping()
->onOneServer();
/*
|--------------------------------------------------------------------------
| Package Download Sync (Plan 263)
|--------------------------------------------------------------------------
*/
// Sync package download counts every hour, but only run when enabled and frequency matches
Schedule::job(new \App\Jobs\SyncPackageDownloadsJob)
->hourly()
->when(function () {
try {
if (! \App\Models\Settings\SystemSetting::getValue('packages.sync_enabled', false)) {
return false;
}
$frequency = \App\Models\Settings\SystemSetting::getValue('packages.sync_frequency', 'daily');
$hour = (int) now()->format('G');
return match ($frequency) {
'hourly' => true,
'every_6_hours' => $hour % 6 === 0,
'every_12_hours' => $hour % 12 === 0,
'daily' => $hour === 6,
'weekly' => $hour === 6 && now()->isDayOfWeek(1),
default => $hour === 6,
};
} catch (\Throwable) {
return false;
}
})
->name('sync-package-downloads')
->withoutOverlapping()
->onOneServer();
/*
|--------------------------------------------------------------------------
| Package Version Sync (Plan 264)
|--------------------------------------------------------------------------
| Runs daily at 07:00 when sync is enabled. Iterates active packages and
| dispatches a unique per-package SyncPackageVersionsJob.
*/
Schedule::call(function () {
if (! \App\Models\Settings\SystemSetting::getValue('packages.sync_enabled', false)) {
return;
}
if (! config('packages.sync_v2_enabled', false)) {
return;
}
foreach (\App\Models\Package::active()
->whereNotIn('registry', [
\App\Enums\PackageRegistry::Go->value,
\App\Enums\PackageRegistry::Maven->value,
\App\Enums\PackageRegistry::SPI->value,
])
->get(['id']) as $package) {
\App\Jobs\SyncPackageVersionsJob::dispatch($package->id);
}
})
->dailyAt('07:00')
->name('sync-package-versions')
->withoutOverlapping()
->onOneServer();
/*
|--------------------------------------------------------------------------
| Websites product (Plan 270 + Plan 271 §1.4)
|--------------------------------------------------------------------------
| The Phase 5/6/7 jobs whose docstrings claim cron cadences are wired here.
| Without these registrations the jobs silently never ran in production
| (scheduled content never published, TenantCostGuard read sum=0, domain
| health never checked, missed Stripe deliveries never replayed).
*/
Schedule::job(new \App\Jobs\Websites\PublishScheduledContentJob)
->everyMinute()
->name('websites-publish-scheduled-content')
->withoutOverlapping()
->onOneServer();
Schedule::job(new \App\Jobs\Websites\TenantCostReportJob)
->dailyAt('00:15')
->name('websites-tenant-cost-report')
->withoutOverlapping()
->onOneServer();
Schedule::command('websites:dispatch-domain-health-checks')
->everySixHours()
->name('websites-dispatch-domain-health-checks')
->withoutOverlapping()
->onOneServer();
Schedule::command('websites:reconcile-stripe-events', ['--hours=24'])
->dailyAt('00:30')
->name('websites-reconcile-stripe-events')
->withoutOverlapping()
->onOneServer();
Schedule::job(new \App\Jobs\Websites\WebsiteDataExportPurgeJob)
->dailyAt('02:00')
->name('websites-purge-data-exports')
->withoutOverlapping()
->onOneServer();
// Plan 272 §A.2 — hourly evaluator that turns TenantCostGuard's
// action ladder into actual notifications.
Schedule::job(new \App\Jobs\Websites\EvaluateTenantCostGuardJob)
->hourly()
->name('websites-evaluate-tenant-cost-guard')
->withoutOverlapping()
->onOneServer();
// Plan 272 §B.1 — daily fan-out for GDPR Article 17 deletions
// whose 30-day grace window has expired.
Schedule::job(new \App\Jobs\Websites\DispatchExpiredDataDeletionsJob)
->dailyAt('03:00')
->name('websites-dispatch-expired-data-deletions')
->withoutOverlapping()
->onOneServer();
// Plan 273 §D.1 — 30-min safety-net Apache SNI regeneration. Event-
// driven dispatches handle the common path; this catches misfires.
Schedule::job(new \App\Jobs\Websites\RegenerateSSLDomainsConfJob)
->everyThirtyMinutes()
->name('websites-regenerate-ssl-domains-conf')
->withoutOverlapping()
->onOneServer();
// Plan 273 §D.3 — nightly reconciliation of (websites.status=live AND
// domains.cert_status=active) against the rendered SNI snippet. Removal
// is dry-run by default per the plan; mismatches land in
// host_reconciliation_findings for admin review.
Schedule::job(new \App\Jobs\Websites\ReconcileActiveHostsJob)
->dailyAt('03:30')
->name('websites-reconcile-active-hosts')
->withoutOverlapping()
->onOneServer();
// Plan 273 §G.3 — daily per-tenant search-backend parity check. The
// admin tool reads the 7-day rolling window from
// search_parity_observations and flips reads for tenants with >= 99%
// parity for 7 consecutive days.
Schedule::job(new \App\Jobs\Websites\SearchParityCheckJob)
->dailyAt('04:00')
->name('websites-search-parity-check')
->withoutOverlapping()
->onOneServer();
// Plan 277 §B.5 — 5-min audit of published addons missing Stripe Price
// IDs + aging unresolved TBD submissions. Emits JSON + Slack alerts.
Schedule::command('websites:addons:audit-stripe-state --notify')
->everyFiveMinutes()
->name('websites-audit-addon-stripe-state')
->withoutOverlapping()
->onOneServer();
// Plan 275 §275-E — daily reconciliation of redemption_count drift.
// Detect-and-alert only; never auto-mutates.
Schedule::command('websites:reconcile-promotion-counts')
->dailyAt('05:00')
->name('websites-reconcile-promotion-counts')
->withoutOverlapping()
->onOneServer();
// Plan 275 §275-E — hourly active-count gauge.
Schedule::command('websites:emit-promotion-metrics')
->hourly()
->name('websites-emit-promotion-metrics')
->withoutOverlapping()
->onOneServer();
// Plan 275 §275-E — alert ops when a promotion's sync_error has been
// sticky for > 15 min (auto-retry exhausted).
Schedule::command('websites:alert-stale-sync-errors')
->everyFiveMinutes()
->name('websites-alert-stale-sync-errors')
->withoutOverlapping()
->onOneServer();
// Plan 278-C.3 — daily post-launch 30-day check-in milestone trigger.
Schedule::command('milestones:complete-30day-checkin')
->dailyAt('02:00')
->name('milestones-30day-checkin')
->withoutOverlapping(60)
->onOneServer();
/*
|--------------------------------------------------------------------------
| Customer-facing custom domain lifecycle (Plan 279)
|--------------------------------------------------------------------------
*/
// Plan 279 §C.2 — daily backstop for the long-delayed FinalizeDomainRemovalJob.
Schedule::command('websites:finalize-removed-domains')
->dailyAt('04:45')
->name('websites-finalize-removed-domains')
->withoutOverlapping()
->onOneServer();
// Plan 279 §B.3 — daily reconciliation of the denormalized custom_domain.
Schedule::command('websites:check-custom-domain-drift')
->dailyAt('05:30')
->name('websites-check-custom-domain-drift')
->withoutOverlapping()
->onOneServer();
// Plan 279 §A.5 — daily ownership re-verification dispatcher.
Schedule::call(function () {
$intervalDays = (int) config('websites.custom_domain.reverify_interval_days', 30);
\App\Models\Websites\Domain::query()
->reverifyDue($intervalDays)
->cursor()
->each(fn ($domain) => \App\Jobs\Websites\ReverifyDomainOwnershipJob::dispatch($domain->id));
})
->dailyAt('06:15')
->name('websites-dispatch-reverify-domains')
->withoutOverlapping()
->onOneServer();
// Plan 279 §E.1 — daily T-7/T-3/T-1 reminder fan-out for domains
// in the removal grace window.
Schedule::command('websites:send-domain-grace-reminders')
->dailyAt('07:30')
->name('websites-send-domain-grace-reminders')
->withoutOverlapping()
->onOneServer();
/*
|--------------------------------------------------------------------------
| Staging environment (PLAN-281 §5.5)
|--------------------------------------------------------------------------
| Gated on APP_ENV=staging so the same code can ship to both environments
| without registering staging-only schedules on production. Laravel 11
| puts schedules in routes/console.php; the plan's reference to
| app/Console/Kernel.php is from a pre-11 template.
*/
if (app()->environment('staging')) {
// Daily 30-day retention purge — keeps audit/job/webhook/api/cache/session
// growth bounded on the shared EC2 disk. Sends a Healthchecks.io ping
// when PURGE_HC_URL is set so a silently-failing purge alerts
// within 24h.
//
// Gated on staging because the underlying command (staging:purge-old-data)
// is staging-only.
Schedule::command('staging:purge-old-data --days=30')
->dailyAt('02:00')
->name('staging-purge-old-data')
->withoutOverlapping()
->onOneServer();
}
/*
|--------------------------------------------------------------------------
| Healthchecks.io scheduler heartbeat (any environment)
|--------------------------------------------------------------------------
| Pings every 10 min so a stalled scheduler/cron is detected within the
| HC check's grace window (30 min recommended). Useful on any environment;
| set SCHEDULER_HC_URL in the env's .env to enable. Without the env var
| set, no schedule is registered (clean no-op).
|
| Originally added under PLAN-281 §9.3 for staging, made env-agnostic
| 2026-06-25 — prod scheduler stalling has higher customer impact than
| staging's, and there's no reason to keep this feature behind the
| environment gate.
|
| Reads via config() not env() — after `php artisan config:cache`,
| env() outside config files returns null.
*/
$schedulerHcUrl = (string) config('healthchecks.scheduler_url', '');
if ($schedulerHcUrl !== '') {
Schedule::call(function () use ($schedulerHcUrl) {
\Illuminate\Support\Facades\Http::timeout(5)->get($schedulerHcUrl);
})
->everyTenMinutes()
->name('scheduler-heartbeat')
->withoutOverlapping();
}