UpdatePost::execute used to return AlreadyPublished for the Published
short-circuit. This PR widened the short-circuit to four terminal
statuses and consolidated them under PostAction::Finalized — so the
old enum case stopped being emitted, and every caller already had a
defensive in_array([AlreadyPublished, Finalized], ...).
Audit before removal: nothing emits AlreadyPublished anymore (only
UpdatePost::execute returns Actions, and it returns Finalized for
the whole terminal set), no test references the case, and no string
'already_published' exists elsewhere in app/resources/tests/lang.
- Drop the enum case
- Simplify the three in_array checks to a direct === Finalized
- Delete the dead App/PostController branch that flashed the old
cannot_edit_published message (its successor branch with
cannot_edit_finalized stays). The old i18n key is left in lang/
for now — orphan but harmless, can ressuscitate if a similar
flash is added back.
Production incident: a customer's Facebook Page post failed with 'The post
is empty. Please enter a message to share.' (error code 197) and ended up
with a contradictory DB state (status=published + error_message=set).
Three independent bugs were uncovered:
A. FacebookPublisher sends 'message'/'description' as null when the user
posts media without text. Graph API requires the key be omitted, not
null. Fixed in publishSingleImagePost, publishMultiImagePost,
publishVideoPost, publishReel.
B. markAsPublished/markAsFailed leak stale fields across transitions
(a published row could retain error_message from a prior failure,
vice-versa). Both transitions now explicitly clear the opposite
side's fields.
C. status='failed' was editable in the UI and the backend, so users
were re-clicking Publish, generating duplicate failure emails and
the contradictory state from bug B. The frontend isReadOnly check
and the UpdatePost backend guard now treat Published/PartiallyPublished/
Failed/Publishing as terminal. To retry, the user duplicates the post.
11 new tests guarantee these can't regress silently: FB payload shape
per content type, PostPlatform field-clearing on transitions, and the
terminal-status block at the controller level.
- Refactor WorkspacePolicy to use pivot role instead of workspace.user_id
- Add manageBilling policy (owner only) to BillingController
- Fix ApiKeyController authorization (view → manageTeam for store/destroy)
- Fix WorkspaceInviteController using workspace.user_id for owner checks
- Fix WorkspaceController settings is_owner using workspace.user_id
- Create PostAction enum for UpdatePost/PostController action strings
- Create ApiToken\Status enum
- Add User::SUBSCRIPTION_NAME constant, replace all hardcoded 'default'
- Convert wantsEmailFor to accept NotificationType enum
- Convert all $data[] to data_get() across publishers, controllers, jobs
- Fix SocialLoginController callback missing try/catch
- Fix SocialController::toggleActive missing workspace null check
- Fix UpdatePost NPE on meta merge when postPlatform not found
- Remove HTML5 required attributes from form inputs
- Convert function declarations to arrow functions in Vue components
- Replace hardcoded URLs with Wayfinder route helpers
- Replace new Date() with dayjs
- Add 16 new test files covering policies, authorization, publishing