Add Performance Max For Travel Goals Campaign

Java

// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.ads.googleads.examples.travel;

import static com.google.ads.googleads.examples.utils.CodeSampleHelper.getPrintableDateTime;

import com.beust.jcommander.Parameter;
import com.google.ads.googleads.examples.utils.ArgumentNames;
import com.google.ads.googleads.examples.utils.CodeSampleHelper;
import com.google.ads.googleads.examples.utils.CodeSampleParams;
import com.google.ads.googleads.lib.GoogleAdsClient;
import com.google.ads.googleads.v16.common.CallToActionAsset;
import com.google.ads.googleads.v16.common.HotelPropertyAsset;
import com.google.ads.googleads.v16.common.ImageAsset;
import com.google.ads.googleads.v16.common.MaximizeConversionValue;
import com.google.ads.googleads.v16.common.TextAsset;
import com.google.ads.googleads.v16.enums.AdvertisingChannelTypeEnum.AdvertisingChannelType;
import com.google.ads.googleads.v16.enums.AssetFieldTypeEnum.AssetFieldType;
import com.google.ads.googleads.v16.enums.AssetGroupStatusEnum.AssetGroupStatus;
import com.google.ads.googleads.v16.enums.AssetSetTypeEnum.AssetSetType;
import com.google.ads.googleads.v16.enums.BudgetDeliveryMethodEnum.BudgetDeliveryMethod;
import com.google.ads.googleads.v16.enums.CampaignStatusEnum.CampaignStatus;
import com.google.ads.googleads.v16.enums.HotelAssetSuggestionStatusEnum.HotelAssetSuggestionStatus;
import com.google.ads.googleads.v16.errors.GoogleAdsError;
import com.google.ads.googleads.v16.errors.GoogleAdsException;
import com.google.ads.googleads.v16.resources.Asset;
import com.google.ads.googleads.v16.resources.AssetGroup;
import com.google.ads.googleads.v16.resources.AssetGroupAsset;
import com.google.ads.googleads.v16.resources.AssetSet;
import com.google.ads.googleads.v16.resources.AssetSetAsset;
import com.google.ads.googleads.v16.resources.Campaign;
import com.google.ads.googleads.v16.resources.CampaignBudget;
import com.google.ads.googleads.v16.services.AssetGroupAssetOperation;
import com.google.ads.googleads.v16.services.AssetGroupOperation;
import com.google.ads.googleads.v16.services.AssetOperation;
import com.google.ads.googleads.v16.services.AssetSetAssetOperation;
import com.google.ads.googleads.v16.services.AssetSetOperation;
import com.google.ads.googleads.v16.services.AssetSetServiceClient;
import com.google.ads.googleads.v16.services.CampaignBudgetOperation;
import com.google.ads.googleads.v16.services.CampaignOperation;
import com.google.ads.googleads.v16.services.GoogleAdsServiceClient;
import com.google.ads.googleads.v16.services.HotelAssetSuggestion;
import com.google.ads.googleads.v16.services.HotelImageAsset;
import com.google.ads.googleads.v16.services.HotelTextAsset;
import com.google.ads.googleads.v16.services.MutateAssetSetsResponse;
import com.google.ads.googleads.v16.services.MutateGoogleAdsResponse;
import com.google.ads.googleads.v16.services.MutateOperation;
import com.google.ads.googleads.v16.services.MutateOperationResponse;
import com.google.ads.googleads.v16.services.MutateOperationResponse.ResponseCase;
import com.google.ads.googleads.v16.services.SuggestTravelAssetsRequest;
import com.google.ads.googleads.v16.services.SuggestTravelAssetsResponse;
import com.google.ads.googleads.v16.services.TravelAssetSuggestionServiceClient;
import com.google.ads.googleads.v16.utils.ResourceNames;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.ByteStreams;
import com.google.protobuf.ByteString;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import org.joda.time.DateTime;

/**
 * This example shows how to create a Performance Max for travel goals campaign. It also uses
 * TravelAssetSuggestionService to fetch suggested assets for creating an asset group. In case there
 * are not enough assets for the asset group (required by Performance Max), this example will create
 * more assets to fulfill the requirements.
 *
 * <p>For more information about Performance Max campaigns, see
 * https://developers.google.com/google-ads/api/docs/performance-max/overview.
 *
 * <p>Prerequisites:
 *
 * <ul>
 *   <li>You must have at least one conversion action in the account. For more about conversion
 *       actions, see
 *       https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.
 * </ul>
 *
 * <p>Notes:
 *
 * <ul>
 *   <li>This example uses the default customer conversion goals. For an example of setting
 *       campaign-specific conversion goals, see {@link
 *       com.google.ads.googleads.examples.shoppingads.AddPerformanceMaxRetailCampaign}.
 *   <li>To learn how to create asset group signals, see {@link
 *       com.google.ads.googleads.examples.advancedoperations.AddPerformanceMaxCampaign}.
 * </ul>
 */
public class AddPerformanceMaxForTravelGoalsCampaign {

  // Minimum requirements of assets required in a Performance Max asset group.
  // See https://developers.google.com/google-ads/api/docs/performance-max/assets for details.
  private static Map<AssetFieldType, Integer> MIN_REQUIRED_TEXT_ASSET_COUNTS =
      ImmutableMap.<AssetFieldType, Integer>builder()
          .put(AssetFieldType.HEADLINE, 3)
          .put(AssetFieldType.LONG_HEADLINE, 1)
          .put(AssetFieldType.DESCRIPTION, 2)
          .put(AssetFieldType.BUSINESS_NAME, 1)
          .build();

  private static Map<AssetFieldType, Integer> MIN_REQUIRED_IMAGE_ASSET_COUNTS =
      ImmutableMap.<AssetFieldType, Integer>builder()
          .put(AssetFieldType.MARKETING_IMAGE, 1)
          .put(AssetFieldType.SQUARE_MARKETING_IMAGE, 1)
          .put(AssetFieldType.LOGO, 1)
          .build();

  // Texts and URLs used to create text and image assets when the TravelAssetSuggestionService
  // doesn't return enough assets required for creating an asset group.
  private static Map<AssetFieldType, List<String>> DEFAULT_TEXT_ASSETS_INFO =
      ImmutableMap.<AssetFieldType, List<String>>builder()
          .put(AssetFieldType.HEADLINE, ImmutableList.of("Hotel", "Travel Reviews", "Book travel"))
          .put(AssetFieldType.LONG_HEADLINE, ImmutableList.of("Travel the World"))
          .put(
              AssetFieldType.DESCRIPTION,
              ImmutableList.of("Great deal for your beloved hotel", "Best rate guaranteed"))
          .put(AssetFieldType.BUSINESS_NAME, ImmutableList.of("Interplanetary Cruises"))
          .build();
  private static Map<AssetFieldType, List<String>> DEFAULT_IMAGE_ASSETS_INFO =
      ImmutableMap.<AssetFieldType, List<String>>builder()
          .put(AssetFieldType.MARKETING_IMAGE, ImmutableList.of("https://gaagl.page.link/Eit5"))
          .put(
              AssetFieldType.SQUARE_MARKETING_IMAGE,
              ImmutableList.of("https://gaagl.page.link/bjYi"))
          .put(AssetFieldType.LOGO, ImmutableList.of("https://gaagl.page.link/bjYi"))
          .build();

  // We specify temporary IDs that are specific to a single mutate request. Temporary IDs are always
  // negative and unique within one mutate request.
  //
  // <p>See https://developers.google.com/google-ads/api/docs/mutating/best-practices for further
  // details.
  //
  // <p>These temporary IDs are fixed because they are used in multiple places.
  private static final int ASSET_TEMPORARY_ID = -1;
  private static final int BUDGET_TEMPORARY_ID = -2;
  private static final int CAMPAIGN_TEMPORARY_ID = -3;
  private static final int ASSET_GROUP_TEMPORARY_ID = -4;

  // There are also entities that will be created in the same request but do not
  // need to be fixed temporary IDs because they are referenced only once.
  private static long temporaryId = ASSET_GROUP_TEMPORARY_ID - 1;

  /** Parameters for the example. */
  private static class AddPerformanceMaxForTravelGoalsCampaignParams extends CodeSampleParams {

    @Parameter(names = ArgumentNames.CUSTOMER_ID, required = true)
    private Long customerId;

    @Parameter(
        names = ArgumentNames.PLACE_ID,
        required = true,
        description =
            "The place ID of a hotel property. A place ID uniquely identifies a place in the Google"
                + " Places database. See https://developers.google.com/places/web-service/place-id"
                + " to learn more.")
    private String placeId;
  }

  public static void main(String[] args) {
    AddPerformanceMaxForTravelGoalsCampaignParams params =
        new AddPerformanceMaxForTravelGoalsCampaignParams();
    if (!params.parseArguments(args)) {

      // Either pass the required parameters for this example on the command line, or insert them
      // into the code here. See the parameter class definition above for descriptions.
      params.customerId = Long.parseLong("INSERT_CUSTOMER_ID_HERE");
      params.placeId = "INSERT_PLACE_ID_HERE";
    }

    GoogleAdsClient googleAdsClient = null;
    try {
      googleAdsClient = GoogleAdsClient.newBuilder().fromPropertiesFile().build();
    } catch (FileNotFoundException fnfe) {
      System.err.printf(
          "Failed to load GoogleAdsClient configuration from file. Exception: %s%n", fnfe);
      System.exit(1);
    } catch (IOException ioe) {
      System.err.printf("Failed to create GoogleAdsClient. Exception: %s%n", ioe);
      System.exit(1);
    }

    try {
      new AddPerformanceMaxForTravelGoalsCampaign()
          .runExample(googleAdsClient, params.customerId, params.placeId);
    } catch (GoogleAdsException gae) {
      // GoogleAdsException is the base class for most exceptions thrown by an API request.
      // Instances of this exception have a message and a GoogleAdsFailure that contains a
      // collection of GoogleAdsErrors that indicate the underlying causes of the
      // GoogleAdsException.
      System.err.printf(
          "Request ID %s failed due to GoogleAdsException. Underlying errors:%n",
          gae.getRequestId());
      int i = 0;
      for (GoogleAdsError googleAdsError : gae.getGoogleAdsFailure().getErrorsList()) {
        System.err.printf("  Error %d: %s%n", i++, googleAdsError);
      }
      System.exit(1);
    }
  }

  /**
   * Runs the example.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param placeId the place ID for a hotel property asset.
   */
  private void runExample(GoogleAdsClient googleAdsClient, long customerId, String placeId) {
    // Gets hotel asset suggestion using the TravelAssetSuggestionService.
    HotelAssetSuggestion hotelAssetSuggestion =
        getHotelAssetSuggestion(googleAdsClient, customerId, placeId);

    // Performance Max campaigns require that repeated assets such as headlines and descriptions be
    // created before the campaign. For the list of required assets for a Performance Max campaign,
    // see
    // https://developers.google.com/google-ads/api/docs/performance-max/assets.
    //
    // This step is the same for any type of Performance Max campaign.

    // Creates the headlines using the hotel asset suggestion.
    List<String> headlineAssetResourceNames =
        createMultipleTextAssets(
            googleAdsClient, customerId, AssetFieldType.HEADLINE, hotelAssetSuggestion);

    // Creates the descriptions using the hotel asset suggestion.
    List<String> descriptionAssetResourceNames =
        createMultipleTextAssets(
            googleAdsClient, customerId, AssetFieldType.DESCRIPTION, hotelAssetSuggestion);

    // Creates a hotel property asset set, which will be used later to link with a newly created
    // campaign.
    String hotelPropertyAssetSetResourceName = createHotelAssetSet(googleAdsClient, customerId);

    // Creates a hotel property asset and link it with the previously created hotel property asset
    // set. This asset will also be linked to an asset group in the later steps. In the real-world
    // scenario, you'd need to create many assets for all your hotel properties. We use one hotel
    // property here for simplicity. Both asset and asset set need to be created before creating a
    // campaign, so we cannot bundle them with other mutate operations below.
    String hotelPropertyAssetResourceName =
        createHotelAsset(googleAdsClient, customerId, placeId, hotelPropertyAssetSetResourceName);

    // It's important to create the below entities in this order because they depend on
    // each other.
    // The below methods create and return mutate operations that we later provide to the
    // GoogleAdsService.Mutate method in order to create the entities in a single request.
    // Since the entities for a Performance Max campaign are closely tied to one-another, it's
    // considered a best practice to create them in a single Mutate request so they all complete
    // successfully or fail entirely, leaving no orphaned entities. See:
    // https://developers.google.com/google-ads/api/docs/mutating/overview.
    List<MutateOperation> mutateOperations = new ArrayList<>();
    mutateOperations.add(createCampaignBudgetOperation(customerId));
    mutateOperations.add(createCampaignOperation(customerId, hotelPropertyAssetSetResourceName));
    mutateOperations.addAll(
        createAssetGroupOperations(
            customerId,
            hotelPropertyAssetResourceName,
            headlineAssetResourceNames,
            descriptionAssetResourceNames,
            hotelAssetSuggestion));

    // Issues a mutate request to create everything and prints the results.
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      MutateGoogleAdsResponse response =
          googleAdsServiceClient.mutate(Long.toString(customerId), mutateOperations);
      System.out.println(
          "Created the following entities for a campaign budget, a campaign, and an asset group for"
              + " Performance Max for travel goals:");
      printResponseDetails(response);
    }
  }

  /**
   * Returns hotel asset suggestion obtained from TravelAssetsSuggestionService.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param placeId the place ID for a hotel property asset.
   * @return a hotel asset suggestion.
   */
  private HotelAssetSuggestion getHotelAssetSuggestion(
      GoogleAdsClient googleAdsClient, long customerId, String placeId) {

    try (TravelAssetSuggestionServiceClient travelAssetSuggestionServiceClient =
        googleAdsClient.getLatestVersion().createTravelAssetSuggestionServiceClient()) {
      // Sends a request to suggest assets to be created as an asset group for the Performance Max
      // for travel goals campaign.
      SuggestTravelAssetsResponse suggestTravelAssetsResponse =
          travelAssetSuggestionServiceClient.suggestTravelAssets(
              SuggestTravelAssetsRequest.newBuilder()
                  .setCustomerId(Long.toString(customerId))
                  // Uses 'en-US' as an example. It can be any language specifications in BCP 47
                  // format.
                  .setLanguageOption("en-US")
                  // The service accepts several place IDs. We use only one here for demonstration.
                  .addPlaceIds(placeId)
                  .build());
      System.out.printf("Fetched a hotel asset suggestion for the place ID '%s'.%n", placeId);
      return suggestTravelAssetsResponse.getHotelAssetSuggestions(0);
    }
  }

  /**
   * Creates multiple text assets and returns the list of resource names. The hotel asset suggestion
   * is used to create a text asset first. If the number of created text assets is still fewer than
   * the minimum required number of assets of the specified asset field type, adds more text assets
   * to fulfill the requirement.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param assetFieldType the asset field type that the text assets will be created for.
   * @param hotelAssetSuggestion the hotel asset suggestion.
   * @return the resource names of the created text assets.
   */
  private List<String> createMultipleTextAssets(
      GoogleAdsClient googleAdsClient,
      long customerId,
      AssetFieldType assetFieldType,
      HotelAssetSuggestion hotelAssetSuggestion) {

    // Uses the GoogleAdService to create multiple text assets in a single request.
    List<MutateOperation> mutateOperations = new ArrayList<>();

    // First, adds all the text assets of the specified asset field type.
    // Filters to only the specified asset field type.
    // Constructs a mutate operation to create the asset.
    // Adds the operation to the list.
    if (HotelAssetSuggestionStatus.SUCCESS.equals(hotelAssetSuggestion.getStatus())) {
      for (HotelTextAsset asset : hotelAssetSuggestion.getTextAssetsList()) {
        if (asset.getAssetFieldType().equals(assetFieldType)) {
          MutateOperation build =
              MutateOperation.newBuilder()
                  .setAssetOperation(
                      AssetOperation.newBuilder()
                          .setCreate(
                              Asset.newBuilder()
                                  .setTextAsset(
                                      TextAsset.newBuilder().setText(asset.getText()).build())
                                  .build()))
                  .build();
          mutateOperations.add(build);
        }
      }
    }

    // If the added assets are still less than the minimum required assets for the asset field type,
    // add more text assets using the default texts.
    int i = 0;
    while (mutateOperations.size() < MIN_REQUIRED_TEXT_ASSET_COUNTS.get(assetFieldType)) {
      String text = DEFAULT_TEXT_ASSETS_INFO.get(assetFieldType).get(i++);
      // Creates a mutate operation for a text asset, using the default text.
      mutateOperations.add(
          MutateOperation.newBuilder()
              .setAssetOperation(
                  AssetOperation.newBuilder()
                      .setCreate(
                          Asset.newBuilder()
                              .setTextAsset(TextAsset.newBuilder().setText(text).build())
                              .build()))
              .build());
    }

    // Issues a mutate request to add all assets.
    List<String> assetResourceNames;
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      MutateGoogleAdsResponse response =
          googleAdsServiceClient.mutate(Long.toString(customerId), mutateOperations);
      assetResourceNames =
          response.getMutateOperationResponsesList().stream()
              .map(resp -> resp.getAssetResult().getResourceName())
              .collect(Collectors.toList());
      System.out.printf(
          "The following assets were created for the asset field type '%s':%n", assetFieldType);
      printResponseDetails(response);
    }

    return assetResourceNames;
  }

  /**
   * Creates a hotel property asset set.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @return the created hotel property asset set resource name.
   */
  private String createHotelAssetSet(GoogleAdsClient googleAdsClient, long customerId) {
    // Creates an asset set operation for a hotel property asset set.
    AssetSetOperation assetSetOperation =
        AssetSetOperation.newBuilder()
            .setCreate(
                AssetSet.newBuilder()
                    .setName(
                        "My Hotel propery asset set #" + CodeSampleHelper.getPrintableDateTime())
                    .setType(AssetSetType.HOTEL_PROPERTY))
            .build();
    try (AssetSetServiceClient assetSetServiceClient =
        googleAdsClient.getLatestVersion().createAssetSetServiceClient()) {
      MutateAssetSetsResponse mutateAssetSetsResponse =
          assetSetServiceClient.mutateAssetSets(
              Long.toString(customerId), ImmutableList.of(assetSetOperation));
      String assetSetResourceName = mutateAssetSetsResponse.getResults(0).getResourceName();
      System.out.printf("Created an asset set with resource name: '%s'%n", assetSetResourceName);
      return assetSetResourceName;
    }
  }

  /**
   * Creates a hotel property asset using the specified place ID. The place ID must belong to a
   * hotel property. Then, links it to the specified asset set.
   *
   * <p>See https://developers.google.com/places/web-service/place-id to search for a hotel place
   * ID.
   *
   * @param googleAdsClient the Google Ads API client.
   * @param customerId the client customer ID.
   * @param placeId the place ID for a hotel.
   * @param hotelPropertyAssetSetResourceName the hotel asset set resource name.
   * @return the created hotel property asset resource name.
   */
  private String createHotelAsset(
      GoogleAdsClient googleAdsClient,
      long customerId,
      String placeId,
      String hotelPropertyAssetSetResourceName) {
    // Uses the GoogleAdService to create an asset and asset set asset in a single request.
    List<MutateOperation> mutateOperations = new ArrayList<>();
    String assetResourceName = ResourceNames.asset(customerId, ASSET_TEMPORARY_ID);
    // Creates a mutate operation for a hotel property asset.
    Asset hotelPropertyAsset =
        Asset.newBuilder()
            .setResourceName(assetResourceName)
            .setHotelPropertyAsset(HotelPropertyAsset.newBuilder().setPlaceId(placeId))
            .build();
    mutateOperations.add(
        MutateOperation.newBuilder()
            .setAssetOperation(AssetOperation.newBuilder().setCreate(hotelPropertyAsset))
            .build());

    // Creates a mutate operation for an asset set asset.
    AssetSetAsset assetSetAsset =
        AssetSetAsset.newBuilder()
            .setAsset(assetResourceName)
            .setAssetSet(hotelPropertyAssetSetResourceName)
            .build();
    mutateOperations.add(
        MutateOperation.newBuilder()
            .setAssetSetAssetOperation(AssetSetAssetOperation.newBuilder().setCreate(assetSetAsset))
            .build());
    // Issues a mutate request to create all entities.
    try (GoogleAdsServiceClient googleAdsServiceClient =
        googleAdsClient.getLatestVersion().createGoogleAdsServiceClient()) {
      MutateGoogleAdsResponse mutateGoogleAdsResponse =
          googleAdsServiceClient.mutate(Long.toString(customerId), mutateOperations);
      System.out.println("Created the following entities for the hotel asset:");
      printResponseDetails(mutateGoogleAdsResponse);
      // Returns the created asset resource name, which will be used later to create an asset
      // group. Other resource names are not used later.
      return mutateGoogleAdsResponse
          .getMutateOperationResponses(0)
          .getAssetResult()
          .getResourceName();
    }
  }

  /**
   * Creates a mutate operation that creates a new campaign budget.
   *
   * <p>A temporary ID will be assigned to this campaign budget so that it can be referenced by
   * other objects being created in the same mutate request.
   *
   * @param customerId the client customer ID.
   * @return a mutate operation that creates a campaign budget.
   */
  private MutateOperation createCampaignBudgetOperation(long customerId) {
    CampaignBudget campaignBudget =
        CampaignBudget.newBuilder()
            .setName("Performance Max for travel goals campaign budget #" + getPrintableDateTime())
            // The budget period already defaults to DAILY.
            .setAmountMicros(50_000_000)
            .setDeliveryMethod(BudgetDeliveryMethod.STANDARD)
            // A Performance Max campaign cannot use a shared campaign budget.
            .setExplicitlyShared(false)
            // Sets a temporary ID in the budget's resource name, so it can be referenced
            // by the campaign in later steps.
            .setResourceName(ResourceNames.campaignBudget(customerId, BUDGET_TEMPORARY_ID))
            .build();

    return MutateOperation.newBuilder()
        .setCampaignBudgetOperation(
            CampaignBudgetOperation.newBuilder().setCreate(campaignBudget).build())
        .build();
  }

  /**
   * Creates a mutate operation that creates a new Performance Max campaign. Links the specified
   * hotel property asset set to this campaign.
   *
   * <p>A temporary ID will be assigned to this campaign so that it can be referenced by other
   * objects being created in the same mutate request.
   *
   * @param customerId the client customer ID.
   * @param hotelPropertyAssetSetResourceName
   * @return the mutate operation that creates a campaign.
   */
  private MutateOperation createCampaignOperation(
      long customerId, String hotelPropertyAssetSetResourceName) {
    Campaign performanceMaxCampaign =
        Campaign.newBuilder()
            .setName("Performance Max for travel goals campaign #" + getPrintableDateTime())
            // Sets the campaign status as PAUSED. The campaign is the only entity in
            // the mutate request that should have its status set.
            .setStatus(CampaignStatus.PAUSED)
            // All Performance Max campaigns have an advertising_channel_type of
            // PERFORMANCE_MAX. The advertising_channel_sub_type should not be set.
            .setAdvertisingChannelType(AdvertisingChannelType.PERFORMANCE_MAX)
            // To create a Performance Max for travel goals campaign, you need to set
            // `hotel_property_asset_set`.
            .setHotelPropertyAssetSet(hotelPropertyAssetSetResourceName)
            // Bidding strategy must be set directly on the campaign.
            // Setting a portfolio bidding strategy by resource name is not supported.
            // Max Conversion and Maximize Conversion Value are the only strategies
            // supported for Performance Max campaigns.
            // An optional ROAS (Return on Advertising Spend) can be set for
            // maximize_conversion_value. The ROAS value must be specified as a ratio in
            // the API. It is calculated by dividing "total value" by "total spend".
            // For more information on Maximize Conversion Value, see the support
            // article: http://support.google.com/google-ads/answer/7684216.
            // A targetRoas of 3.5 corresponds to a 350% return on ad spend.
            .setMaximizeConversionValue(
                MaximizeConversionValue.newBuilder().setTargetRoas(3.5).build())
            // Assigns the resource name with a temporary ID.
            .setResourceName(ResourceNames.campaign(customerId, CAMPAIGN_TEMPORARY_ID))
            // Sets the budget using the given budget resource name.
            .setCampaignBudget(ResourceNames.campaignBudget(customerId, BUDGET_TEMPORARY_ID))
            // Optional fields.
            .setStartDate(new DateTime().plusDays(1).toString("yyyyMMdd"))
            .setEndDate(new DateTime().plusDays(365).toString("yyyyMMdd"))
            .build();

    return MutateOperation.newBuilder()
        .setCampaignOperation(
            CampaignOperation.newBuilder().setCreate(performanceMaxCampaign).build())
        .build();
  }

  /**
   * Creates a list of mutate operations that create a new asset group, composed of suggested
   * assets. In case the number of suggested assets is not enough for the requirements, creates more
   * assets to meet the requirement.
   *
   * <p>For the list of required assets for a Performance Max campaign, see
   * https://developers.google.com/google-ads/api/docs/performance-max/assets.
   *
   * @param hotelPropertyAssetResourceName the hotel property asset resource name that will be used
   *     to create an asset group
   * @param headlineAssetResourceNames a list of headline resource names
   * @param descriptionAssetResourceNames a list of description resource names
   * @param hotelAssetSuggestion the hotel asset suggestion
   * @return a list of mutate operations that create the asset group
   */
  private List<MutateOperation> createAssetGroupOperations(
      long customerId,
      String hotelPropertyAssetResourceName,
      List<String> headlineAssetResourceNames,
      List<String> descriptionAssetResourceNames,
      HotelAssetSuggestion hotelAssetSuggestion) {
    List<MutateOperation> mutateOperations = new ArrayList<>();

    // Creates a new mutate operation that creates an asset group using suggested information when
    // available.
    String assetGroupName;
    List<String> assetGroupFinalUrls;
    if (HotelAssetSuggestionStatus.SUCCESS.equals(hotelAssetSuggestion.getStatus())) {
      assetGroupName = hotelAssetSuggestion.getHotelName();
      assetGroupFinalUrls = ImmutableList.of(hotelAssetSuggestion.getFinalUrl());
    } else {
      assetGroupName = "Performance Max for travel goals asset group #" + getPrintableDateTime();
      assetGroupFinalUrls = ImmutableList.of("https://www.example.com");
    }
    String assetGroupResourceName = ResourceNames.assetGroup(customerId, ASSET_GROUP_TEMPORARY_ID);
    AssetGroup assetGroup =
        AssetGroup.newBuilder()
            .setResourceName(assetGroupResourceName)
            .setName(assetGroupName)
            .setCampaign(ResourceNames.campaign(customerId, CAMPAIGN_TEMPORARY_ID))
            .addAllFinalUrls(assetGroupFinalUrls)
            .setStatus(AssetGroupStatus.PAUSED)
            .build();
    mutateOperations.add(
        MutateOperation.newBuilder()
            .setAssetGroupOperation(AssetGroupOperation.newBuilder().setCreate(assetGroup))
            .build());

    // An asset group is linked to an asset by creating a new asset group asset
    // and providing:
    // -  the resource name of the asset group
    // -  the resource name of the asset
    // -  the field_type of the asset in this asset group
    //
    // To learn more about asset groups, see
    // https://developers.google.com/google-ads/api/docs/performance-max/asset-groups.

    // Headline and description assets were created at the first step of this example. So, we
    // just need to link them with the created asset group.

    // Builds the AssetGroupAssets to link headline assets to the asset group.
    List<AssetGroupAsset> assetGroupAssets = new ArrayList<>();
    assetGroupAssets.addAll(
        headlineAssetResourceNames.stream()
            .map(
                headlineAssetResourceName ->
                    AssetGroupAsset.newBuilder()
                        .setAsset(headlineAssetResourceName)
                        .setAssetGroup(assetGroupResourceName)
                        .setFieldType(AssetFieldType.HEADLINE)
                        .build())
            .collect(Collectors.toList()));
    // Builds the AssetGroupAssets to link description assets to the asset group.
    assetGroupAssets.addAll(
        descriptionAssetResourceNames.stream()
            .map(
                descriptionAssetResourceName ->
                    AssetGroupAsset.newBuilder()
                        .setAsset(descriptionAssetResourceName)
                        .setAssetGroup(assetGroupResourceName)
                        .setFieldType(AssetFieldType.DESCRIPTION)
                        .build())
            .collect(Collectors.toList()));

    // Adds operations to create the AssetGroupAssets for headlines and descriptions.
    mutateOperations.addAll(
        assetGroupAssets.stream()
            .map(
                assetGroupAsset ->
                    MutateOperation.newBuilder()
                        .setAssetGroupAssetOperation(
                            AssetGroupAssetOperation.newBuilder()
                                .setCreate(assetGroupAsset)
                                .build())
                        .build())
            .collect(Collectors.toList()));

    // Link the previously created hotel property asset to the asset group. In the real-world
    // scenario, you'd need to do this step several times for each hotel property asset.
    AssetGroupAsset hotelProperyAssetGroupAsset =
        AssetGroupAsset.newBuilder()
            .setAsset(hotelPropertyAssetResourceName)
            .setAssetGroup(assetGroupResourceName)
            .setFieldType(AssetFieldType.HOTEL_PROPERTY)
            .build();
    // Adds an operation to link the hotel property asset to the asset group.
    mutateOperations.add(
        MutateOperation.newBuilder()
            .setAssetGroupAssetOperation(
                AssetGroupAssetOperation.newBuilder().setCreate(hotelProperyAssetGroupAsset))
            .build());

    // Creates the rest of the required text assets and links them to the asset group.
    mutateOperations.addAll(
        createOperationsForTextAssetsAndAssetGroupAssets(
            customerId, hotelAssetSuggestion, assetGroupResourceName));

    // Creates the image assets and links them to the asset group. Some optional image assets
    // suggested by the TravelAssetSuggestionService might be created too.
    mutateOperations.addAll(
        createOperationsForImageAssetsAndAssetGroupAssets(
            customerId, hotelAssetSuggestion, assetGroupResourceName));
    if (HotelAssetSuggestionStatus.SUCCESS.equals(hotelAssetSuggestion.getStatus())) {
      // Creates a new mutate operation for a suggested call-to-action asset and links it
      // to the asset group.
      Asset callToActionAsset =
          Asset.newBuilder()
              .setResourceName(ResourceNames.asset(customerId, temporaryId))
              .setName("Suggested call-to-action asset #" + CodeSampleHelper.getPrintableDateTime())
              .setCallToActionAsset(
                  CallToActionAsset.newBuilder()
                      .setCallToAction(hotelAssetSuggestion.getCallToAction()))
              .build();
      // Adds an operation to create the call-to-action asset.
      mutateOperations.add(
          MutateOperation.newBuilder()
              .setAssetOperation(AssetOperation.newBuilder().setCreate(callToActionAsset))
              .build());

      AssetGroupAsset callToActionAssetGroupAsset =
          AssetGroupAsset.newBuilder()
              .setAsset(callToActionAsset.getResourceName())
              .setAssetGroup(assetGroupResourceName)
              .setFieldType(AssetFieldType.CALL_TO_ACTION_SELECTION)
              .build();
      // Adds an operation to link the call-to-action asset to the asset group.
      mutateOperations.add(
          MutateOperation.newBuilder()
              .setAssetGroupAssetOperation(
                  AssetGroupAssetOperation.newBuilder().setCreate(callToActionAssetGroupAsset))
              .build());

      temporaryId--;
    }
    return mutateOperations;
  }

  /**
   * Creates text assets required for an asset group using the suggested hotel text assets. It adds
   * more text assets to fulfill the requirements if the suggested hotel text assets are not enough.
   *
   * @param customerId the client customer ID.
   * @param hotelAssetSuggestion the hotel asset suggestion.
   * @return a list of mutate operations that create text assets and asset group assets.
   */
  private List<MutateOperation> createOperationsForTextAssetsAndAssetGroupAssets(
      long customerId, HotelAssetSuggestion hotelAssetSuggestion, String assetGroupResourceName) {
    // Creates mutate operations for the suggested text assets except for headlines and
    // descriptions, which were created previously.
    List<MutateOperation> mutateOperations = new ArrayList<>();
    // Creates a map of asset field type to list of text values to create.
    Map<AssetFieldType, List<String>> textByFieldType = new HashMap<>();

    if (HotelAssetSuggestionStatus.SUCCESS.equals(hotelAssetSuggestion.getStatus())) {
      // Adds text values of suggested text assets.
      for (HotelTextAsset hotelTextAsset : hotelAssetSuggestion.getTextAssetsList()) {
        AssetFieldType assetFieldType = hotelTextAsset.getAssetFieldType();
        if (AssetFieldType.HEADLINE.equals(assetFieldType)
            || AssetFieldType.DESCRIPTION.equals(assetFieldType)) {
          // Headlines and descriptions were already created at the first step of this code example.
          continue;
        }
        System.out.printf(
            "A text asset with text '%s' is suggested for the asset field type '%s'.%n",
            hotelTextAsset.getText(), assetFieldType);

        textByFieldType
            .computeIfAbsent(assetFieldType, ft -> new ArrayList<>())
            .add(hotelTextAsset.getText());
      }
    }

    // Collects more text values by field type to fulfill the requirements.
    for (Entry<AssetFieldType, Integer> requiredEntry : MIN_REQUIRED_TEXT_ASSET_COUNTS.entrySet()) {
      AssetFieldType assetFieldType = requiredEntry.getKey();
      if (AssetFieldType.HEADLINE.equals(assetFieldType)
          || AssetFieldType.DESCRIPTION.equals(assetFieldType)) {
        // Headlines and descriptions were already created at the first step of this code example.
        continue;
      }
      textByFieldType.computeIfAbsent(assetFieldType, k -> new ArrayList<>());
      int i = 0;
      while (textByFieldType.get(assetFieldType).size() < requiredEntry.getValue()) {
        String textFromDefaults = DEFAULT_TEXT_ASSETS_INFO.get(assetFieldType).get(i++);
        System.out.printf(
            "A default text '%s' is used to create a text asset for the asset field type '%s'.%n",
            textFromDefaults, assetFieldType);
        textByFieldType
            .computeIfAbsent(assetFieldType, ft -> new ArrayList<>())
            .add(textFromDefaults);
      }
    }

    // Converts the list of text values by field type into AssetOperations and
    // AssetGroupAssetOperations.
    for (Entry<AssetFieldType, List<String>> fieldTypeEntry : textByFieldType.entrySet()) {
      for (String text : fieldTypeEntry.getValue()) {
        // Builds the Asset.
        Asset asset =
            Asset.newBuilder()
                .setResourceName(ResourceNames.asset(customerId, temporaryId--))
                .setTextAsset(TextAsset.newBuilder().setText(text))
                .build();
        // Adds an operation to create the Asset.
        mutateOperations.add(
            MutateOperation.newBuilder()
                .setAssetOperation(AssetOperation.newBuilder().setCreate(asset))
                .build());

        // Builds the AssetGroupAsset.
        AssetGroupAsset assetGroupAsset =
            AssetGroupAsset.newBuilder()
                // References the Asset above by resource name.
                .setAsset(asset.getResourceName())
                .setAssetGroup(assetGroupResourceName)
                .setFieldType(fieldTypeEntry.getKey())
                .build();
        // Adds an operation to link the Asset to the AssetGroup.
        mutateOperations.add(
            MutateOperation.newBuilder()
                .setAssetGroupAssetOperation(
                    AssetGroupAssetOperation.newBuilder().setCreate(assetGroupAsset))
                .build());
      }
    }
    return mutateOperations;
  }

  /**
   * Creates image assets required for an asset group using the suggested hotel image assets. It
   * adds more image assets to fulfill the requirements if the suggested hotel image assets are not
   * enough.
   *
   * @param customerId the client customer ID.
   * @param hotelAssetSuggestion the hotel asset suggestion.
   * @return a list of mutate operations that create image assets and asset group assets.
   */
  private List<MutateOperation> createOperationsForImageAssetsAndAssetGroupAssets(
      long customerId, HotelAssetSuggestion hotelAssetSuggestion, String assetGroupResourceName) {
    // Creates mutate operations for the suggested image assets.
    List<MutateOperation> mutateOperations = new ArrayList<>();
    // Creates a map of asset field type to list of image URLs for which this method will create
    // assets and asset group assets.
    Map<AssetFieldType, List<String>> imageUrlsByFieldType = new HashMap<>();

    if (HotelAssetSuggestionStatus.SUCCESS.equals(hotelAssetSuggestion.getStatus())) {
      // Adds URLs of suggested image assets.
      for (HotelImageAsset hotelImageAsset : hotelAssetSuggestion.getImageAssetsList()) {
        AssetFieldType assetFieldType = hotelImageAsset.getAssetFieldType();
        System.out.printf(
            "An image asset with URL '%s' is suggested for the asset field type '%s'.%n",
            hotelImageAsset.getUri(), assetFieldType);

        imageUrlsByFieldType
            .computeIfAbsent(assetFieldType, ft -> new ArrayList<>())
            .add(hotelImageAsset.getUri());
      }
    }

    // Collects more image URLs by field type to fulfill the requirements.
    for (Entry<AssetFieldType, Integer> requiredEntry :
        MIN_REQUIRED_IMAGE_ASSET_COUNTS.entrySet()) {
      AssetFieldType assetFieldType = requiredEntry.getKey();
      imageUrlsByFieldType.computeIfAbsent(assetFieldType, k -> new ArrayList<>());
      int i = 0;
      while (imageUrlsByFieldType.get(assetFieldType).size() < requiredEntry.getValue()) {
        String imageUrlFromDefaults = DEFAULT_IMAGE_ASSETS_INFO.get(assetFieldType).get(i++);
        System.out.printf(
            "A default image URL '%s' is used to create an image asset for the asset field type"
                + " '%s'.%n",
            imageUrlFromDefaults, assetFieldType);
        imageUrlsByFieldType
            .computeIfAbsent(assetFieldType, ft -> new ArrayList<>())
            .add(imageUrlFromDefaults);
      }
    }

    // Converts the list of URLs by field type into AssetOperations and AssetGroupAssetOperations.
    for (Entry<AssetFieldType, List<String>> fieldTypeEntry : imageUrlsByFieldType.entrySet()) {
      AssetFieldType assetFieldType = fieldTypeEntry.getKey();
      for (String url : fieldTypeEntry.getValue()) {
        // Retrieves the image data from the URL.
        byte[] imageData;
        try {
          imageData = ByteStreams.toByteArray(new URL(url).openStream());
        } catch (IOException e) {
          throw new RuntimeException("Failed to retrieve image data from URL: " + url, e);
        }

        // Builds the image asset.
        Asset asset =
            Asset.newBuilder()
                .setResourceName(ResourceNames.asset(customerId, temporaryId--))
                // Provide a unique friendly name to identify your asset.
                // When there is an existing image asset with the same content but a different name,
                // the
                // new name will be dropped silently.
                .setName(
                    String.format(
                        "%s#%s", assetFieldType, CodeSampleHelper.getShortPrintableDateTime()))
                .setImageAsset(ImageAsset.newBuilder().setData(ByteString.copyFrom(imageData)))
                .build();
        // Adds an operation to create the Asset.
        mutateOperations.add(
            MutateOperation.newBuilder()
                .setAssetOperation(AssetOperation.newBuilder().setCreate(asset))
                .build());

        // Builds the AssetGroupAsset.
        AssetGroupAsset assetGroupAsset =
            AssetGroupAsset.newBuilder()
                // References the Asset above by resource name.
                .setAsset(asset.getResourceName())
                .setAssetGroup(assetGroupResourceName)
                .setFieldType(assetFieldType)
                .build();
        // Adds an operation to link the Asset to the AssetGroup.
        mutateOperations.add(
            MutateOperation.newBuilder()
                .setAssetGroupAssetOperation(
                    AssetGroupAssetOperation.newBuilder().setCreate(assetGroupAsset))
                .build());
      }
    }
    return mutateOperations;
  }

  /**
   * Prints the details of a MutateGoogleAdsResponse. Parses the "response" oneof field name and
   * uses it to extract the new entity's name and resource name.
   *
   * @param response the mutate Google Ads response.
   */
  private void printResponseDetails(MutateGoogleAdsResponse response) {
    for (MutateOperationResponse operationResponse : response.getMutateOperationResponsesList()) {
      ResponseCase responseCase = operationResponse.getResponseCase();
      String resourceName;
      switch (responseCase) {
        case ASSET_RESULT:
          resourceName = operationResponse.getAssetResult().getResourceName();
          break;
        case ASSET_GROUP_RESULT:
          resourceName = operationResponse.getAssetGroupResult().getResourceName();
          break;
        case ASSET_GROUP_ASSET_RESULT:
          resourceName = operationResponse.getAssetGroupAssetResult().getResourceName();
          break;
        case ASSET_SET_ASSET_RESULT:
          resourceName = operationResponse.getAssetSetAssetResult().getResourceName();
          break;
        case CAMPAIGN_BUDGET_RESULT:
          resourceName = operationResponse.getCampaignBudgetResult().getResourceName();
          break;
        case CAMPAIGN_RESULT:
          resourceName = operationResponse.getCampaignResult().getResourceName();
          break;
        default:
          throw new IllegalArgumentException("Unexpected response case: " + responseCase);
      }
      System.out.printf("Created a(n) %s with resource name: '%s'%n", responseCase, resourceName);
    }
  }
}

      

C#

// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using CommandLine;
using Google.Ads.Gax.Examples;
using Google.Ads.Gax.Util;
using Google.Ads.GoogleAds.Config;
using Google.Ads.GoogleAds.Lib;
using Google.Ads.GoogleAds.V16.Common;
using Google.Ads.GoogleAds.V16.Errors;
using Google.Ads.GoogleAds.V16.Resources;
using Google.Ads.GoogleAds.V16.Services;
using Google.Protobuf;
using System;
using System.Collections.Generic;
using static Google.Ads.GoogleAds.V16.Enums.AdvertisingChannelTypeEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.AssetFieldTypeEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.AssetGroupStatusEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.AssetSetTypeEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.BudgetDeliveryMethodEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.CampaignStatusEnum.Types;
using static Google.Ads.GoogleAds.V16.Enums.HotelAssetSuggestionStatusEnum.Types;

namespace Google.Ads.GoogleAds.Examples.V16
{
    /// <summary>
    /// This example shows how to create a Performance Max for travel goals campaign. It also uses
    /// TravelAssetSuggestionService to fetch suggested assets for creating an asset group. In case
    /// there are not enough assets for the asset group (required by Performance Max), this example
    /// will create more assets to fulfill the requirements.
    ///
    /// <p>For more information about Performance Max campaigns, see
    /// https://developers.google.com/google-ads/api/docs/performance-max/overview.</p>
    /// <p>Prerequisites:</p>
    ///
    /// <ul>
    ///   <li>You must have at least one conversion action in the account. For more about conversion
    ///       actions, see
    ///       https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.
    ///   </li>
    /// </ul>
    ///
    /// <p>Notes:</p>
    ///
    /// <ul>
    ///   <li>This example uses the default customer conversion goals. For an example of setting
    ///       campaign-specific conversion goals, see AddPerformanceMaxRetailCampaign.cs.</li>
    ///   <li>To learn how to create asset group signals, see AddPerformanceMaxCampaign.cs.</li>
    /// </ul>
    /// </summary>
    public class AddPerformanceMaxForTravelGoalsCampaign : ExampleBase
    {
        /// <summary>
        /// Command line options for running the
        /// <see cref="AddPerformanceMaxForTravelGoalsCampaign"/> example.
        /// </summary>
        public class Options : OptionsBase
        {
            /// <summary>
            /// The Google Ads customer ID.
            /// </summary>
            [Option("customerId", Required = true, HelpText =
                "The Google Ads customer ID.")]
            public long CustomerId { get; set; }

            /// <summary>
            /// The place ID of a hotel property. A place ID uniquely identifies a place in the
            /// Google Places database. See
            /// https://developers.google.com/places/web-service-place-id to learn more.
            /// </summary>
            [Option("placeId", Required = true, HelpText =
                "The place ID of a hotel property. A place ID uniquely identifies a place in the" +
                "Google Places database. See " +
                "https://developers.google.com/places/web-service-place-id to learn more.")]
            public string PlaceId { get; set; }
        }

        /// <summary>
        /// Main method, to run this code example as a standalone application.
        /// </summary>
        /// <param name="args">The command line arguments.</param>
        public static void Main(string[] args)
        {
            Options options = ExampleUtilities.ParseCommandLine<Options>(args);

            AddPerformanceMaxForTravelGoalsCampaign codeExample =
                new AddPerformanceMaxForTravelGoalsCampaign();
            Console.WriteLine(codeExample.Description);
            codeExample.Run(new GoogleAdsClient(), options.CustomerId, options.PlaceId);
        }

        // Minimum requirements of assets required in a Performance Max asset group.
        // See https://developers.google.com/google-ads/api/docs/performance-max/assets for details.
        private Dictionary<AssetFieldType, int> MIN_REQUIRED_TEXT_ASSET_COUNTS =
            new Dictionary<AssetFieldType, int>()
            {
                { AssetFieldType.Headline, 3 },
                { AssetFieldType.LongHeadline, 1 },
                { AssetFieldType.Description, 2 },
                { AssetFieldType.BusinessName, 1 },
            };

        private Dictionary<AssetFieldType, int> MIN_REQUIRED_IMAGE_ASSET_COUNTS =
            new Dictionary<AssetFieldType, int>()
            {
                { AssetFieldType.MarketingImage, 1 },
                { AssetFieldType.SquareMarketingImage, 1 },
                { AssetFieldType.Logo, 1 },
            };


        // Texts and URLs used to create text and image assets when the TravelAssetSuggestionService
        // doesn't return enough assets required for creating an asset group.
        private Dictionary<AssetFieldType, List<string>> DEFAULT_TEXT_ASSETS_INFO =
            new Dictionary<AssetFieldType, List<string>>()
            {
                { AssetFieldType.Headline, new List<string>()
                    {
                        "Hotel", "Travel Reviews", "Book travel"
                    }
                },
                { AssetFieldType.LongHeadline, new List<string>() { "Travel the World" } },
                { AssetFieldType.Description, new List<string>()
                    {
                        "Great deal for your beloved hotel",
                        "Best rate guaranteed"
                    }
                },
                { AssetFieldType.BusinessName, new List<string>() { "Interplanetary cruises" } },
            };

        private Dictionary<AssetFieldType, List<string>> DEFAULT_IMAGE_ASSETS_INFO =
            new Dictionary<AssetFieldType, List<string>>()
            {
                { AssetFieldType.MarketingImage, new List<string>()
                    {
                        "https://gaagl.page.link/Eit5"
                    }
                },
                { AssetFieldType.SquareMarketingImage, new List<string>()
                    {
                        "https://gaagl.page.link/bjYi"
                    }
                },
                { AssetFieldType.Logo, new List<string>()
                    {
                        "https://gaagl.page.link/bjYi"
                    }
                },
            };

        // We specify temporary IDs that are specific to a single mutate request. Temporary IDs are always
        // negative and unique within one mutate request.
        //
        // <p>See https://developers.google.com/google-ads/api/docs/mutating/best-practices for
        // further details.
        //
        // <p>These temporary IDs are fixed because they are used in multiple places.
        private int ASSET_TEMPORARY_ID = -1;
        private int BUDGET_TEMPORARY_ID = -2;
        private int CAMPAIGN_TEMPORARY_ID = -3;
        private int ASSET_GROUP_TEMPORARY_ID = -4;

        // There are also entities that will be created in the same request but do not
        // need to be fixed temporary IDs because they are referenced only once.
        private long temporaryId = -5;

        /// <summary>
        /// Returns a description about the code example.
        /// </summary>
        public override string Description => "This example shows how to create a Performance " +
            " Max for travel goals campaign. It also uses TravelAssetSuggestionService to fetch " +
            "suggested assets for creating an asset group. In case there are not enough assets " +
            "for the asset group (required by Performance Max), this example will create more " +
            "assets to fulfill the requirements.\n" +
            "For more information about Performance Max campaigns, see " +
            "https://developers.google.com/google-ads/api/docs/performance-max/overview.\n" +
            "Prerequisites:\n" +
            "You must have at least one conversion action in the account. For more about " +
            "conversion actions, see " +
            "https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.\n" +
            "Notes:\n" +
            "- This example uses the default customer conversion goals. For an example of " +
            "setting campaign-specific conversion goals, see AddPerformanceMaxRetailCampaign.cs.\n" +
            "- To learn how to create asset group signals, see AddPerformanceMaxCampaign.cs.";

        /// <summary>
        /// Runs the code example.
        /// </summary>
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <param name="placeId">The place ID of a hotel property.</param>
        public void Run(GoogleAdsClient client, long customerId, string placeId)
        {
            HotelAssetSuggestion hotelAssetSuggestion =
                GetHotelAssetSuggestion(client, customerId, placeId);

            // Performance Max campaigns require that repeated assets such as headlines and
            // descriptions be created before the campaign. For the list of required assets for a
            // Performance Max campaign, see
            // https://developers.google.com/google-ads/api/docs/performance-max/assets.
            // This step is the same for any type of Performance Max campaign.

            // Creates the headlines using the hotel asset suggestion.
            List<string> headlineAssetResourceNames = CreateMultipleTextAssets(
                client, customerId, AssetFieldType.Headline, hotelAssetSuggestion);

            // Creates the descriptions using the hotel asset suggestion.
            List<string> descriptionAssetResourceNames = CreateMultipleTextAssets(
                client, customerId, AssetFieldType.Description, hotelAssetSuggestion);

            // Creates a hotel property asset set, which will be used later to link with a newly
            // created campaign.
            string hotelPropertyAssetSetResourceName = CreateHotelAssetSet(client, customerId);

            // Creates a hotel property asset and link it with the previously created hotel property
            // asset set. This asset will also be linked to an asset group in the later steps.
            // In the real-world scenario, you'd need to create many assets for all your hotel
            // properties. We use one hotel property here for simplicity.
            // Both asset and asset set need to be created before creating a campaign, so we cannot
            // bundle them with other mutate operations below.
            string hotelPropertyAssetResourceName = CreateHotelAsset(
                client, customerId, placeId, hotelPropertyAssetSetResourceName);

            // It's important to create the entities below in this order because they depend on
            // each other.
            // The methods below create and return mutate operations that we later provide to the
            // GoogleAdsService.Mutate method in order to create the entities in a single request.
            // Since the entities for a Performance Max campaign are closely tied to one-another,
            // it's considered a best practice to create them in a single Mutate request so they
            // all complete successfully or fail entirely, leaving no orphaned entities. See:
            // https://developers.google.com/google-ads/api/docs/mutating/overview.
            List<MutateOperation> mutateOperations = new List<MutateOperation>();
            mutateOperations.Add(CreateCampaignBudgetOperation(customerId));
            mutateOperations.Add(CreateCampaignOperation(customerId,
                hotelPropertyAssetSetResourceName));
            mutateOperations.AddRange(
                CreateAssetGroupOperations(
                    customerId,
                    hotelPropertyAssetResourceName,
                    headlineAssetResourceNames,
                    descriptionAssetResourceNames,
                    hotelAssetSuggestion,
                    client.Config
                )
            );

            // Issues a mutate request to create everything and prints the results.
            GoogleAdsServiceClient googleAdsServiceClient =
                client.GetService(Services.V16.GoogleAdsService);
            MutateGoogleAdsResponse response =
                googleAdsServiceClient.Mutate(customerId.ToString(), mutateOperations);
            Console.WriteLine("Created the following entities for a campaign budget, a campaign, " +
                "and an asset group for Performance Max for travel goals:");
            PrintResponseDetails(response);
        }

        /// <summary>
        /// Returns hotel asset suggestion obtained from TravelAssetsSuggestionService.
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <param name="placeId">The place ID of a hotel property.</param>
        /// <returns>The hotel asset suggestion.</returns>
        /// </summary>
        private HotelAssetSuggestion GetHotelAssetSuggestion(GoogleAdsClient client,
            long customerId, string placeId)
        {
            // Get the TravelAssetSuggestionService client.
            TravelAssetSuggestionServiceClient travelAssetSuggestionService =
                client.GetService(Services.V16.TravelAssetSuggestionService);

            SuggestTravelAssetsRequest request = new SuggestTravelAssetsRequest
            {
                CustomerId = customerId.ToString(),
                LanguageOption = "en-US",
            };

            request.PlaceIds.Add(placeId);

            SuggestTravelAssetsResponse response = travelAssetSuggestionService.SuggestTravelAssets(
                request
            );

            Console.WriteLine($"Fetched a hotel asset suggestion for the place ID {placeId}");
            return response.HotelAssetSuggestions[0];
        }


        ///<summary>
        /// Creates multiple text assets and returns the list of resource names. The hotel asset
        /// suggestion is used to create a text asset first. If the number of created text assets is
        /// still fewer than the minimum required number of assets of the specified asset field
        /// type, adds more text assets  to fulfill the requirement.
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <param name="assetFieldType">The asset field type that the text assets will be created
        /// for.</param>
        /// <param name="hotelAssetSuggestion">The hotel asset suggestion.</param>
        /// <returns>The resource names of the created text assets.</returns>
        /// </summary>
        private List<string> CreateMultipleTextAssets(GoogleAdsClient client, long customerId,
            AssetFieldType assetFieldType, HotelAssetSuggestion hotelAssetSuggestion)
        {
            // Uses the GoogleAdService to create multiple text assets in a single request.
            List<MutateOperation> mutateOperations = new List<MutateOperation>();

            // First, adds all the text assets of the specified asset field type.
            // Filters to only the specified asset field type.
            // Constructs a mutate operation to create the asset.
            // Adds the operation to the list.
            if (hotelAssetSuggestion.Status == HotelAssetSuggestionStatus.Success)
            {
                foreach (HotelTextAsset asset in hotelAssetSuggestion.TextAssets)
                {
                    if (asset.AssetFieldType == assetFieldType)
                    {
                        MutateOperation operation = new MutateOperation
                        {
                            AssetOperation = new AssetOperation
                            {
                                Create = new Asset
                                {
                                    TextAsset = new TextAsset
                                    {
                                        Text = asset.Text
                                    }
                                }
                            }
                        };
                        mutateOperations.Add(operation);
                    }
                }
            }

            // If the added assets are still less than the minimum required assets for the asset
            // field type, add more text assets using the default texts.
            int i = 0;
            while (mutateOperations.Count < MIN_REQUIRED_TEXT_ASSET_COUNTS[assetFieldType])
            {
                string text = DEFAULT_TEXT_ASSETS_INFO[assetFieldType][i++];
                MutateOperation operation = new MutateOperation
                {
                    AssetOperation = new AssetOperation {
                        Create = new Asset {
                            TextAsset = new TextAsset {
                                Text = text
                            }
                        }
                    }
                };
                mutateOperations.Add(operation);
            }

            GoogleAdsServiceClient googleAdsService =
                client.GetService(Services.V16.GoogleAdsService);

            MutateGoogleAdsResponse response =
                googleAdsService.Mutate(customerId.ToString(), mutateOperations);

            List<string> assetResourceNames = new List<string>();

            foreach (MutateOperationResponse operationResponse in response.MutateOperationResponses)
            {
                MutateAssetResult assetResult = operationResponse.AssetResult;
                assetResourceNames.Add(assetResult.ResourceName);
            }

            Console.WriteLine($"The following assets were created for the asset field type " +
                $"{assetFieldType}");
            PrintResponseDetails(response);

            return assetResourceNames;
        }

        /// <summary>
        /// Creates a hotel property asset set.
        /// <param name="client">The Google Ads client.</param>
        /// <param name="customerId">The Google Ads customer ID.</param>
        /// <returns> The created hotel property asset set resource name.</returns>
        /// </summary>
        private string CreateHotelAssetSet(GoogleAdsClient client, long customerId)
        {
            AssetSetOperation operation = new AssetSetOperation()
            {
                Create = new AssetSet {
                    Name = "My Hotel property asset set #" + ExampleUtilities.GetRandomString(),
                    Type = AssetSetType.HotelProperty
                }
            };

            AssetSetServiceClient assetSetService = client.GetService(Services.V16.AssetSetService);

            MutateAssetSetsResponse response = assetSetService.MutateAssetSets(
                customerId.ToString(),
                new List<AssetSetOperation> { operation }
            );

            string assetResourceName = response.Results[0].ResourceName;
            Console.WriteLine($"Created an asset set with resource name: {assetResourceName}");
            return assetResourceName;
        }

        /// <summary>
        /// Creates a hotel property asset using the specified place ID. The place ID must belong
        /// to a hotel property. Then, links it to the specified asset set.
        ///
        /// <p>See https://developers.google.com/places/web-service/place-id to search for a hotel
        /// place ID.</p>
        /// <param name="client">The Google Ads API client.</param>
        /// <param name="customerId">The client customer ID.</param>
        /// <param name="placeId">The place ID for a hotel.</param>
        /// <param name="hotelPropertyAssetSetResourceName">The hotel asset set resource
        /// name.</param>
        /// <returns>The created hotel property asset resource name.</returns>
        /// </summary>
        private string CreateHotelAsset(
            GoogleAdsClient client, long customerId, string placeId,
            string hotelPropertyAssetSetResourceName)
        {
            // Uses the GoogleAdService to create an asset and asset set asset in a single request.
            List<MutateOperation> mutateOperations = new List<MutateOperation>();
            string assetResourceName = ResourceNames.Asset(customerId, ASSET_TEMPORARY_ID);

            // Creates a mutate operation for a hotel property asset.
            Asset hotelPropertyAsset = new Asset()
            {
                ResourceName = assetResourceName,
                HotelPropertyAsset = new HotelPropertyAsset
                {
                    PlaceId = placeId
                }
            };
            mutateOperations.Add(new MutateOperation
            {
                AssetOperation = new AssetOperation
                {
                   Create = hotelPropertyAsset
                }
            });

            // Creates a mutate operation for an asset set asset.
            AssetSetAsset assetSetAsset = new AssetSetAsset
            {
                Asset = assetResourceName,
                AssetSet = hotelPropertyAssetSetResourceName
            };
            mutateOperations.Add(new MutateOperation
            {
                AssetSetAssetOperation = new AssetSetAssetOperation
                {
                    Create = assetSetAsset
                }
            });

            // Issues a mutate request to create all entities.
            GoogleAdsServiceClient googleAdsServiceClient =
                client.GetService(Services.V16.GoogleAdsService);

            MutateGoogleAdsResponse response =
                googleAdsServiceClient.Mutate(customerId.ToString(), mutateOperations);
            Console.WriteLine("Created the following entities for the hotel asset:");
            PrintResponseDetails(response);

            return response.MutateOperationResponses[0].AssetResult.ResourceName;
        }

        /// <summary>
        /// Creates a mutate operation that creates a new campaign budget.
        /// <p>A temporary ID will be assigned to this campaign budget so that it can be referenced
        /// by other objects being created in the same mutate request.</p>
        /// <param name="customerId">The client customer ID.</param>
        /// <returns>A mutate operation that creates a campaign budget.</returns>
        /// </summary>
        private MutateOperation CreateCampaignBudgetOperation(long customerId)
        {
            CampaignBudget campaignBudget = new CampaignBudget
            {
                Name = "Performance Max for travel goals campaign budget #" +
                    ExampleUtilities.GetRandomString(),
                // The budget period already defaults to DAILY.
                AmountMicros = 500000,
                DeliveryMethod = BudgetDeliveryMethod.Standard,
                // A Performance Max campaign cannot use a shared campaign budget.
                ExplicitlyShared = false,
                // Sets a temporary ID in the budget's resource name, so it can be referenced
                // by the campaign in later steps.
                ResourceName = ResourceNames.CampaignBudget(customerId, BUDGET_TEMPORARY_ID)
            };

            return new MutateOperation
            {
                CampaignBudgetOperation = new CampaignBudgetOperation
                {
                    Create = campaignBudget
                }
            };
        }

        /// <summary>
        /// Creates a mutate operation that creates a new Performance Max campaign. Links the
        /// specified hotel property asset set to this campaign.
        /// <p>A temporary ID will be assigned to this campaign so that it can be referenced by
        /// other objects being created in the same mutate request.</p>
        /// <param name="customerId">The client customer ID.</param>
        /// <param name="hotelPropertyAssetSetResourceName"> The resource name of the hotel property
        /// asset set.</param>
        /// <returns>A mutate operation that creates a campaign.</returns>
        /// </summary>
        private MutateOperation CreateCampaignOperation(long customerId,
            string hotelPropertyAssetSetResourceName)
        {
            Campaign performanceMaxCampaign = new Campaign
            {
                Name = "Performance Max for travel goals campaign #" +
                    ExampleUtilities.GetRandomString(),
                // Sets the campaign status as PAUSED. The campaign is the only entity in
                // the mutate request that should have its status set.
                Status = CampaignStatus.Paused,
                // All Performance Max campaigns have an advertising_channel_type of
                // PERFORMANCE_MAX. The advertising_channel_sub_type should not be set.
                AdvertisingChannelType = AdvertisingChannelType.PerformanceMax,
                // To create a Performance Max for travel goals campaign, you need to set
                // `hotel_property_asset_set`.
                HotelPropertyAssetSet = hotelPropertyAssetSetResourceName,
                // Bidding strategy must be set directly on the campaign.
                // Setting a portfolio bidding strategy by resource name is not supported.
                // Max Conversion and Maximize Conversion Value are the only strategies
                // supported for Performance Max campaigns.
                // An optional ROAS (Return on Advertising Spend) can be set for
                // maximize_conversion_value. The ROAS value must be specified as a ratio in
                // the API. It is calculated by dividing "total value" by "total spend".
                // For more information on Maximize Conversion Value, see the support
                // article: http://support.google.com/google-ads/answer/7684216.
                // A targetRoas of 3.5 corresponds to a 350% return on ad spend.
                MaximizeConversionValue = new MaximizeConversionValue
                {
                    TargetRoas = 3.5
                },
                // Assigns the resource name with a temporary ID.
                ResourceName = ResourceNames.Campaign(customerId, CAMPAIGN_TEMPORARY_ID),
                // Sets the budget using the given budget resource name.
                CampaignBudget = ResourceNames.CampaignBudget(customerId, BUDGET_TEMPORARY_ID)
            };

            return new MutateOperation
            {
                CampaignOperation = new CampaignOperation
                {
                    Create = performanceMaxCampaign
                }
            };
        }

        /// <summary>
        /// Creates a mutate operation that creates a new asset group.
        /// <param name="customerId">The client customer ID.</param>
        /// <param name="hotelPropertyAssetResourceName"> The resource name of the hotel property
        /// asset.</param>
        /// <param name="headlineAssetResourceNames">The resource names for headline
        /// assets.</param>
        /// <param name="descriptionAssetResourceNames">The resource names for description
        /// assets.</param>
        /// <param name="hotelAssetSuggestion">The hotel asset suggestion.</param>
        /// <param name="config">The Google Ads configuration.</param>
        /// <returns>A mutate operation that creates an asset group.</returns>
        /// </summary>
        private List<MutateOperation> CreateAssetGroupOperations(
            long customerId,
            string hotelPropertyAssetResourceName,
            List<string> headlineAssetResourceNames,
            List<string> descriptionAssetResourceNames,
            HotelAssetSuggestion hotelAssetSuggestion,
            GoogleAdsConfig config
        )
        {
            List<MutateOperation> mutateOperations = new List<MutateOperation>();

            // Creates a new mutate operation that creates an asset group using suggested
            // information when available.
            string assetGroupName;
            List<string> assetGroupFinalUrls = new List<string>();
            if (hotelAssetSuggestion.Status == HotelAssetSuggestionStatus.Success)
            {
                assetGroupName = hotelAssetSuggestion.HotelName;
                assetGroupFinalUrls.Add(hotelAssetSuggestion.FinalUrl);
            }
            else
            {
                assetGroupName = "Performance Max for travel goals asset group #"
                    + ExampleUtilities.GetRandomString();
                assetGroupFinalUrls.Add("https://www.example.com");
            }

            string assetGroupResourceName = ResourceNames.AssetGroup(customerId,
                ASSET_GROUP_TEMPORARY_ID);

            AssetGroup assetGroup = new AssetGroup
            {
                ResourceName = assetGroupResourceName,
                Name = assetGroupName,
                Campaign = ResourceNames.Campaign(customerId, CAMPAIGN_TEMPORARY_ID),
                Status = AssetGroupStatus.Paused
            };

            assetGroup.FinalUrls.AddRange(assetGroupFinalUrls);

            mutateOperations.Add(new MutateOperation
            {
                AssetGroupOperation = new AssetGroupOperation
                {
                    Create = assetGroup
                }
            });

            // An asset group is linked to an asset by creating a new asset group asset
            // and providing:
            // -  the resource name of the asset group
            // -  the resource name of the asset
            // -  the field_type of the asset in this asset group
            //
            // To learn more about asset groups, see
            // https://developers.google.com/google-ads/api/docs/performance-max/asset-groups.

            // Headline and description assets were created at the first step of this example.
            // So, we just need to link them with the created asset group.
            List<AssetGroupAsset> assetGroupAssets = new List<AssetGroupAsset>();
            foreach (string headlineAssetResourceName in headlineAssetResourceNames)
            {
                assetGroupAssets.Add(new AssetGroupAsset
                {
                    Asset = headlineAssetResourceName,
                    AssetGroup = assetGroupResourceName,
                    FieldType = AssetFieldType.Headline
                });
            }

            foreach (string descriptionAssetResourceName in descriptionAssetResourceNames)
            {
                assetGroupAssets.Add(new AssetGroupAsset
                {
                    Asset = descriptionAssetResourceName,
                    AssetGroup = assetGroupResourceName,
                    FieldType = AssetFieldType.Description
                });
            }

            foreach (AssetGroupAsset assetGroupAsset in assetGroupAssets)
            {
                mutateOperations.Add(new MutateOperation
                {
                    AssetGroupAssetOperation = new AssetGroupAssetOperation
                    {
                        Create = assetGroupAsset
                    }
                });
            }

            // Link the previously created hotel property asset to the asset group. In the
            // real-world scenario, you'd need to do this step several times for each hotel property
            // asset.
            AssetGroupAsset hotelPropertyAssetGroupAsset = new AssetGroupAsset
            {
                Asset = hotelPropertyAssetResourceName,
                AssetGroup = assetGroupResourceName,
                FieldType = AssetFieldType.HotelProperty
            };

            // Adds an operation to link the hotel property asset to the asset group.
            mutateOperations.Add(new MutateOperation
            {
                AssetGroupAssetOperation = new AssetGroupAssetOperation
                {
                    Create = hotelPropertyAssetGroupAsset
                }
            });

            // Creates the rest of the required text assets and links them to the asset group.
            mutateOperations.AddRange(
                CreateOperationsForTextAssetsAndAssetGroupAssets(
                    customerId, hotelAssetSuggestion, assetGroupResourceName
                )
            );

            // Creates the image assets and links them to the asset group. Some optional image
            // assets suggested by the TravelAssetSuggestionService might be created too.
            mutateOperations.AddRange(
                CreateOperationsForImageAssetsAndAssetGroupAssets(
                   customerId, hotelAssetSuggestion, assetGroupResourceName, config
                )
            );

            if (hotelAssetSuggestion.Status == HotelAssetSuggestionStatus.Success)
            {
                // Creates a new mutate operation for a suggested call-to-action asset and links it
                // to the asset group.
                Asset callToActionAsset = new Asset
                {
                    ResourceName = ResourceNames.Asset(customerId, temporaryId),
                    Name = "Suggested call-to-action asset #" + ExampleUtilities.GetRandomString(),
                    CallToActionAsset = new CallToActionAsset
                    {
                        CallToAction = hotelAssetSuggestion.CallToAction
                    }
                };
                // Adds an operation to create the call-to-action asset.
                mutateOperations.Add(new MutateOperation
                {
                    AssetOperation = new AssetOperation
                    {
                        Create = callToActionAsset
                    }
                });

                AssetGroupAsset callToActionAssetGroupAsset = new AssetGroupAsset
                {
                    Asset = callToActionAsset.ResourceName,
                    AssetGroup = assetGroupResourceName,
                    FieldType = AssetFieldType.CallToActionSelection
                };
                // Adds an operation to link the call-to-action asset to the asset group.
                mutateOperations.Add(new MutateOperation
                {
                    AssetGroupAssetOperation = new AssetGroupAssetOperation
                    {
                        Create = callToActionAssetGroupAsset
                    }
                });
                temporaryId--;
            }

            return mutateOperations;
        }

        /// <summary>
        /// Creates text assets required for an asset group using the suggested hotel text assets.
        /// It adds  more text assets to fulfill the requirements if the suggested hotel text assets
        /// are not enough.
        /// <param name="customerId">The client customer ID.</param>
        /// <param name="hotelAssetSuggestion">The hotel asset suggestion.</param>
        /// <param name="assetGroupResourceName">The resource name of the asset group.</param>
        /// <returns>A list of mutate operations that create text assets and asset group
        /// assets.</returns>
        /// </summary>
        private List<MutateOperation> CreateOperationsForTextAssetsAndAssetGroupAssets(
            long customerId,
            HotelAssetSuggestion hotelAssetSuggestion,
            string assetGroupResourceName
        )
        {
            // Creates mutate operations for the suggested text assets except for headlines and
            // descriptions, which were created previously.
            List<MutateOperation> mutateOperations = new List<MutateOperation>();
            // Creates a map of asset field type to list of text values to create.
            Dictionary<AssetFieldType, List<string>> textByFieldType =
                new Dictionary<AssetFieldType, List<string>>();

            if (hotelAssetSuggestion.Status == HotelAssetSuggestionStatus.Success)
            {
                // Adds text values of suggested text assets.
                foreach (HotelTextAsset hotelTextAsset in hotelAssetSuggestion.TextAssets)
                {
                    AssetFieldType assetFieldType = hotelTextAsset.AssetFieldType;
                    if (assetFieldType == AssetFieldType.Headline ||
                        assetFieldType == AssetFieldType.Description)
                    {
                        // Headlines and descriptions were already created at the first step of this
                        // code example.
                        continue;
                    }
                    Console.WriteLine($"A text asset with text {hotelTextAsset.Text} is " +
                        $"suggested for the asset field type {assetFieldType}");
                    List<string> existingTexts = null;
                    if (!textByFieldType.TryGetValue(assetFieldType, out existingTexts))
                    {
                        existingTexts = textByFieldType[assetFieldType] = new List<string>();
                    }

                    existingTexts.Add(hotelTextAsset.Text);
                }
            }

            // Collects more text values by field type to fulfill the requirements.
            foreach (AssetFieldType assetFieldType in MIN_REQUIRED_TEXT_ASSET_COUNTS.Keys)
            {
                if (assetFieldType == AssetFieldType.Headline ||
                        assetFieldType == AssetFieldType.Description)
                {
                    // Headlines and descriptions were already created at the first step of
                    // this code example.
                    continue;
                }

                List<string> existingTexts = null;
                if (!textByFieldType.TryGetValue(assetFieldType, out existingTexts))
                {
                    existingTexts = textByFieldType[assetFieldType] = new List<string>();
                }

                int i = 0;
                while (textByFieldType[assetFieldType].Count <
                    MIN_REQUIRED_TEXT_ASSET_COUNTS[assetFieldType])
                {
                    string textFromDefaults = DEFAULT_TEXT_ASSETS_INFO[assetFieldType][i++];
                    Console.WriteLine($"A default text '{textFromDefaults}' is used to create a " +
                        $"text asset for the asset field type '{assetFieldType}'");
                    existingTexts.Add(textFromDefaults);
                }
            }

            // Converts the list of text values by field type into AssetOperations and
            // AssetGroupAssetOperations.
            foreach (AssetFieldType assetFieldType in textByFieldType.Keys)
            {
                foreach (string text in textByFieldType[assetFieldType])
                {
                    // Builds the asset.
                    Asset asset = new Asset
                    {
                        ResourceName = ResourceNames.Asset(customerId, temporaryId--),
                        TextAsset = new TextAsset
                        {
                            Text = text
                        }
                    };
                    // Adds an operation to create the Asset.
                    mutateOperations.Add(new MutateOperation
                    {
                        AssetOperation = new AssetOperation
                        {
                            Create = asset
                        }
                    });

                    //Builds the AssetGroupAsset.
                    AssetGroupAsset assetGroupAsset = new AssetGroupAsset
                    {
                        Asset = asset.ResourceName,
                        AssetGroup = assetGroupResourceName,
                        FieldType = assetFieldType
                    };
                    // Adds an operation to link the Asset to the AssetGroup.
                    mutateOperations.Add(new MutateOperation
                    {
                        AssetGroupAssetOperation = new AssetGroupAssetOperation
                        {
                            Create = assetGroupAsset
                        }
                    });
                }
            }

            return mutateOperations;
        }

        /// <summary>
        /// Creates image assets required for an asset group using the suggested hotel image assets.
        /// It adds more image assets to fulfill the requirements if the suggested hotel image
        /// assets are not enough.
        /// <param name="customerId">The client customer ID.</param>
        /// <param name="hotelAssetSuggestion">The hotel asset suggestion.</param>
        /// <param name="assetGroupResourceName">The resource name of the asset group.</param>
        /// <param name="config">The Google Ads config.</param>
        /// <returns>A list of mutate operations that create image assets and asset group
        /// assets.</returns>
        /// </summary>
        private List<MutateOperation> CreateOperationsForImageAssetsAndAssetGroupAssets(
            long customerId,
            HotelAssetSuggestion hotelAssetSuggestion,
            string assetGroupResourceName,
            GoogleAdsConfig config
        )
        {
            // Creates mutate operations for the suggested image assets.
            List<MutateOperation> mutateOperations = new List<MutateOperation>();
            // Creates a map of asset field type to list of image URLs for which this method will
            // create assets and asset group assets.
            Dictionary<AssetFieldType, List<string>> imageUrlsByFieldType =
                new Dictionary<AssetFieldType, List<string>>();

            if (hotelAssetSuggestion.Status == HotelAssetSuggestionStatus.Success)
            {
                // Adds URLs of suggested image assets.
                foreach (HotelImageAsset hotelImageAsset in hotelAssetSuggestion.ImageAssets)
                {
                    AssetFieldType assetFieldType = hotelImageAsset.AssetFieldType;
                    Console.WriteLine($"An image asset with URL '{hotelImageAsset.Uri} is " +
                        $"suggested for the asset field type {assetFieldType}");

                    List<string> existingImageUrls = null;
                    if (!imageUrlsByFieldType.TryGetValue(assetFieldType, out existingImageUrls))
                    {
                        existingImageUrls = imageUrlsByFieldType[assetFieldType] =
                            new List<string>();
                    }
                    existingImageUrls.Add(hotelImageAsset.Uri);
                }
            }

            // Collects more image URLs by field type to fulfill the requirements.
            foreach (AssetFieldType assetFieldType in MIN_REQUIRED_IMAGE_ASSET_COUNTS.Keys)
            {
                List<string> existingImageUrls = null;
                if (!imageUrlsByFieldType.TryGetValue(assetFieldType, out existingImageUrls))
                {
                    existingImageUrls = imageUrlsByFieldType[assetFieldType] =
                        new List<string>();
                }

                int i = 0;
                while (imageUrlsByFieldType[assetFieldType].Count <
                    MIN_REQUIRED_IMAGE_ASSET_COUNTS[assetFieldType])
                {
                    string imageUrlFromDefaults = DEFAULT_IMAGE_ASSETS_INFO[assetFieldType][i++];
                    Console.WriteLine($"A default image URL '{imageUrlFromDefaults} is used to " +
                        $"create an image asset for the asset field type {assetFieldType}");
                    existingImageUrls.Add(imageUrlFromDefaults);
                }
            }

            // Converts the list of URLs by field type into AssetOperations and
            // AssetGroupAssetOperations.
            foreach (AssetFieldType assetFieldType in imageUrlsByFieldType.Keys)
            {
                foreach (string imageUrl in imageUrlsByFieldType[assetFieldType])
                {
                    // Builds the image asset.
                    Asset asset = new Asset
                    {
                        ResourceName = ResourceNames.Asset(customerId, temporaryId--),
                        // Provide a unique friendly name to identify your asset.
                        // When there is an existing image asset with the same content but a
                        // different name, the new name will be dropped silently.
                        Name = $"{assetFieldType}{ExampleUtilities.GetRandomString()}",
                        ImageAsset = new ImageAsset
                        {
                            Data = ByteString.CopyFrom(
                                    MediaUtilities.GetAssetDataFromUrl(imageUrl, config)
                            )
                        }
                    };

                    // Adds an operation to create the asset.
                    mutateOperations.Add(new MutateOperation
                    {
                        AssetOperation = new AssetOperation
                        {
                            Create = asset
                        }
                    });

                    // Builds the AssetGroupAsset
                    AssetGroupAsset assetGroupAsset = new AssetGroupAsset
                    {
                        Asset = asset.ResourceName,
                        AssetGroup = assetGroupResourceName,
                        FieldType = assetFieldType
                    };

                    // Adds an operation to link the Asset to the AssetGroup.
                    mutateOperations.Add(new MutateOperation
                    {
                        AssetGroupAssetOperation = new AssetGroupAssetOperation
                        {
                            Create = assetGroupAsset
                        }
                    });
                }

            }

            return mutateOperations;
        }

        /// <summary>
        /// Prints the details of a MutateGoogleAdsResponse. Parses the "response" field name and
        /// uses it to extract the new entity's name and resource name.
        /// <param name="response">The mutate Google Ads response.</param>
        /// </summary>
        private void PrintResponseDetails(MutateGoogleAdsResponse response) {
            foreach (MutateOperationResponse operationResponse in response.MutateOperationResponses)
            {
                string resourceName;

                string entityName = operationResponse.ResponseCase.ToString();
                // Trim the substring "Result" from the end of the entity name.
                entityName = entityName.Remove(entityName.Length - 6);
                switch (operationResponse.ResponseCase)
                {
                    case MutateOperationResponse.ResponseOneofCase.AssetResult:
                        resourceName = operationResponse.AssetResult.ResourceName;
                        break;

                    case MutateOperationResponse.ResponseOneofCase.AssetGroupResult:
                        resourceName = operationResponse.AssetGroupResult.ResourceName;
                        break;

                    case MutateOperationResponse.ResponseOneofCase.AssetGroupAssetResult:
                        resourceName = operationResponse.AssetGroupAssetResult.ResourceName;
                        break;

                    case MutateOperationResponse.ResponseOneofCase.AssetSetAssetResult:
                        resourceName = operationResponse.AssetSetAssetResult.ResourceName;
                        break;

                    case MutateOperationResponse.ResponseOneofCase.CampaignBudgetResult:
                        resourceName = operationResponse.CampaignBudgetResult.ResourceName;
                        break;

                    case MutateOperationResponse.ResponseOneofCase.CampaignResult:
                        resourceName = operationResponse.CampaignResult.ResourceName;
                        break;

                    default:
                        resourceName = "<not found>";
                        break;
                }
                Console.WriteLine(
                    $"Created a(n) {entityName} with resource name: '{resourceName}'.");
            }
        }
    }
}
      

PHP

<?php

/**
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

namespace Google\Ads\GoogleAds\Examples\Travel;

require __DIR__ . '/../../vendor/autoload.php';

use GetOpt\GetOpt;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentNames;
use Google\Ads\GoogleAds\Examples\Utils\ArgumentParser;
use Google\Ads\GoogleAds\Examples\Utils\Helper;
use Google\Ads\GoogleAds\Lib\OAuth2TokenBuilder;
use Google\Ads\GoogleAds\Lib\V16\GoogleAdsClient;
use Google\Ads\GoogleAds\Lib\V16\GoogleAdsClientBuilder;
use Google\Ads\GoogleAds\Lib\V16\GoogleAdsException;
use Google\Ads\GoogleAds\Util\V16\ResourceNames;
use Google\Ads\GoogleAds\V16\Common\CallToActionAsset;
use Google\Ads\GoogleAds\V16\Common\HotelPropertyAsset;
use Google\Ads\GoogleAds\V16\Common\ImageAsset;
use Google\Ads\GoogleAds\V16\Common\MaximizeConversionValue;
use Google\Ads\GoogleAds\V16\Common\TextAsset;
use Google\Ads\GoogleAds\V16\Enums\AdvertisingChannelTypeEnum\AdvertisingChannelType;
use Google\Ads\GoogleAds\V16\Enums\AssetFieldTypeEnum\AssetFieldType;
use Google\Ads\GoogleAds\V16\Enums\AssetGroupStatusEnum\AssetGroupStatus;
use Google\Ads\GoogleAds\V16\Enums\AssetSetTypeEnum\AssetSetType;
use Google\Ads\GoogleAds\V16\Enums\BudgetDeliveryMethodEnum\BudgetDeliveryMethod;
use Google\Ads\GoogleAds\V16\Enums\CampaignStatusEnum\CampaignStatus;
use Google\Ads\GoogleAds\V16\Enums\HotelAssetSuggestionStatusEnum\HotelAssetSuggestionStatus;
use Google\Ads\GoogleAds\V16\Errors\GoogleAdsError;
use Google\Ads\GoogleAds\V16\Resources\Asset;
use Google\Ads\GoogleAds\V16\Resources\AssetGroup;
use Google\Ads\GoogleAds\V16\Resources\AssetGroupAsset;
use Google\Ads\GoogleAds\V16\Resources\AssetSet;
use Google\Ads\GoogleAds\V16\Resources\AssetSetAsset;
use Google\Ads\GoogleAds\V16\Resources\Campaign;
use Google\Ads\GoogleAds\V16\Resources\CampaignBudget;
use Google\Ads\GoogleAds\V16\Services\AssetGroupAssetOperation;
use Google\Ads\GoogleAds\V16\Services\AssetGroupOperation;
use Google\Ads\GoogleAds\V16\Services\AssetOperation;
use Google\Ads\GoogleAds\V16\Services\AssetSetAssetOperation;
use Google\Ads\GoogleAds\V16\Services\AssetSetOperation;
use Google\Ads\GoogleAds\V16\Services\CampaignBudgetOperation;
use Google\Ads\GoogleAds\V16\Services\CampaignOperation;
use Google\Ads\GoogleAds\V16\Services\HotelAssetSuggestion;
use Google\Ads\GoogleAds\V16\Services\HotelImageAsset;
use Google\Ads\GoogleAds\V16\Services\HotelTextAsset;
use Google\Ads\GoogleAds\V16\Services\MutateAssetSetsRequest;
use Google\Ads\GoogleAds\V16\Services\MutateGoogleAdsRequest;
use Google\Ads\GoogleAds\V16\Services\MutateGoogleAdsResponse;
use Google\Ads\GoogleAds\V16\Services\MutateOperation;
use Google\Ads\GoogleAds\V16\Services\MutateOperationResponse;
use Google\Ads\GoogleAds\V16\Services\SuggestTravelAssetsRequest;
use Google\ApiCore\ApiException;
use Google\ApiCore\Serializer;

/**
 * This example shows how to create a Performance Max for travel goals campaign. It also uses
 * TravelAssetSuggestionService to fetch suggested assets for creating an asset group. In case
 * there are not enough assets for the asset group (required by Performance Max), this example will
 * create more assets to fulfill the requirements.
 *
 * For more information about Performance Max campaigns, see
 * https://developers.google.com/google-ads/api/docs/performance-max/overview.
 *
 * Prerequisites:
 * - You must have at least one conversion action in the account. For more about conversion actions,
 * see
 * https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.
 *
 * Notes:
 * - This example uses the default customer conversion goals. For an example of setting
 *   campaign-specific conversion goals, see ShoppingAds/AddPerformanceMaxRetailCampaign.php.
 * - To learn how to create asset group signals, see
 *   AdvancedOperations/AddPerformanceMaxCampaign.php.
 */
class AddPerformanceMaxForTravelGoalsCampaign
{
    private const CUSTOMER_ID = 'INSERT_CUSTOMER_ID_HERE';
    // Sets a place ID that uniquely identifies a place in the Google Places database.
    // See https://developers.google.com/places/web-service/place-id to learn more.
    // The provided place ID must belong to a hotel property.
    private const PLACE_ID = 'INSERT_PLACE_ID_HERE';

    // Minimum requirements of assets required in a Performance Max asset group.
    // See https://developers.google.com/google-ads/api/docs/performance-max/assets for details.
    private const MIN_REQUIRED_TEXT_ASSET_COUNTS = [
        AssetFieldType::HEADLINE => 3,
        AssetFieldType::LONG_HEADLINE => 1,
        AssetFieldType::DESCRIPTION => 2,
        AssetFieldType::BUSINESS_NAME => 1
    ];
    private const MIN_REQUIRED_IMAGE_ASSET_COUNTS = [
        AssetFieldType::MARKETING_IMAGE => 1,
        AssetFieldType::SQUARE_MARKETING_IMAGE => 1,
        AssetFieldType::LOGO => 1
    ];
    // Texts and URLs used to create text and image assets when the TravelAssetSuggestionService
    // doesn't return enough assets required for creating an asset group.
    private const DEFAULT_TEXT_ASSETS_INFO = [
        AssetFieldType::HEADLINE => ['Hotel', 'Travel Reviews', 'Book travel'],
        AssetFieldType::LONG_HEADLINE => ['Travel the World'],
        AssetFieldType::DESCRIPTION => [
            'Great deal for your beloved hotel',
            'Best rate guaranteed'
        ],
        AssetFieldType::BUSINESS_NAME => ['Interplanetary Cruises']
    ];
    private const DEFAULT_IMAGE_ASSETS_INFO = [
        AssetFieldType::MARKETING_IMAGE => ['https://gaagl.page.link/Eit5'],
        AssetFieldType::SQUARE_MARKETING_IMAGE => ['https://gaagl.page.link/bjYi'],
        AssetFieldType::LOGO => ['https://gaagl.page.link/bjYi']
    ];

    // We specify temporary IDs that are specific to a single mutate request.
    // Temporary IDs are always negative and unique within one mutate request.
    //
    // See https://developers.google.com/google-ads/api/docs/mutating/best-practices
    // for further details.
    //
    // These temporary IDs are fixed because they are used in multiple places.
    private const ASSET_TEMPORARY_ID = -1;
    private const BUDGET_TEMPORARY_ID = -2;
    private const CAMPAIGN_TEMPORARY_ID = -3;
    private const ASSET_GROUP_TEMPORARY_ID = -4;

    // There are also entities that will be created in the same request but do not need to be fixed
    // temporary IDs because they are referenced only once.
    /** @var int the negative temporary ID used in bulk mutates. */
    private static $nextTempId = self::ASSET_GROUP_TEMPORARY_ID - 1;

    public static function main()
    {
        // Either pass the required parameters for this example on the command line, or insert them
        // into the constants above.
        $options = (new ArgumentParser())->parseCommandArguments([
            ArgumentNames::CUSTOMER_ID => GetOpt::REQUIRED_ARGUMENT,
            ArgumentNames::PLACE_ID => GetOpt::REQUIRED_ARGUMENT
        ]);

        // Generate a refreshable OAuth2 credential for authentication.
        $oAuth2Credential = (new OAuth2TokenBuilder())->fromFile()->build();

        // Construct a Google Ads client configured from a properties file and the
        // OAuth2 credentials above.
        $googleAdsClient = (new GoogleAdsClientBuilder())
            ->fromFile()
            ->withOAuth2Credential($oAuth2Credential)
            // We set this value to true to show how to use GAPIC v2 source code. You can remove the
            // below line if you wish to use the old-style source code. Note that in that case, you
            // probably need to modify some parts of the code below to make it work.
            // For more information, see
            // https://developers.devsite.corp.google.com/google-ads/api/docs/client-libs/php/gapic.
            ->usingGapicV2Source(true)
            ->build();

        try {
            self::runExample(
                $googleAdsClient,
                $options[ArgumentNames::CUSTOMER_ID] ?: self::CUSTOMER_ID,
                $options[ArgumentNames::PLACE_ID] ?: self::PLACE_ID
            );
        } catch (GoogleAdsException $googleAdsException) {
            printf(
                "Request with ID '%s' has failed.%sGoogle Ads failure details:%s",
                $googleAdsException->getRequestId(),
                PHP_EOL,
                PHP_EOL
            );
            foreach ($googleAdsException->getGoogleAdsFailure()->getErrors() as $error) {
                /** @var GoogleAdsError $error */
                printf(
                    "\t%s: %s%s",
                    $error->getErrorCode()->getErrorCode(),
                    $error->getMessage(),
                    PHP_EOL
                );
            }
            exit(1);
        } catch (ApiException $apiException) {
            printf(
                "ApiException was thrown with message '%s'.%s",
                $apiException->getMessage(),
                PHP_EOL
            );
            exit(1);
        }
    }

    /**
     * Runs the example.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $placeId the place ID for a hotel property asset
     */
    public static function runExample(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $placeId
    ) {
        // Gets hotel asset suggestion using the TravelAssetSuggestionService.
        $hotelAssetSuggestion =
            self::getHotelAssetSuggestion($googleAdsClient, $customerId, $placeId);

        // Performance Max campaigns require that repeated assets such as headlines
        // and descriptions be created before the campaign.
        // For the list of required assets for a Performance Max campaign, see
        // https://developers.google.com/google-ads/api/docs/performance-max/assets.
        //
        // This step is the same for any types of Performance Max campaigns.

        // Creates the headlines using the hotel asset suggestion.
        $headlineAssetResourceNames = self::createMultipleTextAssets(
            $googleAdsClient,
            $customerId,
            AssetFieldType::HEADLINE,
            $hotelAssetSuggestion
        );
        // Creates the descriptions using the hotel asset suggestion.
        $descriptionAssetResourceNames = self::createMultipleTextAssets(
            $googleAdsClient,
            $customerId,
            AssetFieldType::DESCRIPTION,
            $hotelAssetSuggestion
        );

        // Creates a hotel property asset set, which will be used later to link with a newly created
        // campaign.
        $hotelPropertyAssetSetResourceName =
            self::createHotelAssetSet($googleAdsClient, $customerId);
        // Creates a hotel property asset and link it with the previously created hotel property
        // asset set. This asset will also be linked to an asset group in the later steps.
        // In the real-world scenario, you'd need to create many assets for all your hotel
        // properties. We use one hotel property here for simplicity.
        // Both asset and asset set need to be created before creating a campaign, so we cannot
        // bundle them with other mutate operations below.
        $hotelPropertyAssetResourceName = self::createHotelAsset(
            $googleAdsClient,
            $customerId,
            $placeId,
            $hotelPropertyAssetSetResourceName
        );

        // It's important to create the below entities in this order because they depend on
        // each other.
        // The below methods create and return mutate operations that we later provide to the
        // GoogleAdsService.Mutate method in order to create the entities in a single request.
        // Since the entities for a Performance Max campaign are closely tied to one-another, it's
        // considered a best practice to create them in a single Mutate request so they all complete
        // successfully or fail entirely, leaving no orphaned entities. See:
        // https://developers.google.com/google-ads/api/docs/mutating/overview.
        $operations = [];
        $operations[] = self::createCampaignBudgetOperation($customerId);
        $operations[] =
            self::createCampaignOperation($customerId, $hotelPropertyAssetSetResourceName);
        $operations = array_merge($operations, self::createAssetGroupOperations(
            $customerId,
            $hotelPropertyAssetResourceName,
            $headlineAssetResourceNames,
            $descriptionAssetResourceNames,
            $hotelAssetSuggestion
        ));

        // Issues a mutate request to create everything and prints the results.
        $googleAdsServiceClient = $googleAdsClient->getGoogleAdsServiceClient();
        $response = $googleAdsServiceClient->mutate(
            MutateGoogleAdsRequest::build($customerId, $operations)
        );
        print "Created the following entities for a campaign budget, a campaign, and an asset group"
            . " for Performance Max for travel goals:" . PHP_EOL;
        self::printResponseDetails($response);
    }

    /**
     * Returns hotel asset suggestion obtained from TravelAssetsSuggestionService.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $placeId the place ID of the hotel property you want to get its suggested
     *     assets
     * @return HotelAssetSuggestion a hotel asset suggestion
     */
    private static function getHotelAssetSuggestion(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $placeId
    ): HotelAssetSuggestion {
        // Send a request to suggest assets to be created as an asset group for the Performance Max
        // for travel goals campaign.
        $travelAssetSuggestionServiceClient =
            $googleAdsClient->getTravelAssetSuggestionServiceClient();
        // Uses 'en-US' as an example. It can be any language specifications in BCP 47 format.
        $request = SuggestTravelAssetsRequest::build($customerId, 'en-US');
        // The service accepts several place IDs. We use only one here for demonstration.
        $request->setPlaceIds([$placeId]);
        $response = $travelAssetSuggestionServiceClient->suggestTravelAssets($request);
        printf("Fetched a hotel asset suggestion for the place ID '%s'.%s", $placeId, PHP_EOL);
        return $response->getHotelAssetSuggestions()[0];
    }

    /**
     * Creates multiple text assets and returns the list of resource names. The hotel asset
     * suggestion is used to create a text asset first. If the number of created text assets is
     * still fewer than the minimum required number of assets of the specified asset field type,
     * adds more text assets to fulfill the requirement.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param int $assetFieldType the asset field type that this text assets will be created for
     * @param HotelAssetSuggestion $hotelAssetSuggestion the hotel asset suggestion
     * @return string[] a list of asset resource names
     */
    private static function createMultipleTextAssets(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        int $assetFieldType,
        HotelAssetSuggestion $hotelAssetSuggestion
    ): array {
        // We use the GoogleAdService to create multiple text assets in a single request.
        // First, adds all the text assets of the specified asset field type.
        $operations = [];
        $numOperationsAdded = 0;
        if ($hotelAssetSuggestion->getStatus() === HotelAssetSuggestionStatus::SUCCESS) {
            foreach ($hotelAssetSuggestion->getTextAssets() as $textAsset) {
                /** @var HotelTextAsset $textAsset */
                if ($textAsset->getAssetFieldType() !== $assetFieldType) {
                    continue;
                }
                $operations[] = new MutateOperation([
                    'asset_operation' => new AssetOperation([
                        'create' => new Asset([
                            'text_asset' => new TextAsset(['text' => $textAsset->getText()])
                        ])
                    ])
                ]);
                $numOperationsAdded++;
            }
        }
        // If the added assets are still less than the minimum required assets for the asset field
        // type, add more text assets using the default texts.
        if ($numOperationsAdded < self::MIN_REQUIRED_TEXT_ASSET_COUNTS[$assetFieldType]) {
            for (
                $i = 0;
                $i < self::MIN_REQUIRED_TEXT_ASSET_COUNTS[$assetFieldType] - $numOperationsAdded;
                $i++
            ) {
                // Creates a mutate operation for a text asset.
                $operations[] = new MutateOperation([
                    'asset_operation' => new AssetOperation([
                        'create' => new Asset([
                            'text_asset' => new TextAsset([
                                'text' => self::DEFAULT_TEXT_ASSETS_INFO[$assetFieldType][$i]
                            ])
                        ])
                    ])
                ]);
            }
        }

        // Issues a mutate request to add all assets.
        $googleAdsService = $googleAdsClient->getGoogleAdsServiceClient();
        /** @var MutateGoogleAdsResponse $mutateGoogleAdsResponse */
        $mutateGoogleAdsResponse =
            $googleAdsService->mutate(MutateGoogleAdsRequest::build($customerId, $operations));

        $assetResourceNames = [];
        foreach ($mutateGoogleAdsResponse->getMutateOperationResponses() as $response) {
            /** @var MutateOperationResponse $response */
            $assetResourceNames[] = $response->getAssetResult()->getResourceName();
        }
        printf(
            "The following assets are created for the asset field type '%s':%s",
            AssetFieldType::name($assetFieldType),
            PHP_EOL
        );
        self::printResponseDetails($mutateGoogleAdsResponse);

        return $assetResourceNames;
    }

    /**
     * Creates a hotel property asset set.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @return string the created hotel property asset set resource name
     */
    private static function createHotelAssetSet(
        GoogleAdsClient $googleAdsClient,
        int $customerId
    ): string {
        // Creates an asset set operation for a hotel property asset set.
        $assetSetOperation = new AssetSetOperation([
            // Creates a hotel property asset set.
            'create' => new AssetSet([
                'name' => 'My Hotel propery asset set #' . Helper::getPrintableDatetime(),
                'type' => AssetSetType::HOTEL_PROPERTY
            ])
        ]);

        // Issues a mutate request to add a hotel asset set and prints its information.
        $assetSetServiceClient = $googleAdsClient->getAssetSetServiceClient();
        $response = $assetSetServiceClient->mutateAssetSets(
            MutateAssetSetsRequest::build($customerId, [$assetSetOperation])
        );
        $assetSetResourceName = $response->getResults()[0]->getResourceName();
        printf("Created an asset set with resource name: '%s'.%s", $assetSetResourceName, PHP_EOL);
        return $assetSetResourceName;
    }

    /**
     * Creates a hotel property asset using the specified place ID. The place ID must belong to
     * a hotel property. Then, links it to the specified asset set.
     *
     * See https://developers.google.com/places/web-service/place-id to search for a hotel place ID.
     *
     * @param GoogleAdsClient $googleAdsClient the Google Ads API client
     * @param int $customerId the customer ID
     * @param string $placeId the place ID for a hotel
     * @param string $assetSetResourceName the asset set resource name
     * @return string the created hotel property asset resource name
     */
    private static function createHotelAsset(
        GoogleAdsClient $googleAdsClient,
        int $customerId,
        string $placeId,
        string $assetSetResourceName
    ): string {
        // We use the GoogleAdService to create an asset and asset set asset in a single
        // request.
        $operations = [];
        $assetResourceName =
            ResourceNames::forAsset($customerId, self::ASSET_TEMPORARY_ID);
        // Creates a mutate operation for a hotel property asset.
        $operations[] = new MutateOperation([
            'asset_operation' => new AssetOperation([
                // Creates a hotel property asset.
                'create' => new Asset([
                    'resource_name' => $assetResourceName,
                    // Creates a hotel property asset for the place ID.
                    'hotel_property_asset' => new HotelPropertyAsset(['place_id' => $placeId]),
                ])
            ])
        ]);
        // Creates a mutate operation for an asset set asset.
        $operations[] = new MutateOperation([
            'asset_set_asset_operation' => new AssetSetAssetOperation([
                // Creates an asset set asset.
                'create' => new AssetSetAsset([
                    'asset' => $assetResourceName,
                    'asset_set' => $assetSetResourceName
                ])
            ])
        ]);

        // Issues a mutate request to create all entities.
        $googleAdsService = $googleAdsClient->getGoogleAdsServiceClient();
        /** @var MutateGoogleAdsResponse $mutateGoogleAdsResponse */
        $mutateGoogleAdsResponse =
            $googleAdsService->mutate(MutateGoogleAdsRequest::build($customerId, $operations));
        print "Created the following entities for the hotel asset:" . PHP_EOL;
        self::printResponseDetails($mutateGoogleAdsResponse);

        // Returns the created asset resource name, which will be used later to create an asset
        // group. Other resource names are not used later.
        return $mutateGoogleAdsResponse->getMutateOperationResponses()[0]->getAssetResult()
            ->getResourceName();
    }

    /**
     * Creates a mutate operation that creates a new campaign budget.
     *
     * A temporary ID will be assigned to this campaign budget so that it can be
     * referenced by other objects being created in the same mutate request.
     *
     * @param int $customerId the customer ID
     * @return MutateOperation the mutate operation that creates a campaign budget
     */
    private static function createCampaignBudgetOperation(int $customerId): MutateOperation
    {
        // Creates a mutate operation that creates a campaign budget.
        return new MutateOperation([
            'campaign_budget_operation' => new CampaignBudgetOperation([
                'create' => new CampaignBudget([
                    // Sets a temporary ID in the budget's resource name so it can be referenced
                    // by the campaign in later steps.
                    'resource_name' => ResourceNames::forCampaignBudget(
                        $customerId,
                        self::BUDGET_TEMPORARY_ID
                    ),
                    'name' => 'Performance Max for travel goals campaign budget #'
                        . Helper::getPrintableDatetime(),
                    // The budget period already defaults to DAILY.
                    'amount_micros' => 50000000,
                    'delivery_method' => BudgetDeliveryMethod::STANDARD,
                    // A Performance Max campaign cannot use a shared campaign budget.
                    'explicitly_shared' => false
                ])
            ])
        ]);
    }

    /**
     * Creates a mutate operation that creates a new Performance Max campaign. Links the specified
     * hotel property asset set to this campaign.
     *
     * A temporary ID will be assigned to this campaign so that it can be referenced by other
     * objects being created in the same mutate request.
     *
     * @param int $customerId the customer ID
     * @param string $hotelPropertyAssetSetResourceName the asset set resource name
     * @return MutateOperation the mutate operation that creates the campaign
     */
    private static function createCampaignOperation(
        int $customerId,
        string $hotelPropertyAssetSetResourceName
    ): MutateOperation {
        // Creates a mutate operation that creates a campaign.
        return new MutateOperation([
            'campaign_operation' => new CampaignOperation([
                'create' => new Campaign([
                    'name' => 'Performance Max for travel goals campaign #'
                        . Helper::getPrintableDatetime(),
                    // Assigns the resource name with a temporary ID.
                    'resource_name' => ResourceNames::forCampaign(
                        $customerId,
                        self::CAMPAIGN_TEMPORARY_ID
                    ),
                    // Sets the budget using the given budget resource name.
                    'campaign_budget' => ResourceNames::forCampaignBudget(
                        $customerId,
                        self::BUDGET_TEMPORARY_ID
                    ),
                    // The campaign is the only entity in the mutate request that should have its
                    // status set.
                    // Recommendation: Set the campaign to PAUSED when creating it to prevent
                    // the ads from immediately serving.
                    'status' => CampaignStatus::PAUSED,
                    // Performance Max campaigns have an advertising_channel_type of
                    // PERFORMANCE_MAX. The advertising_channel_sub_type should not be set.
                    'advertising_channel_type' => AdvertisingChannelType::PERFORMANCE_MAX,

                    // To create a Performance Max for travel goals campaign, you need to set
                    // `hotel_property_asset_set`.
                    'hotel_property_asset_set' => $hotelPropertyAssetSetResourceName,

                    // Bidding strategy must be set directly on the campaign.
                    // Setting a portfolio bidding strategy by resource name is not supported.
                    // Max Conversion and Maximize Conversion Value are the only strategies
                    // supported for Performance Max campaigns.
                    // An optional ROAS (Return on Advertising Spend) can be set for
                    // maximize_conversion_value. The ROAS value must be specified as a ratio in
                    // the API. It is calculated by dividing "total value" by "total spend".
                    // For more information on Maximize Conversion Value, see the support
                    // article: https://support.google.com/google-ads/answer/7684216.
                    // A target_roas of 3.5 corresponds to a 350% return on ad spend.
                    'maximize_conversion_value' => new MaximizeConversionValue([
                        'target_roas' => 3.5
                    ])
                ])
            ])
        ]);
    }

    /**
     * Creates a list of mutate operations that create a new asset group, composed of suggested
     * assets. In case the number of suggested assets is not enough for the requirements, it'll
     * create more assets to meet the requirement.
     *
     * For the list of required assets for a Performance Max campaign, see
     * https://developers.google.com/google-ads/api/docs/performance-max/assets.
     *
     * @param int $customerId the customer ID
     * @param string $hotelPropertyAssetResourceName the hotel property asset resource name that
     *     will be used to create an asset group
     * @param string[] $headlineAssetResourceNames a list of headline resource names
     * @param string[] $descriptionAssetResourceNames a list of description resource names
     * @param HotelAssetSuggestion $hotelAssetSuggestion the hotel asset suggestion
     * @return MutateOperation[] a list of mutate operations that create the asset group
     */
    private static function createAssetGroupOperations(
        int $customerId,
        string $hotelPropertyAssetResourceName,
        array $headlineAssetResourceNames,
        array $descriptionAssetResourceNames,
        HotelAssetSuggestion $hotelAssetSuggestion
    ): array {
        $operations = [];

        // Creates a new mutate operation that creates an asset group using suggested information
        // when available.
        $assetGroupName = $hotelAssetSuggestion->getStatus() === HotelAssetSuggestionStatus::SUCCESS
            ? $hotelAssetSuggestion->getHotelName()
            : 'Performance Max for travel goals asset group #' . Helper::getPrintableDatetime();
        $assetGroupFinalUrls =
            $hotelAssetSuggestion->getStatus() === HotelAssetSuggestionStatus::SUCCESS
                ? [$hotelAssetSuggestion->getFinalUrl()] : ['http://www.example.com'];
        $assetGroupResourceName =
            ResourceNames::forAssetGroup($customerId, self::ASSET_GROUP_TEMPORARY_ID);
        $operations[] = new MutateOperation([
            'asset_group_operation' => new AssetGroupOperation([
                'create' => new AssetGroup([
                    'resource_name' => $assetGroupResourceName,
                    'name' => $assetGroupName,
                    'campaign' => ResourceNames::forCampaign(
                        $customerId,
                        self::CAMPAIGN_TEMPORARY_ID
                    ),
                    'final_urls' => $assetGroupFinalUrls,
                    'status' => AssetGroupStatus::PAUSED
                ])
            ])
        ]);

        // An asset group is linked to an asset by creating a new asset group asset
        // and providing:
        // -  the resource name of the asset group
        // -  the resource name of the asset
        // -  the field_type of the asset in this asset group
        //
        // To learn more about asset groups, see
        // https://developers.google.com/google-ads/api/docs/performance-max/asset-groups.

        // Headline and description assets were created at the first step of this example. So, we
        // just need to link them with the created asset group.

        // Links the headline assets to the asset group.
        foreach ($headlineAssetResourceNames as $resourceName) {
            $operations[] = new MutateOperation([
                'asset_group_asset_operation' => new AssetGroupAssetOperation([
                    'create' => new AssetGroupAsset([
                        'asset' => $resourceName,
                        'asset_group' => $assetGroupResourceName,
                        'field_type' => AssetFieldType::HEADLINE
                    ])
                ])
            ]);
        }
        // Links the description assets to the asset group.
        foreach ($descriptionAssetResourceNames as $resourceName) {
            $operations[] = new MutateOperation([
                'asset_group_asset_operation' => new AssetGroupAssetOperation([
                    'create' => new AssetGroupAsset([
                        'asset' => $resourceName,
                        'asset_group' => $assetGroupResourceName,
                        'field_type' => AssetFieldType::DESCRIPTION
                    ])
                ])
            ]);
        }

        // Link the previously created hotel property asset to the asset group. In the real-world
        // scenario, you'd need to do this step several times for each hotel property asset.
        $operations[] = new MutateOperation([
            'asset_group_asset_operation' => new AssetGroupAssetOperation([
                'create' => new AssetGroupAsset([
                    'asset' => $hotelPropertyAssetResourceName,
                    'asset_group' => $assetGroupResourceName,
                    'field_type' => AssetFieldType::HOTEL_PROPERTY
                ])
            ])
        ]);

        // Creates the rest of required text assets and link them to the asset group.
        $operations = array_merge(
            $operations,
            self::createTextAssetsForAssetGroup($customerId, $hotelAssetSuggestion)
        );
        // Creates the image assets and link them to the asset group. Some optional image assets
        // suggested by the TravelAssetSuggestionService might be created too.
        $operations = array_merge(
            $operations,
            self::createImageAssetsForAssetGroup($customerId, $hotelAssetSuggestion)
        );

        if ($hotelAssetSuggestion->getStatus() === HotelAssetSuggestionStatus::SUCCESS) {
            // Creates a new mutate operation for a suggested call-to-action asset and link it
            // to the asset group.
            $operations[] = new MutateOperation([
                'asset_operation' => new AssetOperation([
                    'create' => new Asset([
                        'resource_name' => ResourceNames::forAsset($customerId, self::$nextTempId),
                        'name' => 'Suggested call-to-action asset #'
                            . Helper::getShortPrintableDatetime(),
                        'call_to_action_asset' => new CallToActionAsset([
                            'call_to_action' => $hotelAssetSuggestion->getCallToAction()
                        ])
                    ])
                ])
            ]);
            $operations[] = new MutateOperation([
                'asset_group_asset_operation' => new AssetGroupAssetOperation([
                    'create' => new AssetGroupAsset([
                        'asset' => ResourceNames::forAsset($customerId, self::$nextTempId),
                        'asset_group' => $assetGroupResourceName,
                        'field_type' => AssetFieldType::CALL_TO_ACTION_SELECTION
                    ])
                ])
            ]);
            self::$nextTempId--;
        }

        return $operations;
    }

    /**
     * Creates text assets required for an asset group using the suggested hotel text assets. It
     * adds more text assets to fulfill the requirements if the suggested hotel text assets are not
     * enough.
     *
     * @param int $customerId the customer ID
     * @param HotelAssetSuggestion $hotelAssetSuggestion the hotel asset suggestion
     * @return MutateOperation[] a list of mutate operations that create text assets
     */
    private static function createTextAssetsForAssetGroup(
        int $customerId,
        HotelAssetSuggestion $hotelAssetSuggestion
    ): array {
        $operations = [];
        // Creates mutate operations for the suggested text assets except for headlines and
        // descriptions, which were created previously.
        $requiredTextAssetCounts =
            array_fill_keys(array_keys(self::MIN_REQUIRED_TEXT_ASSET_COUNTS), 0);
        if ($hotelAssetSuggestion->getStatus() === HotelAssetSuggestionStatus::SUCCESS) {
            foreach ($hotelAssetSuggestion->getTextAssets() as $textAsset) {
                /** @var HotelTextAsset $textAsset */
                if (
                    $textAsset->getAssetFieldType() === AssetFieldType::HEADLINE
                    || $textAsset->getAssetFieldType() === AssetFieldType::DESCRIPTION
                ) {
                    // Headlines and descriptions were already created at the first step of this
                    // code example.
                    continue;
                }
                printf(
                    "A text asset with text '%s' is suggested for the asset field type '%s'.%s",
                    $textAsset->getText(),
                    AssetFieldType::name($textAsset->getAssetFieldType()),
                    PHP_EOL
                );
                $operations = array_merge(
                    $operations,
                    self::createTextAssetAndAssetGroupAssetOperations(
                        $customerId,
                        $textAsset->getText(),
                        $textAsset->getAssetFieldType()
                    )
                );
                $requiredTextAssetCounts[$textAsset->getAssetFieldType()]++;
            }
        }
        // Adds more text assets to fulfill the requirements.
        foreach (self::MIN_REQUIRED_TEXT_ASSET_COUNTS as $assetFieldType => $minCount) {
            if (
                $assetFieldType === AssetFieldType::HEADLINE
                || $assetFieldType === AssetFieldType::DESCRIPTION
            ) {
                // Headlines and descriptions were already created at the first step of this
                // code example.
                continue;
            }
            for ($i = 0; $i < $minCount - $requiredTextAssetCounts[$assetFieldType]; $i++) {
                printf(
                    "A default text '%s' is used to create a text asset for the asset"
                        . " field type '%s'.%s",
                    self::DEFAULT_TEXT_ASSETS_INFO[$assetFieldType][$i],
                    AssetFieldType::name($assetFieldType),
                    PHP_EOL
                );
                $operations = array_merge(
                    $operations,
                    self::createTextAssetAndAssetGroupAssetOperations(
                        $customerId,
                        self::DEFAULT_TEXT_ASSETS_INFO[$assetFieldType][$i],
                        $assetFieldType
                    )
                );
            }
        }

        return $operations;
    }

    /**
     * Creates image assets required for an asset group using the suggested hotel image assets. It
     * adds more image assets to fulfill the requirements if the suggested hotel image assets are
     * not enough.
     *
     * @param int $customerId the customer ID
     * @param HotelAssetSuggestion $hotelAssetSuggestion the hotel asset suggestion
     * @return MutateOperation[] a list of mutate operations that create image assets
     */
    private static function createImageAssetsForAssetGroup(
        int $customerId,
        HotelAssetSuggestion $hotelAssetSuggestion
    ): array {
        $operations = [];
        // Creates mutate operations for the suggested image assets.
        $requiredImageAssetCounts =
            array_fill_keys(array_keys(self::MIN_REQUIRED_IMAGE_ASSET_COUNTS), 0);
        foreach ($hotelAssetSuggestion->getImageAssets() as $imageAsset) {
            /** @var HotelImageAsset $imageAsset */
            printf(
                "An image asset with URL '%s' is suggested for the asset field type '%s'.%s",
                $imageAsset->getUri(),
                AssetFieldType::name($imageAsset->getAssetFieldType()),
                PHP_EOL
            );
            $operations = array_merge(
                $operations,
                self::createImageAssetAndAssetGroupAssetOperations(
                    $customerId,
                    $imageAsset->getUri(),
                    $imageAsset->getAssetFieldType(),
                    'Suggested image asset #' . Helper::getShortPrintableDatetime()
                )
            );
            // Keeps track of only required image assets. The service may sometimes suggest
            // optional image assets.
            if (array_key_exists($imageAsset->getAssetFieldType(), $requiredImageAssetCounts)) {
                $requiredImageAssetCounts[$imageAsset->getAssetFieldType()]++;
            }
        }
        // Adds more image assets to fulfill the requirements.
        foreach (self::MIN_REQUIRED_IMAGE_ASSET_COUNTS as $assetFieldType => $minCount) {
            for ($i = 0; $i < $minCount - $requiredImageAssetCounts[$assetFieldType]; $i++) {
                printf(
                    "A default image URL '%s' is used to create an image asset for the"
                    . " asset field type '%s'.%s",
                    self::DEFAULT_IMAGE_ASSETS_INFO[$assetFieldType][$i],
                    AssetFieldType::name($assetFieldType),
                    PHP_EOL
                );
                $operations = array_merge(
                    $operations,
                    self::createImageAssetAndAssetGroupAssetOperations(
                        $customerId,
                        self::DEFAULT_IMAGE_ASSETS_INFO[$assetFieldType][$i],
                        $assetFieldType,
                        strtolower(AssetFieldType::name($assetFieldType))
                        . Helper::getShortPrintableDatetime()
                    )
                );
            }
        }

        return $operations;
    }

    /**
     * Creates a list of mutate operations that create a new linked text asset.
     *
     * @param int $customerId the customer ID
     * @param string $text the text of the asset to be created
     * @param int $fieldType the field type of the new asset in the asset group asset
     * @return MutateOperation[] a list of mutate operations that create a new linked text asset
     */
    private static function createTextAssetAndAssetGroupAssetOperations(
        int $customerId,
        string $text,
        int $fieldType
    ): array {
        $operations = [];
        // Creates a new mutate operation that creates a text asset.
        $operations[] = new MutateOperation([
            'asset_operation' => new AssetOperation([
                'create' => new Asset([
                    'resource_name' => ResourceNames::forAsset($customerId, self::$nextTempId),
                    'text_asset' => new TextAsset(['text' => $text])
                ])
            ])
        ]);

        // Creates an asset group asset to link the asset to the asset group.
        $operations[] = new MutateOperation([
            'asset_group_asset_operation' => new AssetGroupAssetOperation([
                'create' => new AssetGroupAsset([
                    'asset' => ResourceNames::forAsset($customerId, self::$nextTempId),
                    'asset_group' => ResourceNames::forAssetGroup(
                        $customerId,
                        self::ASSET_GROUP_TEMPORARY_ID
                    ),
                    'field_type' => $fieldType
                ])
            ])
        ]);
        self::$nextTempId--;

        return $operations;
    }

    /**
     * Creates a list of mutate operations that create a new linked image asset.
     *
     * @param int $customerId the customer ID
     * @param string $url the URL of the image to be retrieved and put into an asset
     * @param int $fieldType the field type of the new asset in the asset group asset
     * @param string $assetName the asset name
     * @return MutateOperation[] a list of mutate operations that create a new linked image asset
     */
    private static function createImageAssetAndAssetGroupAssetOperations(
        int $customerId,
        string $url,
        int $fieldType,
        string $assetName
    ): array {
        $operations = [];
        // Creates a new mutate operation that creates an image asset.
        $operations[] = new MutateOperation([
            'asset_operation' => new AssetOperation([
                'create' => new Asset([
                    'resource_name' => ResourceNames::forAsset($customerId, self::$nextTempId),
                    // Provide a unique friendly name to identify your asset.
                    // When there is an existing image asset with the same content but a different
                    // name, the new name will be dropped silently.
                    'name' => $assetName,
                    'image_asset' => new ImageAsset(['data' => file_get_contents($url)])
                ])
            ])
        ]);

        // Creates an asset group asset to link the asset to the asset group.
        $operations[] = new MutateOperation([
            'asset_group_asset_operation' => new AssetGroupAssetOperation([
                'create' => new AssetGroupAsset([
                    'asset' => ResourceNames::forAsset($customerId, self::$nextTempId),
                    'asset_group' => ResourceNames::forAssetGroup(
                        $customerId,
                        self::ASSET_GROUP_TEMPORARY_ID
                    ),
                    'field_type' => $fieldType
                ])
            ])
        ]);
        self::$nextTempId--;

        return $operations;
    }

    /**
     * Prints the details of a MutateGoogleAdsResponse. Parses the "response" oneof field name and
     * uses it to extract the new entity's name and resource name.
     *
     * @param MutateGoogleAdsResponse $mutateGoogleAdsResponse the mutate Google Ads response
     */
    private static function printResponseDetails(
        MutateGoogleAdsResponse $mutateGoogleAdsResponse
    ): void {
        foreach ($mutateGoogleAdsResponse->getMutateOperationResponses() as $response) {
            /** @var MutateOperationResponse $response */
            $getter = Serializer::getGetter($response->getResponse());
            printf(
                "Created a(n) %s with '%s'.%s",
                preg_replace(
                    '/Result$/',
                    '',
                    ucfirst(Serializer::toCamelCase($response->getResponse()))
                ),
                $response->$getter()->getResourceName(),
                PHP_EOL
            );
        }
    }
}

AddPerformanceMaxForTravelGoalsCampaign::main();

      

Python

#!/usr/bin/env python
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This example shows how to create a Performance Max for travel goals campaign.

It also uses TravelAssetSuggestionService to fetch suggested assets for creating
an asset group. In case there are not enough assets for the asset group
(required by Performance Max), this example will create more assets to fulfill
the requirements.

For more information about Performance Max campaigns, see
https://developers.google.com/google-ads/api/docs/performance-max/overview.

Prerequisites:
- You must have at least one conversion action in the account. For more about
  conversion actions, see
  https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.

Notes:
- This example uses the default customer conversion goals. For an example of
  setting campaign-specific conversion goals, see
  shopping_ads/add_performance_max_retail_campaign.py.
- To learn how to create asset group signals, see
  advanced_operations/add_performance_max_campaign.py.
"""


import argparse
import sys
from timeit import default_timer

from examples.utils.example_helpers import get_printable_datetime
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException

import requests

MIN_REQUIRED_TEXT_ASSET_COUNTS = {
    "HEADLINE": 3,
    "LONG_HEADLINE": 1,
    "DESCRIPTION": 2,
    "BUSINESS_NAME": 1,
}

MIN_REQUIRED_IMAGE_ASSET_COUNTS = {
    "MARKETING_IMAGE": 1,
    "SQUARE_MARKETING_IMAGE": 1,
    "LOGO": 1,
}

DEFAULT_TEXT_ASSETS_INFO = {
    "HEADLINE": ["Hotel", "Travel Reviews", "Book travel"],
    "LONG_HEADLINE": ["Travel the World"],
    "DESCRIPTION": [
        "Great deal for your beloved hotel",
        "Best rate guaranteed",
    ],
    "BUSINESS_NAME": ["Interplanetary Cruises"],
}

DEFAULT_IMAGE_ASSETS_INFO = {
    "MARKETING_IMAGE": ["https://gaagl.page.link/Eit5"],
    "SQUARE_MARKETING_IMAGE": ["https://gaagl.page.link/bjYi"],
    "LOGO": ["https://gaagl.page.link/bjYi"],
}

# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.

# For further details, see:
# https://developers.google.com/google-ads/api/docs/mutating/best-practices

# These temporary IDs are global because they are used throughout the module.
ASSET_TEMPORARY_ID = -1
BUDGET_TEMPORARY_ID = -2
CAMPAIGN_TEMPORARY_ID = -3
ASSET_GROUP_TEMPORARY_ID = -4

# There are also entities that will be created in the same request but do not
# need to be fixed temporary IDs because they are referenced only once.
next_temp_id = ASSET_GROUP_TEMPORARY_ID - 1


def main(client, customer_id, place_id):
    """The main method that creates all necessary entities for the example.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        place_id: a place ID identifying a place in the Google Places database.
    """
    # Gets hotel asset suggestion using the TravelAssetSuggestionService.
    hotel_asset_suggestion = get_hotel_asset_suggestion(
        client, customer_id, place_id
    )

    # Performance Max campaigns require that repeated assets such as headlines
    # and descriptions be created before the campaign. For the list of required
    # assets for a Performance Max campaign, see:
    # https://developers.google.com/google-ads/api/docs/performance-max/assets.

    # This step is the same for all types of Performance Max campaigns.

    # Creates the headlines using the hotel asset suggestion.
    headline_asset_resource_names = create_multiple_text_assets(
        client,
        customer_id,
        client.enums.AssetFieldTypeEnum.HEADLINE,
        hotel_asset_suggestion,
    )

    # Creates the descriptions using the hotel asset suggestion.
    description_asset_resource_names = create_multiple_text_assets(
        client,
        customer_id,
        client.enums.AssetFieldTypeEnum.DESCRIPTION,
        hotel_asset_suggestion,
    )

    # Creates a hotel property asset set, which will be used later to link with
    # a newly created campaign.
    hotel_property_asset_set_resource_name = create_hotel_asset_set(
        client, customer_id
    )

    # Creates a hotel property asset and links it with the previously created
    # hotel property asset set. This asset will also be linked to an asset group
    # in the later steps. In a real-world scenario, you'd need to create assets
    # for each of your hotel properties. We use one hotel property here for
    # simplicity. Both asset and asset set need to be created before creating a
    # campaign, so we cannot bundle them with other mutate operations below.
    hotel_property_asset_resource_name = create_hotel_asset(
        client, customer_id, place_id, hotel_property_asset_set_resource_name
    )

    # It's important to create the below entities in this order because they
    # depend on each other.
    # The below methods create and return mutate operations that we later
    # provide to the GoogleAdsService.Mutate method in order to create the
    # entities in a single request. Since the entities for a Performance Max
    # campaign are closely tied to one-another, it's considered a best practice
    # to create them in a single Mutate request so they all complete
    # successfully or fail entirely, leaving no orphaned entities. See:
    # https://developers.google.com/google-ads/api/docs/mutating/overview.
    campaign_budget_operation = create_campaign_budget_operation(
        client, customer_id
    )
    campaign_operation = create_campaign_operation(
        client, customer_id, hotel_property_asset_set_resource_name
    )
    asset_group_operations = create_asset_group_operations(
        client,
        customer_id,
        hotel_property_asset_resource_name,
        headline_asset_resource_names,
        description_asset_resource_names,
        hotel_asset_suggestion,
    )

    # Issues a mutate request to create everything.
    googleads_service = client.get_service("GoogleAdsService")
    response = googleads_service.mutate(
        customer_id=customer_id,
        mutate_operations=[
            campaign_budget_operation,
            campaign_operation,
            *asset_group_operations,
        ],
    )

    print(
        "Created the following entities for a campaign budget, a campaign, and "
        "an asset group for Performance Max for travel goals:"
    )

    print_response_details(response)


def get_hotel_asset_suggestion(client, customer_id, place_id):
    """Returns hotel asset suggestion from TravelAssetsSuggestionService.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        place_id: a place ID identifying a place in the Google Places database.
    """
    request = client.get_type("SuggestTravelAssetsRequest")
    request.customer_id = customer_id
    # Uses 'en-US' as an example. It can be any language specifications in
    # BCP 47 format.
    request.language_option = "en-US"
    # In this example we only use a single place ID for the purpose of
    # demonstration, but it's possible to append more than one here if needed.
    request.place_ids.append(place_id)
    # Send a request to suggest assets to be created as an asset group for the
    # Performance Max for travel goals campaign.
    travel_asset_suggestion_service = client.get_service(
        "TravelAssetSuggestionService"
    )
    response = travel_asset_suggestion_service.suggest_travel_assets(
        request=request
    )
    print(f"Fetched a hotel asset suggestion for the place ID: '{place_id}'.")

    # Since we sent a single operation in the request, it's guaranteed that
    # there will only be a single item in the response.
    return response.hotel_asset_suggestions[0]


def create_multiple_text_assets(
    client, customer_id, asset_field_type, hotel_asset_suggestion
):
    """Creates multiple text assets and returns the list of resource names.

    The hotel asset suggestion is used to create a text asset first. If the
    number of created text assets is still fewer than the minimum required
    number of assets of the specified asset field type, adds more text assets to
    fulfill the requirement.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        asset_field_type: the asset field type enum that the new assets will be
            created as.
        hotel_asset_suggestion: the hotel asset suggestion.

    Returns:
        a list of asset resource names.
    """
    # We use the GoogleAdService to create multiple text assets in a single
    # request. First, adds all the text assets of the specified asset field
    # type.
    operations = []
    success_status = client.enums.HotelAssetSuggestionStatusEnum.SUCCESS

    if hotel_asset_suggestion.status == success_status:
        for text_asset in hotel_asset_suggestion.text_assets:
            # If the suggested text asset is not of the type specified, then
            # we skip it and move on to the next text asset.
            if text_asset.asset_field_type != asset_field_type:
                continue

            # If the suggested text asset is of the type specified, then we
            # build a mutate operation that creates a new text asset using
            # the text from the suggestion.
            operation = client.get_type("MutateOperation")
            asset = operation.asset_operation.create
            asset.text_asset.text = text_asset.text
            operations.append(operation)

    # If the current number of operations is still less than the minimum
    # required assets for the asset field type, add more operations using the
    # default texts.
    minimum_required_text_asset_count = MIN_REQUIRED_TEXT_ASSET_COUNTS[
        asset_field_type.name
    ]

    if len(operations) < minimum_required_text_asset_count:
        # Calculate the number of additional operations that need to be created.
        difference = minimum_required_text_asset_count - len(operations)
        # Retrieve the list of default texts for the given asset type.
        default_texts = DEFAULT_TEXT_ASSETS_INFO[asset_field_type.name]
        for i in range(difference):
            operation = client.get_type("MutateOperation")
            asset = operation.asset_operation.create
            asset.text_asset.text = default_texts[i]
            operations.append(operation)

    # Issues a mutate request to add all assets.
    googleads_service = client.get_service("GoogleAdsService")
    response = googleads_service.mutate(
        customer_id=customer_id, mutate_operations=operations
    )

    print(
        "The following assets were created for the asset field type "
        f"'{asset_field_type.name}'"
    )
    print_response_details(response)

    return [
        result.asset_result.resource_name
        for result in response.mutate_operation_responses
    ]


def create_hotel_asset_set(client, customer_id):
    """Creates a hotel property asset set.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.

    Returns:
        the created hotel property asset set's resource name.
    """
    # Creates an asset set operation for a hotel property asset set.
    operation = client.get_type("AssetSetOperation")
    # Creates a hotel property asset set.
    asset_set = operation.create
    asset_set.name = f"My hotel property asset set #{get_printable_datetime()}"
    asset_set.type_ = client.enums.AssetSetTypeEnum.HOTEL_PROPERTY

    # Issues a mutate request to add a hotel asset set.
    asset_set_service = client.get_service("AssetSetService")
    response = asset_set_service.mutate_asset_sets(
        customer_id=customer_id, operations=[operation]
    )
    resource_name = response.results[0].resource_name
    print(f"Created an asset set with resource name: '{resource_name}'")

    return resource_name


def create_hotel_asset(client, customer_id, place_id, asset_set_resource_name):
    """Creates a hotel property asset using the specified place ID.

    The place ID must belong to a hotel property. Then, links it to the
    specified asset set.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        place_id: a place ID identifying a place in the Google Places database.
        asset_set_resource_name: an asset set resource name

    Returns:
        the created hotel property asset's resource name.
    """
    # We use the GoogleAdService to create an asset and asset set asset in a
    # single request.
    googleads_service = client.get_service("GoogleAdsService")
    asset_resource_name = googleads_service.asset_path(
        customer_id, ASSET_TEMPORARY_ID
    )

    # Creates a mutate operation for a hotel property asset.
    asset_mutate_operation = client.get_type("MutateOperation")
    # Creates a hotel property asset.
    asset = asset_mutate_operation.asset_operation.create
    asset.resource_name = asset_resource_name
    # Creates a hotel property asset for the place ID.
    asset.hotel_property_asset.place_id = place_id

    # Creates a mutate operation for an asset set asset.
    asset_set_asset_mutate_operation = client.get_type("MutateOperation")
    # Creates an asset set asset.
    asset_set_asset = (
        asset_set_asset_mutate_operation.asset_set_asset_operation.create
    )
    asset_set_asset.asset = asset_resource_name
    asset_set_asset.asset_set = asset_set_resource_name

    # Issues a mutate request to create all entities.
    response = googleads_service.mutate(
        customer_id=customer_id,
        mutate_operations=[
            asset_mutate_operation,
            asset_set_asset_mutate_operation,
        ],
    )
    print("Created the following entities for the hotel asset:")
    print_response_details(response)

    return response.mutate_operation_responses[0].asset_result.resource_name


def create_campaign_budget_operation(client, customer_id):
    """Creates a mutate operation that creates a new campaign budget.

    A temporary ID will be assigned to this campaign budget so that it can be
    referenced by other objects being created in the same mutate request.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.

    Returns:
        a MutateOperation message that creates a new campaign budget
    """
    googleads_service = client.get_service("GoogleAdsService")
    # Creates a mutate operation that creates a campaign budget.
    operation = client.get_type("MutateOperation")
    budget = operation.campaign_budget_operation.create
    # Sets a temporary ID in the budget's resource name so it can be referenced
    # by the campaign in later steps.
    budget.resource_name = googleads_service.campaign_budget_path(
        customer_id, BUDGET_TEMPORARY_ID
    )
    budget.name = (
        "Performance Max for travel goals campaign budget "
        f"#{get_printable_datetime()}"
    )
    # The budget period already defaults to DAILY.
    budget.amount_micros = 50000000
    budget.delivery_method = client.enums.BudgetDeliveryMethodEnum.STANDARD
    # A Performance Max campaign cannot use a shared campaign budget.
    budget.explicitly_shared = False

    return operation


def create_campaign_operation(
    client, customer_id, hotel_property_asset_set_resource_name
):
    """Creates a mutate operation that creates a new Performance Max campaign.

    Links the specified hotel property asset set to this campaign.

    A temporary ID will be assigned to this campaign budget so that it can be
    referenced by other objects being created in the same mutate request.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        hotel_property_asset_set_resource_name: the resource name for a hotel
            property asset set.

    Returns:
        a MutateOperation message that creates a new Performance Max campaign.
    """
    googleads_service = client.get_service("GoogleAdsService")
    # Creates a mutate operation that creates a campaign.
    operation = client.get_type("MutateOperation")
    campaign = operation.campaign_operation.create
    campaign.name = (
        f"Performance Max for travel goals campaign #{get_printable_datetime}"
    )
    # Assigns the resource name with a temporary ID.
    campaign.resource_name = googleads_service.campaign_path(
        customer_id, CAMPAIGN_TEMPORARY_ID
    )
    # Sets the budget using the given budget resource name.
    campaign.campaign_budget = googleads_service.campaign_budget_path(
        customer_id, BUDGET_TEMPORARY_ID
    )
    # The campaign is the only entity in the mutate request that should have its
    # status set.
    # Recommendation: Set the campaign to PAUSED when creating it to prevent
    # the ads from immediately serving.
    campaign.status = client.enums.CampaignStatusEnum.PAUSED
    # Performance Max campaigns have an advertising_channel_type of
    # PERFORMANCE_MAX. The advertising_channel_sub_type should not be set.
    campaign.advertising_channel_type = (
        client.enums.AdvertisingChannelTypeEnum.PERFORMANCE_MAX
    )
    # To create a Performance Max for travel goals campaign, you need to set
    # the `hotel_property_asset_set` field.
    campaign.hotel_property_asset_set = hotel_property_asset_set_resource_name
    # Bidding strategy must be set directly on the campaign.
    # Setting a portfolio bidding strategy by resource name is not supported.
    # Max Conversion and Maximize Conversion Value are the only strategies
    # supported for Performance Max campaigns.
    # An optional ROAS (Return on Advertising Spend) can be set for
    # maximize_conversion_value. The ROAS value must be specified as a ratio in
    # the API. It is calculated by dividing "total value" by "total spend".
    # For more information on Maximize Conversion Value, see the support
    # article: https://support.google.com/google-ads/answer/7684216.
    # A target_roas of 3.5 corresponds to a 350% return on ad spend.
    campaign.maximize_conversion_value.target_roas = 3.5

    return operation


def create_asset_group_operations(
    client,
    customer_id,
    hotel_property_asset_resource_name,
    headline_asset_resource_names,
    description_asset_resource_names,
    hotel_asset_suggestion,
):
    """Creates a list of mutate operations that create a new asset group.

    The asset group is composed of suggested assets. In case the number of
    suggested assets is not enough for the requirements, it will create more
    assets to meet the requirement.

    For the list of required assets for a Performance Max campaign, see
    https://developers.google.com/google-ads/api/docs/performance-max/assets.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        hotel_property_asset_resource_name: the hotel property asset resource
            name that will be used to create an asset group.
        headline_asset_resource_names: a list of headline asset resource names.
        description_asset_resource_names: a list of description asset resource
            names.
        hotel_asset_suggestion: the hotel asset suggestion.

    Returns:
        a list of mutate operations that create the asset group.
    """
    global next_temp_id
    googleads_service = client.get_service("GoogleAdsService")
    operations = []

    # Creates a new mutate operation that creates an asset group using suggested
    # information when available.
    success_status = client.enums.HotelAssetSuggestionStatusEnum.SUCCESS
    if hotel_asset_suggestion.status == success_status:
        asset_group_name = hotel_asset_suggestion.hotel_name
        asset_group_final_urls = [hotel_asset_suggestion.final_url]
    else:
        asset_group_name = (
            "Performance Max for travel goals asset group "
            f"#{get_printable_datetime()}"
        )
        asset_group_final_urls = ["http://www.example.com"]

    asset_group_resource_name = googleads_service.asset_group_path(
        customer_id, ASSET_GROUP_TEMPORARY_ID
    )
    asset_group_mutate_operation = client.get_type("MutateOperation")
    asset_group = asset_group_mutate_operation.asset_group_operation.create
    asset_group.resource_name = asset_group_resource_name
    asset_group.name = asset_group_name
    asset_group.campaign = googleads_service.campaign_path(
        customer_id, CAMPAIGN_TEMPORARY_ID
    )
    asset_group.final_urls = asset_group_final_urls
    asset_group.status = client.enums.AssetGroupStatusEnum.PAUSED
    # Append the asset group operation to the list of operations.
    operations.append(asset_group_mutate_operation)

    # An asset group is linked to an asset by creating a new asset group asset
    # and providing:
    # -  the resource name of the asset group
    # -  the resource name of the asset
    # -  the field_type of the asset in this asset group

    # To learn more about asset groups, see
    # https://developers.google.com/google-ads/api/docs/performance-max/asset-groups.

    # Headline and description assets were created at the first step of this
    # example. So, we just need to link them with the created asset group.

    # Links the headline assets to the asset group.
    for resource_name in headline_asset_resource_names:
        headline_operation = client.get_type("MutateOperation")
        asset_group_asset = (
            headline_operation.asset_group_asset_operation.create
        )
        asset_group_asset.asset = resource_name
        asset_group_asset.asset_group = asset_group_resource_name
        asset_group_asset.field_type = client.enums.AssetFieldTypeEnum.HEADLINE
        operations.append(headline_operation)

    # Links the description assets to the asset group.
    for resource_name in description_asset_resource_names:
        description_operation = client.get_type("MutateOperation")
        asset_group_asset = (
            description_operation.asset_group_asset_operation.create
        )
        asset_group_asset.asset = resource_name
        asset_group_asset.asset_group = asset_group_resource_name
        asset_group_asset.field_type = (
            client.enums.AssetFieldTypeEnum.DESCRIPTION
        )
        operations.append(description_operation)

    # Link the previously created hotel property asset to the asset group. If
    # there are multiple assets, these steps to create a new operation need to
    # be performed for each asset.
    asset_group_asset_mutate_operation = client.get_type("MutateOperation")
    asset_group_asset = (
        asset_group_asset_mutate_operation.asset_group_asset_operation.create
    )
    asset_group_asset.asset = hotel_property_asset_resource_name
    asset_group_asset.asset_group = asset_group_resource_name
    asset_group_asset.field_type = (
        client.enums.AssetFieldTypeEnum.HOTEL_PROPERTY
    )
    operations.append(asset_group_asset_mutate_operation)

    # Creates the rest of required text assets and link them to the asset group.
    operations.extend(
        create_text_assets_for_asset_group(
            client, customer_id, hotel_asset_suggestion
        )
    )

    # Creates the image assets and link them to the asset group. Some optional
    # image assets suggested by the TravelAssetSuggestionService might be
    # created too.
    operations.extend(
        create_image_assets_for_asset_group(
            client, customer_id, hotel_asset_suggestion
        )
    )

    if hotel_asset_suggestion.status == success_status:
        # Creates a new mutate operation for a suggested call-to-action asset
        # and link it to the asset group.
        asset_mutate_operation = client.get_type("MutateOperation")
        asset = asset_mutate_operation.asset_operation.create
        asset.resource_name = googleads_service.asset_path(
            customer_id, next_temp_id
        )
        asset.name = (
            f"Suggested call-to-action asset #{get_printable_datetime()}"
        )
        asset.call_to_action_asset.call_to_action = (
            hotel_asset_suggestion.call_to_action
        )

        # Creates a new mutate operation for a call-to-action asset group.
        asset_group_asset_mutate_operation = client.get_type("MutateOperation")
        asset_group_asset = (
            asset_group_asset_mutate_operation.asset_group_asset_operation.create
        )
        asset_group_asset.asset = googleads_service.asset_path(
            customer_id, next_temp_id
        )
        asset_group_asset.asset_group = asset_group_resource_name
        asset_group_asset.field_type = (
            client.enums.AssetFieldTypeEnum.CALL_TO_ACTION_SELECTION
        )

        next_temp_id -= 1

    return operations


def create_text_assets_for_asset_group(
    client, customer_id, hotel_asset_suggestion
):
    """Creates text assets for an asset group using the given hotel text assets.

    It adds more text assets to fulfill the requirements if the suggested hotel
    text assets are not enough.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        hotel_asset_suggestion: the hotel asset suggestion.

    Returns:
        a list of mutate operations that create text assets.
    """
    operations = []

    # Creates mutate operations for the suggested text assets except for
    # headlines and descriptions, which were created previously.
    required_text_asset_counts = {
        key: 0 for key in MIN_REQUIRED_TEXT_ASSET_COUNTS.keys()
    }
    success_status = client.enums.HotelAssetSuggestionStatusEnum.SUCCESS
    if hotel_asset_suggestion.status == success_status:
        for text_asset in hotel_asset_suggestion.text_assets:
            text = text_asset.text
            asset_field_type = text_asset.asset_field_type

            if asset_field_type.name in ("HEADLINE", "DESCRIPTION"):
                # Headlines and descriptions were already created at the first
                # step of this code example
                continue

            print(
                f"A test asset with text {text} is suggested for the asset "
                f"field type `{asset_field_type.name}`"
            )

            operations.extend(
                create_text_asset_and_asset_group_asset_operations(
                    client, customer_id, text, asset_field_type
                )
            )

            required_text_asset_counts[asset_field_type.name] += 1

    # Adds more text assets to fulfill the requirements.
    for field_type_name, min_count in MIN_REQUIRED_TEXT_ASSET_COUNTS.items():
        if field_type_name in ("HEADLINE", "DESCRIPTION"):
            # Headlines and descriptions were already created at the first step
            # of this code example.
            continue

        difference = min_count - required_text_asset_counts[field_type_name]
        if difference > 0:
            for i in range(difference):
                default_text = DEFAULT_TEXT_ASSETS_INFO[field_type_name][i]
                field_type_enum = client.enums.AssetFieldTypeEnum[
                    field_type_name
                ]

                print(
                    f"A default text {default_text} is used to create a "
                    f"text asset for the asset field type {field_type_name}"
                )

                operations.extend(
                    create_text_asset_and_asset_group_asset_operations(
                        client, customer_id, default_text, field_type_enum
                    )
                )

    return operations


def create_text_asset_and_asset_group_asset_operations(
    client, customer_id, text, field_type_enum
):
    """Creates a list of mutate operations that create a new linked text asset.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        text: the text of an asset to be created.
        field_type_enum: the field type enum of a new asset in the asset group
            asset.

    Returns:
        a list of mutate operations that create a new linked text asset.
    """
    global next_temp_id
    googleads_service = client.get_service("GoogleAdsService")
    operations = []

    # Creates a new mutate operation that creates a text asset.
    asset_mutate_operation = client.get_type("MutateOperation")
    asset = asset_mutate_operation.asset_operation.create
    asset.resource_name = googleads_service.asset_path(
        customer_id, next_temp_id
    )
    asset.text_asset.text = text
    operations.append(asset_mutate_operation)

    # Creates an asset group asset operation to link the asset to the asset
    # group.
    asset_group_asset_mutate_operation = client.get_type("MutateOperation")
    asset_group_asset = (
        asset_group_asset_mutate_operation.asset_group_asset_operation.create
    )
    asset_group_asset.asset = googleads_service.asset_path(
        customer_id, next_temp_id
    )
    asset_group_asset.asset_group = googleads_service.asset_group_path(
        customer_id, ASSET_GROUP_TEMPORARY_ID
    )
    asset_group_asset.field_type = field_type_enum
    operations.append(asset_group_asset_mutate_operation)

    next_temp_id -= 1

    return operations


def create_image_assets_for_asset_group(
    client, customer_id, hotel_asset_suggestion
):
    """Creates image assets for an asset group with the given hotel suggestions.

    It adds more image assets to fulfill the requirements if the suggested hotel
    image assets are not enough.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        hotel_asset_suggestion: the hotel asset suggestion.

    Returns:
        a list of mutate operations that create image assets.
    """
    operations = []

    # Creates mutate operations for the suggested image assets.
    required_image_asset_counts = {
        key: 0 for key in MIN_REQUIRED_IMAGE_ASSET_COUNTS.keys()
    }
    for image_asset in hotel_asset_suggestion.image_assets:
        url = image_asset.uri
        field_type_enum = image_asset.asset_field_type
        name = f"Suggested image asset #{get_printable_datetime()}"

        print(
            f"An image asset with URL '{url}' is suggested for the asset field "
            f"type '{field_type_enum.name}'"
        )

        operations.extend(
            create_image_asset_and_image_asset_group_asset_operations(
                client, customer_id, url, field_type_enum, name
            )
        )

        # Keeps track of only required image assets. The
        # TravelAssetSuggestionService may sometimes suggest optional image
        # assets.
        if field_type_enum.name in required_image_asset_counts:
            required_image_asset_counts[field_type_enum.name] += 1

    # Adds more image assets to fulfill the requirements.
    for field_type_name, min_count in MIN_REQUIRED_IMAGE_ASSET_COUNTS.items():
        difference = min_count - required_image_asset_counts[field_type_name]
        if difference > 0:
            for i in range(difference):
                default_url = DEFAULT_IMAGE_ASSETS_INFO[field_type_name][i]
                name = f"{field_type_name.lower()} {get_printable_datetime()}"
                field_type_enum = client.enums.AssetFieldTypeEnum[
                    field_type_name
                ]

                print(
                    f"A default image URL {default_url} is used to create an "
                    f"image asset for the asset field type {field_type_name}"
                )

                operations.extend(
                    create_image_asset_and_image_asset_group_asset_operations(
                        client, customer_id, default_url, field_type_enum, name
                    )
                )

    return operations


def create_image_asset_and_image_asset_group_asset_operations(
    client, customer_id, url, field_type_enum, asset_name
):
    """Creates a list of mutate operations that create a new linked image asset.

    Args:
        client: an initialized GoogleAdsClient instance.
        customer_id: a client customer ID.
        url: the URL of the image to be retrieved and put into an asset.
        field_type_enum: the field type enum of the new asset in the asset group
            asset.
        asset_name: the asset name.

    Returns:
        a list of mutate operations that create a new linked image asset.
    """
    global next_temp_id
    googleads_service = client.get_service("GoogleAdsService")
    operations = []

    # Creates a new mutate operation that creates an image asset.
    asset_mutate_operation = client.get_type("MutateOperation")
    asset = asset_mutate_operation.asset_operation.create
    asset.resource_name = googleads_service.asset_path(
        customer_id, next_temp_id
    )
    # Provide a unique friendly name to identify your asset. When there is an
    # existing image asset with the same content but a different name, the new
    # name will be dropped silently.
    asset.name = asset_name
    asset.image_asset.data = requests.get(url).content
    operations.append(asset_mutate_operation)

    # Creates an asset group asset operation to link the asset to the asset
    # group.
    asset_group_asset_mutate_operation = client.get_type("MutateOperation")
    asset_group_asset = (
        asset_group_asset_mutate_operation.asset_group_asset_operation.create
    )
    asset_group_asset.asset = googleads_service.asset_path(
        customer_id, next_temp_id
    )
    asset_group_asset.asset_group = googleads_service.asset_group_path(
        customer_id, ASSET_GROUP_TEMPORARY_ID
    )
    asset_group_asset.field_type = field_type_enum
    operations.append(asset_group_asset_mutate_operation)

    next_temp_id -= 1

    return operations


def print_response_details(mutate_response):
    """Prints the details of a MutateGoogleAdsResponse message.

    Parses the "response" oneof field name and uses it to extract the new
    entity's name and resource name.

    Args:
        mutate_response: a MutateGoogleAdsResponse message.
    """
    for result in mutate_response.mutate_operation_responses:
        resource_type = "unrecognized"
        resource_name = "not found"

        if "asset_result" in result:
            resource_type = "Asset"
            resource_name = result.asset_result.resource_name
        elif "asset_set_asset_result" in result:
            resource_type = "AssetSetAsset"
            resource_name = result.asset_set_asset_result.resource_name
        elif "campaign_budget_result" in result:
            resource_type = "CampaignBudget"
            resource_name = result.campaign_budget_result.resource_name
        elif "campaign_result" in result:
            resource_type = "Campaign"
            resource_name = result.campaign_result.resource_name
        elif "asset_group_result" in result:
            resource_type = "AssetGroup"
            resource_name = result.asset_group_result.resource_name
        elif "asset_group_asset_result" in result:
            resource_type = "AssetGroupAsset"
            resource_name = result.asset_group_asset_result.resource_name

        print(
            f"Created a(n) {resource_type} with "
            f"resource_name: '{resource_name}'."
        )


if __name__ == "__main__":
    # GoogleAdsClient will read the google-ads.yaml configuration file in the
    # home directory if none is specified.
    googleads_client = GoogleAdsClient.load_from_storage(version="v16")

    parser = argparse.ArgumentParser(
        description=("Creates a Performance Max for travel goals campaign.")
    )
    # The following argument(s) should be provided to run the example.
    parser.add_argument(
        "-c",
        "--customer_id",
        type=str,
        required=True,
        help="The Google Ads customer ID.",
    )
    parser.add_argument(
        "-p",
        "--place_id",
        type=str,
        required=True,
        help=(
            "Sets a place ID that uniquely identifies a place in the Google "
            "Places database. The provided place ID must belong to a hotel "
            "property. To learn more, see: "
            "https://developers.google.com/places/web-service/place-id "
        ),
    )

    args = parser.parse_args()

    try:
        main(googleads_client, args.customer_id, args.place_id)
    except GoogleAdsException as ex:
        print(
            f'Request with ID "{ex.request_id}" failed with status '
            f'"{ex.error.code().name}" and includes the following errors:'
        )
        for error in ex.failure.errors:
            print(f'Error with message "{error.message}".')
            if error.location:
                for field_path_element in error.location.field_path_elements:
                    print(f"\t\tOn field: {field_path_element.field_name}")
        sys.exit(1)

      

Ruby

#!/usr/bin/env ruby
# Encoding: utf-8
#
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This example shows how to create a Performance Max for travel goals campaign.
# It also uses TravelAssetSuggestionService to fetch suggested assets for
# creating an asset group. In case there are not enough assets for the asset
# group (required by Performance Max), this example will create more assets to
# fulfill the requirements.
#
# For more information about Performance Max campaigns, see
# https://developers.google.com/google-ads/api/docs/performance-max/overview.
#
# Prerequisites:
# - You must have at least one conversion action in the account.
# For more about conversion actions, see
# https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.
#
# Notes:
# - This example uses the default customer conversion goals.
#   For an example of setting campaign-specific conversion goals, see
#   shopping_ads/add_performance_max_retail_campaign.rb
# - To learn how to create asset group signals, see
#   advanced_perations/add_performance_max_campaign.rb

require 'open-uri'
require 'optparse'
require 'google/ads/google_ads'

# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.
#
# See https://developers.google.com/google-ads/api/docs/mutating/best-practices
# for further details.
#
# These temporary IDs are fixed because they are used in multiple places.
BUDGET_TEMPORARY_ID = '-1'
PERFORMANCE_MAX_CAMPAIGN_TEMPORARY_ID = '-2'
ASSET_GROUP_TEMPORARY_ID = '-3'

# There are also entities that will be created in the same request but do not
# need to be fixed temporary IDs because they are referenced only once.
def next_temp_id
  @id ||= ASSET_GROUP_TEMPORARY_ID.to_i
  @id -= 1
end

# Minimum requirements of assets required in a Performance Max Asset Group.
# See https://developers.google.com/google-ads/api/docs/performance-max/assets
# for details.
MIN_REQUIRED_TEXT_ASSET_COUNTS = {
  HEADLINE: 3,
  LONG_HEADLINE: 1,
  DESCRIPTION: 2,
  BUSINESS_NAME: 1
}
MIN_REQUIRED_IMAGE_ASSET_COUNTS = {
  MARKETING_IMAGE: 1,
  SQUARE_MARKETING_IMAGE: 1,
  LOGO: 1
}

# Texts and URLs used to create text and image assets when the
# TravelAssetSuggestionService doesn't return enough assets required for
# creating an asset group.
DEFAULT_TEXT_ASSETS_INFO = {
  HEADLINE: ['Hotel', 'Travel Reviews', 'Book travel'],
  LONG_HEADLINE: ['Travel the World'],
  DESCRIPTION: ['Great deal for your beloved hotel', 'Best rate guaranteed'],
  BUSINESS_NAME: ['Interplanetary Cruises']
}

DEFAULT_IMAGE_ASSETS_INFO = {
  MARKETING_IMAGE: ['https://gaagl.page.link/Eit5'],
  SQUARE_MARKETING_IMAGE: ['https://gaagl.page.link/bjYi'],
  LOGO: ['https://gaagl.page.link/bjYi']
}

def add_performance_max_for_travel_goals(customer_id, place_id)
  # GoogleAdsClient will read a config file from
  # ENV['HOME']/google_ads_config.rb when called without parameters
  client = Google::Ads::GoogleAds::GoogleAdsClient.new

  # Creates a hotel property asset set, which will be used later to link with a
  # newly created campaign.
  hotel_property_asset_set_resource_name =
    create_hotel_asset_set(client, customer_id)

  # Creates a hotel property asset and link it with the previously created hotel property
  # asset set. This asset will also be linked to an asset group in the later steps.
  # In the real-world scenario, you'd need to create many assets for all your hotel
  # properties. We use one hotel property here for simplicity.
  # Both asset and asset set need to be created before creating a campaign,
  # so we cannot bundle them with other mutate operations below.
  hotel_property_asset_resource_name =
    create_hotel_asset(
      client,
      customer_id,
      place_id,
      hotel_property_asset_set_resource_name
    )

  # The below methods create and return MutateOperations that we later
  # provide to the GoogleAdsService.Mutate method in order to create the
  # entities in a single request. Since the entities for a Performance Max
  # campaign are closely tied to one-another, it's considered a best practice
  # to create them in a single Mutate request so they all complete
  # successfully or fail entirely, leaving no orphaned entities. See:
  # https://developers.google.com/google-ads/api/docs/mutating/overview
  campaign_budget_operation =
    create_campaign_budget_operation(client, customer_id)

  performance_max_campaign_operation =
    create_performance_max_campaign_operation(
      client,
      customer_id,
      hotel_property_asset_set_resource_name
    )

  # Gets hotel asset suggestion using the TravelAssetSuggestionService.
  hotel_asset_suggestion =
    get_hotel_asset_suggestion(client, customer_id, place_id)

  # Creates the headlines using the hotel asset suggestion.
  headline_asset_resource_names =
    create_multiple_text_assets(
      client,
      customer_id,
      :HEADLINE,
      hotel_asset_suggestion
    )

  # Creates the descriptions using the hotel asset suggestion.
  descriptions_asset_resource_names =
    create_multiple_text_assets(
      client,
      customer_id,
      :DESCRIPTION,
      hotel_asset_suggestion
    )

  asset_group_operation =
    create_asset_group_operations(
      client,
      customer_id,
      hotel_property_asset_resource_name,
      headline_asset_resource_names,
      descriptions_asset_resource_names,
      hotel_asset_suggestion
    )

  # Send the operations in a single Mutate request.
  response =
    client.service.google_ads.mutate(
      customer_id: customer_id,
      mutate_operations: [
        # It's important to create these entities in this order because
        # they depend on each other.
        campaign_budget_operation,
        performance_max_campaign_operation,
        asset_group_operation
      ].flatten
    )

  print_response_details(response)
end

# Creates a hotel property asset set.
def create_hotel_asset_set(client, customer_id)
  operation =
    client.operation.create_resource.asset_set do |asset_set|
      asset_set.name = "My Hotel propery asset set #{Time.now}"
      asset_set.type = :HOTEL_PROPERTY
    end

  # Sends the mutate request.
  response =
    client.service.asset_set.mutate_asset_sets(
      customer_id: customer_id,
      operations: [operation]
    )

  # Prints some information about the response.
  response.results.first.resource_name
end

# Creates a hotel property asset using the specified place ID.
# The place ID must belong to a hotel property. Then, links it to the
# specified asset set.
# See https://developers.google.com/places/web-service/place-id to search for a
# hotel place ID.
def create_hotel_asset(
  client,
  customer_id,
  place_id,
  hotel_property_asset_set_resource_name
)
  asset_operation =
    client.operation.create_resource.asset do |asset|
      asset.name = 'Ad Media Bundle'
      asset.hotel_property_asset =
        client.resource.hotel_property_asset do |hotel_asset|
          hotel_asset.place_id = place_id
        end
    end

  # Send the mutate request.
  response =
    client.service.asset.mutate_assets(
      customer_id: customer_id,
      operations: [asset_operation]
    )

  asset_resource_name = response.results.first.resource_name

  # Creates a mutate operation for an asset set asset.
  asset_set_asset_operation =
    client.operation.create_resource.asset_set_asset do |asa|
      asa.asset = asset_resource_name
      asa.asset_set = hotel_property_asset_set_resource_name
    end

  # Sends the mutate request.
  response =
    client.service.asset_set_asset.mutate_asset_set_assets(
      customer_id: customer_id,
      operations: [asset_set_asset_operation]
    )

  asset_resource_name
end

# Creates a MutateOperation that creates a new Performance Max campaign.
#
# A temporary ID will be assigned to this campaign so that it can
# be referenced by other objects being created in the same Mutate request.
def create_performance_max_campaign_operation(
  client,
  customer_id,
  hotel_property_asset_set_resource_name
)
  client.operation.mutate do |m|
    m.campaign_operation =
      client.operation.create_resource.campaign do |c|
        c.name = "Performance Max for Travel Goals #{SecureRandom.uuid}"

        # Set the campaign status as PAUSED. The campaign is the only entity in
        # the mutate request that should have its status set.
        c.status = :PAUSED

        # All Performance Max campaigns have an advertising_channel_type of
        # PERFORMANCE_MAX. The advertising_channel_sub_type should not be set.
        c.advertising_channel_type = :PERFORMANCE_MAX

        # To create a Performance Max for travel goals campaign, you need to set hotel_property_asset_set
        c.hotel_property_asset_set = hotel_property_asset_set_resource_name

        # Bidding strategy must be set directly on the campaign.
        # Setting a portfolio bidding strategy by resource name is not supported.
        # Max Conversion and Maximize Conversion Value are the only strategies
        # supported for Performance Max campaigns.
        # An optional ROAS (Return on Advertising Spend) can be set for
        # maximize_conversion_value. The ROAS value must be specified as a ratio
        # in the API. It is calculated by dividing "total value" by "total spend".
        # For more information on Maximize Conversion Value, see the support
        # article: http://support.google.com/google-ads/answer/7684216.
        # A target_roas of 3.5 corresponds to a 350% return on ad spend.
        c.bidding_strategy_type = :MAXIMIZE_CONVERSION_VALUE
        c.maximize_conversion_value =
          client.resource.maximize_conversion_value do |mcv|
            mcv.target_roas = 3.5
          end

        # Assign the resource name with a temporary ID.
        c.resource_name =
          client.path.campaign(
            customer_id,
            PERFORMANCE_MAX_CAMPAIGN_TEMPORARY_ID
          )

        # Set the budget using the given budget resource name.
        c.campaign_budget =
          client.path.campaign_budget(customer_id, BUDGET_TEMPORARY_ID)
      end
  end
end

def create_campaign_budget_operation(client, customer_id)
  client.operation.mutate do |m|
    m.campaign_budget_operation =
      client.operation.create_resource.campaign_budget do |cb|
        cb.name = "Performance Max campaign budget #{SecureRandom.uuid}"
        # The budget period already defaults to DAILY.
        cb.amount_micros = 50_000_000
        cb.delivery_method = :STANDARD
        # A Performance Max campaign cannot use a shared campaign budget.
        cb.explicitly_shared = false

        # Set a temporary ID in the budget's resource name so it can be referenced
        # by the campaign in later steps.
        cb.resource_name =
          client.path.campaign_budget(customer_id, BUDGET_TEMPORARY_ID)
      end
  end
end

def get_hotel_asset_suggestion(client, customer_id, place_id)
  response =
    client.service.travel_asset_suggestion.suggest_travel_assets(
      customer_id: customer_id,
      language_option: 'en-US',
      place_ids: [place_id]
    )

  response.hotel_asset_suggestions.first
end

# Creates multiple text assets and returns the list of resource names.
# The hotel asset suggestion is used to create a text asset first.
# If the number of created text assets is still fewer than the minimum required
# number of assets of the specified asset field type, adds more text assets to
# fulfill the requirement.
def create_multiple_text_assets(
  client,
  customer_id,
  asset_field_type,
  hotel_asset_suggestion
)
  # Creates the first text asset using the hotel asset suggestions.
  texts = []
  numText = 0
  if hotel_asset_suggestion.status == :SUCCESS
    hotel_asset_suggestion.text_assets.each do |hotel_text_asset|
      if hotel_text_asset.asset_field_type == asset_field_type
        texts.append(hotel_text_asset.text)
        numText += 1
      end
    end
  end

  # If the added assets are still less than the minimum required assets for the
  # asset field type, add more text assets using the default texts.
  if numText < MIN_REQUIRED_TEXT_ASSET_COUNTS[asset_field_type]
    for i in numText..MIN_REQUIRED_TEXT_ASSET_COUNTS[asset_field_type] - 1
      texts.append(DEFAULT_TEXT_ASSETS_INFO[asset_field_type][i])
    end
  end

  # Create the operations with the selected texts.
  operations =
    texts.map do |text|
      client.operation.mutate do |m|
        m.asset_operation =
          client.operation.create_resource.asset do |asset|
            asset.text_asset =
              client.resource.text_asset { |text_asset| text_asset.text = text }
          end
      end
    end

  # Send the operations in a single Mutate request.
  response =
    client.service.google_ads.mutate(
      customer_id: customer_id,
      mutate_operations: operations
    )

  asset_resource_names = []
  response.mutate_operation_responses.each do |result|
    asset_resource_names.append(result.asset_result.resource_name) if result.asset_result
  end
  print_response_details(response)
  asset_resource_names
end

# Creates a list of MutateOperations that create a new asset_group.
#
# A temporary ID will be assigned to this asset group so that it can
# be referenced by other objects being created in the same Mutate request.
def create_asset_group_operations(
  client,
  customer_id,
  hotel_property_asset_resource_name,
  headline_asset_resource_names,
  description_asset_resource_names,
  hotel_asset_suggestion
)
  operations = []

  # Creates a new mutate operation that creates an asset group using suggested
  # information when available.
  asset_group_final_urls =
    (
      if hotel_asset_suggestion.status == :SUCCESS
        hotel_asset_suggestion.final_url
      else
        'http://www.example.com'
      end
    )

  operations << client.operation.mutate do |m|
    m.asset_group_operation =
      client.operation.create_resource.asset_group do |ag|
        ag.name =
          "Performance Max for Travel Goals asset group #{SecureRandom.uuid}"
        ag.campaign =
          client.path.campaign(
            customer_id,
            PERFORMANCE_MAX_CAMPAIGN_TEMPORARY_ID
          )
        ag.final_urls << asset_group_final_urls
        ag.status = :PAUSED
        ag.resource_name =
          client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
      end
  end

  # An AssetGroup is linked to an Asset by creating a new AssetGroupAsset
  # and providing:
  #   the resource name of the AssetGroup
  #   the resource name of the Asset
  #   the field_type of the Asset in this AssetGroup.
  #
  # To learn more about AssetGroups, see
  # https://developers.google.com/google-ads/api/docs/performance-max/asset-groups

  # Link the previously created multiple text assets.

  # Link the headline assets.
  headline_asset_resource_names.each do |resource_name|
    operations << client.operation.mutate do |m|
      m.asset_group_asset_operation =
        client.operation.create_resource.asset_group_asset do |aga|
          aga.field_type = :HEADLINE
          aga.asset_group =
            client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
          aga.asset = resource_name
        end
    end
  end

  #  Link the description assets.
  description_asset_resource_names.each do |resource_name|
    operations << client.operation.mutate do |m|
      m.asset_group_asset_operation =
        client.operation.create_resource.asset_group_asset do |aga|
          aga.field_type = :DESCRIPTION
          aga.asset_group =
            client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
          aga.asset = resource_name
        end
    end
  end

  # Link the previously created hotel property asset to the asset group.
  # In the real-world scenario, you'd need to do this step several times for
  # each hotel property asset.
  operations << client.operation.mutate do |m|
    m.asset_group_asset_operation =
      client.operation.create_resource.asset_group_asset do |aga|
        aga.field_type = :HOTEL_PROPERTY
        aga.asset_group =
          client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
        aga.asset = hotel_property_asset_resource_name
      end
  end

  # Creates the rest of required text assets and link them to the asset group.
  operations +=
    create_text_assets_for_asset_group(
      client,
      customer_id,
      hotel_asset_suggestion
    )

  # Creates the image assets and link them to the asset group. Some optional
  # image assets suggested by the TravelAssetSuggestionService might be created too.
  operations +=
    create_image_assets_for_asset_group(
      client,
      customer_id,
      hotel_asset_suggestion
    )

  operations
end

# Creates text assets required for an asset group using the suggested hotel text
# assets. It adds more text assets to fulfill the requirements if the suggested
# hotel text assets are not enough.
def create_text_assets_for_asset_group(
  client,
  customer_id,
  hotel_asset_suggestion
)
  operations = []

  required_text_asset_counts = deep_copy(MIN_REQUIRED_TEXT_ASSET_COUNTS)
  required_text_asset_counts.each do |asset_field_type, _count|
    required_text_asset_counts[asset_field_type] = 0
  end

  # Creates mutate operations for the suggested text assets except for headlines
  # and descriptions, which were created previously.
  if hotel_asset_suggestion.status == :SUCCESS
    hotel_asset_suggestion.text_assets.each do |hotel_text_asset|
      if hotel_text_asset.asset_field_type == :HEADLINE ||
         hotel_text_asset.asset_field_type == :DESCRIPTION
        # Headlines and descriptions were already created at the first step of
        # this code example.
        next
      end

      puts "A text asset with text '#{hotel_text_asset.text}' is suggested for the asset field type '#{hotel_text_asset.asset_field_type}'."

      operations +=
        create_and_link_text_asset_operations(
          client,
          customer_id,
          hotel_text_asset.text,
          hotel_text_asset.asset_field_type
        )

      required_text_asset_counts[hotel_text_asset.asset_field_type] += 1
    end
  end

  # Adds more text assets to fulfill the requirements.
  MIN_REQUIRED_TEXT_ASSET_COUNTS.each do |asset_field_type, count|
    if %i[HEADLINE DESCRIPTION].include?(asset_field_type)
      # Headlines and descriptions were already created at the first step of this
      # code example.
      next
    end

    next unless required_text_asset_counts[asset_field_type] < count

    for i in required_text_asset_counts[asset_field_type]...count
      default_text = DEFAULT_TEXT_ASSETS_INFO[asset_field_type][i]
      puts "A default text '#{default_text}' is used to create a text asset for the asset field type '#{asset_field_type}'."

      operations +=
        create_and_link_text_asset_operations(
          client,
          customer_id,
          default_text,
          asset_field_type
        )
    end
  end

  operations
end

# Creates a list of MutateOperations that create a new linked text asset.
def create_and_link_text_asset_operations(client, customer_id, text, field_type)
  operations = []
  temp_id = next_temp_id

  # Create the Text Asset.
  operations << client.operation.mutate do |m|
    m.asset_operation =
      client.operation.create_resource.asset do |a|
        a.resource_name = client.path.asset(customer_id, temp_id)
        a.text_asset =
          client.resource.text_asset { |text_asset| text_asset.text = text }
      end
  end

  # Create an AssetGroupAsset to link the Asset to the AssetGroup.
  operations << client.operation.mutate do |m|
    m.asset_group_asset_operation =
      client.operation.create_resource.asset_group_asset do |aga|
        aga.field_type = field_type
        aga.asset_group =
          client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
        aga.asset = client.path.asset(customer_id, temp_id)
      end
  end

  operations
end

# Creates image assets required for an asset group using the suggested hotel
# image assets. It adds more image assets to fulfill the requirements if the
# suggested hotel image assets are not enough.
def create_image_assets_for_asset_group(
  client,
  customer_id,
  hotel_asset_suggestion
)
  operations = []

  required_image_asset_counts = deep_copy(MIN_REQUIRED_IMAGE_ASSET_COUNTS)
  required_image_asset_counts.each do |asset_field_type, _count|
    required_image_asset_counts[asset_field_type] = 0
  end

  # Creates mutate operations for the suggested image assets.
  if hotel_asset_suggestion.status == :SUCCESS
    hotel_asset_suggestion.image_assets.each do |hotel_image_asset|
      puts "An image asset with URL '#{hotel_image_asset.uri}' is suggested for the asset field type '#{hotel_image_asset.asset_field_type}'."

      operations +=
        create_and_link_image_asset_operations(
          client,
          customer_id,
          hotel_image_asset.uri,
          hotel_image_asset.asset_field_type,
          "Suggested image asset for the asset field type '%s'." %
            hotel_image_asset.asset_field_type
        )

      # Keeps track of only required image assets. The service may sometimes
      # suggest optional image assets.
      unless required_image_asset_counts.has_key?(
        hotel_image_asset.asset_field_type
      )
        next
      end

      required_image_asset_counts[hotel_image_asset.asset_field_type] += 1
    end
  end

  # Adds more image assets to fulfill the requirements.
  MIN_REQUIRED_IMAGE_ASSET_COUNTS.each do |asset_field_type, count|
    next unless required_image_asset_counts[asset_field_type] < count

    for i in required_image_asset_counts[asset_field_type]...count
      default_uri = DEFAULT_IMAGE_ASSETS_INFO[asset_field_type][i]
      puts "A default image URL '#{default_uri}' is used to create an image asset for the asset field type '#{asset_field_type}'."

      operations +=
        create_and_link_image_asset_operations(
          client,
          customer_id,
          default_uri,
          asset_field_type,
          "Default image asset for the asset field type '%s'." %
            asset_field_type
        )
    end
  end

  operations
end

# Creates a list of MutateOperations that create a new linked image asset.
def create_and_link_image_asset_operations(
  client,
  customer_id,
  url,
  field_type,
  asset_name
)
  operations = []
  temp_id = next_temp_id

  # Create the Image Asset.
  operations << client.operation.mutate do |m|
    m.asset_operation =
      client.operation.create_resource.asset do |a|
        a.resource_name = client.path.asset(customer_id, temp_id)
        # Provide a unique friendly name to identify your asset.
        # When there is an existing image asset with the same content but a different
        # name, the new name will be dropped silently.
        a.name = asset_name
        a.type = :IMAGE
        a.image_asset =
          client.resource.image_asset do |image_asset|
            image_asset.data = get_image_bytes(url)
          end
      end
  end

  # Create an AssetGroupAsset to link the Asset to the AssetGroup.
  operations << client.operation.mutate do |m|
    m.asset_group_asset_operation =
      client.operation.create_resource.asset_group_asset do |aga|
        aga.field_type = field_type
        aga.asset_group =
          client.path.asset_group(customer_id, ASSET_GROUP_TEMPORARY_ID)
        aga.asset = client.path.asset(customer_id, temp_id)
      end
  end

  operations
end

# Loads image data from a URL.
def get_image_bytes(url)
  URI.open(url).read
end

# Prints the details of a MutateGoogleAdsResponse.
def print_response_details(response)
  # Parse the mutate response to print details about the entities that
  # were created by the request.
  suffix = '_result'
  response.mutate_operation_responses.each do |result|
    result
      .to_h
      .select { |_k, v| v }
      .each do |name, value|
        name = name.to_s.delete_suffix(suffix) if name.to_s.end_with?(suffix)

        puts "Created a(n) #{::Google::Ads::GoogleAds::Utils.camelize(name)} " \
               "with #{value.to_s.strip}."
      end
  end
end

def deep_copy(o)
  Marshal.load(Marshal.dump(o))
end

if __FILE__ == $0
  options = {}

  OptionParser
    .new do |opts|
      opts.banner = format('Usage: %s [options]', File.basename(__FILE__))

      opts.separator ''
      opts.separator 'Options:'

      opts.on('-C', '--customer-id CUSTOMER-ID', String, 'Customer ID') do |v|
        options[:customer_id] = v
      end

      opts.on('-P', '--place-id PLACE-ID', String, 'Place ID') do |v|
        options[:place_id] = v
      end

      opts.separator ''
      opts.separator 'Help:'

      opts.on_tail('-h', '--help', 'Show this message') do
        puts opts
        exit
      end
    end
    .parse!

  begin
    add_performance_max_for_travel_goals(
      options.fetch(:customer_id).tr('-', ''),
      options[:place_id]
    )
  rescue Google::Ads::GoogleAds::Errors::GoogleAdsError => e
    e.failure.errors.each do |error|
      STDERR.printf("Error with message: %s\n", error.message)
      if error.location
        error.location.field_path_elements.each do |field_path_element|
          STDERR.printf("\tOn field: %s\n", field_path_element.field_name)
        end
      end
      error.error_code.to_h.each do |k, v|
        next if v == :UNSPECIFIED

        STDERR.printf("\tType: %s\n\tCode: %s\n", k, v)
      end
    end
    raise
  end
end

      

Perl

#!/usr/bin/perl -w
#
# Copyright 2023, Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This example shows how to create a Performance Max for travel goals campaign. It also uses
# TravelAssetSuggestionService to fetch suggested assets for creating an asset group. In case
# there are not enough assets for the asset group (required by Performance Max), this example will
# create more assets to fulfill the requirements.
#
# For more information about Performance Max campaigns, see
# https://developers.google.com/google-ads/api/docs/performance-max/overview.
#
# Prerequisites:
# - You must have at least one conversion action in the account. For more about conversion actions,
# see https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.
#
# Notes:
# - This example uses the default customer conversion goals. For an example of setting
#   campaign-specific conversion goals, see shopping_ads/add_performance_max_retail_campaign.pl.
# - To learn how to create asset group signals, see
#   advanced_operations/add_performance_max_campaign.pl.

use strict;
use warnings;
use utf8;

use FindBin qw($Bin);
use lib "$Bin/../../lib";
use Google::Ads::GoogleAds::Client;
use Google::Ads::GoogleAds::Utils::GoogleAdsHelper;
use Google::Ads::GoogleAds::Utils::MediaUtils;
use Google::Ads::GoogleAds::V16::Resources::CampaignBudget;
use Google::Ads::GoogleAds::V16::Resources::Campaign;
use Google::Ads::GoogleAds::V16::Resources::Asset;
use Google::Ads::GoogleAds::V16::Resources::AssetGroup;
use Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset;
use Google::Ads::GoogleAds::V16::Resources::AssetSet;
use Google::Ads::GoogleAds::V16::Resources::AssetSetAsset;
use Google::Ads::GoogleAds::V16::Common::CallToActionAsset;
use Google::Ads::GoogleAds::V16::Common::MaximizeConversionValue;
use Google::Ads::GoogleAds::V16::Common::TextAsset;
use Google::Ads::GoogleAds::V16::Common::HotelPropertyAsset;
use Google::Ads::GoogleAds::V16::Common::ImageAsset;
use Google::Ads::GoogleAds::V16::Enums::BudgetDeliveryMethodEnum qw(STANDARD);
use Google::Ads::GoogleAds::V16::Enums::CampaignStatusEnum;
use Google::Ads::GoogleAds::V16::Enums::AdvertisingChannelTypeEnum
  qw(PERFORMANCE_MAX);
use Google::Ads::GoogleAds::V16::Enums::AssetGroupStatusEnum;
use Google::Ads::GoogleAds::V16::Enums::AssetFieldTypeEnum
  qw(HEADLINE DESCRIPTION LONG_HEADLINE BUSINESS_NAME LOGO MARKETING_IMAGE SQUARE_MARKETING_IMAGE HOTEL_PROPERTY CALL_TO_ACTION_SELECTION);
use Google::Ads::GoogleAds::V16::Enums::HotelAssetSuggestionStatusEnum
  qw(SUCCESS);
use Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation;
use
  Google::Ads::GoogleAds::V16::Services::CampaignBudgetService::CampaignBudgetOperation;
use Google::Ads::GoogleAds::V16::Services::CampaignService::CampaignOperation;
use Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation;
use
  Google::Ads::GoogleAds::V16::Services::AssetGroupService::AssetGroupOperation;
use
  Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation;
use Google::Ads::GoogleAds::V16::Services::AssetSetService::AssetSetOperation;
use
  Google::Ads::GoogleAds::V16::Services::AssetSetAssetService::AssetSetAssetOperation;
use Google::Ads::GoogleAds::V16::Utils::ResourceNames;

use Getopt::Long qw(:config auto_help);
use Pod::Usage;
use Cwd          qw(abs_path);
use Data::Uniqid qw(uniqid);

# Minimum requirements of assets required in a Performance Max asset group.
# See https://developers.google.com/google-ads/api/docs/performance-max/assets for details.
my $min_required_text_asset_counts = {
  HEADLINE      => 3,
  LONG_HEADLINE => 1,
  DESCRIPTION   => 2,
  BUSINESS_NAME => 1,
};

my $min_required_image_asset_counts = {
  MARKETING_IMAGE        => 1,
  SQUARE_MARKETING_IMAGE => 1,
  LOGO                   => 1,
};

# Texts and URLs used to create text and image assets when the TravelAssetSuggestionService
# doesn't return enough assets required for creating an asset group.
my $default_text_assets_info = {
  HEADLINE      => ['Hotel', 'Travel Reviews', 'Book travel'],
  LONG_HEADLINE => ['Travel the World'],
  DESCRIPTION => ['Great deal for your beloved hotel', 'Best rate guaranteed',],
  BUSINESS_NAME => ['Interplanetary Cruises'],
};

my $default_image_assets_info = {
  MARKETING_IMAGE        => ['https://gaagl.page.link/Eit5'],
  SQUARE_MARKETING_IMAGE => ['https://gaagl.page.link/bjYi'],
  LOGO                   => ['https://gaagl.page.link/bjYi'],
};

# We specify temporary IDs that are specific to a single mutate request.
# Temporary IDs are always negative and unique within one mutate request.
#
# See https://developers.google.com/google-ads/api/docs/mutating/best-practices
# for further details.
#
# These temporary IDs are fixed because they are used in multiple places.
use constant ASSET_TEMPORARY_ID       => -1;
use constant BUDGET_TEMPORARY_ID      => -2;
use constant CAMPAIGN_TEMPORARY_ID    => -3;
use constant ASSET_GROUP_TEMPORARY_ID => -4;

# There are also entities that will be created in the same request but do not need to be fixed
# temporary IDs because they are referenced only once.
our $next_temp_id = ASSET_GROUP_TEMPORARY_ID - 1;

sub add_performance_max_for_travel_goals_campaign {
  my ($api_client, $customer_id, $place_id) = @_;

  my $hotel_asset_suggestion =
    get_hotel_asset_suggestion($api_client, $customer_id, $place_id);

  # Performance Max campaigns require that repeated assets such as headlines
  # and descriptions be created before the campaign.
  # For the list of required assets for a Performance Max campaign, see
  # https://developers.google.com/google-ads/api/docs/performance-max/assets.
  #
  # This step is the same for any types of Performance Max campaigns.

  # Create the headlines using the hotel asset suggestion.
  my $headline_asset_resource_names =
    create_multiple_text_assets($api_client, $customer_id, HEADLINE,
    $hotel_asset_suggestion);

  my $description_asset_resource_names =
    create_multiple_text_assets($api_client, $customer_id, DESCRIPTION,
    $hotel_asset_suggestion);

  # Create a hotel property asset set, which will be used later to link with a newly created
  # campaign.
  my $hotel_property_asset_set_resource_name =
    create_hotel_asset_set($api_client, $customer_id);

  # Create a hotel property asset and link it with the previously created hotel property
  # asset set. This asset will also be linked to an asset group in the later steps.
  # In the real-world scenario, you'd need to create many assets for all your hotel
  # properties. We use one hotel property here for simplicity.
  # Both asset and asset set need to be created before creating a campaign, so we cannot
  # bundle them with other mutate operations below.
  my $hotel_property_asset_resource_name =
    create_hotel_asset($api_client, $customer_id, $place_id,
    $hotel_property_asset_set_resource_name);

  # It's important to create the below entities in this order because they depend on
  # each other.
  # The below methods create and return mutate operations that we later provide to the
  # GoogleAdsService.Mutate method in order to create the entities in a single request.
  # Since the entities for a Performance Max campaign are closely tied to one-another, it's
  # considered a best practice to create them in a single Mutate request so they all complete
  # successfully or fail entirely, leaving no orphaned entities. See:
  # https://developers.google.com/google-ads/api/docs/mutating/overview.
  my $operations = [];
  push @$operations, create_campaign_budget_operation($customer_id);
  push @$operations,
    create_campaign_operation($customer_id,
    $hotel_property_asset_set_resource_name);
  push @$operations,
    @{
    create_asset_group_operations(
      $customer_id,                   $hotel_property_asset_resource_name,
      $headline_asset_resource_names, $description_asset_resource_names,
      $hotel_asset_suggestion
    )};

  # Issue a mutate request to create everything and print its information.
  my $mutate_google_ads_response = $api_client->GoogleAdsService()->mutate({
    customerId       => $customer_id,
    mutateOperations => $operations
  });
  printf
"Created the following entities for a campaign budget, a campaign, and an asset group"
    . " for Performance Max for travel goals:\n";
  print_response_details($mutate_google_ads_response);
}

# Return hotel asset suggestion obtained from TravelAssetsSuggestionService.
sub get_hotel_asset_suggestion {
  my ($api_client, $customer_id, $place_id) = @_;

  # Send a request to suggest assets to be created as an asset group for the Performance Max
  # for travel goals campaign.
  my $suggest_travel_assets_response =
    $api_client->TravelAssetSuggestionService()->suggest_travel_assets({
      customerId => $customer_id,
      # Uses 'en-US' as an example. It can be any language specifications in BCP 47 format.
      languageOption => 'en-US',
      # The service accepts several place IDs. We use only one here for demonstration.
      placeIds => [$place_id],
    });

  printf "Fetched a hotel asset suggestion for the place ID '%s'.\n", $place_id;
  return $suggest_travel_assets_response->{hotelAssetSuggestions}[0];
}

# Create multiple text assets and returns the list of resource names. The hotel asset
# suggestion is used to create a text asset first. If the number of created text assets is
# still fewer than the minimum required number of assets of the specified asset field type,
# adds more text assets to fulfill the requirement.
sub create_multiple_text_assets {
  my ($api_client, $customer_id, $asset_field_type, $hotel_asset_suggestion) =
    @_;
  # We use the GoogleAdService to create multiple text assets in a single request.
  # First, add all the text assets of the specified asset field type.
  my $operations = [];

  if ($hotel_asset_suggestion->{status} eq SUCCESS) {
    foreach my $text_asset (@{$hotel_asset_suggestion->{textAssets}}) {
      if ($text_asset->{assetFieldType} ne $asset_field_type) {
        next;
      }
      push @$operations,
        Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
        ->new({
          assetOperation =>
            Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation
            ->new({
              create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
                  textAsset =>
                    Google::Ads::GoogleAds::V16::Common::TextAsset->new({
                      text => $text_asset->{text}})})})});
    }
  }

  # If the added assets are still less than the minimum required assets for the asset field
  # type, add more text assets using the default texts.
  my $min_count = $min_required_text_asset_counts->{$asset_field_type};
  my $num_operations_added = scalar @$operations;
  for (my $i = 0 ; $i < $min_count - $num_operations_added ; $i++) {
    my $text = $default_text_assets_info->{$asset_field_type}[$i++];
    # Creates a mutate operation for a text asset, using the default text.
    push @$operations,
      Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
      ->new({
        assetOperation =>
          Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation->
          new({
            create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
                textAsset =>
                  Google::Ads::GoogleAds::V16::Common::TextAsset->new({
                    text => $text
                  })})})});
  }

  # Issue a mutate request to add all assets.
  my $mutate_google_ads_response = $api_client->GoogleAdsService()->mutate({
    customerId       => $customer_id,
    mutateOperations => $operations
  });

  my $asset_resource_names = [];
  foreach
    my $response (@{$mutate_google_ads_response->{mutateOperationResponses}})
  {
    push @$asset_resource_names, $response->{assetResult}{resourceName};
  }
  print_response_details($mutate_google_ads_response);

  return $asset_resource_names;
}

# Create a hotel property asset set.
sub create_hotel_asset_set {
  my ($api_client, $customer_id) = @_;

  my $asset_set_operation =
    Google::Ads::GoogleAds::V16::Services::AssetSetService::AssetSetOperation->
    new({
      # Creates a hotel property asset set.
      create => Google::Ads::GoogleAds::V16::Resources::AssetSet->new({
          name => 'My Hotel propery asset set #' . uniqid(),
          type => HOTEL_PROPERTY
        })});
  # Issues a mutate request to add a hotel asset set and prints its information.
  my $response = $api_client->AssetSetService()->mutate({
      customerId => $customer_id,
      operations => [$asset_set_operation]});

  my $asset_set_resource_name = $response->{results}[0]{resourceName};
  printf "Created an asset set with resource name: '%s'.\n",
    $asset_set_resource_name;

  return $asset_set_resource_name;
}

# Create a hotel property asset using the specified place ID. The place ID must belong to
# a hotel property. Then, links it to the specified asset set.
#
# See https://developers.google.com/places/web-service/place-id to search for a hotel place ID.
sub create_hotel_asset {
  my ($api_client, $customer_id, $place_id, $asset_set_resource_name) = @_;

  # We use the GoogleAdService to create an asset and asset set asset in a single request.
  my $operations = [];
  my $asset_resource_name =
    Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset($customer_id,
    ASSET_TEMPORARY_ID);

  # Create a mutate operation for a hotel property asset.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation->
        new({
          create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
              resourceName       => $asset_resource_name,
              hotelPropertyAsset =>
                Google::Ads::GoogleAds::V16::Common::HotelPropertyAsset->new({
                  placeId => $place_id
                })})})});

  # Create a mutate operation for an asset set asset.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetSetAssetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetSetAssetService::AssetSetAssetOperation
        ->new({
          create => Google::Ads::GoogleAds::V16::Resources::AssetSetAsset->new({
              asset    => $asset_resource_name,
              assetSet => $asset_set_resource_name
            })})});

  # Issue a mutate request to create all entities.
  my $mutate_google_ads_response = $api_client->GoogleAdsService()->mutate({
    customerId       => $customer_id,
    mutateOperations => $operations
  });

  printf "Created the following entities for the hotel asset:\n";
  print_response_details($mutate_google_ads_response);

  # Return the created asset resource name, which will be used later to create an asset
  # group. Other resource names are not used later.
  return $mutate_google_ads_response->{mutateOperationResponses}[0]
    {assetResult}{resourceName};
}

# Create a mutate operation that creates a new campaign budget.
#
# A temporary ID will be assigned to this campaign budget so that it can be
# referenced by other objects being created in the same mutate request.
sub create_campaign_budget_operation {
  my ($customer_id) = @_;

  return
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      campaignBudgetOperation =>
        Google::Ads::GoogleAds::V16::Services::CampaignBudgetService::CampaignBudgetOperation
        ->new({
          create => Google::Ads::GoogleAds::V16::Resources::CampaignBudget->new(
            {
              # Set a temporary ID in the budget's resource name so it can be
              # referenced by the campaign in later steps.
              resourceName =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::campaign_budget(
                $customer_id, BUDGET_TEMPORARY_ID
                ),
              name => "Performance Max for travel goals campaign budget #" .
                uniqid(),
              # The budget period already defaults to DAILY.
              amountMicros   => 50000000,
              deliveryMethod => STANDARD,
              # A Performance Max campaign cannot use a shared campaign budget.
              explicitlyShared => "false",
            })})});
}

# Create a mutate operation that creates a new Performance Max campaign. Links the specified
# hotel property asset set to this campaign.
#
# A temporary ID will be assigned to this campaign so that it can be referenced by other
# objects being created in the same mutate request.
sub create_campaign_operation {
  my ($customer_id, $hotel_property_asset_set_resource_name) = @_;

  # Create a mutate operation that creates a campaign operation.
  return
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      campaignOperation =>
        Google::Ads::GoogleAds::V16::Services::CampaignService::CampaignOperation
        ->new({
          create => Google::Ads::GoogleAds::V16::Resources::Campaign->new({
              # Assign the resource name with a temporary ID.
              resourceName =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::campaign(
                $customer_id, CAMPAIGN_TEMPORARY_ID
                ),
              name => "Performance Max for travel goals campaign #'" . uniqid(),
              # Set the budget using the given budget resource name.
              campaignBudget =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::campaign_budget(
                $customer_id, BUDGET_TEMPORARY_ID
                ),
              # Set the campaign status as PAUSED. The campaign is the only entity in
              # the mutate request that should have its status set.
              status =>
                Google::Ads::GoogleAds::V16::Enums::CampaignStatusEnum::PAUSED,
              # All Performance Max campaigns have an advertisingChannelType of
              # PERFORMANCE_MAX. The advertisingChannelSubType should not be set.
              advertisingChannelType => PERFORMANCE_MAX,

              # To create a Performance Max for travel goals campaign, you need to set
              # `hotelPropertyAssetSet`.
              hotelPropertyAssetSet => $hotel_property_asset_set_resource_name,

              # Bidding strategy must be set directly on the campaign.
              # Setting a portfolio bidding strategy by resource name is not supported.
              # Max Conversion and Max Conversion Value are the only strategies
              # supported for Performance Max campaigns.
              # An optional ROAS (Return on Advertising Spend) can be set for
              # maximizeConversionValue. The ROAS value must be specified as a ratio in
              # the API. It is calculated by dividing "total value" by "total spend".
              # For more information on Max Conversion Value, see the support article:
              # http://support.google.com/google-ads/answer/7684216.
              # A targetRoas of 3.5 corresponds to a 350% return on ad spend.
              maximizeConversionValue =>
                Google::Ads::GoogleAds::V16::Common::MaximizeConversionValue->
                new({
                  targetRoas => 3.5
                })})})});
}

# Create a list of mutate operations that create a new asset group, composed of suggested
# assets. In case the number of suggested assets is not enough for the requirements, it'll
# create more assets to meet the requirement.
#
# For the list of required assets for a Performance Max campaign, see
# https://developers.google.com/google-ads/api/docs/performance-max/assets.
sub create_asset_group_operations {
  my (
    $customer_id,
    $hotel_property_asset_resource_name,
    $headline_asset_resource_names,
    $description_asset_resource_names,
    $hotel_asset_suggestion
  ) = @_;
  my $operations = [];

  # Create a new mutate operation that creates an asset group using suggested information
  # when available.
  my $asset_group_name =
      $hotel_asset_suggestion->{status} eq SUCCESS
    ? $hotel_asset_suggestion->{hotelName}
    : 'Performance Max for travel goals asset group #' . uniqid();
  my $asset_group_final_urls =
    $hotel_asset_suggestion->{status} eq SUCCESS
    ? [$hotel_asset_suggestion->{finalUrl}]
    : ['http://www.example.com'];
  my $asset_group_resource_name =
    Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset_group($customer_id,
    ASSET_GROUP_TEMPORARY_ID);
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetGroupService::AssetGroupOperation
        ->new({
          create => Google::Ads::GoogleAds::V16::Resources::AssetGroup->new({
              resourceName => $asset_group_resource_name,
              name         => $asset_group_name,
              campaign     =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::campaign(
                $customer_id, CAMPAIGN_TEMPORARY_ID
                ),
              finalUrls => $asset_group_final_urls,
              status    =>
                Google::Ads::GoogleAds::V16::Enums::AssetGroupStatusEnum::PAUSED
            })})});

  # An asset group is linked to an asset by creating a new asset group asset and providing:
  # -  the resource name of the asset group
  # -  the resource name of the asset
  # -  the field_type of the asset in this asset group
  #
  # To learn more about asset groups, see
  # https://developers.google.com/google-ads/api/docs/performance-max/asset-groups.
  #
  # Headline and description assets were created at the first step of this example. So, we
  # just need to link them with the created asset group.
  #
  # Link the headline assets to the asset group.
  foreach my $resource_name (@$headline_asset_resource_names) {
    push @$operations,
      Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
      ->new({
        assetGroupAssetOperation =>
          Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
          ->new({
            create =>
              Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
                asset      => $resource_name,
                assetGroup =>
                  Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset_group(
                  $customer_id, ASSET_GROUP_TEMPORARY_ID
                  ),
                fieldType => HEADLINE
              })})});
  }

  # Link the description assets.
  foreach my $resource_name (@$description_asset_resource_names) {
    push @$operations,
      Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
      ->new({
        assetGroupAssetOperation =>
          Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
          ->new({
            create =>
              Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
                asset      => $resource_name,
                assetGroup =>
                  Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset_group(
                  $customer_id, ASSET_GROUP_TEMPORARY_ID
                  ),
                fieldType => DESCRIPTION
              })})});
  }

  # Link the previously created hotel property asset to the asset group. In the real-world
  # scenario, you'd need to do this step several times for each hotel property asset.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupAssetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
        ->new({
          create =>
            Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
              asset      => $hotel_property_asset_resource_name,
              assetGroup => $asset_group_resource_name,
              fieldType  => HOTEL_PROPERTY
            })})});

  # Create the rest of required text assets and link them to the asset group.
  push @$operations,
    @{create_text_assets_for_asset_group($customer_id, $hotel_asset_suggestion)
    };

  # Create the image assets and link them to the asset group. Some optional image assets
  # suggested by the TravelAssetSuggestionService might be created too.
  push @$operations,
    @{create_image_assets_for_asset_group($customer_id, $hotel_asset_suggestion)
    };

  if ($hotel_asset_suggestion->{status} eq SUCCESS) {
    # Create a new mutate operation for a suggested call-to-action asset and link it
    # to the asset group.
    push @$operations,
      Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
      ->new({
        assetOperation =>
          Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation->
          new({
            create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
                resourceName =>
                  Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                  $customer_id, $next_temp_id
                  ),
                name => 'Suggested call-to-action asset #' . uniqid(),
                callToActionAsset =>
                  Google::Ads::GoogleAds::V16::Common::CallToActionAsset->new({
                    callToAction => $hotel_asset_suggestion->{callToAction}})})}
          )});
    push @$operations,
      Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation
      ->new({
        assetGroupAssetOperation =>
          Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
          ->new({
            create =>
              Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
                asset =>
                  Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                  $customer_id, $next_temp_id
                  ),
                assetGroup => $asset_group_resource_name,
                fieldType  => CALL_TO_ACTION_SELECTION
              })})});
    $next_temp_id--;
  }

  return $operations;
}

# Create text assets required for an asset group using the suggested hotel text assets. It adds
# more text assets to fulfill the requirements if the suggested hotel text assets are not enough.
sub create_text_assets_for_asset_group {
  my ($customer_id, $hotel_asset_suggestion) = @_;

  # Create mutate operations for the suggested text assets except for headlines and
  # descriptions, which were created previously.
  my $operations = [];

  # Create a map of asset field type to number of text values.
  my $required_text_asset_counts = {};
  foreach my $field_type (keys %$min_required_text_asset_counts) {
    $required_text_asset_counts->{$field_type} = 0;
  }

  if ($hotel_asset_suggestion->{status} eq SUCCESS) {
    # Add text values of suggested text assets.
    foreach my $hotel_text_asset (@{$hotel_asset_suggestion->{textAssetsList}})
    {
      my $asset_field_type = $hotel_text_asset->{assetFieldType};
      if ($asset_field_type eq HEADLINE or $asset_field_type eq DESCRIPTION) {
        # Headlines and descriptions were already created at the first step of this code example.
        next;
      }
      printf
"A text asset with text '%s' is suggested for the asset field type '%s'.\n",
        $hotel_text_asset->{text}, $asset_field_type;

      push @$operations,
        @{
        create_text_asset_and_asset_group_asset_operations(
          $customer_id, $hotel_text_asset->{text},
          $hotel_text_asset->{assetFieldType})};
      $required_text_asset_counts->{$asset_field_type}++;
    }
  }

  # Add more text values by field type to fulfill the requirements.
  foreach my $asset_field_type (keys %$min_required_text_asset_counts) {
    if ($asset_field_type eq HEADLINE or $asset_field_type eq DESCRIPTION) {
      # Headlines and descriptions were already created at the first step of this code example.
      next;
    }

    my $min_count = $min_required_text_asset_counts->{$asset_field_type};
    for (
      my $i = 0 ;
      $i < $min_count - $required_text_asset_counts->{$asset_field_type} ;
      $i++
      )
    {
      my $text_from_defaults =
        $default_text_assets_info->{$asset_field_type}[$i++];
      printf
"A default text '%s' is used to create a text asset for the asset field type '%s'.\n",
        $text_from_defaults, $asset_field_type;
      push @$operations,
        @{
        create_text_asset_and_asset_group_asset_operations($customer_id,
          $text_from_defaults, $asset_field_type)};

    }
  }

  return $operations;
}

# Create image assets required for an asset group using the suggested hotel image assets. It
# adds more image assets to fulfill the requirements if the suggested hotel image assets are
# not enough.
sub create_image_assets_for_asset_group {
  my ($customer_id, $hotel_asset_suggestion) = @_;

  my $operations = [];
  # Create mutate operations for the suggested image assets.
  # Create a map of asset field type to number of text values.
  my $required_image_asset_counts = {};
  foreach my $field_type (keys %$min_required_image_asset_counts) {
    $required_image_asset_counts->{$field_type} = 0;
  }
  foreach my $hotel_image_asset (@{$hotel_asset_suggestion->{imageAssets}}) {
    printf
"An image asset with url '%s' is suggested for the asset field type '%s'.\n",
      $hotel_image_asset->{uri}, $hotel_image_asset->{assetFieldType};
    push @$operations,
      @{
      create_image_asset_and_asset_group_asset_operations(
        $customer_id,
        $hotel_image_asset->{uri},
        $hotel_image_asset->{assetFieldType},
        'Suggested image asset #' . uniqid())};
    # Keeps track of only required image assets. The service may sometimes suggest optional
    # image assets.
    if (
      exists $required_image_asset_counts->
      {$hotel_image_asset->{assetFieldType}})
    {
      $required_image_asset_counts->{$hotel_image_asset->{assetFieldType}}++;
    }
  }

  # Add more image assets to fulfill the requirements.
  foreach my $asset_field_type (keys %$min_required_image_asset_counts) {
    my $min_count = $min_required_image_asset_counts->{$asset_field_type};
    for (
      my $i = 0 ;
      $i < $min_count - $required_image_asset_counts->{$asset_field_type} ;
      $i++
      )
    {
      my $image_from_defaults =
        $default_image_assets_info->{$asset_field_type}[$i++];
      printf
"A default image URL '%s' is used to create an image asset for the asset field type '%s'.\n",
        $image_from_defaults, $asset_field_type;
      push @$operations,
        @{
        create_image_asset_and_asset_group_asset_operations(
          $customer_id,      $image_from_defaults,
          $asset_field_type, lc $asset_field_type . uniqid())};
    }
  }

  return $operations;
}

# Create a list of mutate operations that create a new linked text asset.
sub create_text_asset_and_asset_group_asset_operations {
  my ($customer_id, $text, $field_type) = @_;

  my $operations = [];
  # Create a new mutate operation that creates a text asset.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation->
        new({
          create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
              resourceName =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                $customer_id, $next_temp_id
                ),
              textAsset => Google::Ads::GoogleAds::V16::Common::TextAsset->new({
                  text => $text
                })})})});

  # Create an asset group asset to link the asset to the asset group.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupAssetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
        ->new({
          create =>
            Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
              asset => Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                $customer_id, $next_temp_id
              ),
              assetGroup =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset_group(
                $customer_id, ASSET_GROUP_TEMPORARY_ID
                ),
              fieldType => $field_type
            })})});

  $next_temp_id--;

  return $operations;
}

# Create a list of mutate operations that create a new linked image asset.
sub create_image_asset_and_asset_group_asset_operations {
  my ($customer_id, $url, $field_type, $asset_name) = @_;

  my $operations = [];
  # Create a new mutate operation that creates an image asset.
  # Create a new mutate operation that creates a text asset.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetService::AssetOperation->
        new({
          create => Google::Ads::GoogleAds::V16::Resources::Asset->new({
              resourceName =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                $customer_id, $next_temp_id
                ),
              # Provide a unique friendly name to identify your asset.
              # When there is an existing image asset with the same content but a different
              # name, the new name will be dropped silently.
              name       => $asset_name,
              imageAsset =>
                Google::Ads::GoogleAds::V16::Common::ImageAsset->new({
                  data => get_base64_data_from_url($url)})})})});

  # Create an asset group asset to link the asset to the asset group.
  push @$operations,
    Google::Ads::GoogleAds::V16::Services::GoogleAdsService::MutateOperation->
    new({
      assetGroupAssetOperation =>
        Google::Ads::GoogleAds::V16::Services::AssetGroupAssetService::AssetGroupAssetOperation
        ->new({
          create =>
            Google::Ads::GoogleAds::V16::Resources::AssetGroupAsset->new({
              asset => Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset(
                $customer_id, $next_temp_id
              ),
              assetGroup =>
                Google::Ads::GoogleAds::V16::Utils::ResourceNames::asset_group(
                $customer_id, ASSET_GROUP_TEMPORARY_ID
                ),
              fieldType => $field_type
            })})});

  $next_temp_id--;

  return $operations;
}

# Prints the details of a MutateGoogleAdsResponse.
# Parses the "response" oneof field name and uses it to extract the new entity's
# name and resource name.
sub print_response_details {
  my ($mutate_google_ads_response) = @_;

  foreach
    my $response (@{$mutate_google_ads_response->{mutateOperationResponses}})
  {
    my $result_type = [keys %$response]->[0];

    printf "Created a(n) %s with '%s'.\n",
      ucfirst $result_type =~ s/Result$//r,
      $response->{$result_type}{resourceName};
  }
}

# Don't run the example if the file is being included.
if (abs_path($0) ne abs_path(__FILE__)) {
  return 1;
}

# Get Google Ads Client, credentials will be read from ~/googleads.properties.
my $api_client = Google::Ads::GoogleAds::Client->new();

# By default examples are set to die on any server returned fault.
$api_client->set_die_on_faults(1);

my $customer_id = undef;
my $place_id    = undef;

# Parameters passed on the command line will override any parameters set in code.
GetOptions(
  "customer_id=s" => \$customer_id,
  "place_id=s"    => \$place_id,
);

# Print the help message if the parameters are not initialized in the code nor
# in the command line.
pod2usage(2)
  if not check_params($customer_id, $place_id);

# Call the example.
add_performance_max_for_travel_goals_campaign($api_client,
  $customer_id =~ s/-//gr, $place_id);

=pod

=head1 NAME

add_performance_max_for_travel_goals_campaign

=head1 DESCRIPTION

This example shows how to create a Performance Max for travel goals campaign. It also uses
TravelAssetSuggestionService to fetch suggested assets for creating an asset group. In case
there are not enough assets for the asset group (required by Performance Max), this example will
create more assets to fulfill the requirements.

For more information about Performance Max campaigns, see
https://developers.google.com/google-ads/api/docs/performance-max/overview.

Prerequisites:
- You must have at least one conversion action in the account. For more about conversion actions,
see https://developers.google.com/google-ads/api/docs/conversions/overview#conversion_actions.

Notes:
- This example uses the default customer conversion goals. For an example of setting
  campaign-specific conversion goals, see shopping_ads/add_performance_max_retail_campaign.pl.
- To learn how to create asset group signals, see
  advanced_operations/add_performance_max_campaign.pl.

=head1 SYNOPSIS

add_performance_max_for_travel_goals_campaign.pl [options]

    -help                         Show the help message.
    -customer_id                  The Google Ads customer ID.
    -place_id 					  The place ID of a hotel property. A place ID uniquely identifies a place in the Google Places database. See https://developers.google.com/places/web-service/place-id to learn more.

=cut