Skip to content

Add Google Business Profile as a supported social platform - #270

Draft
paulocastellano wants to merge 49 commits into
mainfrom
worktree-google-business-profile
Draft

Add Google Business Profile as a supported social platform#270
paulocastellano wants to merge 49 commits into
mainfrom
worktree-google-business-profile

Conversation

@paulocastellano

@paulocastellano paulocastellano commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Google Business Profile as a supported social platform:

  • Connect a Google Business Profile location via OAuth
  • Publish posts (Update, Event, Offer) with an optional call-to-action button and image
  • View location performance analytics
  • Full token refresh, error handling, and validation across web, API, and MCP

Test plan before merging

  • Connect a real Google Business Profile account end-to-end
  • Publish one post of each type (Update, Event, Offer)
  • Confirm the post shows up correctly on Google
  • Check the analytics tab shows real data
  • Automated test suite passes

Implements GoogleBusinessAnalytics service to fetch five Google Business
Profile performance metrics (website clicks, call clicks, direction
requests, desktop map impressions, mobile map impressions) via the
Performance API. Adds i18n keys to all 16 locale files.

TDD: Red test passing, 3 assertions validated.
Replace English translations in 15 non-English locale files with proper
translations matching each locale's existing terminology and style:
- Arabic: Arabic translations
- German: German translations
- Greek: Greek translations
- Spanish: Spanish translations
- French: French translations
- Italian: Italian translations
- Japanese: Japanese translations
- Korean: Korean translations
- Dutch: Dutch translations
- Polish: Polish translations
- Portuguese-BR: Portuguese translations
- Russian: Russian translations
- Turkish: Turkish translations
- Ukrainian: Ukrainian translations
- Chinese: Chinese (Simplified) translations

Verified by: LocalizationParityTest (18 passed) + GoogleBusinessAnalytics (2 passed)
Adds GoogleBusinessController with connect/callback/select-location/select
routes, mirroring FacebookController's auto-connect-on-single-location and
picker-on-multiple pattern. Registers the 'google-business' Socialite driver
(separate from 'google-auth' and YouTube's 'google') and adds the required
i18n keys across all 16 locales.
…form's post update

platforms.*.meta.call_to_action.url used required_unless, an implicit Laravel
validation rule that still runs when the field (and its whole call_to_action
block) is absent. Since no other platform sends call_to_action at all, the
missing action_type was treated as "not NONE/CALL" and made the URL required
on every post update (Pinterest, Discord, TikTok, etc.) across web, the REST
API, and MCP.

Follow the existing board_id/channel_id pattern: keep the rules() entry
unconditional and enforce the CTA-required-unless-NONE/CALL check only in
requiredMetaViolation(), scoped to Platform::GoogleBusiness. Adds the
cta_url_required translation key (all 16 locales) and test coverage for both
the regression and the still-enforced Google Business requirement.
…ry points

Closes the CLAUDE.md-mandated round-trip coverage gap for PostPlatformMetaRules'
topic_type/call_to_action/event/offer fields, proving they survive the full
validate-then-persist path through all three post entry points, plus a
token-refresh-window regression test for the Google Business rotating
refresh_token model.
…sTest

The file has no namespace declaration, so ReflectionMethod is already in
the global namespace. Importing it via 'use' is a no-op and raises an error
under the test environment's error-reporting configuration.
…nd OAuth popup routes

Platform.GoogleBusiness was undefined, so the composer settings card never
mounted and the Connect button silently no-opped.
…s API

The v1 Business Information API returns Location.name in its short
locations/{id} form, but the v4 localPosts endpoint requires the full
accounts/*/locations/* parent. fetchLocationsForAccount now composes the
full name for meta.location_id and keeps the short name in
meta.location_name, which is what the v1 verify call expects.
…ller

The controller had no SUPPORTED_PLATFORMS entry or match arm, so the
service was unreachable. The Performance API call also used the full
location resource name (it wants the short one) and encoded dailyMetrics
as bracket-indexed array params instead of repeated scalars.
…-side

Mirrors PostPlatformMetaRules::requiredMetaViolation() so the composer
shows an inline tooltip instead of letting the user hit a raw 422.
Google's v4 Local Posts API requires the `event` field for both the EVENT
and OFFER topic types, so Offer posts were being rejected with
INVALID_ARGUMENT because no `event` object was ever attached.

Both topic types now reuse the same `meta.event.*` fields (title + date
range), matching the API shape and Google's own Business Profile app flow,
where creating an Offer also asks for a title and date range before the
coupon details. The backend publish requirement, the editor sub-form, and
the frontend compliance gate are all gated on the shared topic-type list.
…ation

findError() matched backend error keys by suffix only, so a post targeting
two Google Business Profile locations showed one location's error under
both settings panels.

ChannelConfigurator now passes each settings panel its position in the
selected-channel list, and GoogleBusinessSettings matches the full
`platforms.{index}.meta.*` key. That position is the submitted index: the
editor builds the `platforms` payload by filtering the same post_platforms
list by the same selection, which is the mapping ScheduleTab already relies
on for content-type errors.

Pinterest and Discord share the same suffix-match limitation; fixing them
is left to a separate change.
callback() never read session('social_reconnect_id'), so the reconnect
branch in select() could never fire even once a reconnect trigger exists.
Captures it and stores it in the google_business_oauth session payload, the
same way InstagramFacebookController already does.
…or and preview

The settings panel's option list and the preview's label lookup each
hand-wrote the same action types, so adding or renaming one meant editing
both. Both now read resources/js/lib/googleBusiness.ts.
It had no callers: the connect flow has no SocialAccount yet, so the
controller always calls fetchLocations() with the raw access token.
- selectLocation() now checks manageAccounts like callback()/select() do
- the locations fetched during callback() are cached in the OAuth session payload, so a multi-location connect hits Google once instead of three times
- select()'s inline validation moves to a dedicated FormRequest
Also disables the topic-type pills in preview-only mode, like every other input in the panel.
…type

The connect grid had no google_business entry in its platform theme map, so the
tile fell back to an empty image, and the automation Generate node had no case
for the platform, leaving its content type blank.
data_get()'s default only applies when the key is missing, so a stored
'topic_type' => null published an empty topicType. Same bug class as the CTA
action type fixed earlier. Also hoists the repeated EVENT/OFFER guard out of
the match(true) arms and points the publisher at the shared topic-type
constant instead of a literal copy of the list.
…oday

CarbonImmutable::parse('') resolves to now(), so a blank start or end date
reaching publish() silently sent today's date to Google. buildEvent() now
throws the same way the missing-location guard does.
…path

The location-not-found exit left live access and refresh tokens in the session,
unlike every other exit in select(). Also extracts the shared location
attribute builder so the reconnect branch stops duplicating connectLocation().
…atform meta validation

Captures two gotchas discovered while building the Google Business
Profile integration, so the next platform addition finds them instead
of re-deriving the same bugs.
@paulocastellano paulocastellano self-assigned this Aug 11, 2026
@paulocastellano
paulocastellano marked this pull request as draft August 11, 2026 14:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant