option('limit')); $posts = SocialPost::query() ->where('status', 'scheduled') ->where('requires_approval', false) ->whereNotNull('scheduled_at') ->where('scheduled_at', '<=', now()) ->where(function ($q) { $q->whereNull('next_retry_at') ->orWhere('next_retry_at', '<=', now()); }) ->with('user.socialPlatformCredentials') ->orderBy('scheduled_at') ->limit($limit) ->get(); if ($posts->isEmpty()) { $this->info('Tidak ada scheduled post yang siap publish.'); return self::SUCCESS; } foreach ($posts as $post) { $setting = $this->settingFor($post->user_id); [$allowed, $limitMessage] = $this->canPublishNow($post->user_id, $post->platform, $setting); if (! $allowed) { $this->warn("[{$post->id}] skip: {$limitMessage}"); continue; } $credential = $post->user->socialPlatformCredentials ->firstWhere('platform', $post->platform); if (! $credential) { $this->markFailure($post, 'Credential platform tidak ditemukan.', $setting); $this->warn("[{$post->id}] gagal: credential {$post->platform} tidak ada."); continue; } try { if ($mediaPreparation->prepareForPublishing($post)) { $post->refresh(); } $result = $publisher->publish($post, $credential); $currentMeta = is_array($post->meta) ? $post->meta : []; $currentMeta['publish_payload'] = $result['payload'] ?? []; $post->update([ 'status' => 'posted', 'posted_at' => now(), 'approved_at' => $post->approved_at ?? now(), 'external_post_id' => $result['external_post_id'] ?? null, 'error_message' => null, 'meta' => $currentMeta, 'next_retry_at' => null, ]); $this->info("[{$post->id}] posted ke {$post->platform}."); } catch (\Throwable $e) { $this->markFailure($post, $e->getMessage(), $setting); $this->error("[{$post->id}] gagal: {$e->getMessage()}"); } } return self::SUCCESS; } private function settingFor(int $userId): SocialAutomationSetting { return SocialAutomationSetting::query()->firstOrCreate( ['user_id' => $userId], [ 'approval_mode' => 'manual', 'daily_limit' => 20, 'platform_daily_limits' => [ 'facebook' => 8, 'instagram' => 8, 'tiktok' => 4, 'x' => 12, ], 'max_retries' => 2, 'retry_delay_minutes' => 15, ] ); } private function canPublishNow(int $userId, string $platform, SocialAutomationSetting $setting): array { $today = now()->toDateString(); $postedToday = SocialPost::query() ->where('user_id', $userId) ->where('status', 'posted') ->whereDate('posted_at', $today) ->count(); if ($postedToday >= $setting->daily_limit) { return [false, 'batas harian total tercapai']; } $platformLimits = $setting->platform_daily_limits ?? []; $platformLimit = (int) ($platformLimits[$platform] ?? 0); if ($platformLimit > 0) { $postedPerPlatform = SocialPost::query() ->where('user_id', $userId) ->where('platform', $platform) ->where('status', 'posted') ->whereDate('posted_at', $today) ->count(); if ($postedPerPlatform >= $platformLimit) { return [false, "batas harian {$platform} tercapai"]; } } return [true, null]; } private function markFailure(SocialPost $post, string $message, SocialAutomationSetting $setting): void { $attempt = $post->attempt_count + 1; $maxRetries = max(0, (int) $setting->max_retries); if ($attempt <= $maxRetries) { $post->update([ 'status' => 'scheduled', 'attempt_count' => $attempt, 'next_retry_at' => now()->addMinutes(max(1, (int) $setting->retry_delay_minutes)), 'error_message' => $message, ]); return; } $post->update([ 'status' => 'failed', 'attempt_count' => $attempt, 'error_message' => $message, 'next_retry_at' => null, ]); } }