From 69e8944437298191d6dae834aa0f584e3dece714 Mon Sep 17 00:00:00 2001 From: Nevo David Date: Thu, 27 Nov 2025 20:56:57 +0700 Subject: [PATCH] feat: google my business --- .../src/api/routes/integrations.controller.ts | 2 +- .../continue-provider/gmb/gmb.continue.tsx | 8 +-- .../new-launch/providers/gmb/gmb.provider.tsx | 29 +++++----- .../integrations/integration.service.ts | 2 +- .../providers-settings/gmb.settings.dto.ts | 3 +- .../src/integrations/social/gmb.provider.ts | 55 ++++++++++++++----- 6 files changed, 63 insertions(+), 36 deletions(-) diff --git a/apps/backend/src/api/routes/integrations.controller.ts b/apps/backend/src/api/routes/integrations.controller.ts index c1c2990b..3cd717c1 100644 --- a/apps/backend/src/api/routes/integrations.controller.ts +++ b/apps/backend/src/api/routes/integrations.controller.ts @@ -563,7 +563,7 @@ export class IntegrationsController { @Post('/gmb/:id') async saveGmb( @Param('id') id: string, - @Body() body: { id: string; accountId: string; locationName: string }, + @Body() body: { id: string; accountName: string; locationName: string }, @GetOrgFromRequest() org: Organization ) { return this._integrationService.saveGmb(org.id, id, body); diff --git a/apps/frontend/src/components/new-launch/providers/continue-provider/gmb/gmb.continue.tsx b/apps/frontend/src/components/new-launch/providers/continue-provider/gmb/gmb.continue.tsx index 54ceb1ec..a1862c0a 100644 --- a/apps/frontend/src/components/new-launch/providers/continue-provider/gmb/gmb.continue.tsx +++ b/apps/frontend/src/components/new-launch/providers/continue-provider/gmb/gmb.continue.tsx @@ -18,7 +18,7 @@ export const GmbContinue: FC<{ const { integration } = useIntegration(); const [location, setSelectedLocation] = useState(null); const fetch = useFetch(); @@ -34,7 +34,7 @@ export const GmbContinue: FC<{ }, []); const setLocation = useCallback( - (param: { id: string; accountId: string; locationName: string }) => () => { + (param: { id: string; accountName: string; locationName: string }) => () => { setSelectedLocation(param); }, [] @@ -95,7 +95,7 @@ export const GmbContinue: FC<{ (p: { id: string; name: string; - accountId: string; + accountName: string; locationName: string; picture: { data: { @@ -111,7 +111,7 @@ export const GmbContinue: FC<{ )} onClick={setLocation({ id: p.id, - accountId: p.accountId, + accountName: p.accountName, locationName: p.locationName, })} > diff --git a/apps/frontend/src/components/new-launch/providers/gmb/gmb.provider.tsx b/apps/frontend/src/components/new-launch/providers/gmb/gmb.provider.tsx index 2a1b428c..7b32a4dd 100644 --- a/apps/frontend/src/components/new-launch/providers/gmb/gmb.provider.tsx +++ b/apps/frontend/src/components/new-launch/providers/gmb/gmb.provider.tsx @@ -29,7 +29,7 @@ const topicTypes = [ const callToActionTypes = [ { label: 'None', - value: '', + value: 'NONE', }, { label: 'Book', @@ -84,7 +84,7 @@ const GmbSettings: FC = () => { - {callToActionType && callToActionType !== 'CALL' && ( - - )} + {callToActionType && + callToActionType !== 'NONE' && + callToActionType !== 'CALL' && ( + + )} {topicType === 'EVENT' && (
@@ -116,11 +118,7 @@ const GmbSettings: FC = () => { type="date" {...register('eventStartDate')} /> - +
0 && items[0].length > 1) { return 'Google My Business posts can only have one image'; } - + // Check for video - GMB doesn't support video in local posts if (items.length > 0 && items[0].length > 0) { const media = items[0][0]; @@ -190,4 +188,3 @@ export default withProvider({ }, maximumCharacters: 1500, }); - diff --git a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts index ef843407..554c6c35 100644 --- a/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts +++ b/libraries/nestjs-libraries/src/database/prisma/integrations/integration.service.ts @@ -371,7 +371,7 @@ export class IntegrationService { async saveGmb( org: string, id: string, - data: { id: string; accountId: string; locationName: string } + data: { id: string; accountName: string; locationName: string } ) { const getIntegration = await this._integrationRepository.getIntegrationById( org, diff --git a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/gmb.settings.dto.ts b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/gmb.settings.dto.ts index 221880d8..a4fb5e9d 100644 --- a/libraries/nestjs-libraries/src/dtos/posts/providers-settings/gmb.settings.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/posts/providers-settings/gmb.settings.dto.ts @@ -7,6 +7,7 @@ export class GmbSettingsDto { @IsOptional() @IsIn([ + 'NONE', 'BOOK', 'ORDER', 'SHOP', @@ -16,6 +17,7 @@ export class GmbSettingsDto { 'CALL', ]) callToActionType?: + | 'NONE' | 'BOOK' | 'ORDER' | 'SHOP' @@ -65,4 +67,3 @@ export class GmbSettingsDto { @IsString() offerTerms?: string; } - diff --git a/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts b/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts index 002de587..cd20998a 100644 --- a/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts +++ b/libraries/nestjs-libraries/src/integrations/social/gmb.provider.ts @@ -186,7 +186,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { id: string; name: string; picture: { data: { url: string } }; - accountId: string; + accountName: string; locationName: string; }> = []; @@ -206,6 +206,11 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { if (locationsData.locations) { for (const location of locationsData.locations) { + // location.name is in format: locations/{locationId} + // We need the full path: accounts/{accountId}/locations/{locationId} + const locationId = location.name.replace('locations/', ''); + const fullResourceName = `${accountName}/locations/${locationId}`; + // Get profile photo if available let photoUrl = ''; try { @@ -235,17 +240,21 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { } allLocations.push({ - id: location.name, // format: locations/{locationId} + // id is the full resource path for the v4 API: accounts/{accountId}/locations/{locationId} + id: fullResourceName, name: location.title || 'Unnamed Location', picture: { data: { url: photoUrl } }, - accountId: accountName, + accountName: accountName, locationName: location.name, }); } } } catch (error) { // Continue with other accounts if one fails - console.error(`Failed to fetch locations for account ${accountName}:`, error); + console.error( + `Failed to fetch locations for account ${accountName}:`, + error + ); } } @@ -254,11 +263,13 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { async fetchPageInformation( accessToken: string, - data: { id: string; accountId: string; locationName: string } + data: { id: string; accountName: string; locationName: string } ) { - // Fetch location details + // data.id is the full resource path: accounts/{accountId}/locations/{locationId} + // data.locationName is the v1 API format: locations/{locationId} + // Fetch location details using the v1 API format const locationResponse = await fetch( - `https://mybusinessbusinessinformation.googleapis.com/v1/${data.id}?readMask=name,title,storefrontAddress,metadata`, + `https://mybusinessbusinessinformation.googleapis.com/v1/${data.locationName}?readMask=name,title,storefrontAddress,metadata`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -271,7 +282,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { let photoUrl = ''; try { const mediaResponse = await fetch( - `https://mybusinessbusinessinformation.googleapis.com/v1/${data.id}/media`, + `https://mybusinessbusinessinformation.googleapis.com/v1/${data.locationName}/media`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -296,6 +307,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { } return { + // Return the full resource path as id (for v4 Local Posts API) id: data.id, name: locationData.title || 'Unnamed Location', access_token: accessToken, @@ -318,7 +330,7 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { const information = await this.fetchPageInformation(accessToken, { id: requiredId, - accountId: findPage.accountId, + accountName: findPage.accountName, locationName: findPage.locationName, }); @@ -348,8 +360,12 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { topicType: settings?.topicType || 'STANDARD', }; - // Add call to action if provided - if (settings?.callToActionType && settings?.callToActionUrl) { + // Add call to action if provided (and not NONE) + if ( + settings?.callToActionType && + settings.callToActionType !== 'NONE' && + settings?.callToActionUrl + ) { postBody.callToAction = { actionType: settings.callToActionType, url: settings.callToActionUrl, @@ -466,7 +482,17 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { // Use the Business Profile Performance API const response = await fetch( - `https://businessprofileperformance.googleapis.com/v1/${id}:getDailyMetricsTimeSeries?dailyMetric=WEBSITE_CLICKS&dailyMetric=CALL_CLICKS&dailyMetric=BUSINESS_DIRECTION_REQUESTS&dailyMetric=BUSINESS_IMPRESSIONS_DESKTOP_MAPS&dailyMetric=BUSINESS_IMPRESSIONS_MOBILE_MAPS&dailyRange.startDate.year=${dayjs(startDate).year()}&dailyRange.startDate.month=${dayjs(startDate).month() + 1}&dailyRange.startDate.day=${dayjs(startDate).date()}&dailyRange.endDate.year=${dayjs(endDate).year()}&dailyRange.endDate.month=${dayjs(endDate).month() + 1}&dailyRange.endDate.day=${dayjs(endDate).date()}`, + `https://businessprofileperformance.googleapis.com/v1/${id}:getDailyMetricsTimeSeries?dailyMetric=WEBSITE_CLICKS&dailyMetric=CALL_CLICKS&dailyMetric=BUSINESS_DIRECTION_REQUESTS&dailyMetric=BUSINESS_IMPRESSIONS_DESKTOP_MAPS&dailyMetric=BUSINESS_IMPRESSIONS_MOBILE_MAPS&dailyRange.startDate.year=${dayjs( + startDate + ).year()}&dailyRange.startDate.month=${ + dayjs(startDate).month() + 1 + }&dailyRange.startDate.day=${dayjs( + startDate + ).date()}&dailyRange.endDate.year=${dayjs( + endDate + ).year()}&dailyRange.endDate.month=${ + dayjs(endDate).month() + 1 + }&dailyRange.endDate.day=${dayjs(endDate).date()}`, { headers: { Authorization: `Bearer ${accessToken}`, @@ -497,7 +523,10 @@ export class GmbProvider extends SocialAbstract implements SocialProvider { const dataPoints = series.timeSeries?.datedValues?.map((dv: any) => ({ total: dv.value || 0, - date: `${dv.date.year}-${String(dv.date.month).padStart(2, '0')}-${String(dv.date.day).padStart(2, '0')}`, + date: `${dv.date.year}-${String(dv.date.month).padStart( + 2, + '0' + )}-${String(dv.date.day).padStart(2, '0')}`, })) || []; if (dataPoints.length > 0) {