diff --git a/internal/ghl/oauth.go b/internal/ghl/oauth.go index be636f5..45d9ea2 100644 --- a/internal/ghl/oauth.go +++ b/internal/ghl/oauth.go @@ -227,31 +227,48 @@ func (h *OAuthHandler) postToken(ctx context.Context, data url.Values) (*TokenRe if err := json.Unmarshal(body, &tokenResp); err != nil { return nil, fmt.Errorf("failed to parse token response: %w", err) } - slog.Info("ghl token response fields", "location_id", tokenResp.LocationID, "company_id", tokenResp.CompanyID, "user_type", tokenResp.UserType) + slog.Info("ghl token response fields", + "location_id", tokenResp.LocationID, + "company_id", tokenResp.CompanyID, + "user_type", tokenResp.UserType, + "installed_locations", tokenResp.InstalledLocations, + "raw_body", string(body), + ) return &tokenResp, nil } -// installAllLocations fetches all locations for the company and exchanges the company token -// for a per-location token for each, storing them all. Returns the count installed. +// installAllLocations exchanges the company token for per-location tokens and stores them. +// It first tries to use the InstalledLocations list from the token response (provided by GHL +// for bulk installs). Falls back to GET /locations/search if that list is empty. func (h *OAuthHandler) installAllLocations(ctx context.Context, companyToken *TokenResponse) (int, error) { - locations, err := h.getCompanyLocations(ctx, companyToken.AccessToken, companyToken.CompanyID) - if err != nil { - return 0, fmt.Errorf("list locations: %w", err) - } - if len(locations) == 0 { - return 0, fmt.Errorf("no locations found for company %s", companyToken.CompanyID) + // Build location ID list from token response when GHL provides it (bulk install path). + locationIDs := companyToken.InstalledLocations + if len(locationIDs) == 0 { + slog.Info("ghl bulk install: no installedLocations in token, falling back to locations search", "company_id", companyToken.CompanyID) + locations, err := h.getCompanyLocations(ctx, companyToken.AccessToken, companyToken.CompanyID) + if err != nil { + return 0, fmt.Errorf("list locations: %w", err) + } + if len(locations) == 0 { + return 0, fmt.Errorf("no locations found for company %s", companyToken.CompanyID) + } + for _, loc := range locations { + locationIDs = append(locationIDs, loc.ID) + } + } else { + slog.Info("ghl bulk install: using installedLocations from token", "company_id", companyToken.CompanyID, "count", len(locationIDs)) } installed := 0 - for _, loc := range locations { - locToken, err := h.exchangeForLocationToken(ctx, companyToken.AccessToken, companyToken.CompanyID, loc.ID) + for _, locID := range locationIDs { + locToken, err := h.exchangeForLocationToken(ctx, companyToken.AccessToken, companyToken.CompanyID, locID) if err != nil { - slog.Warn("ghl location token exchange failed", "location_id", loc.ID, "location_name", loc.Name, "err", err) + slog.Warn("ghl location token exchange failed", "location_id", locID, "err", err) continue } expiresAt := time.Now().Add(time.Duration(locToken.ExpiresIn) * time.Second) record := &store.TokenRecord{ - LocationID: loc.ID, + LocationID: locID, CompanyID: companyToken.CompanyID, AccessToken: locToken.AccessToken, RefreshToken: locToken.RefreshToken, @@ -260,10 +277,10 @@ func (h *OAuthHandler) installAllLocations(ctx context.Context, companyToken *To UpdatedAt: time.Now(), } if err := h.store.SaveToken(ctx, record); err != nil { - slog.Warn("ghl location token save failed", "location_id", loc.ID, "err", err) + slog.Warn("ghl location token save failed", "location_id", locID, "err", err) continue } - slog.Info("ghl location installed", "location_id", loc.ID, "location_name", loc.Name) + slog.Info("ghl location installed", "location_id", locID) installed++ } return installed, nil diff --git a/internal/ghl/types.go b/internal/ghl/types.go index a911c16..539161b 100644 --- a/internal/ghl/types.go +++ b/internal/ghl/types.go @@ -1,13 +1,14 @@ package ghl type TokenResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - ExpiresIn int `json:"expires_in"` - TokenType string `json:"token_type"` - LocationID string `json:"locationId"` - CompanyID string `json:"companyId"` - UserType string `json:"userType"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + LocationID string `json:"locationId"` + CompanyID string `json:"companyId"` + UserType string `json:"userType"` + InstalledLocations []string `json:"installedLocations"` // populated on bulk company installs } type LocationInfo struct {