2026-01-15 01:13:44 +00:00
|
|
|
<?php
|
|
|
|
|
|
refactor: settings redesign, Spanish translations, language system, strict_types
Settings pages:
- Redesign layout to match Sendkit (max-w-4xl, space-y-12, Separator sections)
- Merge Members page into Workspace settings with Table, invite Dialog, ConfirmDeleteModal
- Add workspace logo upload/delete routes and controller methods
- Translate all hardcoded strings in Workspace.vue modals
Language system:
- Drop languages table, replace language_id FK with locale string column on users
- Create config/languages.php for available languages and default locale
- Add Spanish (es) translations (13 files)
- Simplify HandleInertiaRequests, ProfileController, RegisteredUserController
Code quality:
- Add declare(strict_types=1) to all PHP files
- Fix MastodonPublisher using wrong attribute (filename -> original_filename)
- Fix HasMediaTest for new has_photo/photo_url accessors
- Fix PublishToSocialPlatformTest type error revealed by strict_types
- Remove orphaned Language model from AppServiceProvider morph map
- Update User TypeScript interface (has_photo, photo_url, locale)
- Eager load media relation on workspaces to prevent N+1
- Add 8 new tests for workspace logo upload/delete
- Update workspace settings test to assert members/invitations props
All 710 tests passing.
2026-03-30 03:20:43 +00:00
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
2026-01-15 01:13:44 +00:00
|
|
|
namespace Database\Factories;
|
|
|
|
|
|
2026-01-17 17:44:37 +00:00
|
|
|
use App\Enums\PostPlatform\ContentType;
|
feat: PostPlatform enum, failure email, DB indexes, rate limiting, tests
Publishing improvements:
- Create PostPlatformStatus enum (Pending, Publishing, Published, Failed)
- Update PostPlatform model, jobs, factories to use enum
- Add PostPublishFailed email notification when post fails to publish
- Maizzle template + blade for failure email with platform details
- PublishPost job: add $tries=3, $backoff=30, failed() method
- Fix broadcast event to serialize enum status value
Security:
- Add rate limiting (throttle:6,1) on social connect endpoints
- Fix MediaController::reorder IDOR vulnerability
- Fix Connect.vue broken import (storeStep2 -> storeConnect)
- Fix UpdatePost data_get() consistency
Database:
- Add composite index on post_platforms (post_id, enabled)
- Add index on post_platforms (social_account_id)
Tests:
- Add 3 tests for profile photo upload/delete
- Add 2 tests for media reorder (including IDOR check)
- Fix publish tests for PostPlatformStatus enum
- Add Mail::fake() to publish tests
Cleanup:
- Remove unused AppHeader.vue and AppHeaderLayout.vue
- Remove dead BillingController methods
All 733 tests passing.
2026-03-30 19:11:38 +00:00
|
|
|
use App\Enums\PostPlatform\Status;
|
2026-01-17 17:44:37 +00:00
|
|
|
use App\Enums\SocialAccount\Platform;
|
|
|
|
|
use App\Models\Post;
|
2026-03-29 22:24:28 +00:00
|
|
|
use App\Models\PostPlatform;
|
2026-01-17 17:44:37 +00:00
|
|
|
use App\Models\SocialAccount;
|
2026-01-15 01:13:44 +00:00
|
|
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-29 22:24:28 +00:00
|
|
|
* @extends Factory<PostPlatform>
|
2026-01-15 01:13:44 +00:00
|
|
|
*/
|
|
|
|
|
class PostPlatformFactory extends Factory
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Define the model's default state.
|
|
|
|
|
*
|
|
|
|
|
* @return array<string, mixed>
|
|
|
|
|
*/
|
|
|
|
|
public function definition(): array
|
|
|
|
|
{
|
|
|
|
|
return [
|
2026-01-17 17:44:37 +00:00
|
|
|
'post_id' => Post::factory(),
|
|
|
|
|
'social_account_id' => SocialAccount::factory(),
|
|
|
|
|
'enabled' => true,
|
|
|
|
|
'platform' => Platform::LinkedIn,
|
2026-01-18 18:56:51 +00:00
|
|
|
'content_type' => ContentType::LinkedInPost,
|
feat: PostPlatform enum, failure email, DB indexes, rate limiting, tests
Publishing improvements:
- Create PostPlatformStatus enum (Pending, Publishing, Published, Failed)
- Update PostPlatform model, jobs, factories to use enum
- Add PostPublishFailed email notification when post fails to publish
- Maizzle template + blade for failure email with platform details
- PublishPost job: add $tries=3, $backoff=30, failed() method
- Fix broadcast event to serialize enum status value
Security:
- Add rate limiting (throttle:6,1) on social connect endpoints
- Fix MediaController::reorder IDOR vulnerability
- Fix Connect.vue broken import (storeStep2 -> storeConnect)
- Fix UpdatePost data_get() consistency
Database:
- Add composite index on post_platforms (post_id, enabled)
- Add index on post_platforms (social_account_id)
Tests:
- Add 3 tests for profile photo upload/delete
- Add 2 tests for media reorder (including IDOR check)
- Fix publish tests for PostPlatformStatus enum
- Add Mail::fake() to publish tests
Cleanup:
- Remove unused AppHeader.vue and AppHeaderLayout.vue
- Remove dead BillingController methods
All 733 tests passing.
2026-03-30 19:11:38 +00:00
|
|
|
'status' => Status::Pending,
|
2026-01-17 17:44:37 +00:00
|
|
|
'meta' => [],
|
2026-01-15 01:13:44 +00:00
|
|
|
];
|
|
|
|
|
}
|
2026-01-17 17:44:37 +00:00
|
|
|
|
|
|
|
|
public function disabled(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'enabled' => false,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function published(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
feat: PostPlatform enum, failure email, DB indexes, rate limiting, tests
Publishing improvements:
- Create PostPlatformStatus enum (Pending, Publishing, Published, Failed)
- Update PostPlatform model, jobs, factories to use enum
- Add PostPublishFailed email notification when post fails to publish
- Maizzle template + blade for failure email with platform details
- PublishPost job: add $tries=3, $backoff=30, failed() method
- Fix broadcast event to serialize enum status value
Security:
- Add rate limiting (throttle:6,1) on social connect endpoints
- Fix MediaController::reorder IDOR vulnerability
- Fix Connect.vue broken import (storeStep2 -> storeConnect)
- Fix UpdatePost data_get() consistency
Database:
- Add composite index on post_platforms (post_id, enabled)
- Add index on post_platforms (social_account_id)
Tests:
- Add 3 tests for profile photo upload/delete
- Add 2 tests for media reorder (including IDOR check)
- Fix publish tests for PostPlatformStatus enum
- Add Mail::fake() to publish tests
Cleanup:
- Remove unused AppHeader.vue and AppHeaderLayout.vue
- Remove dead BillingController methods
All 733 tests passing.
2026-03-30 19:11:38 +00:00
|
|
|
'status' => Status::Published,
|
2026-01-17 17:44:37 +00:00
|
|
|
'platform_post_id' => $this->faker->uuid(),
|
|
|
|
|
'platform_url' => $this->faker->url(),
|
|
|
|
|
'published_at' => now(),
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function failed(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
feat: PostPlatform enum, failure email, DB indexes, rate limiting, tests
Publishing improvements:
- Create PostPlatformStatus enum (Pending, Publishing, Published, Failed)
- Update PostPlatform model, jobs, factories to use enum
- Add PostPublishFailed email notification when post fails to publish
- Maizzle template + blade for failure email with platform details
- PublishPost job: add $tries=3, $backoff=30, failed() method
- Fix broadcast event to serialize enum status value
Security:
- Add rate limiting (throttle:6,1) on social connect endpoints
- Fix MediaController::reorder IDOR vulnerability
- Fix Connect.vue broken import (storeStep2 -> storeConnect)
- Fix UpdatePost data_get() consistency
Database:
- Add composite index on post_platforms (post_id, enabled)
- Add index on post_platforms (social_account_id)
Tests:
- Add 3 tests for profile photo upload/delete
- Add 2 tests for media reorder (including IDOR check)
- Fix publish tests for PostPlatformStatus enum
- Add Mail::fake() to publish tests
Cleanup:
- Remove unused AppHeader.vue and AppHeaderLayout.vue
- Remove dead BillingController methods
All 733 tests passing.
2026-03-30 19:11:38 +00:00
|
|
|
'status' => Status::Failed,
|
2026-01-17 17:44:37 +00:00
|
|
|
'error_message' => 'Failed to publish',
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function linkedin(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::LinkedIn,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function x(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::X,
|
2026-01-18 18:56:51 +00:00
|
|
|
'content_type' => ContentType::XPost,
|
2026-01-17 17:44:37 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function instagram(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Instagram,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-18 18:56:51 +00:00
|
|
|
|
|
|
|
|
public function bluesky(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Bluesky,
|
|
|
|
|
'content_type' => ContentType::BlueskyPost,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function mastodon(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Mastodon,
|
|
|
|
|
'content_type' => ContentType::MastodonPost,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function threads(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Threads,
|
|
|
|
|
'content_type' => ContentType::ThreadsPost,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function tiktok(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::TikTok,
|
|
|
|
|
'content_type' => ContentType::TikTokVideo,
|
feat(tiktok): photo carousel support + UX Content Sharing API compliance
## Photo carousel support
- Adds `ContentType::TikTokPhoto` enum case (max 35 photos, 1:1 aspect,
supportsImage true, supportsVideo false) and JS mirror in content-type.ts.
- Variant pill picker (Video / Photo carousel) at the top of TikTokSettings,
mirroring the Instagram pattern. Wired through ScheduleTab to the parent
editor's existing update:platformContentType emit.
- i18n keys for variant_label / variant.video / variant.photo in en/pt-BR/es.
- Publisher: split buildPostInfo into buildVideoPostInfo (uses `title`,
TikTok cap 2200 chars) and buildPhotoPostInfo (uses `description`, cap
4000 chars; omits Duet/Stitch/AIGC since they don't apply). Removed the
no-longer-needed queryCreatorInfo() call from publishVideo/publishPhotos
— its only previous consumer (silent privacy_level fallback) is gone.
## UX Content Sharing API compliance
Per TikTok review feedback citing
https://developers.tiktok.com/doc/content-sharing-guidelines#required_ux_implementation_in_your_app
Point 1 — already satisfied (creator_info fetch + nickname display).
Point 2/4 — Music Usage Confirmation declaration is now always visible
in TikTokSettings; text changes between "Music Usage Confirmation" and
"Branded Content Policy and Music Usage Confirmation" based on toggle
state. Previously the entire `<p>` block was conditional on a brand
toggle being selected, hiding the baseline declaration.
Point 2b — privacy_level may not have a default. UI was already correct;
backend hardened: UpdatePostRequest now requires meta.privacy_level for
tiktok platforms when status is publishing/scheduled (via withValidator);
TikTokPublisher::resolveRequiredPrivacyLevel throws TikTokPublishException
(ContentPolicy category) when missing instead of silently falling back to
the creator's preferred level.
Point 2c — interaction settings now condition on content type:
- Photo posts hide Duet/Stitch (they don't apply per TikTok docs).
- Photo posts hide AIGC (also video-only).
- Video posts hide Auto Add Music (photos-only feature).
- Max-duration warning hidden when not a video post.
Source of truth is the user-selected contentType prop, not inferred
from media — ensures the UI reacts immediately to the variant pill.
Point 3a — publish button stays disabled when Disclose toggle is on
without a sub-selection (already the case via tiktokComplianceValid).
The disabled tooltip now uses the verbatim TikTok-required text "You
need to indicate if your content promotes yourself, a third party, or
both." instead of the generic "Some platform settings are incomplete..."
when the only blocker is TikTok disclosure incompleteness.
Point 3b — SELF_ONLY (Only me) privacy option is no longer filtered out
when Branded Content is checked. It is rendered disabled with a hover
tooltip "Branded content visibility cannot be set to private." plus a
persistent amber warning paragraph below the dropdown. When the user
toggles Branded Content while privacy is SELF_ONLY, the privacy clears
and a vue-sonner warning toast surfaces the change.
## Cross-cutting
- New `resources/js/enums/platform.ts` mirrors the PHP Platform enum,
used in Edit.vue (tiktokComplianceValid + tiktokDisclosureIncomplete)
and ScheduleTab.vue (all selected*Platforms computeds) to replace
string literal comparisons against `'tiktok'` / `'facebook'` / etc.
- PostPlatformFactory tiktok() state defaults meta.privacy_level to
SELF_ONLY so existing test fixtures keep passing under the new
publisher/FormRequest requirements.
## Tests
- New tests/Unit/Enums/PostPlatform/TikTokPhotoContentTypeTest.php
covering the new enum case (4 tests).
- TikTokPublisherTest: added "video uses title not description" and
"throws when meta.privacy_level missing" regression tests; renamed
two existing tests that depended on the removed silent fallback.
- New tests/Feature/UpdatePostRequestTest.php with 3 tests covering
the FormRequest's privacy_level enforcement (publish-rejected,
publish-passes, draft-allowed).
Full Pest suite: 1490 passed, 2 skipped (pre-existing).
2026-05-09 15:47:49 +00:00
|
|
|
'meta' => ['privacy_level' => 'SELF_ONLY'],
|
2026-01-18 18:56:51 +00:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function youtube(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::YouTube,
|
|
|
|
|
'content_type' => ContentType::YouTubeShort,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pinterest(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Pinterest,
|
|
|
|
|
'content_type' => ContentType::PinterestPin,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pinterestVideoPin(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Pinterest,
|
|
|
|
|
'content_type' => ContentType::PinterestVideoPin,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function pinterestCarousel(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Pinterest,
|
|
|
|
|
'content_type' => ContentType::PinterestCarousel,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function facebook(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Facebook,
|
|
|
|
|
'content_type' => ContentType::FacebookPost,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 20:39:07 +00:00
|
|
|
public function discord(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Discord,
|
|
|
|
|
'content_type' => ContentType::DiscordMessage,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 18:56:51 +00:00
|
|
|
public function facebookReel(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Facebook,
|
|
|
|
|
'content_type' => ContentType::FacebookReel,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function facebookStory(): static
|
|
|
|
|
{
|
|
|
|
|
return $this->state(fn (array $attributes) => [
|
|
|
|
|
'platform' => Platform::Facebook,
|
|
|
|
|
'content_type' => ContentType::FacebookStory,
|
|
|
|
|
]);
|
|
|
|
|
}
|
2026-01-15 01:13:44 +00:00
|
|
|
}
|