Convert all 'foo.'.$bar style concatenations to "foo.{$bar}" interpolation
in event broadcast channels (PostCreated, PostDeleted, PostCommentCreated,
PostPlatformStatusUpdated, NotificationCreated) and the related event
tests.
Document the convention in CLAUDE.md so new code follows the pattern.
The event was dispatched by StripeEventListener but had no consumers
on either side (no Event::listen registration in PHP, no Echo listener
in JS, no subscriber on the users.{owner_id} channel). Removed along
with its tests; the listener no longer references it. Cleans up the
StripeEventListenerTest to match (drops Event::fake(SubscriptionCreated)
in five negative-path tests, replaces with Bus::assertNotDispatched(
TrackBilling) which is the actual cascade-side-effect we care about).
Adds a private workspace channel (WorkspaceChannel, authorised by
membership) so list views can subscribe once and receive events for
every post in the workspace, instead of opening N per-post channels.
New events:
- PostCreated (broadcast on workspace.{id})
- PostDeleted (broadcast on workspace.{id}; carries primitive ids so
it fires after \$post->delete())
- PostPlatformStatusUpdated now broadcasts on both post.{id} and
workspace.{id} so focused views and lists share the same trigger.
All broadcast events migrated to the namespaced 'entity.action'
convention from Laravel's broadcasting docs and switched from
ShouldBroadcastNow to ShouldBroadcast on a dedicated 'broadcasts'
queue (added to the supervisor-1 list in config/horizon.php) to keep
HTTP responses fast:
PostCreated -> post.created
PostDeleted -> post.deleted
PostPlatformStatusUpdated -> post.platform.status.updated
PostCommentCreated -> post.comment.created
NotificationCreated -> notification.created
PostCreationReady -> ai.creation.completed
Drops the SubscriptionCreated event — it was broadcast on
users.{owner_id} but had no listeners (frontend or backend) and the
billing/PostHog flow already handles plan-change tracking elsewhere.
PostPlatformStatusUpdated payload trimmed to {post_id} since every
consumer (Show, Edit, Index) does router.reload({ only: [...] }) and
ignored the rich shape.
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing
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.