| namespace App\Console\Commands; | |
| use App\Models\SocialAutomationSetting; | |
| use App\Models\SocialPost; | |
| use App\Services\SocialPostMediaPreparationService; | |
| use App\Services\SocialPublisherService; | |
| use Illuminate\Console\Command; | |
| class PublishScheduledSocialPosts extends Command | |
| { | |
| protected $signature = 'social:publish-scheduled {--limit=20 : Jumlah post yang diproses per run}'; | |
| protected $description = 'Publish social posts yang status scheduled dan waktunya sudah lewat.'; | |
| public function handle( | |
| SocialPublisherService $publisher, | |
| SocialPostMediaPreparationService $mediaPreparation | |
| ): int | |
| { | |
| $limit = max(1, (int) $this->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, | |
| ]); | |
| } | |
| } | |