diff --git a/app/Ai/PlatformRules/BlueskyRules.php b/app/Ai/PlatformRules/BlueskyRules.php deleted file mode 100644 index 22f5df4a..00000000 --- a/app/Ai/PlatformRules/BlueskyRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 300, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Bluesky: 300 chars max. Up to 4 images or 1 video. Similar to X but more concise.'; - } -} diff --git a/app/Ai/PlatformRules/Contract.php b/app/Ai/PlatformRules/Contract.php deleted file mode 100644 index 119047d1..00000000 --- a/app/Ai/PlatformRules/Contract.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ - public function specs(): array; - - public function summary(): string; -} diff --git a/app/Ai/PlatformRules/FacebookRules.php b/app/Ai/PlatformRules/FacebookRules.php deleted file mode 100644 index 8e6bf1d0..00000000 --- a/app/Ai/PlatformRules/FacebookRules.php +++ /dev/null @@ -1,39 +0,0 @@ - 10000, - 'max_images' => 10, - 'text_only_allowed' => true, - 'formats' => [ - 'post' => ['max_media' => 10, 'text_only_allowed' => true], - 'reel' => ['aspect_ratio' => '9:16', 'video_only' => true, 'max_duration_seconds' => 90], - 'story' => ['aspect_ratio' => '9:16', 'ephemeral' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Facebook: caption up to 10k chars but short posts perform better. Text-only allowed. -- Post: flexible aspect ratio, up to 10 images/videos. -- Reel: vertical 9:16 video only, up to 90s. -- Story: vertical 9:16, single image/video, ephemeral. -TXT; - } -} diff --git a/app/Ai/PlatformRules/InstagramRules.php b/app/Ai/PlatformRules/InstagramRules.php deleted file mode 100644 index dc844352..00000000 --- a/app/Ai/PlatformRules/InstagramRules.php +++ /dev/null @@ -1,42 +0,0 @@ - 2200, - 'max_images' => 10, - 'max_videos' => 10, - 'text_only_allowed' => false, - 'formats' => [ - 'feed' => ['aspect_ratio' => '1:1 or 4:5', 'media_limit' => 10], - 'carousel' => ['aspect_ratio' => '1:1 or 4:5', 'media_limit' => 10], - 'reel' => ['aspect_ratio' => '9:16', 'video_only' => true, 'max_duration_seconds' => 90], - 'story' => ['aspect_ratio' => '9:16', 'media_limit' => 1, 'ephemeral' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Instagram: caption max 2200 chars. -- Feed: square (1:1) or portrait (4:5). Up to 10 images/videos. -- Carousel: 2-10 slides, all same aspect ratio. First slide is the hook. -- Reel: vertical 9:16 video only, up to 90s (30-60s performs best). Hook in first 1-3s. -- Story: vertical 9:16, single image/video, ephemeral (24h). -TXT; - } -} diff --git a/app/Ai/PlatformRules/LinkedInRules.php b/app/Ai/PlatformRules/LinkedInRules.php deleted file mode 100644 index 4c0c7f35..00000000 --- a/app/Ai/PlatformRules/LinkedInRules.php +++ /dev/null @@ -1,39 +0,0 @@ - 3000, - 'max_images' => 1, - 'max_videos' => 1, - 'text_only_allowed' => true, - 'formats' => [ - 'post' => ['max_media' => 1], - 'carousel' => ['max_slides' => 20, 'optimal_slides' => '8-12', 'images_only' => true, 'document_based' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -LinkedIn: 3000 chars max. First 2-3 lines visible before "see more" — make them compelling. -Professional tone. Educational/how-to content performs well. -- Post: 1 image or 1 video. -- Carousel: PDF-based document, up to 20 slides (8-12 optimal), images only. -TXT; - } -} diff --git a/app/Ai/PlatformRules/MastodonRules.php b/app/Ai/PlatformRules/MastodonRules.php deleted file mode 100644 index 99dd770d..00000000 --- a/app/Ai/PlatformRules/MastodonRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 500, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Mastodon: 500 chars max. Up to 4 images or 1 video. Community-focused, avoid commercial tone.'; - } -} diff --git a/app/Ai/PlatformRules/PinterestRules.php b/app/Ai/PlatformRules/PinterestRules.php deleted file mode 100644 index 11e60f43..00000000 --- a/app/Ai/PlatformRules/PinterestRules.php +++ /dev/null @@ -1,40 +0,0 @@ - 800, - 'max_images' => 5, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'formats' => [ - 'pin' => ['aspect_ratio' => '2:3', 'media_limit' => 1, 'images_only' => true], - 'video_pin' => ['aspect_ratio' => '9:16 or 2:3', 'video_only' => true, 'min_duration_seconds' => 4, 'max_duration_seconds' => 900], - 'carousel' => ['aspect_ratio' => '2:3', 'max_slides' => 5, 'images_only' => true], - ], - ]; - } - - public function summary(): string - { - return <<<'TXT' -Pinterest: title max 100 chars, description max 500 chars. Text overlay on images helps discovery. -- Pin: portrait 2:3 (1000x1500px), single image. -- Video Pin: vertical 9:16 or portrait 2:3, 4s to 15 minutes. -- Carousel: portrait 2:3, up to 5 images. -TXT; - } -} diff --git a/app/Ai/PlatformRules/Registry.php b/app/Ai/PlatformRules/Registry.php deleted file mode 100644 index 11eac343..00000000 --- a/app/Ai/PlatformRules/Registry.php +++ /dev/null @@ -1,53 +0,0 @@ -> - */ - private static array $map = []; - - public static function for(Platform $platform): ?Contract - { - $class = self::$map[$platform->value] ?? null; - - return $class ? new $class : null; - } - - /** - * @param array $platforms - * @return array - */ - public static function forMany(array $platforms): array - { - $rules = []; - - foreach ($platforms as $platform) { - $rule = self::for($platform); - if ($rule) { - $rules[] = $rule; - } - } - - return $rules; - } - - /** - * @param class-string $ruleClass - */ - public static function register(Platform $platform, string $ruleClass): void - { - self::$map[$platform->value] = $ruleClass; - } - - public static function clear(): void - { - self::$map = []; - } -} diff --git a/app/Ai/PlatformRules/ThreadsRules.php b/app/Ai/PlatformRules/ThreadsRules.php deleted file mode 100644 index 9743a473..00000000 --- a/app/Ai/PlatformRules/ThreadsRules.php +++ /dev/null @@ -1,30 +0,0 @@ - 500, - 'max_images' => 10, - 'max_videos' => 10, - 'text_only_allowed' => true, - ]; - } - - public function summary(): string - { - return 'Threads: 500 chars max. Up to 10 images/videos. Conversational tone. Ask questions to drive replies.'; - } -} diff --git a/app/Ai/PlatformRules/TikTokRules.php b/app/Ai/PlatformRules/TikTokRules.php deleted file mode 100644 index f694171b..00000000 --- a/app/Ai/PlatformRules/TikTokRules.php +++ /dev/null @@ -1,31 +0,0 @@ - 2200, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'aspect_ratio' => '9:16', - 'video_only' => true, - ]; - } - - public function summary(): string - { - return 'TikTok: vertical 9:16 video only. 15-60s performs best. Hook in first 1-2s. Trendy, authentic style. Caption max 2200 chars.'; - } -} diff --git a/app/Ai/PlatformRules/XRules.php b/app/Ai/PlatformRules/XRules.php deleted file mode 100644 index 380d5fee..00000000 --- a/app/Ai/PlatformRules/XRules.php +++ /dev/null @@ -1,31 +0,0 @@ - 280, - 'max_images' => 4, - 'max_videos' => 1, - 'text_only_allowed' => true, - 'best_aspect_ratio' => '16:9', - ]; - } - - public function summary(): string - { - return 'X (Twitter): 280 chars max. Up to 4 images OR 1 video. 16:9 landscape performs best. Concise, punchy copy.'; - } -} diff --git a/app/Ai/PlatformRules/YouTubeRules.php b/app/Ai/PlatformRules/YouTubeRules.php deleted file mode 100644 index 17d0ec34..00000000 --- a/app/Ai/PlatformRules/YouTubeRules.php +++ /dev/null @@ -1,33 +0,0 @@ - 5000, - 'max_videos' => 1, - 'text_only_allowed' => false, - 'aspect_ratio' => '9:16', - 'video_only' => true, - 'max_duration_seconds' => 60, - 'requires_title' => true, - ]; - } - - public function summary(): string - { - return 'YouTube Shorts: vertical 9:16 video only, up to 60s. Hook immediately. Loop-friendly content helps. Description max 5000 chars. Title required.'; - } -} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index dbe4bcea..88b3c30b 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -4,19 +4,7 @@ namespace App\Providers; -use App\Ai\PlatformRules\BlueskyRules; -use App\Ai\PlatformRules\FacebookRules; -use App\Ai\PlatformRules\InstagramRules; -use App\Ai\PlatformRules\LinkedInRules; -use App\Ai\PlatformRules\MastodonRules; -use App\Ai\PlatformRules\PinterestRules; -use App\Ai\PlatformRules\Registry; -use App\Ai\PlatformRules\ThreadsRules; -use App\Ai\PlatformRules\TikTokRules; -use App\Ai\PlatformRules\XRules; -use App\Ai\PlatformRules\YouTubeRules; use App\Ai\Providers\ExtendedGeminiProvider; -use App\Enums\SocialAccount\Platform; use App\Listeners\StripeEventListener; use App\Models\AccessToken; use App\Models\Account; @@ -97,7 +85,6 @@ public function boot(): void $this->configureDefaults(); $this->configureMorphMap(); $this->configureAi(); - $this->configurePlatformRules(); $this->configurePostHog(); $this->configureRateLimiting(); $this->configureSocialite(); @@ -136,31 +123,6 @@ protected function configureAi(): void }); } - protected function configurePlatformRules(): void - { - $map = [ - Platform::Instagram->value => InstagramRules::class, - Platform::InstagramFacebook->value => InstagramRules::class, - Platform::Facebook->value => FacebookRules::class, - Platform::X->value => XRules::class, - Platform::TikTok->value => TikTokRules::class, - Platform::YouTube->value => YouTubeRules::class, - Platform::LinkedIn->value => LinkedInRules::class, - Platform::LinkedInPage->value => LinkedInRules::class, - Platform::Threads->value => ThreadsRules::class, - Platform::Pinterest->value => PinterestRules::class, - Platform::Bluesky->value => BlueskyRules::class, - Platform::Mastodon->value => MastodonRules::class, - ]; - - foreach ($map as $value => $class) { - Registry::register( - Platform::from($value), - $class, - ); - } - } - protected function configureMorphMap(): void { Relation::enforceMorphMap([ diff --git a/tests/Feature/Ai/PlatformRules/RegistryTest.php b/tests/Feature/Ai/PlatformRules/RegistryTest.php deleted file mode 100644 index 14cc74bb..00000000 --- a/tests/Feature/Ai/PlatformRules/RegistryTest.php +++ /dev/null @@ -1,61 +0,0 @@ -toBeInstanceOf(Contract::class) - ->and(strlen($rules->summary()))->toBeGreaterThan(30) - ->and($rules->specs())->toBeArray(); -})->with([ - [Platform::Instagram], - [Platform::InstagramFacebook], - [Platform::Facebook], - [Platform::X], - [Platform::TikTok], - [Platform::YouTube], - [Platform::LinkedIn], - [Platform::LinkedInPage], - [Platform::Threads], - [Platform::Pinterest], - [Platform::Bluesky], - [Platform::Mastodon], -]); - -test('forMany returns rules for the given platforms in order', function () { - $rules = Registry::forMany([Platform::X, Platform::Instagram, Platform::TikTok]); - - expect($rules)->toHaveCount(3); - expect($rules[0]->platform())->toBe(Platform::X); - expect($rules[1]->platform())->toBe(Platform::Instagram); - expect($rules[2]->platform())->toBe(Platform::TikTok); -}); - -test('forMany skips platforms with no registered rules', function () { - Registry::clear(); - Registry::register(Platform::X, XRules::class); - - $rules = Registry::forMany([Platform::X, Platform::Instagram]); - - expect($rules)->toHaveCount(1); - expect($rules[0]->platform())->toBe(Platform::X); -}); - -test('X rules summary mentions 280 char limit', function () { - expect(Registry::for(Platform::X)->summary())->toContain('280'); -}); - -test('Instagram rules summary mentions 9:16 for Reel', function () { - expect(Registry::for(Platform::Instagram)->summary())->toContain('9:16'); -}); - -test('TikTok rules specs indicate video only', function () { - expect(Registry::for(Platform::TikTok)->specs()['video_only'])->toBeTrue(); -});