File: /home/xedaptot/ai.naniguide.com/routes/console.php
<?php
use Illuminate\Support\Facades\Schedule;
use App\Model\Automation2;
use App\Notifications\System\BackendErrorOccurred;
use App\Services\Notifications\Notifier;
use App\Cashier\Cashier;
use App\Model\Subscription;
use App\Model\Setting;
use App\Model\Campaign;
use App\Model\Customer;
use Laravel\Tinker\Console\TinkerCommand;
use App\Library\Facades\SubscriptionFacade;
use App\Helpers\LicenseHelper;
// Call \Artisan::call(...) in the application will exit(). tmp fixed
// if (isInitiated()) {
// exit();
// }
if (isInitiated()) {
// Make sure CLI process is NOT executed as root
/*
Notification::recordIfFails(function () {
if (!exec_enabled()) {
throw new Exception('The exec() function is missing or disabled on the hosting server');
}
if (exec('whoami') == 'root') {
throw new Exception("Cronjob process is executed by 'root' which might cause permission issues. Make sure the cronjob process owner is the same as the acellemail/ folder's owner");
}
}, 'CronJob issue');
// Make sure CLI process is NOT executed as root
Notification::recordIfFails(function () {
$minPHPRecommended = config('custom.php_recommended');
if (!version_compare(PHP_VERSION, $minPHPRecommended, '>=')) {
throw new Exception(trans('messages.requirement.php_version.not_supuported.description', ['current' => PHP_VERSION, 'required' => $minPHPRecommended]));
}
}, $phpMsgTitle = trans('messages.requirement.php_version.not_supuported.title'));
*/
Schedule::call(function () {
event(new \App\Events\CronJobExecuted());
})->name('cronjob_event:log')->everyMinute();
// Echo okie
Schedule::command('automation:dispatch')->everyFiveMinutes();
// Bounce/feedback handler
Schedule::command('handler:run')->everyThirtyMinutes();
// Sender verifying
Schedule::command('sender:verify')->everyFiveMinutes();
// System clean up
Schedule::command('system:cleanup')->daily();
// Re verify tracking domains
// No longer verify tracking domains periodically, it might cause a bulk update that
// makes all schedule set back to "unverified" (network issue for example)
// Schedule::command('tracking-domains:verify')->weekly()->sundays()->at('13:00');
// GeoIp database check
Schedule::command('geoip:check')->everyMinute()->withoutOverlapping(60);
// Subscription: check expiration. On failure, mirror to the admin
// shared inbox so it's visible alongside other system alerts. On
// success the same group_key is cleared so the banner auto-clears.
Schedule::call(function () {
try {
SubscriptionFacade::endExpiredSubscriptions();
SubscriptionFacade::createRenewOrders();
SubscriptionFacade::autoChargeRenewOrders();
app(Notifier::class)->clearGroup('subscription-monitor');
} catch (\Throwable $e) {
\Log::warning('subscription:monitor failed', ['error' => $e->getMessage()]);
app(Notifier::class)->shareWithAdmins(new BackendErrorOccurred($e));
}
})->name('subscription:monitor')->hourly()->withoutOverlapping(60);
// // Remote subscription sync: primary mechanism to keep local ↔ remote in sync.
// // Runs every 5 minutes, only syncs subscriptions not synced in the last hour.
// // Webhooks are secondary — if they fail, this catches everything.
// Schedule::command('remote:sync --all --scheduled')
// ->name('remote:sync')
// ->everyFiveMinutes()
// ->withoutOverlapping(10);
// Check for scheduled campaign to execute
Schedule::command('campaign:schedule')->everyMinute();
// R7 — Ads pipeline maintenance.
// Metrics pull runs every `ads.metrics_sync_interval` minutes (default 15).
// Reconcile log sweep runs every `ads.reconcile_interval` minutes (default 5).
// Both jobs are no-ops when the customer has no connected ad platforms,
// and mock-mode installs still exercise the full pipeline against the
// decorator chain so customers can verify their Horizon dashboards.
Schedule::job(new App\Jobs\Ads\PullAdMetrics())
->name('ads:pull-metrics')
->cron('*/' . (int) config('ads.metrics_sync_interval', 15) . ' * * * *')
->withoutOverlapping(30);
Schedule::job(new App\Jobs\Ads\ReconcileAdPublishLog())
->name('ads:reconcile-publish-logs')
->cron('*/' . (int) config('ads.reconcile_interval', 5) . ' * * * *')
->withoutOverlapping(10);
// R11.4 — Ads retention prune. Runs daily at 03:00 to delete ad_leads /
// ad_metrics / ad_activity_logs / ad_publish_logs rows older than the
// configured TTL (`config/ads.php` retention.* — set 0 to disable).
Schedule::command('ads:prune-retention')
->name('ads:prune-retention')
->dailyAt('03:00')
->withoutOverlapping(60);
// R14 — A/B test winner detection. Hourly sweep over every running
// test; writes one AdAbTestEvaluationLog row per test (always — that's
// the audit trail), declares winner + auto-pauses losers when the
// computed confidence meets the test's threshold. Cadence is hourly
// because metric pulls (PullAdMetrics) run every 15min, so an hourly
// evaluator stays one full data refresh behind the freshest metrics.
Schedule::job(new App\Jobs\Ads\EvaluateAdAbTests())
->name('ads:evaluate-ab-tests')
->hourly()
->withoutOverlapping(60);
// R15.1 — Product catalog auto-sync sweep. Daily fan-out: finds every
// catalog bound to an ad_platform_id whose source supports auto-refresh
// (anything except `manual`) and whose last_synced_at is older than
// `ads.catalog.sweep_min_age_hours` (default 24h), then dispatches one
// SyncAdProductCatalog job per row. Sweeper is safe to run with all
// R15 flags OFF — fan-out jobs do nothing when service guard rejects.
Schedule::job(new App\Jobs\Ads\SweepProductCatalogSync())
->name('ads:sweep-catalog-sync')
->dailyAt('04:00')
->withoutOverlapping(60);
$licenseTask = Schedule::call(function () {
try {
$license = LicenseHelper::getCurrentLicense();
if (is_null($license)) {
throw new Exception(trans('messages.license.error.no_license'));
}
LicenseHelper::refreshLicense();
app(Notifier::class)->clearGroup('verify-license');
} catch (\Throwable $e) {
\Log::warning('verify_license failed', ['error' => $e->getMessage()]);
app(Notifier::class)->shareWithAdmins(new BackendErrorOccurred($e));
}
})->name('verify_license');
if (config('custom.japan')) {
$licenseTask->everyMinute();
} else {
$licenseTask->weeklyOn(rand(1, 6), '10:'.rand(10, 59)); // randomly from Mon to Sat, at 10:10 - 10:59
}
// Rerun "sending" campaigns that are stuck for too long
Schedule::command('campaign:rerun')->everyTenMinutes();
// AI observability schedules (`ai:obs:retention` / `ai:obs:rollup-refresh`
// / `ai:obs:classify-sentiment`) are registered by the acelle/ai
// plugin's ServiceProvider — see `Acelle\Ai\ServiceProvider::registerSchedules()`.
// When the plugin is removed the schedules disappear with it.
// Launch more processes for priority campaigns
Schedule::command('queue:adjust')->everyMinute();
/*
// Update list/user cache every 30 minutes
// @important: potential performance issue here
Schedule::call(function() {
$customers = Customer::all();
foreach($customers as $customer) {
if (is_null($customer->getCurrentActiveSubscription())) {
continue;
}
$lists = $customer->local()->lists;
foreach ($lists as $list) {
dispatch(new \App\Jobs\UpdateMailListJob($list));
}
dispatch(new \App\Jobs\UpdateUserJob($customer));
}
})->name('update_list_stats')->daily();
*/
// Queued import/export/campaign
// Allow overlapping: max 10 proccess as a given time (if cronjob interval is every minute)
// Job is killed after timeout
// Notice that the following queue must be exected on master (involved in local filesystem on the same web server)
// + default
// + import
//
if (!config('custom.distributed_worker')) {
Schedule::command('queue:work --queue=import,default,high,batch,single,automation-dispatch,automation --tries=1 --max-time=180')->everyMinute();
}
}